Implement skeleton for a new DB backend.

This commit is contained in:
Devananda van der Veen 2013-05-10 11:48:08 -07:00
parent c4c6204acf
commit 9e242f8d2d
17 changed files with 689 additions and 33 deletions

View File

@ -39,9 +39,8 @@ def get_pecan_config():
def setup_app(pecan_config=None, extra_hooks=None):
# FIXME: Replace DBHook with a hooks.TransactionHook
app_hooks = [hooks.ConfigHook()]
# hooks.DBHook()]
app_hooks = [hooks.ConfigHook(),
hooks.DBHook()]
if extra_hooks:
app_hooks.extend(extra_hooks)
@ -62,6 +61,7 @@ def setup_app(pecan_config=None, extra_hooks=None):
force_canonical=getattr(pecan_config.app, 'force_canonical', True),
hooks=app_hooks,
)
# TODO: add this back in
# wrap_app=middleware.ParsableErrorMiddleware,
if pecan_config.app.enable_acl:

View File

@ -61,9 +61,10 @@ class Interface(Base):
node_id = int
address = wtypes.text
def __init__(self, node_id=None, address=None):
self.node_id = node_id
self.address = address
def __init__(self, **kwargs):
self.fields = list(kwargs)
for k, v in kwargs.iteritems():
setattr(self, k, v)
@classmethod
def sample(cls):
@ -108,19 +109,15 @@ class Node(Base):
"""A representation of a bare metal node"""
uuid = wtypes.text
cpus = int
memory_mb = int
def __init__(self, uuid=None, cpus=None, memory_mb=None):
self.uuid = uuid
self.cpus = cpus
self.memory_mb = memory_mb
def __init__(self, **kwargs):
self.fields = list(kwargs)
for k, v in kwargs.iteritems():
setattr(self, k, v)
@classmethod
def sample(cls):
return cls(uuid='1be26c0b-03f2-4d2e-ae87-c02d7f33c123',
cpus=2,
memory_mb=1024,
)
@ -141,9 +138,8 @@ class NodesController(rest.RestController):
@wsme_pecan.wsexpose(Node, unicode)
def get_one(self, node_id):
"""Retrieve information about the given node."""
one = Node.sample()
one.uuid = node_id
return one
r = pecan.request.dbapi.get_node_by_id(node_id)
return r
@wsme_pecan.wsexpose()
def delete(self, node_id):

View File

@ -19,7 +19,7 @@
from oslo.config import cfg
from pecan import hooks
from ironic import db
from ironic.db import api as dbapi
class ConfigHook(hooks.PecanHook):
@ -34,12 +34,4 @@ class ConfigHook(hooks.PecanHook):
class DBHook(hooks.PecanHook):
def before(self, state):
# FIXME
storage_engine = storage.get_engine(state.request.cfg)
state.request.storage_engine = storage_engine
state.request.storage_conn = storage_engine.get_connection(
state.request.cfg)
# def after(self, state):
# print 'method:', state.request.method
# print 'response:', state.response.status
state.request.dbapi = dbapi.get_instance()

38
ironic/cmd/dbsync.py Normal file
View File

@ -0,0 +1,38 @@
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
#
# vim: tabstop=4 shiftwidth=4 softtabstop=4
#
# Copyright 2013 Hewlett-Packard Development Company, L.P.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
Run storage database migration.
"""
import sys
from oslo.config import cfg
from ironic.common import service
from ironic.db import migration
CONF = cfg.CONF
CONF.import_opt('db_backend',
'ironic.openstack.common.db.api')
def main():
service.prepare_service(sys.argv)
migration.db_sync()

122
ironic/db/api.py Normal file
View File

@ -0,0 +1,122 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# -*- encoding: utf-8 -*-
#
# Copyright 2013 Hewlett-Packard Development Company, L.P.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
Base classes for storage engines
"""
import abc
from ironic.openstack.common.db import api as db_api
_BACKEND_MAPPING = {'sqlalchemy': 'ironic.db.sqlalchemy.api'}
def get_instance():
"""Return a DB API instance."""
IMPL = db_api.DBAPI(backend_mapping=_BACKEND_MAPPING)
return IMPL
class Connection(object):
"""Base class for storage system connections."""
__metaclass__ = abc.ABCMeta
@abc.abstractmethod
def __init__(self):
"""Constructor."""
@abc.abstractmethod
def get_nodes(self, columns):
"""Return a list of dicts of all nodes.
:param columns: List of columns to return.
"""
@abc.abstractmethod
def get_associated_nodes(self):
"""Return a list of ids of all associated nodes."""
@abc.abstractmethod
def get_unassociated_nodes(self):
"""Return a list of ids of all unassociated nodes."""
@abc.abstractmethod
def reserve_node(self, *args, **kwargs):
"""Find a free node and associate it.
TBD
"""
@abc.abstractmethod
def create_node(self, *args, **kwargs):
"""Create a new node."""
@abc.abstractmethod
def get_node_by_id(self, node_id):
"""Return a node.
:param node_id: The id or uuid of a node.
"""
@abc.abstractmethod
def get_node_by_instance_id(self, instance_id):
"""Return a node.
:param instance_id: The instance id or uuid of a node.
"""
@abc.abstractmethod
def destroy_node(self, node_id):
"""Destroy a node.
:param node_id: The id or uuid of a node.
"""
@abc.abstractmethod
def update_node(self, node_id, *args, **kwargs):
"""Update properties of a node.
:param node_id: The id or uuid of a node.
TBD
"""
@abc.abstractmethod
def get_iface(self, iface_id):
"""Return an interface.
:param iface_id: The id or MAC of an interface.
"""
@abc.abstractmethod
def create_iface(self, *args, **kwargs):
"""Create a new iface."""
@abc.abstractmethod
def update_iface(self, iface_id, *args, **kwargs):
"""Update properties of an iface.
:param iface_id: The id or MAC of an interface.
TBD
"""
@abc.abstractmethod
def destroy_iface(self, iface_id):
"""Destroy an iface.
:param iface_id: The id or MAC of an interface.
"""

69
ironic/db/models.py Normal file
View File

@ -0,0 +1,69 @@
# -*- encoding: utf-8 -*-
#
# Copyright © 2013 New Dream Network, LLC (DreamHost)
#
# Author: Doug Hellmann <doug.hellmann@dreamhost.com>
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
Model classes for use in the storage API.
"""
class Model(object):
"""Base class for storage API models.
"""
def __init__(self, **kwds):
self.fields = list(kwds)
for k, v in kwds.iteritems():
setattr(self, k, v)
def as_dict(self):
d = {}
for f in self.fields:
v = getattr(self, f)
if isinstance(v, Model):
v = v.as_dict()
elif isinstance(v, list) and v and isinstance(v[0], Model):
v = [sub.as_dict() for sub in v]
d[f] = v
return d
def __eq__(self, other):
return self.as_dict() == other.as_dict()
class Node(Model):
"""Representation of a bare metal node."""
def __init__(self, uuid, power_info, task_state, image_path,
instance_uuid, instance_name, extra):
Model.__init__(uuid=uuid,
power_info=power_info,
task_state=task_state,
image_path=image_path,
instance_uuid=instance_uuid,
instance_name=instance_name,
extra=extra,
)
class Iface(Model):
"""Representation of a NIC."""
def __init__(self, mac, node_id, extra):
Model.__init__(mac=mac,
node_id=node_id,
extra=extra,
)

View File

195
ironic/db/sqlalchemy/api.py Normal file
View File

@ -0,0 +1,195 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# -*- encoding: utf-8 -*-
#
# Copyright 2013 Hewlett-Packard Development Company, L.P.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""SQLAlchemy storage backend."""
import sys
import uuid
from oslo.config import cfg
from sqlalchemy.orm.exc import NoResultFound
from sqlalchemy.orm.exc import MultipleResultsFound
from ironic.common import exception
from ironic.db import api
from ironic.db.sqlalchemy.models import Node, Iface
from ironic.openstack.common.db.sqlalchemy import session as db_session
from ironic.openstack.common import log
from ironic.openstack.common import uuidutils
CONF = cfg.CONF
CONF.import_opt('sql_connection',
'ironic.openstack.common.db.sqlalchemy.session')
LOG = log.getLogger(__name__)
get_engine = db_session.get_engine
get_session = db_session.get_session
def get_backend():
"""The backend is this module itself."""
return Connection()
# - nodes
# - { id: AUTO_INC INTEGER
# uuid: node uuid
# power_info: JSON of power mgmt information
# task_state: current task
# image_path: URL of associated image
# instance_uuid: uuid of associated instance
# instance_name: name of associated instance
# hw_spec_id: hw specification id (->hw_specs.id)
# inst_spec_id: instance specification id (->inst_specs.id)
# extra: JSON blob of extra data
# }
# - ifaces
# - { id: AUTO_INC INTEGER
# mac: MAC address of this iface
# node_id: associated node (->nodes.id)
# ?datapath_id
# ?port_no
# ?model
# extra: JSON blob of extra data
# }
# - hw_specs
# - { id: AUTO_INC INTEGER
# cpu_arch:
# n_cpu:
# n_disk:
# ram_mb:
# storage_gb:
# }
# - inst_specs
# - { id: AUTO_INC INTEGER
# root_mb:
# swap_mb:
# image_path:
# }
def model_query(model, *args, **kwargs):
"""Query helper for simpler session usage.
:param session: if present, the session to use
"""
session = kwargs.get('session') or get_session()
query = session.query(model, *args)
return query
class Connection(api.Connection):
"""SqlAlchemy connection."""
def __init__(self):
pass
def get_nodes(self, columns):
"""Return a list of dicts of all nodes.
:param columns: List of columns to return.
"""
pass
def get_associated_nodes(self):
"""Return a list of ids of all associated nodes."""
pass
def get_unassociated_nodes(self):
"""Return a list of ids of all unassociated nodes."""
pass
def reserve_node(self, *args, **kwargs):
"""Find a free node and associate it.
TBD
"""
pass
def create_node(self, *args, **kwargs):
"""Create a new node."""
node = Node()
def get_node_by_id(self, node_id):
"""Return a node.
:param node_id: The id or uuid of a node.
"""
query = model_query(Node)
if uuidutils.is_uuid_like(node_id):
query.filter_by(uuid=node_id)
else:
query.filter_by(id=node_id)
try:
result = query.one()
except NoResultFound:
raise
except MultipleResultsFound:
raise
return result
def get_node_by_instance_id(self, instance_id):
"""Return a node.
:param instance_id: The instance id or uuid of a node.
"""
pass
def destroy_node(self, node_id):
"""Destroy a node.
:param node_id: The id or uuid of a node.
"""
pass
def update_node(self, node_id, *args, **kwargs):
"""Update properties of a node.
:param node_id: The id or uuid of a node.
TBD
"""
pass
def get_iface(self, iface_id):
"""Return an interface.
:param iface_id: The id or MAC of an interface.
"""
pass
def create_iface(self, *args, **kwargs):
"""Create a new iface."""
pass
def update_iface(self, iface_id, *args, **kwargs):
"""Update properties of an iface.
:param iface_id: The id or MAC of an interface.
TBD
"""
pass
def destroy_iface(self, iface_id):
"""Destroy an iface.
:param iface_id: The id or MAC of an interface.
"""
pass

View File

@ -0,0 +1,22 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# -*- encoding: utf-8 -*-
#
# Copyright 2013 Hewlett-Packard Development Company, L.P.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from migrate.versioning.shell import main
if __name__ == '__main__':
main(debug='False', repository='.')

View File

@ -0,0 +1,20 @@
[db_settings]
# Used to identify which repository this database is versioned under.
# You can use the name of your project.
repository_id=ironic
# The name of the database table used to track the schema version.
# This name shouldn't already be used by your project.
# If this is changed once a database is under version control, you'll need to
# change the table name in each database too.
version_table=migrate_version
# When committing a change script, Migrate will attempt to generate the
# sql for all supported databases; normally, if one of them fails - probably
# because you don't have that database installed - it is ignored and the
# commit continues, perhaps ending successfully.
# Databases in this list MUST compile successfully during a commit, or the
# entire commit will fail. List the databases your application will actually
# be using to ensure your updates to that database work properly.
# This must be a list; example: ['postgres','sqlite']
required_dbs=[]

View File

@ -0,0 +1,81 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# -*- encoding: utf-8 -*-
#
# Copyright 2013 Hewlett-Packard Development Company, L.P.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from migrate.changeset import UniqueConstraint
from sqlalchemy import Table, Column, Index, ForeignKey, MetaData
from sqlalchemy import Boolean, DateTime, Float, Integer, String, Text
from ironic.openstack.common import log as logging
LOG = logging.getLogger(__name__)
ENGINE='InnoDB'
CHARSET='utf8'
def upgrade(migrate_engine):
meta = MetaData()
meta.bind = migrate_engine
nodes = Table('nodes', meta,
Column('id', Integer, primary_key=True, nullable=False),
Column('uuid', String(length=26)),
Column('power_info', Text),
Column('task_state', String(length=255)),
Column('image_path', String(length=255), nullable=True),
Column('instance_uuid', String(length=255), nullable=True),
Column('instance_name', String(length=255), nullable=True),
Column('extra', Text),
Column('created_at', DateTime),
Column('updated_at', DateTime),
mysql_engine=ENGINE,
mysql_charset=CHARSET,
)
ifaces = Table('ifaces', meta,
Column('id', Integer, primary_key=True, nullable=False),
Column('uuid', String(length=26)),
Column('node_id', Integer, ForeignKey('nodes.id'),
nullable=True),
Column('extra', Text),
Column('created_at', DateTime),
Column('updated_at', DateTime),
mysql_engine=ENGINE,
mysql_charset=CHARSET,
)
tables = [nodes, ifaces]
for table in tables:
try:
table.create()
except Exception:
LOG.info(repr(table))
LOG.Exception(_('Exception while creating table.'))
raise
indexes = [
Index('uuid', nodes.c.uuid),
Index('uuid', ifaces.c.uuid),
]
if migrate_engine.name == 'mysql' or migrate_engine.name == 'postgresql':
for index in indexes:
index.create(migrate_engine)
def downgrade(migrate_engine):
raise NotImplementedError('Downgrade from Folsom is unsupported.')

View File

@ -19,15 +19,14 @@
import distutils.version as dist_version
import os
from ironic.common import exception
from ironic.db import migration
from ironic.openstack.common.db.sqlalchemy import session as db_session
import migrate
from migrate.versioning import util as migrate_util
import sqlalchemy
from ironic.common import exception
from ironic.db import migration
from ironic.openstack.common.db.sqlalchemy import session as db_session
@migrate_util.decorator
def patched_with_engine(f, *a, **kw):

View File

@ -0,0 +1,102 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# -*- encoding: utf-8 -*-
#
# Copyright 2013 Hewlett-Packard Development Company, L.P.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
SQLAlchemy models for baremetal data.
"""
import urlparse
from oslo.config import cfg
from sqlalchemy import Table, Column, Index, ForeignKey
from sqlalchemy import Boolean, DateTime, Float, Integer, String, Text
from sqlalchemy.ext.declarative import declarative_base
from ironic.openstack.common.db.sqlalchemy import models
sql_opts = [
cfg.StrOpt('mysql_engine',
default='InnoDB',
help='MySQL engine')
]
cfg.CONF.register_opts(sql_opts)
def table_args():
engine_name = urlparse.urlparse(cfg.CONF.database_connection).scheme
if engine_name == 'mysql':
return {'mysql_engine': cfg.CONF.mysql_engine,
'mysql_charset': "utf8"}
return None
class IronicBase(models.TimestampMixin,
models.ModelBase):
metadata = None
Base = declarative_base(cls=IronicBase)
class Node(Base):
"""Represents a bare metal node."""
__tablename__ = 'nodes'
id = Column(Integer, primary_key=True)
uuid = Column(String(36))
power_info = Column(Text)
task_state = Column(String(255))
image_path = Column(String(255), nullable=True)
instance_uuid = Column(String(36), nullable=True)
instance_name = Column(String(255), nullable=True)
extra = Column(Text)
class Iface(Base):
"""Represents a NIC in a bare metal node."""
__tablename__ = 'ifaces'
id = Column(Integer, primary_key=True)
mac = Column(String(255), unique=True)
node_id = Column(Integer, ForeignKey('nodes.id'), nullable=True)
extra = Column(Text)
class HwSpec(Base):
"""Represents a unique hardware class."""
__tablename__ = 'hw_specs'
id = Column(Integer, primary_key=True)
uuid = Column(String(36))
cpu_arch = Column(String(255))
n_cpu = Column(Integer)
ram_mb = Column(Integer)
storage_gb = Column(Integer)
name = Column(String(255), nullable=True)
n_disk = Column(Integer, nullable=True)
class InstSpec(Base):
"""Represents a unique instance class."""
__tablename__ = 'inst_specs'
id = Column(Integer, primary_key=True)
uuid = Column(String(36))
root_mb = Column(Integer)
swap_mb = Column(Integer)
name = Column(String(255), nullable=True)

View File

@ -29,6 +29,7 @@ packages =
[entry_points]
console_scripts =
ironic-api = ironic.cmd.api:main
ironic-dbsync = ironic.cmd.dbsync:main
ironic-manager = ironic.cmd.manager:main
[build_sphinx]

View File

@ -16,6 +16,25 @@
import setuptools
from ironic.openstack.common import setup as common_setup
project = 'ironic'
setuptools.setup(
name=project,
version=common_setup.get_version(project, '2013.1'),
description='Bare Metal controller',
classifiers=[
'Environment :: OpenStack',
'Intended Audience :: Information Technology',
'Intended Audience :: System Administrators',
'License :: OSI Approved :: Apache Software License',
'Operating System :: POSIX :: Linux',
'Programming Language :: Python',
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 2.7',
],
include_package_data=True,
setup_requires=['d2to1>=0.2.10,<0.3', 'pbr>=0.5,<0.6'],
d2to1=True)
d2to1=True,
)