Browse Source

Merge "Nova object changes for forbidden aggregates request filter"

changes/93/671793/24
Zuul 1 week ago
parent
commit
0c2e77a983

+ 1
- 1
doc/source/cli/nova-status.rst View File

@@ -132,7 +132,7 @@ Upgrade
132 132
 
133 133
   **20.0.0 (Train)**
134 134
 
135
-  * Checks for the Placement API are modified to require version 1.31.
135
+  * Checks for the Placement API are modified to require version 1.32.
136 136
   * Checks to ensure block-storage (cinder) API version 3.44 is
137 137
     available in order to support multi-attach volumes.
138 138
     If ``[cinder]/auth_type`` is not configured this is a no-op check.

+ 4
- 3
nova/cmd/status.py View File

@@ -46,11 +46,12 @@ from nova.volume import cinder
46 46
 
47 47
 CONF = nova.conf.CONF
48 48
 
49
-# NOTE(tetsuro): 1.31 is required by nova-scheduler to use in_tree
50
-# queryparam to get allocation candidates.
49
+# NOTE(vrushali): 1.32 is required by nova-scheduler to use member_of
50
+# queryparam to prepare a list of forbidden aggregates that should be
51
+# ignored by placement service in the allocation candidates API.
51 52
 # NOTE: If you bump this version, remember to update the history
52 53
 # section in the nova-status man page (doc/source/cli/nova-status).
53
-MIN_PLACEMENT_MICROVERSION = "1.31"
54
+MIN_PLACEMENT_MICROVERSION = "1.32"
54 55
 
55 56
 # NOTE(mriedem): 3.44 is needed to work with volume attachment records which
56 57
 # are required for supporting multi-attach capable volumes.

+ 33
- 2
nova/objects/request_spec.py View File

@@ -879,7 +879,8 @@ class Destination(base.NovaObject):
879 879
     # Version 1.1: Add cell field
880 880
     # Version 1.2: Add aggregates field
881 881
     # Version 1.3: Add allow_cross_cell_move field.
882
-    VERSION = '1.3'
882
+    # Version 1.4: Add forbidden_aggregates field
883
+    VERSION = '1.4'
883 884
 
884 885
     fields = {
885 886
         'host': fields.StringField(),
@@ -897,11 +898,18 @@ class Destination(base.NovaObject):
897 898
         # scheduler by default selects hosts from the cell specified in the
898 899
         # cell field.
899 900
         'allow_cross_cell_move': fields.BooleanField(default=False),
901
+        # NOTE(vrushali): These are forbidden aggregates passed to placement as
902
+        # query params to the allocation candidates API.
903
+        'forbidden_aggregates': fields.SetOfStringsField(nullable=True,
904
+                                                         default=None),
900 905
     }
901 906
 
902 907
     def obj_make_compatible(self, primitive, target_version):
903 908
         super(Destination, self).obj_make_compatible(primitive, target_version)
904 909
         target_version = versionutils.convert_version_to_tuple(target_version)
910
+        if target_version < (1, 4):
911
+            if 'forbidden_aggregates' in primitive:
912
+                del primitive['forbidden_aggregates']
905 913
         if target_version < (1, 3) and 'allow_cross_cell_move' in primitive:
906 914
             del primitive['allow_cross_cell_move']
907 915
         if target_version < (1, 2):
@@ -936,6 +944,20 @@ class Destination(base.NovaObject):
936 944
             self.aggregates = []
937 945
         self.aggregates.append(','.join(aggregates))
938 946
 
947
+    def append_forbidden_aggregates(self, forbidden_aggregates):
948
+        """Add a set of aggregates to the forbidden aggregates.
949
+
950
+        This will take a set of forbidden aggregates that should be
951
+        ignored by the placement service.
952
+
953
+        :param forbidden_aggregates: A set of aggregates which should be
954
+                                    ignored by the placement service.
955
+
956
+        """
957
+        if self.forbidden_aggregates is None:
958
+            self.forbidden_aggregates = set([])
959
+        self.forbidden_aggregates |= forbidden_aggregates
960
+
939 961
 
940 962
 @base.NovaObjectRegistry.register
941 963
 class SchedulerRetries(base.NovaObject):
@@ -1013,7 +1035,8 @@ class RequestGroup(base.NovaObject):
1013 1035
     # Version 1.0: Initial version
1014 1036
     # Version 1.1: add requester_id and provider_uuids fields
1015 1037
     # Version 1.2: add in_tree field
1016
-    VERSION = '1.2'
1038
+    # Version 1.3: Add forbidden_aggregates field
1039
+    VERSION = '1.3'
1017 1040
 
1018 1041
     fields = {
1019 1042
         'use_same_provider': fields.BooleanField(default=True),
@@ -1027,6 +1050,11 @@ class RequestGroup(base.NovaObject):
1027 1050
         # member of the aggregate aggregate_UUID1 and member of the aggregate
1028 1051
         # aggregate_UUID2 or aggregate_UUID3 .
1029 1052
         'aggregates': fields.ListOfListsOfStringsField(default=[]),
1053
+        # The forbidden_aggregates field has a form of
1054
+        #     set(['aggregate_UUID1', 'aggregate_UUID12', 'aggregate_UUID3'])
1055
+        # meaning that the request should not be fulfilled from an RP
1056
+        # belonging to any of the aggregates in forbidden_aggregates field.
1057
+        'forbidden_aggregates': fields.SetOfStringsField(default=set()),
1030 1058
         # The entity the request is coming from (e.g. the Neutron port uuid)
1031 1059
         # which may not always be a UUID.
1032 1060
         'requester_id': fields.StringField(nullable=True, default=None),
@@ -1079,6 +1107,9 @@ class RequestGroup(base.NovaObject):
1079 1107
         super(RequestGroup, self).obj_make_compatible(
1080 1108
             primitive, target_version)
1081 1109
         target_version = versionutils.convert_version_to_tuple(target_version)
1110
+        if target_version < (1, 3):
1111
+            if 'forbidden_aggregates' in primitive:
1112
+                del primitive['forbidden_aggregates']
1082 1113
         if target_version < (1, 2):
1083 1114
             if 'in_tree' in primitive:
1084 1115
                 del primitive['in_tree']

+ 2
- 2
nova/scheduler/client/report.py View File

@@ -41,9 +41,9 @@ from nova import utils
41 41
 CONF = nova.conf.CONF
42 42
 LOG = logging.getLogger(__name__)
43 43
 WARN_EVERY = 10
44
+NEGATIVE_MEMBER_OF_VERSION = '1.32'
44 45
 RESHAPER_VERSION = '1.30'
45 46
 CONSUMER_GENERATION_VERSION = '1.28'
46
-INTREE_AC_VERSION = '1.31'
47 47
 ALLOW_RESERVED_EQUAL_TOTAL_INVENTORY_VERSION = '1.26'
48 48
 POST_RPS_RETURNS_PAYLOAD_API_VERSION = '1.20'
49 49
 AGGREGATE_GENERATION_VERSION = '1.19'
@@ -291,7 +291,7 @@ class SchedulerReportClient(object):
291 291
         """
292 292
         # Note that claim_resources() will use this version as well to
293 293
         # make allocations by `PUT /allocations/{consumer_uuid}`
294
-        version = INTREE_AC_VERSION
294
+        version = NEGATIVE_MEMBER_OF_VERSION
295 295
         qparams = resources.to_querystring()
296 296
         url = "/allocation_candidates?%s" % qparams
297 297
         resp = self.get(url, version=version,

+ 10
- 0
nova/scheduler/utils.py View File

@@ -304,6 +304,7 @@ class ResourceRequest(object):
304 304
             forbidden_traits = request_group.forbidden_traits
305 305
             aggregates = request_group.aggregates
306 306
             in_tree = request_group.in_tree
307
+            forbidden_aggregates = request_group.forbidden_aggregates
307 308
 
308 309
             resource_query = ",".join(
309 310
                 sorted("%s:%s" % (rc, amount)
@@ -327,6 +328,12 @@ class ResourceRequest(object):
327 328
                 qs_params.extend(sorted(aggs))
328 329
             if in_tree:
329 330
                 qs_params.append(('in_tree%s' % suffix, in_tree))
331
+            if forbidden_aggregates:
332
+                # member_ofN is a list of aggregate uuids. We need a
333
+                # tuple of ('member_ofN, '!in:uuid,uuid,...').
334
+                forbidden_aggs = '!in:' + ','.join(
335
+                    sorted(forbidden_aggregates))
336
+                qs_params.append(('member_of%s' % suffix, forbidden_aggs))
330 337
             return qs_params
331 338
 
332 339
         if self._limit is not None:
@@ -479,6 +486,9 @@ def resources_from_request_spec(ctxt, spec_obj, host_manager):
479 486
                 #     [['aggA', 'aggB'], ['aggC']]
480 487
                 grp.aggregates = [ored.split(',')
481 488
                                   for ored in destination.aggregates]
489
+            if destination.forbidden_aggregates:
490
+                grp = res_req.get_request_group(None)
491
+                grp.forbidden_aggregates |= destination.forbidden_aggregates
482 492
 
483 493
     if 'force_hosts' in spec_obj and spec_obj.force_hosts:
484 494
         # Prioritize the value from requested_destination just in case

+ 2
- 2
nova/tests/unit/objects/test_objects.py View File

@@ -1049,7 +1049,7 @@ object_data = {
1049 1049
     'CpuDiagnostics': '1.0-d256f2e442d1b837735fd17dfe8e3d47',
1050 1050
     'DNSDomain': '1.0-7b0b2dab778454b6a7b6c66afe163a1a',
1051 1051
     'DNSDomainList': '1.0-4ee0d9efdfd681fed822da88376e04d2',
1052
-    'Destination': '1.3-07240d223a95c8b9399f7af21091ccfd',
1052
+    'Destination': '1.4-3b440d29459e2c98987ad5b25ad1cb2c',
1053 1053
     'DeviceBus': '1.0-77509ea1ea0dd750d5864b9bd87d3f9d',
1054 1054
     'DeviceMetadata': '1.0-04eb8fd218a49cbc3b1e54b774d179f7',
1055 1055
     'Diagnostics': '1.0-38ad3e9b1a59306253fc03f97936db95',
@@ -1118,7 +1118,7 @@ object_data = {
1118 1118
     'PowerVMLiveMigrateData': '1.4-a745f4eda16b45e1bc5686a0c498f27e',
1119 1119
     'Quotas': '1.3-40fcefe522111dddd3e5e6155702cf4e',
1120 1120
     'QuotasNoOp': '1.3-347a039fc7cfee7b225b68b5181e0733',
1121
-    'RequestGroup': '1.2-b9f9db748fe8cde0573af69db771c5ce',
1121
+    'RequestGroup': '1.3-0458d350a8ec9d0673f9be5640a990ce',
1122 1122
     'RequestSpec': '1.12-25010470f219af9b6163f2a457a513f5',
1123 1123
     'S3ImageMapping': '1.0-7dd7366a890d82660ed121de9092276e',
1124 1124
     'SCSIDeviceBus': '1.0-61c1e89a00901069ab1cf2991681533b',

+ 68
- 1
nova/tests/unit/objects/test_request_spec.py View File

@@ -921,6 +921,25 @@ class _TestRequestSpecObject(object):
921 921
         req_obj.create()
922 922
         req_obj.save()
923 923
 
924
+    def test_destination_forbidden_aggregates_default(self):
925
+        destination = objects.Destination()
926
+        self.assertIsNone(destination.forbidden_aggregates)
927
+
928
+    def test_destination_append_forbidden_aggregates(self):
929
+        destination = objects.Destination()
930
+        destination.append_forbidden_aggregates(set(['foo', 'bar']))
931
+        self.assertEqual(
932
+            set(['foo', 'bar']), destination.forbidden_aggregates)
933
+        destination.append_forbidden_aggregates(set(['bar', 'baz']))
934
+        self.assertEqual(
935
+            set(['foo', 'bar', 'baz']), destination.forbidden_aggregates)
936
+
937
+    def test_destination_delete_forbidden_aggregates(self):
938
+        destination = objects.Destination()
939
+        destination.append_forbidden_aggregates(set(['foo']))
940
+        primitive = destination.obj_to_primitive(target_version='1.0')
941
+        self.assertNotIn('forbidden_aggregates', primitive['nova_object.data'])
942
+
924 943
 
925 944
 class TestRequestSpecObject(test_objects._LocalTest,
926 945
                             _TestRequestSpecObject):
@@ -997,11 +1016,23 @@ class TestRequestGroupObject(test.NoDBTestCase):
997 1016
     def test_compat_requester_and_provider(self):
998 1017
         req_obj = objects.RequestGroup(
999 1018
             requester_id=uuids.requester, provider_uuids=[uuids.rp1],
1000
-            required_traits=set(['CUSTOM_PHYSNET_2']))
1019
+            required_traits=set(['CUSTOM_PHYSNET_2']),
1020
+            forbidden_aggregates=set(['agg3', 'agg4']))
1001 1021
         versions = ovo_base.obj_tree_get_versions('RequestGroup')
1022
+        primitive = req_obj.obj_to_primitive(
1023
+            target_version='1.3',
1024
+            version_manifest=versions)['nova_object.data']
1025
+        self.assertIn('forbidden_aggregates', primitive)
1026
+        self.assertIn('in_tree', primitive)
1027
+        self.assertIn('requester_id', primitive)
1028
+        self.assertIn('provider_uuids', primitive)
1029
+        self.assertIn('required_traits', primitive)
1030
+        self.assertItemsEqual(
1031
+                primitive['forbidden_aggregates'], set(['agg3', 'agg4']))
1002 1032
         primitive = req_obj.obj_to_primitive(
1003 1033
             target_version='1.2',
1004 1034
             version_manifest=versions)['nova_object.data']
1035
+        self.assertNotIn('forbidden_aggregates', primitive)
1005 1036
         self.assertIn('in_tree', primitive)
1006 1037
         self.assertIn('requester_id', primitive)
1007 1038
         self.assertIn('provider_uuids', primitive)
@@ -1009,6 +1040,7 @@ class TestRequestGroupObject(test.NoDBTestCase):
1009 1040
         primitive = req_obj.obj_to_primitive(
1010 1041
             target_version='1.1',
1011 1042
             version_manifest=versions)['nova_object.data']
1043
+        self.assertNotIn('forbidden_aggregates', primitive)
1012 1044
         self.assertNotIn('in_tree', primitive)
1013 1045
         self.assertIn('requester_id', primitive)
1014 1046
         self.assertIn('provider_uuids', primitive)
@@ -1016,12 +1048,47 @@ class TestRequestGroupObject(test.NoDBTestCase):
1016 1048
         primitive = req_obj.obj_to_primitive(
1017 1049
             target_version='1.0',
1018 1050
             version_manifest=versions)['nova_object.data']
1051
+        self.assertNotIn('forbidden_aggregates', primitive)
1019 1052
         self.assertNotIn('in_tree', primitive)
1020 1053
         self.assertNotIn('requester_id', primitive)
1021 1054
         self.assertNotIn('provider_uuids', primitive)
1022 1055
         self.assertIn('required_traits', primitive)
1023 1056
 
1024 1057
 
1058
+class TestDestinationObject(test.NoDBTestCase):
1059
+    def setUp(self):
1060
+        super(TestDestinationObject, self).setUp()
1061
+        self.user_id = uuids.user_id
1062
+        self.project_id = uuids.project_id
1063
+        self.context = context.RequestContext(uuids.user_id, uuids.project_id)
1064
+
1065
+    def test_obj_make_compatible_destination(self):
1066
+        values = {
1067
+            'host': 'fake_host',
1068
+            'node': 'fake_node',
1069
+            'aggregates': ['agg1', 'agg2'],
1070
+            'forbidden_aggregates': set(['agg3', 'agg4'])}
1071
+        obj = objects.Destination(self.context, **values)
1072
+        data = lambda x: x['nova_object.data']
1073
+        obj_primitive = data(obj.obj_to_primitive(target_version='1.3'))
1074
+        self.assertNotIn('forbidden_aggregates', obj_primitive)
1075
+        self.assertIn('aggregates', obj_primitive)
1076
+
1077
+    def test_obj_make_compatible_destination_with_forbidden_aggregates(self):
1078
+        values = {
1079
+            'host': 'fake_host',
1080
+            'node': 'fake_node',
1081
+            'aggregates': ['agg1', 'agg2'],
1082
+            'forbidden_aggregates': set(['agg3', 'agg4'])}
1083
+        obj = objects.Destination(self.context, **values)
1084
+        data = lambda x: x['nova_object.data']
1085
+        obj_primitive = data(obj.obj_to_primitive(target_version='1.4'))
1086
+        self.assertIn('forbidden_aggregates', obj_primitive)
1087
+        self.assertItemsEqual(obj_primitive['forbidden_aggregates'],
1088
+                              set(['agg3', 'agg4']))
1089
+        self.assertIn('aggregates', obj_primitive)
1090
+
1091
+
1025 1092
 class TestMappingRequestGroupsToProviders(test.NoDBTestCase):
1026 1093
     def setUp(self):
1027 1094
         super(TestMappingRequestGroupsToProviders, self).setUp()

+ 6
- 3
nova/tests/unit/scheduler/client/test_report.py View File

@@ -2085,10 +2085,13 @@ class TestProviderOperations(SchedulerReportClientTestCase):
2085 2085
         resources = scheduler_utils.ResourceRequest(req_spec)
2086 2086
         resources.get_request_group(None).aggregates = [
2087 2087
             ['agg1', 'agg2', 'agg3'], ['agg1', 'agg2']]
2088
+        forbidden_aggs = set(['agg1', 'agg5', 'agg6'])
2089
+        resources.get_request_group(None).forbidden_aggregates = forbidden_aggs
2088 2090
         expected_path = '/allocation_candidates'
2089 2091
         expected_query = [
2090 2092
             ('group_policy', 'isolate'),
2091 2093
             ('limit', '1000'),
2094
+            ('member_of', '!in:agg1,agg5,agg6'),
2092 2095
             ('member_of', 'in:agg1,agg2'),
2093 2096
             ('member_of', 'in:agg1,agg2,agg3'),
2094 2097
             ('required', 'CUSTOM_TRAIT1,HW_CPU_X86_AVX,!CUSTOM_TRAIT3,'
@@ -2115,7 +2118,7 @@ class TestProviderOperations(SchedulerReportClientTestCase):
2115 2118
         expected_url = '/allocation_candidates?%s' % parse.urlencode(
2116 2119
             expected_query)
2117 2120
         self.ks_adap_mock.get.assert_called_once_with(
2118
-            expected_url, microversion='1.31',
2121
+            expected_url, microversion='1.32',
2119 2122
             global_request_id=self.context.global_id)
2120 2123
         self.assertEqual(mock.sentinel.alloc_reqs, alloc_reqs)
2121 2124
         self.assertEqual(mock.sentinel.p_sums, p_sums)
@@ -2159,7 +2162,7 @@ class TestProviderOperations(SchedulerReportClientTestCase):
2159 2162
             expected_query)
2160 2163
         self.assertEqual(mock.sentinel.alloc_reqs, alloc_reqs)
2161 2164
         self.ks_adap_mock.get.assert_called_once_with(
2162
-            expected_url, microversion='1.31',
2165
+            expected_url, microversion='1.32',
2163 2166
             global_request_id=self.context.global_id)
2164 2167
         self.assertEqual(mock.sentinel.p_sums, p_sums)
2165 2168
 
@@ -2185,7 +2188,7 @@ class TestProviderOperations(SchedulerReportClientTestCase):
2185 2188
         res = self.client.get_allocation_candidates(self.context, resources)
2186 2189
 
2187 2190
         self.ks_adap_mock.get.assert_called_once_with(
2188
-            mock.ANY, microversion='1.31',
2191
+            mock.ANY, microversion='1.32',
2189 2192
             global_request_id=self.context.global_id)
2190 2193
         url = self.ks_adap_mock.get.call_args[0][0]
2191 2194
         split_url = parse.urlsplit(url)

+ 43
- 0
nova/tests/unit/scheduler/test_utils.py View File

@@ -410,6 +410,49 @@ class TestUtils(TestUtilsBase):
410 410
                 self.context, reqspec, self.mock_host_manager)
411 411
         self.assertEqual([], req.get_request_group(None).aggregates)
412 412
 
413
+    def test_resources_from_request_spec_forbidden_aggregates(self):
414
+        flavor = objects.Flavor(vcpus=1, memory_mb=1024,
415
+                                root_gb=1, ephemeral_gb=0,
416
+                                swap=0)
417
+        reqspec = objects.RequestSpec(
418
+            flavor=flavor,
419
+            requested_destination=objects.Destination(
420
+                forbidden_aggregates=set(['foo', 'bar'])))
421
+
422
+        req = utils.resources_from_request_spec(self.context, reqspec,
423
+                                                self.mock_host_manager)
424
+        self.assertEqual(set(['foo', 'bar']),
425
+                         req.get_request_group(None).forbidden_aggregates)
426
+
427
+    def test_resources_from_request_spec_no_forbidden_aggregates(self):
428
+        flavor = objects.Flavor(vcpus=1, memory_mb=1024,
429
+                                root_gb=1, ephemeral_gb=0,
430
+                                swap=0)
431
+        reqspec = objects.RequestSpec(flavor=flavor)
432
+
433
+        req = utils.resources_from_request_spec(
434
+                self.context, reqspec, self.mock_host_manager)
435
+        self.assertEqual(set([]), req.get_request_group(None).
436
+                         forbidden_aggregates)
437
+
438
+        reqspec.requested_destination = None
439
+        req = utils.resources_from_request_spec(
440
+                self.context, reqspec, self.mock_host_manager)
441
+        self.assertEqual(set([]), req.get_request_group(None).
442
+                         forbidden_aggregates)
443
+
444
+        reqspec.requested_destination = objects.Destination()
445
+        req = utils.resources_from_request_spec(
446
+                self.context, reqspec, self.mock_host_manager)
447
+        self.assertEqual(set([]), req.get_request_group(None).
448
+                         forbidden_aggregates)
449
+
450
+        reqspec.requested_destination.forbidden_aggregates = None
451
+        req = utils.resources_from_request_spec(
452
+                self.context, reqspec, self.mock_host_manager)
453
+        self.assertEqual(set([]), req.get_request_group(None).
454
+                         forbidden_aggregates)
455
+
413 456
     def test_process_extra_specs_granular_called(self):
414 457
         flavor = objects.Flavor(vcpus=1,
415 458
                                 memory_mb=1024,

Loading…
Cancel
Save