From 2358ebacd8dce5f891c48249bfff4bbd6ea93cb2 Mon Sep 17 00:00:00 2001 From: Sylvain Bauza Date: Wed, 4 May 2016 23:16:13 +0200 Subject: [PATCH] Add requested_destination field to RequestSpec As agreed in the spec, we want to provide to the scheduler the possible destination asked by the admin when calling evacuate or live-migrate. Adding a possibility to default the field to None if an old RequestSpec or a legacy dict wants to hydrate. Change-Id: I6ddcaaca37fc5387c2d2e9f51c67ea9e85acb5c5 Partially-Implements: blueprint check-destination-on-migrations-newton --- nova/objects/request_spec.py | 50 +++++++++++++++++++- nova/tests/unit/fake_request_spec.py | 1 + nova/tests/unit/objects/test_objects.py | 3 +- nova/tests/unit/objects/test_request_spec.py | 12 +++++ 4 files changed, 64 insertions(+), 2 deletions(-) diff --git a/nova/objects/request_spec.py b/nova/objects/request_spec.py index 8c5ffcd749cb..35d7862e843c 100644 --- a/nova/objects/request_spec.py +++ b/nova/objects/request_spec.py @@ -13,6 +13,7 @@ # under the License. from oslo_serialization import jsonutils +from oslo_utils import versionutils import six from nova.db.sqlalchemy import api as db @@ -25,6 +26,8 @@ from nova.objects import instance as obj_instance from nova.scheduler import utils as scheduler_utils from nova.virt import hardware +REQUEST_SPEC_OPTIONAL_ATTRS = ['requested_destination'] + @base.NovaObjectRegistry.register class RequestSpec(base.NovaObject): @@ -34,7 +37,8 @@ class RequestSpec(base.NovaObject): # Version 1.3: InstanceGroup version 1.10 # Version 1.4: ImageMeta version 1.7 # Version 1.5: Added get_by_instance_uuid(), create(), save() - VERSION = '1.5' + # Version 1.6: Added requested_destination + VERSION = '1.6' fields = { 'id': fields.IntegerField(), @@ -50,6 +54,9 @@ class RequestSpec(base.NovaObject): 'ignore_hosts': fields.ListOfStringsField(nullable=True), 'force_hosts': fields.ListOfStringsField(nullable=True), 'force_nodes': fields.ListOfStringsField(nullable=True), + 'requested_destination': fields.ObjectField('Destination', + nullable=True, + default=None), 'retry': fields.ObjectField('SchedulerRetries', nullable=True), 'limits': fields.ObjectField('SchedulerLimits', nullable=True), 'instance_group': fields.ObjectField('InstanceGroup', nullable=True), @@ -60,6 +67,24 @@ class RequestSpec(base.NovaObject): 'instance_uuid': fields.UUIDField(), } + def obj_make_compatible(self, primitive, target_version): + super(RequestSpec, self).obj_make_compatible(primitive, target_version) + target_version = versionutils.convert_version_to_tuple(target_version) + if target_version < (1, 6): + if 'requested_destination' in primitive: + del primitive['requested_destination'] + + def obj_load_attr(self, attrname): + if attrname not in REQUEST_SPEC_OPTIONAL_ATTRS: + raise exception.ObjectActionError( + action='obj_load_attr', + reason='attribute %s not lazy-loadable' % attrname) + + # NOTE(sbauza): In case the primitive was not providing that field + # because of a previous RequestSpec version, we want to default + # that field in order to have the same behaviour. + self.obj_set_defaults(attrname) + @property def vcpus(self): return self.flavor.vcpus @@ -222,6 +247,11 @@ class RequestSpec(base.NovaObject): spec._populate_group_info(filter_properties) scheduler_hints = filter_properties.get('scheduler_hints', {}) spec._from_hints(scheduler_hints) + + # NOTE(sbauza): Default the other fields that are not part of the + # original contract + spec.obj_set_defaults() + return spec def get_scheduler_hint(self, hint_name, default=None): @@ -365,6 +395,10 @@ class RequestSpec(base.NovaObject): spec_obj._from_limits(filter_properties.get('limits', {})) spec_obj._from_hints(filter_properties.get('scheduler_hints', {})) spec_obj.availability_zone = availability_zone + + # NOTE(sbauza): Default the other fields that are not part of the + # original contract + spec_obj.obj_set_defaults() return spec_obj @staticmethod @@ -548,6 +582,20 @@ def migrate_instances_add_request_spec(context, max_count): return count_all, count_hit +@base.NovaObjectRegistry.register +class Destination(base.NovaObject): + # Version 1.0: Initial version + VERSION = '1.0' + + fields = { + 'host': fields.StringField(), + # NOTE(sbauza): Given we want to split the host/node relationship later + # and also remove the possibility to have multiple nodes per service, + # let's provide a possible nullable node here. + 'node': fields.StringField(nullable=True), + } + + @base.NovaObjectRegistry.register class SchedulerRetries(base.NovaObject): # Version 1.0: Initial version diff --git a/nova/tests/unit/fake_request_spec.py b/nova/tests/unit/fake_request_spec.py index 8d6e8d96a523..0f0a5e390922 100644 --- a/nova/tests/unit/fake_request_spec.py +++ b/nova/tests/unit/fake_request_spec.py @@ -85,6 +85,7 @@ def fake_spec_obj(remove_id=False): req_obj.force_hosts = ['host1', 'host3'] req_obj.force_nodes = ['node1', 'node2'] req_obj.scheduler_hints = {'hint': ['over-there']} + req_obj.requested_destination = None # This should never be a changed field req_obj.obj_reset_changes(['id']) return req_obj diff --git a/nova/tests/unit/objects/test_objects.py b/nova/tests/unit/objects/test_objects.py index 02fc6170cb74..99b5b41a706b 100644 --- a/nova/tests/unit/objects/test_objects.py +++ b/nova/tests/unit/objects/test_objects.py @@ -1112,6 +1112,7 @@ object_data = { 'ComputeNodeList': '1.14-3b6f4f5ade621c40e70cb116db237844', 'DNSDomain': '1.0-7b0b2dab778454b6a7b6c66afe163a1a', 'DNSDomainList': '1.0-4ee0d9efdfd681fed822da88376e04d2', + 'Destination': '1.0-4c59dd1288b2e7adbda6051a2de59183', 'DeviceMetadata': '1.0-04eb8fd218a49cbc3b1e54b774d179f7', 'DeviceMetadataList': '1.0-15ecf022a68ddbb8c2a6739cfc9f8f5e', 'DiskMetadata': '1.0-e7a0f1ccccf10d26a76b28e7492f3788', @@ -1178,7 +1179,7 @@ object_data = { 'PciDevicePoolList': '1.1-15ecf022a68ddbb8c2a6739cfc9f8f5e', 'Quotas': '1.2-1fe4cd50593aaf5d36a6dc5ab3f98fb3', 'QuotasNoOp': '1.2-e041ddeb7dc8188ca71706f78aad41c1', - 'RequestSpec': '1.5-576a249869c161e17b7cd6d55f9d85f3', + 'RequestSpec': '1.6-c1cb516acdf120d367a42d343ed695b5', 'ResourceProvider': '1.0-57a9a344b0faed9cf6d6811835b6deb6', 'S3ImageMapping': '1.0-7dd7366a890d82660ed121de9092276e', 'SchedulerLimits': '1.0-249c4bd8e62a9b327b7026b7f19cc641', diff --git a/nova/tests/unit/objects/test_request_spec.py b/nova/tests/unit/objects/test_request_spec.py index f053b2498203..22a8657b4078 100644 --- a/nova/tests/unit/objects/test_request_spec.py +++ b/nova/tests/unit/objects/test_request_spec.py @@ -15,6 +15,7 @@ import mock from oslo_serialization import jsonutils from oslo_utils import uuidutils +from oslo_versionedobjects import base as ovo_base from nova import context from nova import exception @@ -546,6 +547,17 @@ class _TestRequestSpecObject(object): self.assertIsNone(req_obj.force_nodes) mock_reset.assert_called_once_with(['force_hosts', 'force_nodes']) + def test_compat_requested_destination(self): + req_obj = objects.RequestSpec() + versions = ovo_base.obj_tree_get_versions('RequestSpec') + primitive = req_obj.obj_to_primitive(target_version='1.5', + version_manifest=versions) + self.assertNotIn('requested_destination', primitive) + + def test_default_requested_destination(self): + req_obj = objects.RequestSpec() + self.assertIsNone(req_obj.requested_destination) + class TestRequestSpecObject(test_objects._LocalTest, _TestRequestSpecObject):