summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDmitry Tantsur <dtantsur@protonmail.com>2020-01-22 12:43:08 +0100
committerDmitry Tantsur <dtantsur@protonmail.com>2020-04-30 12:09:20 +0200
commit9b4a63aa62c0dd7dcd582e20277c5eddb58c16b9 (patch)
tree99e057f0f24adcb9416a040c643c427368ee78a0
parent39bcb00f3cc624e8e215baa1b7e26eb7b2af5710 (diff)
downloadironic-9b4a63aa62c0dd7dcd582e20277c5eddb58c16b9.tar.gz
Add RPC objects for deployment API
This changes adds a virtual Deployment object that is backed by a corresponding Node. This change skips two fields: * boot_iso_ref (requires updating drivers) * owner (requires careful thoughts, may be not needed) Story: #2006910 Task: #38361 Change-Id: I0cacdfaea5d3ba58a0a97cbb5231c224a8f9428e
-rw-r--r--ironic/common/release_mappings.py1
-rw-r--r--ironic/objects/__init__.py1
-rw-r--r--ironic/objects/deployment.py259
-rw-r--r--ironic/tests/unit/common/test_release_mappings.py2
-rw-r--r--ironic/tests/unit/objects/test_deployment.py117
-rw-r--r--ironic/tests/unit/objects/test_objects.py1
6 files changed, 381 insertions, 0 deletions
diff --git a/ironic/common/release_mappings.py b/ironic/common/release_mappings.py
index cbd7b7f0f..bdbb98384 100644
--- a/ironic/common/release_mappings.py
+++ b/ironic/common/release_mappings.py
@@ -238,6 +238,7 @@ RELEASE_MAPPING = {
'Node': ['1.34'],
'Conductor': ['1.3'],
'Chassis': ['1.3'],
+ 'Deployment': ['1.0'],
'DeployTemplate': ['1.1'],
'Port': ['1.9'],
'Portgroup': ['1.4'],
diff --git a/ironic/objects/__init__.py b/ironic/objects/__init__.py
index 2afe75003..7f199c6aa 100644
--- a/ironic/objects/__init__.py
+++ b/ironic/objects/__init__.py
@@ -29,6 +29,7 @@ def register_all():
__import__('ironic.objects.chassis')
__import__('ironic.objects.conductor')
__import__('ironic.objects.deploy_template')
+ __import__('ironic.objects.deployment')
__import__('ironic.objects.node')
__import__('ironic.objects.port')
__import__('ironic.objects.portgroup')
diff --git a/ironic/objects/deployment.py b/ironic/objects/deployment.py
new file mode 100644
index 000000000..7fe7f7544
--- /dev/null
+++ b/ironic/objects/deployment.py
@@ -0,0 +1,259 @@
+# 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 oslo_utils import uuidutils
+from oslo_versionedobjects import base as object_base
+
+from ironic.common import exception
+from ironic.db import api as dbapi
+from ironic.objects import base
+from ironic.objects import fields as object_fields
+from ironic.objects import node as node_obj
+
+
+@base.IronicObjectRegistry.register
+class Deployment(base.IronicObject, object_base.VersionedObjectDictCompat):
+ # Version 1.0: Initial version
+ VERSION = '1.0'
+
+ dbapi = dbapi.get_instance()
+
+ fields = {
+ 'uuid': object_fields.UUIDField(nullable=True),
+ 'node_uuid': object_fields.UUIDField(nullable=True),
+ 'image_checksum': object_fields.StringField(nullable=True),
+ 'image_ref': object_fields.StringField(nullable=True),
+ 'kernel_ref': object_fields.StringField(nullable=True),
+ 'ramdisk_ref': object_fields.StringField(nullable=True),
+ 'root_device': object_fields.FlexibleDictField(nullable=True),
+ 'root_gib': object_fields.IntegerField(nullable=True),
+ 'state': object_fields.StringField(nullable=True),
+ 'swap_mib': object_fields.IntegerField(nullable=True),
+ }
+
+ node_mapping = {
+ 'instance_uuid': 'uuid',
+ 'provision_state': 'state',
+ 'uuid': 'node_uuid',
+ }
+
+ instance_info_mapping = {
+ 'image_checksum': 'image_checksum',
+ 'image_source': 'image_ref',
+ 'kernel': 'kernel_ref',
+ 'ramdisk': 'ramdisk_ref',
+ 'root_device': 'root_device',
+ 'root_gb': 'root_gib',
+ 'swap_mb': 'swap_mib',
+ }
+
+ instance_info_mapping_rev = {v: k
+ for k, v in instance_info_mapping.items()}
+
+ assert (set(node_mapping.values()) | set(instance_info_mapping.values())
+ == set(fields))
+
+ def _convert_to_version(self, target_version,
+ remove_unavailable_fields=True):
+ """Convert to the target version.
+
+ Convert the object to the target version. The target version may be
+ the same, older, or newer than the version of the object. This is
+ used for DB interactions as well as for serialization/deserialization.
+
+ :param target_version: the desired version of the object
+ :param remove_unavailable_fields: True to remove fields that are
+ unavailable in the target version; set this to True when
+ (de)serializing. False to set the unavailable fields to appropriate
+ values; set this to False for DB interactions.
+ """
+
+ @classmethod
+ def _from_node_object(cls, context, node):
+ """Convert a node into a virtual `Deployment` object."""
+ result = cls(context)
+ result._update_from_node_object(node)
+ return result
+
+ def _update_from_node_object(self, node):
+ """Update the Deployment object from the node."""
+ for src, dest in self.node_mapping.items():
+ setattr(self, dest, getattr(node, src, None))
+ for src, dest in self.instance_info_mapping.items():
+ setattr(self, dest, node.instance_info.get(src))
+
+ def _update_node_object(self, node):
+ """Update the given node object with the changes here."""
+ changes = self.obj_get_changes()
+ try:
+ new_instance_uuid = changes.pop('uuid')
+ except KeyError:
+ pass
+ else:
+ node.instance_uuid = new_instance_uuid
+
+ changes.pop('node_uuid', None)
+ instance_info = node.instance_info
+
+ for field, value in changes.items():
+ # NOTE(dtantsur): only instance_info fields can be updated here.
+ try:
+ dest = self.instance_info_mapping_rev[field]
+ except KeyError:
+ # NOTE(dtantsur): this should not happen because of API-level
+ # validations, but checking just in case.
+ raise exception.BadRequest('Field %s cannot be set or updated'
+ % changes)
+ instance_info[dest] = value
+
+ node.instance_info = instance_info
+ return node
+
+ # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable
+ # methods can be used in the future to replace current explicit RPC calls.
+ # Implications of calling new remote procedures should be thought through.
+ # @object_base.remotable_classmethod
+ @classmethod
+ def get_by_uuid(cls, context, uuid):
+ """Find a deployment by its UUID.
+
+ :param cls: the :class:`Deployment`
+ :param context: Security context
+ :param uuid: The UUID of a deployment.
+ :returns: An :class:`Deployment` object.
+ :raises: InstanceNotFound
+
+ """
+ node = node_obj.Node.get_by_instance_uuid(context, uuid)
+ return cls._from_node_object(context, node)
+
+ # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable
+ # methods can be used in the future to replace current explicit RPC calls.
+ # Implications of calling new remote procedures should be thought through.
+ # @object_base.remotable_classmethod
+ @classmethod
+ def get_by_node_uuid(cls, context, node_uuid):
+ """Find a deployment based by its node's UUID.
+
+ :param cls: the :class:`Deployment`
+ :param context: Security context
+ :param node_uuid: The UUID of a corresponding node.
+ :returns: An :class:`Deployment` object.
+ :raises: NodeNotFound
+
+ """
+ node = node_obj.Node.get_by_uuid(context, node_uuid)
+ return cls._from_node_object(context, node)
+
+ # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable
+ # methods can be used in the future to replace current explicit RPC calls.
+ # Implications of calling new remote procedures should be thought through.
+ # @object_base.remotable_classmethod
+ @classmethod
+ def list(cls, context, filters=None, limit=None, marker=None,
+ sort_key=None, sort_dir=None):
+ """Return a list of Deployment objects.
+
+ :param cls: the :class:`Deployment`
+ :param context: Security context.
+ :param filters: Filters to apply.
+ :param limit: Maximum number of resources to return in a single result.
+ :param marker: Pagination marker for large data sets.
+ :param sort_key: Column to sort results by.
+ :param sort_dir: Direction to sort. "asc" or "desc".
+ :returns: A list of :class:`Deployment` object.
+ :raises: InvalidParameterValue
+
+ """
+ nodes = node_obj.Node.list(context, filters=filters, limit=limit,
+ marker=marker, sort_key=sort_key,
+ sort_dir=sort_dir)
+ return [cls._from_node_object(context, node) for node in nodes]
+
+ # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable
+ # methods can be used in the future to replace current explicit RPC calls.
+ # Implications of calling new remote procedures should be thought through.
+ # @object_base.remotable
+ def create(self, context=None, node=None):
+ """Create a Deployment.
+
+ Updates the corresponding node under the hood.
+
+ :param context: Security context. NOTE: This should only
+ be used internally by the indirection_api.
+ Unfortunately, RPC requires context as the first
+ argument, even though we don't use it.
+ A context should be set when instantiating the
+ object, e.g.: Deployment(context)
+ :param node: Node object for deployment.
+ :raises: InstanceAssociated, NodeAssociated, NodeNotFound
+
+ """
+ if node is None:
+ node = node_obj.Node.get_by_uuid(self._context, self.node_uuid)
+ elif 'node_uuid' in self and self.node_uuid:
+ # NOTE(dtantsur): this is only possible if a bug happens on
+ # a higher level.
+ assert self.node_uuid == node.uuid
+
+ if 'uuid' not in self or not self.uuid:
+ self.uuid = uuidutils.generate_uuid()
+ node.instance_uuid = self.uuid
+ self._update_node_object(node)
+ node.save()
+ self._update_from_node_object(node)
+ self.obj_reset_changes()
+
+ # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable
+ # methods can be used in the future to replace current explicit RPC calls.
+ # Implications of calling new remote procedures should be thought through.
+ # @object_base.remotable
+ def destroy(self, context=None, node=None):
+ """Delete the Deployment.
+
+ Updates the corresponding node under the hood.
+
+ :param context: Security context. NOTE: This should only
+ be used internally by the indirection_api.
+ Unfortunately, RPC requires context as the first
+ argument, even though we don't use it.
+ A context should be set when instantiating the
+ object, e.g.: Node(context)
+ :param node: Node object for deployment.
+ """
+ if node is None:
+ node = node_obj.Node.get_by_uuid(self._context, self.node_uuid)
+ else:
+ assert node.uuid == self.node_uuid
+ node.instance_uuid = None
+ node.instance_info = {}
+ node.save()
+ self._update_from_node_object(node)
+ self.obj_reset_changes()
+
+ # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable
+ # methods can be used in the future to replace current explicit RPC calls.
+ # Implications of calling new remote procedures should be thought through.
+ # @object_base.remotable
+ def refresh(self, context=None):
+ """Refresh the object by re-fetching from the DB.
+
+ :param context: Security context. NOTE: This should only
+ be used internally by the indirection_api.
+ Unfortunately, RPC requires context as the first
+ argument, even though we don't use it.
+ A context should be set when instantiating the
+ object, e.g.: Node(context)
+ """
+ current = self.get_by_uuid(self._context, self.uuid)
+ self.obj_refresh(current)
+ self.obj_reset_changes()
diff --git a/ironic/tests/unit/common/test_release_mappings.py b/ironic/tests/unit/common/test_release_mappings.py
index db3f18f85..defd04be2 100644
--- a/ironic/tests/unit/common/test_release_mappings.py
+++ b/ironic/tests/unit/common/test_release_mappings.py
@@ -91,6 +91,8 @@ class ReleaseMappingsTestCase(base.TestCase):
model_names -= exceptions
# NodeTrait maps to two objects
model_names |= set(['Trait', 'TraitList'])
+ # Deployment is purely virtual.
+ model_names.add('Deployment')
object_names = set(
release_mappings.RELEASE_MAPPING['master']['objects'])
self.assertEqual(model_names, object_names)
diff --git a/ironic/tests/unit/objects/test_deployment.py b/ironic/tests/unit/objects/test_deployment.py
new file mode 100644
index 000000000..cb62fad8e
--- /dev/null
+++ b/ironic/tests/unit/objects/test_deployment.py
@@ -0,0 +1,117 @@
+# 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 oslo_utils import uuidutils
+
+from ironic.common import exception
+from ironic import objects
+from ironic.tests.unit.db import base as db_base
+from ironic.tests.unit.objects import utils as obj_utils
+
+
+class TestDeploymentObject(db_base.DbTestCase, obj_utils.SchemasTestMixIn):
+
+ def setUp(self):
+ super(TestDeploymentObject, self).setUp()
+ self.uuid = uuidutils.generate_uuid()
+ self.instance_info = {
+ 'image_source': 'http://source',
+ 'kernel': 'http://kernel',
+ 'ramdisk': 'http://ramdisk',
+ 'image_checksum': '1234',
+ 'root_device': {'size': 42},
+ }
+ self.node = obj_utils.create_test_node(
+ self.context,
+ provision_state='active',
+ instance_uuid=self.uuid,
+ instance_info=self.instance_info)
+
+ def _check(self, do):
+ self.assertEqual(self.uuid, do.uuid)
+ self.assertEqual(self.node.uuid, do.node_uuid)
+ self.assertEqual(self.context, do._context)
+ self.assertEqual('http://source', do.image_ref)
+ self.assertEqual('http://kernel', do.kernel_ref)
+ self.assertEqual('http://ramdisk', do.ramdisk_ref)
+ self.assertEqual('1234', do.image_checksum)
+ self.assertEqual({'size': 42}, do.root_device)
+
+ def test_get_by_uuid(self):
+ do = objects.Deployment.get_by_uuid(self.context, self.uuid)
+ self._check(do)
+
+ def test_get_by_node_uuid(self):
+ do = objects.Deployment.get_by_node_uuid(self.context, self.node.uuid)
+ self._check(do)
+
+ def test_not_found(self):
+ self.assertRaises(exception.InstanceNotFound,
+ objects.Deployment.get_by_uuid,
+ self.context, uuidutils.generate_uuid())
+ self.assertRaises(exception.NodeNotFound,
+ objects.Deployment.get_by_node_uuid,
+ self.context, uuidutils.generate_uuid())
+
+ def test_create(self):
+ do = objects.Deployment(self.context)
+ do.node_uuid = self.node.uuid
+ do.image_ref = 'new-image'
+ do.create()
+ self.assertIsNotNone(do.uuid)
+
+ node = objects.Node.get_by_uuid(self.context, do.node_uuid)
+ self.assertEqual(do.uuid, node.instance_uuid)
+ self.assertEqual('new-image', node.instance_info['image_source'])
+ self.assertFalse(do.obj_what_changed())
+
+ def test_create_with_node(self):
+ do = objects.Deployment(self.context)
+ do.node_uuid = self.node.uuid
+ do.image_ref = 'new-image'
+ do.create(node=self.node)
+ self.assertIsNotNone(do.uuid)
+ self.assertEqual(do.uuid, self.node.instance_uuid)
+ self.assertEqual('new-image', self.node.instance_info['image_source'])
+ self.assertFalse(do.obj_what_changed())
+ self.assertFalse(self.node.obj_what_changed())
+
+ def test_destroy(self):
+ do = objects.Deployment(self.context)
+ do.node_uuid = self.node.uuid
+ do.image_ref = 'new-image'
+ do.create()
+ do.destroy()
+
+ node = objects.Node.get_by_uuid(self.context, do.node_uuid)
+ self.assertIsNone(node.instance_uuid)
+ self.assertEqual({}, node.instance_info)
+ self.assertFalse(do.obj_what_changed())
+
+ def test_destroy_with_node(self):
+ do = objects.Deployment(self.context)
+ do.node_uuid = self.node.uuid
+ do.image_ref = 'new-image'
+ do.create()
+ do.destroy(node=self.node)
+ self.assertIsNone(self.node.instance_uuid)
+ self.assertEqual({}, self.node.instance_info)
+ self.assertFalse(do.obj_what_changed())
+ self.assertFalse(self.node.obj_what_changed())
+
+ def test_refresh(self):
+ do = objects.Deployment.get_by_uuid(self.context, self.uuid)
+ do.node_uuid = None
+ do.image_source = 'updated'
+ do.refresh()
+ self._check(do)
+ self.assertFalse(do.obj_what_changed())
diff --git a/ironic/tests/unit/objects/test_objects.py b/ironic/tests/unit/objects/test_objects.py
index 621b7f939..601915d87 100644
--- a/ironic/tests/unit/objects/test_objects.py
+++ b/ironic/tests/unit/objects/test_objects.py
@@ -719,6 +719,7 @@ expected_object_fingerprints = {
'DeployTemplate': '1.1-4e30c8e9098595e359bb907f095bf1a9',
'DeployTemplateCRUDNotification': '1.0-59acc533c11d306f149846f922739c15',
'DeployTemplateCRUDPayload': '1.0-200857e7e715f58a5b6d6b700ab73a3b',
+ 'Deployment': '1.0-ff10ae028c5968f1596131d85d7f5f9d',
}