summaryrefslogtreecommitdiff
path: root/ironic/db
diff options
context:
space:
mode:
authorDmitry Tantsur <divius.inside@gmail.com>2018-12-10 16:50:42 +0100
committerDmitry Tantsur <divius.inside@gmail.com>2019-01-07 12:51:10 +0100
commita4717d9958c5b434ab01afaca095e018db5aaa7d (patch)
treed6c1d4bb5019549570c344d787e2083ba74f9a44 /ironic/db
parentc10ee94b92174ce78073afc2eacb300fa649736f (diff)
downloadironic-a4717d9958c5b434ab01afaca095e018db5aaa7d.tar.gz
Allocation API: database and RPC
This change adds the database models and API, as well as RPC objects for the allocation API. Also the node database API is extended with query by power state and list of UUIDs. There is one discrepancy from the initially approved spec: since we do not have to separately update traits in an allocation, the planned allocation_traits table was replaced by a simple field. Change-Id: I6af132e2bfa6e4f7b93bd20f22a668790a22a30e Story: #2004341 Task: #28367
Diffstat (limited to 'ironic/db')
-rw-r--r--ironic/db/api.py79
-rw-r--r--ironic/db/sqlalchemy/alembic/versions/dd67b91a1981_add_allocations_table.py56
-rw-r--r--ironic/db/sqlalchemy/api.py207
-rw-r--r--ironic/db/sqlalchemy/models.py26
4 files changed, 367 insertions, 1 deletions
diff --git a/ironic/db/api.py b/ironic/db/api.py
index c98880c7f..e43dc55da 100644
--- a/ironic/db/api.py
+++ b/ironic/db/api.py
@@ -1079,3 +1079,82 @@ class Connection(object):
:returns: A list of BIOSSetting objects.
:raises: NodeNotFound if the node is not found.
"""
+
+ @abc.abstractmethod
+ def get_allocation_by_id(self, allocation_id):
+ """Return an allocation representation.
+
+ :param allocation_id: The id of an allocation.
+ :returns: An allocation.
+ :raises: AllocationNotFound
+ """
+
+ @abc.abstractmethod
+ def get_allocation_by_uuid(self, allocation_uuid):
+ """Return an allocation representation.
+
+ :param allocation_uuid: The uuid of an allocation.
+ :returns: An allocation.
+ :raises: AllocationNotFound
+ """
+
+ @abc.abstractmethod
+ def get_allocation_by_name(self, name):
+ """Return an allocation representation.
+
+ :param name: The logical name of an allocation.
+ :returns: An allocation.
+ :raises: AllocationNotFound
+ """
+
+ @abc.abstractmethod
+ def get_allocation_list(self, filters=None, limit=None, marker=None,
+ sort_key=None, sort_dir=None):
+ """Return a list of allocations.
+
+ :param filters: Filters to apply. Defaults to None.
+
+ :node_uuid: uuid of node
+ :state: allocation state
+ :resource_class: requested resource class
+ :param limit: Maximum number of allocations to return.
+ :param marker: The last item of the previous page; we return the next
+ result set.
+ :param sort_key: Attribute by which results should be sorted.
+ :param sort_dir: Direction in which results should be sorted.
+ (asc, desc)
+ :returns: A list of allocations.
+ """
+
+ @abc.abstractmethod
+ def create_allocation(self, values):
+ """Create a new allocation.
+
+ :param values: Dict of values to create an allocation with
+ :returns: An allocation
+ :raises: AllocationDuplicateName
+ :raises: AllocationAlreadyExists
+ """
+
+ @abc.abstractmethod
+ def update_allocation(self, allocation_id, values, update_node=True):
+ """Update properties of an allocation.
+
+ :param allocation_id: Allocation ID
+ :param values: Dict of values to update.
+ :param update_node: If True and node_id is updated, update the node
+ with instance_uuid and traits from the allocation
+ :returns: An allocation.
+ :raises: AllocationNotFound
+ :raises: AllocationDuplicateName
+ :raises: InstanceAssociated
+ :raises: NodeAssociated
+ """
+
+ @abc.abstractmethod
+ def destroy_allocation(self, allocation_id):
+ """Destroy an allocation.
+
+ :param allocation_id: Allocation ID
+ :raises: AllocationNotFound
+ """
diff --git a/ironic/db/sqlalchemy/alembic/versions/dd67b91a1981_add_allocations_table.py b/ironic/db/sqlalchemy/alembic/versions/dd67b91a1981_add_allocations_table.py
new file mode 100644
index 000000000..55560dc68
--- /dev/null
+++ b/ironic/db/sqlalchemy/alembic/versions/dd67b91a1981_add_allocations_table.py
@@ -0,0 +1,56 @@
+# 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.
+
+"""Add Allocations table
+
+Revision ID: dd67b91a1981
+Revises: f190f9d00a11
+Create Date: 2018-12-10 15:24:30.555995
+
+"""
+
+from alembic import op
+from oslo_db.sqlalchemy import types
+import sqlalchemy as sa
+
+# revision identifiers, used by Alembic.
+revision = 'dd67b91a1981'
+down_revision = 'f190f9d00a11'
+
+
+def upgrade():
+ op.create_table(
+ 'allocations',
+ sa.Column('created_at', sa.DateTime(), nullable=True),
+ sa.Column('updated_at', sa.DateTime(), nullable=True),
+ sa.Column('version', sa.String(length=15), nullable=True),
+ sa.Column('id', sa.Integer(), nullable=False),
+ sa.Column('uuid', sa.String(length=36), nullable=False),
+ sa.Column('name', sa.String(length=255), nullable=True),
+ sa.Column('node_id', sa.Integer(), nullable=True),
+ sa.Column('state', sa.String(length=15), nullable=False),
+ sa.Column('last_error', sa.Text(), nullable=True),
+ sa.Column('resource_class', sa.String(length=80), nullable=True),
+ sa.Column('traits', types.JsonEncodedList(), nullable=True),
+ sa.Column('candidate_nodes', types.JsonEncodedList(), nullable=True),
+ sa.Column('extra', types.JsonEncodedDict(), nullable=True),
+ sa.Column('conductor_affinity', sa.Integer(), nullable=True),
+ sa.ForeignKeyConstraint(['conductor_affinity'], ['conductors.id'], ),
+ sa.ForeignKeyConstraint(['node_id'], ['nodes.id'], ),
+ sa.PrimaryKeyConstraint('id'),
+ sa.UniqueConstraint('name', name='uniq_allocations0name'),
+ sa.UniqueConstraint('uuid', name='uniq_allocations0uuid')
+ )
+ op.add_column('nodes', sa.Column('allocation_id', sa.Integer(),
+ nullable=True))
+ op.create_foreign_key(None, 'nodes', 'allocations',
+ ['allocation_id'], ['id'])
diff --git a/ironic/db/sqlalchemy/api.py b/ironic/db/sqlalchemy/api.py
index 2c096f002..beaca9339 100644
--- a/ironic/db/sqlalchemy/api.py
+++ b/ironic/db/sqlalchemy/api.py
@@ -224,7 +224,8 @@ class Connection(api.Connection):
'chassis_uuid', 'associated', 'reserved',
'reserved_by_any_of', 'provisioned_before',
'inspection_started_before', 'fault',
- 'conductor_group', 'owner'}
+ 'conductor_group', 'owner',
+ 'uuid_in', 'with_power_state'}
unsupported_filters = set(filters).difference(supported_filters)
if unsupported_filters:
msg = _("SqlAlchemy API does not support "
@@ -263,9 +264,38 @@ class Connection(api.Connection):
- (datetime.timedelta(
seconds=filters['inspection_started_before'])))
query = query.filter(models.Node.inspection_started_at < limit)
+ if 'uuid_in' in filters:
+ query = query.filter(models.Node.uuid.in_(filters['uuid_in']))
+ if 'with_power_state' in filters:
+ if filters['with_power_state']:
+ query = query.filter(models.Node.power_state != sql.null())
+ else:
+ query = query.filter(models.Node.power_state == sql.null())
return query
+ def _add_allocations_filters(self, query, filters):
+ if filters is None:
+ filters = dict()
+ supported_filters = {'state', 'resource_class', 'node_uuid'}
+ unsupported_filters = set(filters).difference(supported_filters)
+ if unsupported_filters:
+ msg = _("SqlAlchemy API does not support "
+ "filtering by %s") % ', '.join(unsupported_filters)
+ raise ValueError(msg)
+
+ try:
+ node_uuid = filters.pop('node_uuid')
+ except KeyError:
+ pass
+ else:
+ node_obj = self.get_node_by_uuid(node_uuid)
+ filters['node_id'] = node_obj.id
+
+ if filters:
+ query = query.filter_by(**filters)
+ return query
+
def get_nodeinfo_list(self, columns=None, filters=None, limit=None,
marker=None, sort_key=None, sort_dir=None):
# list-ify columns default values because it is bad form
@@ -452,6 +482,11 @@ class Connection(api.Connection):
models.BIOSSetting).filter_by(node_id=node_id)
bios_settings_query.delete()
+ # delete all allocations for this node
+ allocation_query = model_query(
+ models.Allocation).filter_by(node_id=node_id)
+ allocation_query.delete()
+
query.delete()
def update_node(self, node_id, values):
@@ -1482,3 +1517,173 @@ class Connection(api.Connection):
.filter_by(node_id=node_id)
.all())
return result
+
+ def get_allocation_by_id(self, allocation_id):
+ """Return an allocation representation.
+
+ :param allocation_id: The id of an allocation.
+ :returns: An allocation.
+ :raises: AllocationNotFound
+ """
+ query = model_query(models.Allocation).filter_by(id=allocation_id)
+ try:
+ return query.one()
+ except NoResultFound:
+ raise exception.AllocationNotFound(allocation=allocation_id)
+
+ def get_allocation_by_uuid(self, allocation_uuid):
+ """Return an allocation representation.
+
+ :param allocation_uuid: The uuid of an allocation.
+ :returns: An allocation.
+ :raises: AllocationNotFound
+ """
+ query = model_query(models.Allocation).filter_by(uuid=allocation_uuid)
+ try:
+ return query.one()
+ except NoResultFound:
+ raise exception.AllocationNotFound(allocation=allocation_uuid)
+
+ def get_allocation_by_name(self, name):
+ """Return an allocation representation.
+
+ :param name: The logical name of an allocation.
+ :returns: An allocation.
+ :raises: AllocationNotFound
+ """
+ query = model_query(models.Allocation).filter_by(name=name)
+ try:
+ return query.one()
+ except NoResultFound:
+ raise exception.AllocationNotFound(allocation=name)
+
+ def get_allocation_list(self, filters=None, limit=None, marker=None,
+ sort_key=None, sort_dir=None):
+ """Return a list of allocations.
+
+ :param filters: Filters to apply. Defaults to None.
+
+ :node_uuid: uuid of node
+ :state: allocation state
+ :resource_class: requested resource class
+ :param limit: Maximum number of allocations to return.
+ :param marker: The last item of the previous page; we return the next
+ result set.
+ :param sort_key: Attribute by which results should be sorted.
+ :param sort_dir: Direction in which results should be sorted.
+ (asc, desc)
+ :returns: A list of allocations.
+ """
+ query = self._add_allocations_filters(model_query(models.Allocation),
+ filters)
+ return _paginate_query(models.Allocation, limit, marker,
+ sort_key, sort_dir, query)
+
+ @oslo_db_api.retry_on_deadlock
+ def create_allocation(self, values):
+ """Create a new allocation.
+
+ :param values: Dict of values to create an allocation with
+ :returns: An allocation
+ :raises: AllocationDuplicateName
+ :raises: AllocationAlreadyExists
+ """
+ if not values.get('uuid'):
+ values['uuid'] = uuidutils.generate_uuid()
+
+ allocation = models.Allocation()
+ allocation.update(values)
+ with _session_for_write() as session:
+ try:
+ session.add(allocation)
+ session.flush()
+ except db_exc.DBDuplicateEntry as exc:
+ if 'name' in exc.columns:
+ raise exception.AllocationDuplicateName(
+ name=values['name'])
+ else:
+ raise exception.AllocationAlreadyExists(
+ uuid=values['uuid'])
+ return allocation
+
+ @oslo_db_api.retry_on_deadlock
+ def update_allocation(self, allocation_id, values, update_node=True):
+ """Update properties of an allocation.
+
+ :param allocation_id: Allocation ID
+ :param values: Dict of values to update.
+ :param update_node: If True and node_id is updated, update the node
+ with instance_uuid and traits from the allocation
+ :returns: An allocation.
+ :raises: AllocationNotFound
+ :raises: AllocationDuplicateName
+ :raises: InstanceAssociated
+ :raises: NodeAssociated
+ """
+ if 'uuid' in values:
+ msg = _("Cannot overwrite UUID for an existing allocation.")
+ raise exception.InvalidParameterValue(err=msg)
+
+ # These values are used in exception handling. They should always be
+ # initialized, but set them to None just in case.
+ instance_uuid = node_uuid = None
+
+ with _session_for_write() as session:
+ try:
+ query = model_query(models.Allocation, session=session)
+ query = add_identity_filter(query, allocation_id)
+ ref = query.one()
+ ref.update(values)
+ instance_uuid = ref.uuid
+
+ if 'node_id' in values and update_node:
+ node = model_query(models.Node, session=session).filter_by(
+ id=ref.node_id).with_lockmode('update').one()
+ node_uuid = node.uuid
+ if node.instance_uuid and node.instance_uuid != ref.uuid:
+ raise exception.NodeAssociated(
+ node=node.uuid, instance=node.instance_uuid)
+ iinfo = node.instance_info.copy()
+ iinfo['traits'] = ref.traits or []
+ node.update({'allocation_id': ref.id,
+ 'instance_uuid': instance_uuid,
+ 'instance_info': iinfo})
+ session.flush()
+ except NoResultFound:
+ raise exception.AllocationNotFound(allocation=allocation_id)
+ except db_exc.DBDuplicateEntry as exc:
+ if 'name' in exc.columns:
+ raise exception.AllocationDuplicateName(
+ name=values['name'])
+ elif 'instance_uuid' in exc.columns:
+ # Case when the referenced node is associated with an
+ # instance already.
+ raise exception.InstanceAssociated(
+ instance_uuid=instance_uuid, node=node_uuid)
+ else:
+ raise
+ return ref
+
+ @oslo_db_api.retry_on_deadlock
+ def destroy_allocation(self, allocation_id):
+ """Destroy an allocation.
+
+ :param allocation_id: Allocation ID or UUID
+ :raises: AllocationNotFound
+ """
+ with _session_for_write() as session:
+ query = model_query(models.Allocation)
+ query = add_identity_filter(query, allocation_id)
+
+ try:
+ ref = query.one()
+ except NoResultFound:
+ raise exception.AllocationNotFound(allocation=allocation_id)
+
+ allocation_id = ref['id']
+
+ node_query = model_query(models.Node, session=session).filter_by(
+ allocation_id=allocation_id)
+ node_query.update({'allocation_id': None, 'instance_uuid': None})
+
+ query.delete()
diff --git a/ironic/db/sqlalchemy/models.py b/ironic/db/sqlalchemy/models.py
index 2a17dfa65..db76a9dbd 100644
--- a/ironic/db/sqlalchemy/models.py
+++ b/ironic/db/sqlalchemy/models.py
@@ -180,6 +180,9 @@ class Node(Base):
server_default=false())
protected_reason = Column(Text, nullable=True)
owner = Column(String(255), nullable=True)
+ allocation_id = Column(Integer, ForeignKey('allocations.id'),
+ nullable=True)
+
bios_interface = Column(String(255), nullable=True)
boot_interface = Column(String(255), nullable=True)
console_interface = Column(String(255), nullable=True)
@@ -322,6 +325,29 @@ class BIOSSetting(Base):
value = Column(Text, nullable=True)
+class Allocation(Base):
+ """Represents an allocation of a node for deployment."""
+
+ __tablename__ = 'allocations'
+ __table_args__ = (
+ schema.UniqueConstraint('name', name='uniq_allocations0name'),
+ schema.UniqueConstraint('uuid', name='uniq_allocations0uuid'),
+ table_args())
+ id = Column(Integer, primary_key=True)
+ uuid = Column(String(36), nullable=False)
+ name = Column(String(255), nullable=True)
+ node_id = Column(Integer, ForeignKey('nodes.id'), nullable=True)
+ state = Column(String(15), nullable=False)
+ last_error = Column(Text, nullable=True)
+ resource_class = Column(String(80), nullable=True)
+ traits = Column(db_types.JsonEncodedList)
+ candidate_nodes = Column(db_types.JsonEncodedList)
+ extra = Column(db_types.JsonEncodedDict)
+ # The last conductor to handle this allocation (internal field).
+ conductor_affinity = Column(Integer, ForeignKey('conductors.id'),
+ nullable=True)
+
+
def get_class(model_name):
"""Returns the model class with the specified name.