diff --git a/ironic/api/controllers/v1.py b/ironic/api/controllers/v1.py index 9e87e68a..09ab4e78 100644 --- a/ironic/api/controllers/v1.py +++ b/ironic/api/controllers/v1.py @@ -109,6 +109,16 @@ class Node(Base): """A representation of a bare metal node""" uuid = wtypes.text + cpu_arch = wtypes.text + cpu_num = int + memory = int + local_storage_max = int + task_state = wtypes.text + image_path = wtypes.text + instance_uuid = wtypes.text + instance_name = wtypes.text + power_info = wtypes.text + extra = wtypes.text def __init__(self, **kwargs): self.fields = list(kwargs) @@ -117,17 +127,35 @@ class Node(Base): @classmethod def sample(cls): + power_info = "{'driver': 'ipmi', 'user': 'fake', " \ + + "'password': 'password', 'address': '1.2.3.4'}" return cls(uuid='1be26c0b-03f2-4d2e-ae87-c02d7f33c123', + cpu_arch='x86_64', + cpu_num=4, + memory=16384, + local_storage_max=1000, + task_state='NOSTATE', + image_path='/fake/image/path', + instance_uuid='8227348d-5f1d-4488-aad1-7c92b2d42504', + power_info=power_info, + extra='{}', ) class NodesController(rest.RestController): """REST controller for Nodes""" - @wsme_pecan.wsexpose(Node, unicode) - def post(self, node): + @wsme_pecan.wsexpose(Node, body=Node) + def post(self, data): """Ceate a new node.""" - return Node.sample() + try: + node = pecan.request.dbapi.create_node( + data.as_dict(db.models.Node)) + except Exception as e: + LOG.exception(e) + raise wsme.exc.ClientSideError(_("Invalid data")) + return node + @wsme_pecan.wsexpose() def get_all(self): @@ -139,7 +167,7 @@ class NodesController(rest.RestController): def get_one(self, node_id): """Retrieve information about the given node.""" r = pecan.request.dbapi.get_node_by_id(node_id) - return r + return Node.from_db_model(r) @wsme_pecan.wsexpose() def delete(self, node_id): diff --git a/ironic/common/utils.py b/ironic/common/utils.py index 90d7f92c..f33d7678 100644 --- a/ironic/common/utils.py +++ b/ironic/common/utils.py @@ -305,6 +305,12 @@ def is_valid_boolstr(val): return str(val).lower() in boolstrs +def is_valid_mac(address): + """Verify the format of a MAC addres.""" + if re.match("[0-9a-f]{2}([-:])[0-9a-f]{2}(\\1[0-9a-f]{2}){4}$", address.lower()): + return True + return False + def is_valid_ipv4(address): """Verify that address represents a valid IPv4 address.""" try: diff --git a/ironic/db/api.py b/ironic/db/api.py index 3791deec..5f4e34b9 100644 --- a/ironic/db/api.py +++ b/ironic/db/api.py @@ -56,67 +56,98 @@ class Connection(object): """Return a list of ids of all unassociated nodes.""" @abc.abstractmethod - def reserve_node(self, *args, **kwargs): - """Find a free node and associate it. + def reserve_node(self, node, values): + """Associate a node with an instance. - TBD + :param node: The id or uuid of a node. + :param values: Values to set while reserving the node. + Must include 'instance_uuid'. + :return: The reserved Node. """ @abc.abstractmethod - def create_node(self, *args, **kwargs): - """Create a new node.""" + def create_node(self, values): + """Create a new node. + + :param values: Values to instantiate the node with. + :returns: Node. + """ @abc.abstractmethod - def get_node_by_id(self, node_id): + def get_node(self, node): """Return a node. - :param node_id: The id or uuid of a node. + :param node: The id or uuid of a node. + :return Node: """ @abc.abstractmethod - def get_node_by_instance_id(self, instance_id): + def get_node_by_instance(self, instance): """Return a node. - :param instance_id: The instance id or uuid of a node. + :param instance: The instance name or uuid to search for. + :returns: Node. """ @abc.abstractmethod - def destroy_node(self, node_id): - """Destroy a node. + def destroy_node(self, node): + """Destroy a node and all associated interfaces. - :param node_id: The id or uuid of a node. + :param node: The id or uuid of a node. """ @abc.abstractmethod - def update_node(self, node_id, *args, **kwargs): + def update_node(self, node, values): """Update properties of a node. - :param node_id: The id or uuid of a node. - TBD + :param node: The id or uuid of a node. + :param values: Dict of values to update. + :returns: Node. """ @abc.abstractmethod - def get_iface(self, iface_id): + def get_iface(self, iface): """Return an interface. - :param iface_id: The id or MAC of an interface. + :param iface: The id or MAC of an interface. + :returns: Iface. """ @abc.abstractmethod - def create_iface(self, *args, **kwargs): - """Create a new iface.""" + def get_iface_by_vif(self, vif): + """Return the interface corresponding to this VIF. + + :param vif: The uuid of the VIF. + :returns: Iface. + """ @abc.abstractmethod - def update_iface(self, iface_id, *args, **kwargs): + def get_iface_by_node(self, node): + """List all the interfaces for a given node. + + :param node: The id or uuid of a node. + :returns: list of Iface. + """ + + @abc.abstractmethod + def create_iface(self, values): + """Create a new iface. + + :param values: Dict of values. + """ + + @abc.abstractmethod + def update_iface(self, iface, values): """Update properties of an iface. - :param iface_id: The id or MAC of an interface. - TBD + :param iface: The id or MAC of an interface. + :param values: Dict of values to update. + :returns: Iface. """ @abc.abstractmethod - def destroy_iface(self, iface_id): + def destroy_iface(self, iface): """Destroy an iface. - :param iface_id: The id or MAC of an interface. + :param iface: The id or MAC of an interface. """ diff --git a/ironic/db/models.py b/ironic/db/models.py index 22083800..6bda973e 100644 --- a/ironic/db/models.py +++ b/ironic/db/models.py @@ -16,13 +16,14 @@ # License for the specific language governing permissions and limitations # under the License. """ -Model classes for use in the storage API. +Model classes for use above the storage layer. + +NOT YET IMPLEMENTED. """ class Model(object): - """Base class for storage API models. - """ + """Base class for API models.""" def __init__(self, **kwds): self.fields = list(kwds) @@ -60,7 +61,7 @@ class Node(Model): class Iface(Model): - """Representation of a NIC.""" + """Representation of a network interface.""" def __init__(self, mac, node_id, extra): Model.__init__(mac=mac, diff --git a/ironic/db/sqlalchemy/api.py b/ironic/db/sqlalchemy/api.py index c615b485..ca8ae943 100644 --- a/ironic/db/sqlalchemy/api.py +++ b/ironic/db/sqlalchemy/api.py @@ -26,8 +26,9 @@ from sqlalchemy.orm.exc import NoResultFound from sqlalchemy.orm.exc import MultipleResultsFound from ironic.common import exception +from ironic.common import utils from ironic.db import api -from ironic.db.sqlalchemy.models import Node, Iface +from ironic.db.sqlalchemy import models from ironic.openstack.common.db.sqlalchemy import session as db_session from ironic.openstack.common import log from ironic.openstack.common import uuidutils @@ -41,48 +42,12 @@ 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. @@ -94,6 +59,24 @@ def model_query(model, *args, **kwargs): return query +def add_uuid_filter(query, value): + if utils.is_int_like(value): + return query.filter_by(id=value) + elif uuidutils.is_uuid_like(value): + return query.filter_by(uuid=value) + else: + raise exception.InvalidUUID(uuid=value) + + +def add_mac_filter(query, value): + if utils.is_int_like(iface): + query.filter_by(id=iface) + elif utils.is_valid_mac(iface): + query.filter_by(address=iface) + else: + raise exception.InvalidMAC(mac=value) + + class Connection(api.Connection): """SqlAlchemy connection.""" @@ -101,95 +84,139 @@ class Connection(api.Connection): 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. + def reserve_node(self, node, values): + if values.get('instance_uuid', None) is None: + raise exception.IronicException(_("Instance UUID not specified")) - TBD - """ - pass + session = get_session() + with session.begin(): + query = model_query(models.Node, session=session) + query = add_uuid_filter(query, node) - def create_node(self, *args, **kwargs): - """Create a new node.""" - node = Node() + count = query.filter_by(instance_uuid=None).\ + update(values) + if count != 1: + raise exception.IronicException(_( + "Failed to associate instance %(i)s to node %(n)s.") % + {'i': values['instance_uuid'], 'n': node}) + ref = query.one() - def get_node_by_id(self, node_id): - """Return a node. + return ref - :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) + def create_node(self, values): + node = models.Node() + node.update(values) + node.save() + + def get_node(self, node): + query = model_query(models.Node) + query = add_uuid_filter(query, node) try: result = query.one() except NoResultFound: - raise - except MultipleResultsFound: - raise + raise exception.NodeNotFound(node=node) + return result + def get_node_by_instance(self, instance): + query = model_query(models.Node) + if uuidutils.is_uuid_like(instance): + query.filter_by(instance_uuid=instance) + else: + query.filter_by(instance_name=instance) - def get_node_by_instance_id(self, instance_id): - """Return a node. + try: + result = query.one() + except NoResultFound: + raise exception.NodeNotFound(node=node) - :param instance_id: The instance id or uuid of a node. - """ + return result + + def destroy_node(self, node): + session = get_session() + with session.begin(): + query = model_query(models.Node, session=session) + query = add_uuid_filter(query, node) + + count = query.delete() + if count != 1: + raise exception.NodeNotFound(node=node) + + def update_node(self, node, values): + session = get_session() + with session.begin(): + query = model_query(models.Node, session=session) + query = add_uuid_filter(query, node) + + count = query.update(values) + if count != 1: + raise exception.NodeNotFound(node=node) + ref = query.one() + return ref + + def get_iface(self, iface): + query = model_query(models.Iface) + query = add_mac_filter(query, iface) + + try: + result = query.one() + except NoResultFound: + raise exception.InterfaceNotFound(iface=iface) + + return result + + def get_iface_by_vif(self, vif): pass - def destroy_node(self, node_id): - """Destroy a node. + def get_iface_by_node(self, node): + session = get_session() - :param node_id: The id or uuid of a node. - """ - pass + if is_int_like(node): + query = session.query(models.Iface).\ + filter_by(node_id=node) + else: + query = session.query(models.Iface).\ + join(models.Node, + models.Iface.node_id == models.Node.id).\ + filter_by(models.Node.uuid == node) + result = query.all() - def update_node(self, node_id, *args, **kwargs): - """Update properties of a node. + return result - :param node_id: The id or uuid of a node. - TBD - """ - pass + def create_iface(self, values): + iface = models.Iface() + iface.update(values) + iface.save() - def get_iface(self, iface_id): - """Return an interface. + def update_iface(self, iface, values): + session = get_session() + with session.begin(): + query = model_query(models.Iface, session=session) + query = add_mac_filter(query, iface) + + count = query.update(values) + if count != 1: + raise exception.InterfaceNotFound(iface=iface) + ref = query.one() + return ref - :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 + def destroy_iface(self, iface): + session = get_session() + with session.begin(): + query = model_query(models.Iface, session=session) + query = add_mac_filter(query, iface) + + count = query.update(values) + if count != 1: + raise exception.NodeNotFound(node=node) + ref = query.one() + return ref diff --git a/ironic/db/sqlalchemy/migrate_repo/versions/001_init.py b/ironic/db/sqlalchemy/migrate_repo/versions/001_init.py index a0a79ca1..05258266 100644 --- a/ironic/db/sqlalchemy/migrate_repo/versions/001_init.py +++ b/ironic/db/sqlalchemy/migrate_repo/versions/001_init.py @@ -34,8 +34,12 @@ def upgrade(migrate_engine): nodes = Table('nodes', meta, Column('id', Integer, primary_key=True, nullable=False), - Column('uuid', String(length=26)), + Column('uuid', String(length=36)), Column('power_info', Text), + Column('cpu_arch', String(length=10)), + Column('cpu_num', Integer), + Column('memory', Integer), + Column('local_storage_max', Integer), Column('task_state', String(length=255)), Column('image_path', String(length=255), nullable=True), Column('instance_uuid', String(length=255), nullable=True), @@ -49,7 +53,7 @@ def upgrade(migrate_engine): ifaces = Table('ifaces', meta, Column('id', Integer, primary_key=True, nullable=False), - Column('uuid', String(length=26)), + Column('address', String(length=18)), Column('node_id', Integer, ForeignKey('nodes.id'), nullable=True), Column('extra', Text), @@ -69,13 +73,24 @@ def upgrade(migrate_engine): raise indexes = [ - Index('uuid', nodes.c.uuid), - Index('uuid', ifaces.c.uuid), + Index('node_cpu_mem_disk', nodes.c.cpu_num, + nodes.c.memory, nodes.c.local_storage_max), + Index('node_instance_uuid', nodes.c.instance_uuid), + ] + + uniques = [ + UniqueConstraint('uuid', table=nodes, + name='node_uuid_ux'), + UniqueConstraint('address', table=ifaces, + name='iface_address_ux'), ] if migrate_engine.name == 'mysql' or migrate_engine.name == 'postgresql': for index in indexes: index.create(migrate_engine) + for index in uniques: + index.create(migrate_engine) + def downgrade(migrate_engine): raise NotImplementedError('Downgrade from Folsom is unsupported.') diff --git a/ironic/db/sqlalchemy/models.py b/ironic/db/sqlalchemy/models.py index 547ddb5f..e0a24b48 100644 --- a/ironic/db/sqlalchemy/models.py +++ b/ironic/db/sqlalchemy/models.py @@ -58,11 +58,15 @@ class Node(Base): __tablename__ = 'nodes' id = Column(Integer, primary_key=True) - uuid = Column(String(36)) + uuid = Column(String(36), unique=True) power_info = Column(Text) + cpu_arch = Column(String(10)) + cpu_num = Column(Integer) + memory = Column(Integer) + local_storage_max = Column(Integer) task_state = Column(String(255)) image_path = Column(String(255), nullable=True) - instance_uuid = Column(String(36), nullable=True) + instance_uuid = Column(String(36), nullable=True, unique=True) instance_name = Column(String(255), nullable=True) extra = Column(Text) @@ -72,31 +76,6 @@ class Iface(Base): __tablename__ = 'ifaces' id = Column(Integer, primary_key=True) - mac = Column(String(255), unique=True) + address = Column(String(18), 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)