diff --git a/api-ref/source/os-server-groups.inc b/api-ref/source/os-server-groups.inc index 295e11bf38a4..761d89da7272 100644 --- a/api-ref/source/os-server-groups.inc +++ b/api-ref/source/os-server-groups.inc @@ -38,13 +38,15 @@ Response - name: name_server_group - policies: policies - members: members - - metadata: metadata_object + - metadata: metadata_server_group_max_2_63 - project_id: project_id_server_group - user_id: user_id_server_group + - policy: policy_name + - rules: policy_rules **Example List Server Groups: JSON response** -.. literalinclude:: ../../doc/api_samples/os-server-groups/server-groups-list-resp.json +.. literalinclude:: ../../doc/api_samples/os-server-groups/v2.64/server-groups-list-resp.json :language: javascript Create Server Group @@ -56,7 +58,7 @@ Creates a server group. Normal response codes: 200 -Error response codes: badRequest(400), unauthorized(401), forbidden(403) +Error response codes: badRequest(400), unauthorized(401), forbidden(403), conflict(409) Request ------- @@ -66,10 +68,12 @@ Request - server_group: server_group - name: name_server_group - policies: policies + - policy: policy_name + - rules: policy_rules_optional **Example Create Server Group: JSON request** -.. literalinclude:: ../../doc/api_samples/os-server-groups/server-groups-post-req.json +.. literalinclude:: ../../doc/api_samples/os-server-groups/v2.64/server-groups-post-req.json :language: javascript Response @@ -82,13 +86,15 @@ Response - name: name_server_group - policies: policies - members: members - - metadata: metadata_object + - metadata: metadata_server_group_max_2_63 - project_id: project_id_server_group - user_id: user_id_server_group + - policy: policy_name + - rules: policy_rules **Example Create Server Group: JSON response** -.. literalinclude:: ../../doc/api_samples/os-server-groups/server-groups-post-resp.json +.. literalinclude:: ../../doc/api_samples/os-server-groups/v2.64/server-groups-post-resp.json :language: javascript Show Server Group Details @@ -119,13 +125,15 @@ Response - name: name_server_group - policies: policies - members: members - - metadata: metadata_object + - metadata: metadata_server_group_max_2_63 - project_id: project_id_server_group - user_id: user_id_server_group + - policy: policy_name + - rules: policy_rules **Example Show Server Group Details: JSON response** -.. literalinclude:: ../../doc/api_samples/os-server-groups/server-groups-get-resp.json +.. literalinclude:: ../../doc/api_samples/os-server-groups/v2.64/server-groups-get-resp.json :language: javascript Delete Server Group diff --git a/api-ref/source/parameters.yaml b/api-ref/source/parameters.yaml index f129505115c6..5a55eb2fa994 100644 --- a/api-ref/source/parameters.yaml +++ b/api-ref/source/parameters.yaml @@ -4299,6 +4299,14 @@ metadata_object: in: body required: true type: object +metadata_server_group_max_2_63: + description: | + Metadata key and value pairs. The maximum size for each metadata key and value + pair is 255 bytes. It's always empty and only used for keeping compatibility. + in: body + required: true + type: object + max_version: 2.63 migrate: description: | The action to cold migrate a server. @@ -5111,6 +5119,50 @@ policies: in: body required: true type: array + max_version: 2.63 +policy_name: + description: | + The ``policy`` field represents the name of the policy. The current + valid policy names are: + + - ``anti-affinity`` - servers in this group must be scheduled to + different hosts. + - ``affinity`` - servers in this group must be scheduled to the same host. + - ``soft-anti-affinity`` - servers in this group should be scheduled to + different hosts if possible, but if not possible then they should still + be scheduled instead of resulting in a build failure. + - ``soft-affinity`` - servers in this group should be scheduled to the same + host if possible, but if not possible then they should still be scheduled + instead of resulting in a build failure. + in: body + required: true + type: object + min_version: 2.64 +policy_rules: + description: | + The ``rules`` field, which is a dict, can be applied to the policy. + Currently, only the ``max_server_per_host`` rule is supported for the + ``anti-affinity`` policy. The ``max_server_per_host`` rule allows + specifying how many members of the anti-affinity group can reside on the + same compute host. If not specified, only one member from the same + anti-affinity group can reside on a given host. + in: body + required: true + type: object + min_version: 2.64 +policy_rules_optional: + description: | + The ``rules`` field, which is a dict, can be applied to the policy. + Currently, only the ``max_server_per_host`` rule is supported for the + ``anti-affinity`` policy. The ``max_server_per_host`` rule allows + specifying how many members of the anti-affinity group can reside on the + same compute host. If not specified, only one member from the same + anti-affinity group can reside on a given host. Requesting policy rules + with any other policy than ``anti-affinity`` will be 400. + in: body + required: false + type: object + min_version: 2.64 pool: description: | Pool from which to allocate the IP address. If you omit this parameter, the call diff --git a/doc/api_samples/os-server-groups/v2.64/server-groups-get-resp.json b/doc/api_samples/os-server-groups/v2.64/server-groups-get-resp.json new file mode 100644 index 000000000000..2dba808f39bc --- /dev/null +++ b/doc/api_samples/os-server-groups/v2.64/server-groups-get-resp.json @@ -0,0 +1,11 @@ +{ + "server_group": { + "id": "5bbcc3c4-1da2-4437-a48a-66f15b1b13f9", + "name": "test", + "policy": "anti-affinity", + "rules": {"max_server_per_host": 3}, + "members": [], + "project_id": "6f70656e737461636b20342065766572", + "user_id": "fake" + } +} diff --git a/doc/api_samples/os-server-groups/v2.64/server-groups-list-resp.json b/doc/api_samples/os-server-groups/v2.64/server-groups-list-resp.json new file mode 100644 index 000000000000..6e4ec47a43cf --- /dev/null +++ b/doc/api_samples/os-server-groups/v2.64/server-groups-list-resp.json @@ -0,0 +1,13 @@ +{ + "server_groups": [ + { + "id": "616fb98f-46ca-475e-917e-2563e5a8cd19", + "name": "test", + "policy": "anti-affinity", + "rules": {"max_server_per_host": 3}, + "members": [], + "project_id": "6f70656e737461636b20342065766572", + "user_id": "fake" + } + ] +} diff --git a/doc/api_samples/os-server-groups/v2.64/server-groups-post-req.json b/doc/api_samples/os-server-groups/v2.64/server-groups-post-req.json new file mode 100644 index 000000000000..a6defffdfb0c --- /dev/null +++ b/doc/api_samples/os-server-groups/v2.64/server-groups-post-req.json @@ -0,0 +1,7 @@ +{ + "server_group": { + "name": "test", + "policy": "anti-affinity", + "rules": {"max_server_per_host": 3} + } +} diff --git a/doc/api_samples/os-server-groups/v2.64/server-groups-post-resp.json b/doc/api_samples/os-server-groups/v2.64/server-groups-post-resp.json new file mode 100644 index 000000000000..2dba808f39bc --- /dev/null +++ b/doc/api_samples/os-server-groups/v2.64/server-groups-post-resp.json @@ -0,0 +1,11 @@ +{ + "server_group": { + "id": "5bbcc3c4-1da2-4437-a48a-66f15b1b13f9", + "name": "test", + "policy": "anti-affinity", + "rules": {"max_server_per_host": 3}, + "members": [], + "project_id": "6f70656e737461636b20342065766572", + "user_id": "fake" + } +} diff --git a/doc/api_samples/versions/v21-version-get-resp.json b/doc/api_samples/versions/v21-version-get-resp.json index 308f43aaf31b..c362dafd504e 100644 --- a/doc/api_samples/versions/v21-version-get-resp.json +++ b/doc/api_samples/versions/v21-version-get-resp.json @@ -19,7 +19,7 @@ } ], "status": "CURRENT", - "version": "2.63", + "version": "2.64", "min_version": "2.1", "updated": "2013-07-23T11:33:21Z" } diff --git a/doc/api_samples/versions/versions-get-resp.json b/doc/api_samples/versions/versions-get-resp.json index ec1ecbe1c59d..7e4cc0aab0c7 100644 --- a/doc/api_samples/versions/versions-get-resp.json +++ b/doc/api_samples/versions/versions-get-resp.json @@ -22,7 +22,7 @@ } ], "status": "CURRENT", - "version": "2.63", + "version": "2.64", "min_version": "2.1", "updated": "2013-07-23T11:33:21Z" } diff --git a/doc/notification_samples/common_payloads/ServerGroupPayload.json b/doc/notification_samples/common_payloads/ServerGroupPayload.json index 27053fcdbc81..bb544e4af1c0 100644 --- a/doc/notification_samples/common_payloads/ServerGroupPayload.json +++ b/doc/notification_samples/common_payloads/ServerGroupPayload.json @@ -11,7 +11,7 @@ "anti-affinity" ], "policy": "anti-affinity", - "rules": {}, + "rules": {"max_server_per_host": "3"}, "members": [], "hosts": null } diff --git a/nova/api/openstack/api_version_request.py b/nova/api/openstack/api_version_request.py index 8ffa6ec2fe5f..586395904ce2 100644 --- a/nova/api/openstack/api_version_request.py +++ b/nova/api/openstack/api_version_request.py @@ -150,6 +150,12 @@ REST_API_VERSION_HISTORY = """REST API Version History: responses. * 2.63 - Add support for applying trusted certificates when creating or rebuilding a server. + * 2.64 - Add support for the "max_server_per_host" policy rule for + ``anti-affinity`` server group policy, the ``policies`` and + ``metadata`` fields are removed and the ``policy`` (required) + and ``rules`` (optional) fields are added in response body of + GET, POST /os-server-groups APIs and GET + /os-server-groups/{group_id} API. """ # The minimum and maximum versions of the API supported @@ -158,7 +164,7 @@ REST_API_VERSION_HISTORY = """REST API Version History: # Note(cyeoh): This only applies for the v2.1 API once microversions # support is fully merged. It does not affect the V2 API. _MIN_API_VERSION = "2.1" -_MAX_API_VERSION = "2.63" +_MAX_API_VERSION = "2.64" DEFAULT_API_VERSION = _MIN_API_VERSION # Almost all proxy APIs which are related to network, images and baremetal diff --git a/nova/api/openstack/compute/rest_api_version_history.rst b/nova/api/openstack/compute/rest_api_version_history.rst index 629fcb891125..607dd55f740a 100644 --- a/nova/api/openstack/compute/rest_api_version_history.rst +++ b/nova/api/openstack/compute/rest_api_version_history.rst @@ -818,3 +818,21 @@ the following APIs: * ``GET /servers/{server_id}`` * ``PUT /servers/{server_id}`` * ``POST /servers/{server_id}/action (rebuild)`` + +2.64 +---- + +Enable users to define the policy rules on server group policy to meet more +advanced policy requirement. This microversion brings the following changes +in server group APIs: + +* Add ``policy`` and ``rules`` fields in the request of POST + ``/os-server-groups``. The ``policy`` represents the name of policy. The + ``rules`` field, which is a dict, can be applied to the policy, which + currently only support ``max_server_per_host`` for ``anti-affinity`` policy. +* The ``policy`` and ``rules`` fields will be returned in response + body of POST, GET ``/os-server-groups`` API and GET + ``/os-server-groups/{server_group_id}`` API. +* The ``policies`` and ``metadata`` fields have been removed from the response + body of POST, GET ``/os-server-groups`` API and GET + ``/os-server-groups/{server_group_id}`` API. \ No newline at end of file diff --git a/nova/api/openstack/compute/schemas/server_groups.py b/nova/api/openstack/compute/schemas/server_groups.py index c2771ba4d2ea..d8401e158349 100644 --- a/nova/api/openstack/compute/schemas/server_groups.py +++ b/nova/api/openstack/compute/schemas/server_groups.py @@ -51,6 +51,25 @@ create_v215 = copy.deepcopy(create) policies = create_v215['properties']['server_group']['properties']['policies'] policies['items'][0]['enum'].extend(['soft-anti-affinity', 'soft-affinity']) +create_v264 = copy.deepcopy(create_v215) +del create_v264['properties']['server_group']['properties']['policies'] +sg_properties = create_v264['properties']['server_group'] +sg_properties['required'].remove('policies') +sg_properties['required'].append('policy') +sg_properties['properties']['policy'] = { + 'type': 'string', + 'enum': ['anti-affinity', 'affinity', + 'soft-anti-affinity', 'soft-affinity'], +} + +sg_properties['properties']['rules'] = { + 'type': 'object', + 'properties': { + 'max_server_per_host': + parameter_types.positive_integer, + }, + 'additionalProperties': False, +} server_groups_query_param = { 'type': 'object', diff --git a/nova/api/openstack/compute/server_groups.py b/nova/api/openstack/compute/server_groups.py index 025bca8ada74..14fdd485f4ee 100644 --- a/nova/api/openstack/compute/server_groups.py +++ b/nova/api/openstack/compute/server_groups.py @@ -31,6 +31,7 @@ from nova import context as nova_context import nova.exception from nova.i18n import _ from nova import objects +from nova.objects import service from nova.policies import server_groups as sg_policies LOG = logging.getLogger(__name__) @@ -38,6 +39,9 @@ LOG = logging.getLogger(__name__) CONF = nova.conf.CONF +GROUP_POLICY_OBJ_MICROVERSION = "2.64" + + def _authorize_context(req, action): context = req.environ['nova.context'] context.can(sg_policies.POLICY_ROOT % action) @@ -78,6 +82,15 @@ def _get_not_deleted(context, uuids): return found_inst_uuids +def _should_enable_custom_max_server_rules(context, rules): + if rules and int(rules.get('max_server_per_host', 1)) > 1: + minver = service.get_minimum_version_all_cells( + context, ['nova-compute']) + if minver < 33: + return False + return True + + class ServerGroupController(wsgi.Controller): """The Server group API controller for the OpenStack API.""" @@ -89,10 +102,15 @@ class ServerGroupController(wsgi.Controller): server_group = {} server_group['id'] = group.uuid server_group['name'] = group.name - server_group['policies'] = group.policies or [] - # NOTE(danms): This has been exposed to the user, but never used. - # Since we can't remove it, just make sure it's always empty. - server_group['metadata'] = {} + if api_version_request.is_supported( + req, min_version=GROUP_POLICY_OBJ_MICROVERSION): + server_group['policy'] = group.policy + server_group['rules'] = group.rules + else: + server_group['policies'] = group.policies or [] + # NOTE(yikun): Before v2.64, a empty metadata is exposed to the + # user, and it is removed since v2.64. + server_group['metadata'] = {} members = [] if group.members: # Display the instances that are not deleted. @@ -146,9 +164,10 @@ class ServerGroupController(wsgi.Controller): return {'server_groups': result} @wsgi.Controller.api_version("2.1") - @wsgi.expected_errors((400, 403)) + @wsgi.expected_errors((400, 403, 409)) @validation.schema(schema.create, "2.0", "2.14") - @validation.schema(schema.create_v215, "2.15") + @validation.schema(schema.create_v215, "2.15", "2.63") + @validation.schema(schema.create_v264, GROUP_POLICY_OBJ_MICROVERSION) def create(self, req, body): """Creates a new server group.""" context = _authorize_context(req, 'create') @@ -161,13 +180,28 @@ class ServerGroupController(wsgi.Controller): raise exc.HTTPForbidden(explanation=msg) vals = body['server_group'] - sg = objects.InstanceGroup(context) - sg.project_id = context.project_id - sg.user_id = context.user_id + + if api_version_request.is_supported( + req, GROUP_POLICY_OBJ_MICROVERSION): + policy = vals['policy'] + rules = vals.get('rules', {}) + if policy != 'anti-affinity' and rules: + msg = _("Only anti-affinity policy supports rules.") + raise exc.HTTPBadRequest(explanation=msg) + # NOTE(yikun): This should be removed in Stein version. + if not _should_enable_custom_max_server_rules(context, rules): + msg = _("Creating an anti-affinity group with rule " + "max_server_per_host > 1 is not yet supported.") + raise exc.HTTPConflict(explanation=msg) + sg = objects.InstanceGroup(context, policy=policy, + rules=rules) + else: + policies = vals.get('policies') + sg = objects.InstanceGroup(context, policy=policies[0]) try: sg.name = vals.get('name') - policies = vals.get('policies') - sg.policy = policies[0] + sg.project_id = context.project_id + sg.user_id = context.user_id sg.create() except ValueError as e: raise exc.HTTPBadRequest(explanation=e) diff --git a/nova/tests/functional/api_sample_tests/api_samples/os-server-groups/v2.64/server-groups-get-resp.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/os-server-groups/v2.64/server-groups-get-resp.json.tpl new file mode 100644 index 000000000000..185db9e04827 --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/os-server-groups/v2.64/server-groups-get-resp.json.tpl @@ -0,0 +1,11 @@ +{ + "server_group": { + "id": "%(id)s", + "name": "%(name)s", + "policy": "anti-affinity", + "rules": {"max_server_per_host": 3}, + "members": [], + "project_id": "6f70656e737461636b20342065766572", + "user_id": "fake" + } +} diff --git a/nova/tests/functional/api_sample_tests/api_samples/os-server-groups/v2.64/server-groups-list-resp.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/os-server-groups/v2.64/server-groups-list-resp.json.tpl new file mode 100644 index 000000000000..44f3cdf5e0e5 --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/os-server-groups/v2.64/server-groups-list-resp.json.tpl @@ -0,0 +1,13 @@ +{ + "server_groups": [ + { + "id": "%(id)s", + "name": "test", + "policy": "anti-affinity", + "rules": {"max_server_per_host": 3}, + "members": [], + "project_id": "6f70656e737461636b20342065766572", + "user_id": "fake" + } + ] +} diff --git a/nova/tests/functional/api_sample_tests/api_samples/os-server-groups/v2.64/server-groups-post-req.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/os-server-groups/v2.64/server-groups-post-req.json.tpl new file mode 100644 index 000000000000..4d766b21a1ee --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/os-server-groups/v2.64/server-groups-post-req.json.tpl @@ -0,0 +1,7 @@ +{ + "server_group": { + "name": "%(name)s", + "policy": "anti-affinity", + "rules": {"max_server_per_host": 3} + } +} diff --git a/nova/tests/functional/api_sample_tests/api_samples/os-server-groups/v2.64/server-groups-post-resp.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/os-server-groups/v2.64/server-groups-post-resp.json.tpl new file mode 100644 index 000000000000..185db9e04827 --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/os-server-groups/v2.64/server-groups-post-resp.json.tpl @@ -0,0 +1,11 @@ +{ + "server_group": { + "id": "%(id)s", + "name": "%(name)s", + "policy": "anti-affinity", + "rules": {"max_server_per_host": 3}, + "members": [], + "project_id": "6f70656e737461636b20342065766572", + "user_id": "fake" + } +} diff --git a/nova/tests/functional/api_sample_tests/test_server_groups.py b/nova/tests/functional/api_sample_tests/test_server_groups.py index b7ef598fafd5..aff6e8c2068c 100644 --- a/nova/tests/functional/api_sample_tests/test_server_groups.py +++ b/nova/tests/functional/api_sample_tests/test_server_groups.py @@ -68,3 +68,13 @@ class ServerGroupsV213SampleJsonTest(ServerGroupsSampleJsonTest): def setUp(self): super(ServerGroupsV213SampleJsonTest, self).setUp() self.api.microversion = self.microversion + + +class ServerGroupsV264SampleJsonTest(ServerGroupsV213SampleJsonTest): + scenarios = [ + ("v2_64", {'api_major_version': 'v2.1', 'microversion': '2.64'}) + ] + + def setUp(self): + super(ServerGroupsV264SampleJsonTest, self).setUp() + self.api.microversion = self.microversion diff --git a/nova/tests/functional/notification_sample_tests/test_server_group.py b/nova/tests/functional/notification_sample_tests/test_server_group.py index 138626995176..8c04193ebd2f 100644 --- a/nova/tests/functional/notification_sample_tests/test_server_group.py +++ b/nova/tests/functional/notification_sample_tests/test_server_group.py @@ -27,7 +27,9 @@ class TestServerGroupNotificationSample( def test_server_group_create_delete(self): group_req = { "name": "test-server-group", - "policies": ["anti-affinity"]} + "policy": "anti-affinity", + "rules": {"max_server_per_host": 3} + } group = self.api.post_server_groups(group_req) self.assertEqual(1, len(fake_notifier.VERSIONED_NOTIFICATIONS)) @@ -48,7 +50,9 @@ class TestServerGroupNotificationSample( def test_server_group_add_member(self): group_req = { "name": "test-server-group", - "policies": ["anti-affinity"]} + "policy": "anti-affinity", + "rules": {"max_server_per_host": 3} + } group = self.api.post_server_groups(group_req) fake_notifier.reset() diff --git a/nova/tests/functional/test_server_group.py b/nova/tests/functional/test_server_group.py index 8fa7546f1470..f87fe589b66b 100644 --- a/nova/tests/functional/test_server_group.py +++ b/nova/tests/functional/test_server_group.py @@ -93,8 +93,9 @@ class ServerGroupTestBase(test.TestCase, def _boot_a_server_to_group(self, group, expected_status='ACTIVE', flavor=None): - server = self._build_minimal_create_server_request(self.api, - 'some-server') + server = self._build_minimal_create_server_request( + self.api, 'some-server', + image_uuid='a2459075-d96c-40d5-893e-577ff92e721c', networks=[]) if flavor: server['flavorRef'] = ('http://fake.server/%s' % flavor['id']) @@ -686,6 +687,11 @@ class ServerGroupTestV215(ServerGroupTestV21): host.start() + def _check_group_format(self, group, created_group): + self.assertEqual(group['policies'], created_group['policies']) + self.assertEqual({}, created_group['metadata']) + self.assertNotIn('rules', created_group) + def test_create_and_delete_groups(self): groups = [self.anti_affinity, self.affinity, @@ -698,9 +704,8 @@ class ServerGroupTestV215(ServerGroupTestV21): created_group = self.api.post_server_groups(group) created_groups.append(created_group) self.assertEqual(group['name'], created_group['name']) - self.assertEqual(group['policies'], created_group['policies']) + self._check_group_format(group, created_group) self.assertEqual([], created_group['members']) - self.assertEqual({}, created_group['metadata']) self.assertIn('id', created_group) group_details = self.api.get_server_group(created_group['id']) @@ -845,3 +850,47 @@ class ServerGroupTestV215(ServerGroupTestV21): def test_soft_affinity_not_supported(self): pass + + +class ServerGroupTestV264(ServerGroupTestV215): + api_major_version = 'v2.1' + microversion = '2.64' + anti_affinity = {'name': 'fake-name-1', 'policy': 'anti-affinity'} + affinity = {'name': 'fake-name-2', 'policy': 'affinity'} + soft_anti_affinity = {'name': 'fake-name-3', + 'policy': 'soft-anti-affinity'} + soft_affinity = {'name': 'fake-name-4', 'policy': 'soft-affinity'} + + def _check_group_format(self, group, created_group): + self.assertEqual(group['policy'], created_group['policy']) + self.assertEqual(group.get('rules', {}), created_group['rules']) + self.assertNotIn('metadata', created_group) + self.assertNotIn('policies', created_group) + + def test_boot_server_with_anti_affinity_rules(self): + anti_affinity_max_2 = { + 'name': 'fake-name-1', + 'policy': 'anti-affinity', + 'rules': {'max_server_per_host': 2} + } + created_group = self.api.post_server_groups(anti_affinity_max_2) + servers1st = self._boot_servers_to_group(created_group) + servers2nd = self._boot_servers_to_group(created_group) + + # We have 2 computes so the fifth server won't fit into the same group + failed_server = self._boot_a_server_to_group(created_group, + expected_status='ERROR') + self.assertEqual('No valid host was found. ' + 'There are not enough hosts available.', + failed_server['fault']['message']) + + hosts = map(lambda x: x['OS-EXT-SRV-ATTR:host'], + servers1st + servers2nd) + hosts = [h for h in hosts] + # 4 servers + self.assertEqual(4, len(hosts)) + # schedule to 2 host + self.assertEqual(2, len(set(hosts))) + # each host has 2 servers + for host in set(hosts): + self.assertEqual(2, hosts.count(host)) diff --git a/nova/tests/unit/api/openstack/compute/test_server_groups.py b/nova/tests/unit/api/openstack/compute/test_server_groups.py index c2aaa521e705..0a05875e3aa0 100644 --- a/nova/tests/unit/api/openstack/compute/test_server_groups.py +++ b/nova/tests/unit/api/openstack/compute/test_server_groups.py @@ -13,10 +13,13 @@ # License for the specific language governing permissions and limitations # under the License. +import copy import mock from oslo_utils import uuidutils +import six import webob +from nova.api.openstack import api_version_request as avr from nova.api.openstack.compute import server_groups as sg_v21 from nova import context from nova import exception @@ -43,13 +46,14 @@ def server_group_template(**kwargs): def server_group_resp_template(**kwargs): sgroup = kwargs.copy() sgroup.setdefault('name', 'test') - sgroup.setdefault('policies', []) + if 'policy' not in kwargs: + sgroup.setdefault('policies', []) sgroup.setdefault('members', []) return sgroup def server_group_db(sg): - attrs = sg.copy() + attrs = copy.deepcopy(sg) if 'id' in attrs: attrs['uuid'] = attrs.pop('id') if 'policies' in attrs: @@ -57,6 +61,8 @@ def server_group_db(sg): attrs['policies'] = policies else: attrs['policies'] = [] + if 'policy' in attrs: + del attrs['policies'] if 'members' in attrs: members = attrs.pop('members') attrs['members'] = members @@ -111,7 +117,8 @@ class ServerGroupTestV21(test.NoDBTestCase): self.assertRaises(self.validation_error, self.controller.create, self.req, body={'server_group': sgroup}) - def _create_server_group_normal(self, policies): + def _create_server_group_normal(self, policies=None, policy=None, + rules=None): sgroup = server_group_template() sgroup['policies'] = policies res_dict = self.controller.create(self.req, @@ -120,10 +127,33 @@ class ServerGroupTestV21(test.NoDBTestCase): self.assertTrue(uuidutils.is_uuid_like(res_dict['server_group']['id'])) self.assertEqual(res_dict['server_group']['policies'], policies) + def test_create_server_group_with_new_policy_before_264(self): + req = fakes.HTTPRequest.blank('', version='2.63') + policy = 'anti-affinity' + rules = {'max_server_per_host': 3} + # 'policy' isn't an acceptable request key before 2.64 + sgroup = server_group_template(policy=policy) + result = self.assertRaises( + self.validation_error, self.controller.create, + req, body={'server_group': sgroup}) + self.assertIn( + "Invalid input for field/attribute server_group", + six.text_type(result) + ) + # 'rules' isn't an acceptable request key before 2.64 + sgroup = server_group_template(rules=rules) + result = self.assertRaises( + self.validation_error, self.controller.create, + req, body={'server_group': sgroup}) + self.assertIn( + "Invalid input for field/attribute server_group", + six.text_type(result) + ) + def test_create_server_group(self): policies = ['affinity', 'anti-affinity'] for policy in policies: - self._create_server_group_normal([policy]) + self._create_server_group_normal(policies=[policy]) def test_create_server_group_rbac_default(self): sgroup = server_group_template() @@ -204,12 +234,29 @@ class ServerGroupTestV21(test.NoDBTestCase): def _test_list_server_group(self, mock_get_all, mock_get_by_project, path, api_version='2.1', limited=None): policies = ['anti-affinity'] + policy = "anti-affinity" members = [] metadata = {} # always empty names = ['default-x', 'test'] p_id = fakes.FAKE_PROJECT_ID u_id = fakes.FAKE_USER_ID - if api_version >= '2.13': + ver = avr.APIVersionRequest(api_version) + if ver >= avr.APIVersionRequest("2.64"): + sg1 = server_group_resp_template(id=uuidsentinel.sg1_id, + name=names[0], + policy=policy, + rules={}, + members=members, + project_id=p_id, + user_id=u_id) + sg2 = server_group_resp_template(id=uuidsentinel.sg2_id, + name=names[1], + policy=policy, + rules={}, + members=members, + project_id=p_id, + user_id=u_id) + elif ver >= avr.APIVersionRequest("2.13"): sg1 = server_group_resp_template(id=uuidsentinel.sg1_id, name=names[0], policies=policies, @@ -664,3 +711,140 @@ class ServerGroupTestV213(ServerGroupTestV21): def test_list_server_group_by_tenant(self): self._test_list_server_group_by_tenant(api_version='2.13') + + +class ServerGroupTestV264(ServerGroupTestV213): + wsgi_api_version = '2.64' + + def _setup_controller(self): + self.controller = sg_v21.ServerGroupController() + + def _create_server_group_normal(self, policies=None, policy=None, + rules=None): + req = fakes.HTTPRequest.blank('', version=self.wsgi_api_version) + sgroup = server_group_template() + sgroup['rules'] = rules or {} + sgroup['policy'] = policy + res_dict = self.controller.create(req, + body={'server_group': sgroup}) + self.assertEqual(res_dict['server_group']['name'], 'test') + self.assertTrue(uuidutils.is_uuid_like(res_dict['server_group']['id'])) + self.assertEqual(res_dict['server_group']['policy'], policy) + self.assertEqual(res_dict['server_group']['rules'], rules or {}) + return res_dict['server_group']['id'] + + def test_list_server_group_all(self): + self._test_list_server_group_all(api_version=self.wsgi_api_version) + + def test_create_and_show_server_group(self): + policies = ['affinity', 'anti-affinity'] + for policy in policies: + g_uuid = self._create_server_group_normal( + policy=policy) + res_dict = self._display_server_group(g_uuid) + self.assertEqual(res_dict['server_group']['policy'], policy) + self.assertEqual(res_dict['server_group']['rules'], {}) + + def _display_server_group(self, uuid): + req = fakes.HTTPRequest.blank('', version=self.wsgi_api_version) + group = self.controller.show(req, uuid) + return group + + @mock.patch('nova.objects.service.get_minimum_version_all_cells', + return_value=33) + def test_create_and_show_server_group_with_rules(self, mock_get_v): + policy = 'anti-affinity' + rules = {'max_server_per_host': 3} + g_uuid = self._create_server_group_normal( + policy=policy, rules=rules) + res_dict = self._display_server_group(g_uuid) + self.assertEqual(res_dict['server_group']['policy'], policy) + self.assertEqual(res_dict['server_group']['rules'], rules) + + def test_create_affinity_server_group_with_invalid_policy(self): + req = fakes.HTTPRequest.blank('', version=self.wsgi_api_version) + sgroup = server_group_template(policy='affinity', + rules={'max_server_per_host': 3}) + result = self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.create, req, body={'server_group': sgroup}) + self.assertIn("Only anti-affinity policy supports rules", + six.text_type(result)) + + def test_create_anti_affinity_server_group_with_invalid_rules(self): + req = fakes.HTTPRequest.blank('', version=self.wsgi_api_version) + # A negative test for key is unknown, the value is not positive + # and not integer + invalid_rules = [{'unknown_key': '3'}, + {'max_server_per_host': 0}, + {'max_server_per_host': 'foo'}] + + for r in invalid_rules: + sgroup = server_group_template(policy='anti-affinity', rules=r) + result = self.assertRaises( + self.validation_error, self.controller.create, + req, body={'server_group': sgroup}) + self.assertIn( + "Invalid input for field/attribute", six.text_type(result) + ) + + @mock.patch('nova.objects.service.get_minimum_version_all_cells', + return_value=32) + def test_create_server_group_with_low_version_compute_service(self, + mock_get_v): + req = fakes.HTTPRequest.blank('', version=self.wsgi_api_version) + sgroup = server_group_template(policy='anti-affinity', + rules={'max_server_per_host': 3}) + result = self.assertRaises( + webob.exc.HTTPConflict, + self.controller.create, req, body={'server_group': sgroup}) + self.assertIn("Creating an anti-affinity group with rule " + "max_server_per_host > 1 is not yet supported.", + six.text_type(result)) + + def test_create_server_group(self): + policies = ['affinity', 'anti-affinity'] + for policy in policies: + self._create_server_group_normal(policy=policy) + + def test_policies_since_264(self): + req = fakes.HTTPRequest.blank('', version=self.wsgi_api_version) + # 'policies' isn't allowed in request >= 2.64 + sgroup = server_group_template(policies=['anti-affinity']) + self.assertRaises( + self.validation_error, self.controller.create, + req, body={'server_group': sgroup}) + + def test_create_server_group_without_policy(self): + req = fakes.HTTPRequest.blank('', version=self.wsgi_api_version) + # 'policy' is required request key in request >= 2.64 + sgroup = server_group_template() + self.assertRaises(self.validation_error, self.controller.create, + req, body={'server_group': sgroup}) + + def test_create_server_group_with_illegal_policies(self): + req = fakes.HTTPRequest.blank('', version=self.wsgi_api_version) + # blank policy + sgroup = server_group_template(policy='') + self.assertRaises(self.validation_error, self.controller.create, + req, body={'server_group': sgroup}) + + # policy as integer + sgroup = server_group_template(policy=7) + self.assertRaises(self.validation_error, self.controller.create, + req, body={'server_group': sgroup}) + + # policy as string + sgroup = server_group_template(policy='invalid') + self.assertRaises(self.validation_error, self.controller.create, + req, body={'server_group': sgroup}) + + # policy as None + sgroup = server_group_template(policy=None) + self.assertRaises(self.validation_error, self.controller.create, + req, body={'server_group': sgroup}) + + def test_additional_params(self): + req = fakes.HTTPRequest.blank('', version=self.wsgi_api_version) + sgroup = server_group_template(unknown='unknown') + self.assertRaises(self.validation_error, self.controller.create, + req, body={'server_group': sgroup}) diff --git a/releasenotes/notes/complex-anti-affinity-policies-dcf4719e859093be.yaml b/releasenotes/notes/complex-anti-affinity-policies-dcf4719e859093be.yaml new file mode 100644 index 000000000000..f4c1b0e81d88 --- /dev/null +++ b/releasenotes/notes/complex-anti-affinity-policies-dcf4719e859093be.yaml @@ -0,0 +1,18 @@ +--- +features: + - | + Enable users to define the policy rules on server group policy to meet + more advanced policy requirement. This microversion 2.64 brings the + following changes in server group APIs: + + * Add ``policy`` and ``rules`` fields in the request of POST + ``/os-server-groups``. The ``policy`` represents the name of policy. The + ``rules`` field, which is a dict, can be applied to the policy, which + currently only supports ``max_server_per_host`` for ``anti-affinity`` + policy. + * The ``policy`` and ``rules`` fields will be returned in response + body of POST, GET ``/os-server-groups`` API and GET + ``/os-server-groups/{server_group_id}`` API. + * The ``policies`` and ``metadata`` fields have been removed from the + response body of POST, GET ``/os-server-groups`` API and GET + ``/os-server-groups/{server_group_id}`` API.