From 5cdb1ce26b0ec53c231723bcc6ce880aa8f43f0f Mon Sep 17 00:00:00 2001 From: Yikun Jiang Date: Thu, 10 May 2018 17:19:42 +0800 Subject: [PATCH] Microversion 2.64 - Use new format policy in server group 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`` 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. Part of blueprint: complex-anti-affinity-policies Change-Id: I6911e97bd7f8df92511e90518dba21c127e106a5 --- api-ref/source/os-server-groups.inc | 24 ++- api-ref/source/parameters.yaml | 52 +++++ .../v2.64/server-groups-get-resp.json | 11 + .../v2.64/server-groups-list-resp.json | 13 ++ .../v2.64/server-groups-post-req.json | 7 + .../v2.64/server-groups-post-resp.json | 11 + .../versions/v21-version-get-resp.json | 2 +- .../versions/versions-get-resp.json | 2 +- .../common_payloads/ServerGroupPayload.json | 2 +- nova/api/openstack/api_version_request.py | 8 +- .../compute/rest_api_version_history.rst | 18 ++ .../compute/schemas/server_groups.py | 19 ++ nova/api/openstack/compute/server_groups.py | 56 ++++- .../v2.64/server-groups-get-resp.json.tpl | 11 + .../v2.64/server-groups-list-resp.json.tpl | 13 ++ .../v2.64/server-groups-post-req.json.tpl | 7 + .../v2.64/server-groups-post-resp.json.tpl | 11 + .../api_sample_tests/test_server_groups.py | 10 + .../test_server_group.py | 8 +- nova/tests/functional/test_server_group.py | 57 ++++- .../openstack/compute/test_server_groups.py | 194 +++++++++++++++++- ...ti-affinity-policies-dcf4719e859093be.yaml | 18 ++ 22 files changed, 520 insertions(+), 34 deletions(-) create mode 100644 doc/api_samples/os-server-groups/v2.64/server-groups-get-resp.json create mode 100644 doc/api_samples/os-server-groups/v2.64/server-groups-list-resp.json create mode 100644 doc/api_samples/os-server-groups/v2.64/server-groups-post-req.json create mode 100644 doc/api_samples/os-server-groups/v2.64/server-groups-post-resp.json create mode 100644 nova/tests/functional/api_sample_tests/api_samples/os-server-groups/v2.64/server-groups-get-resp.json.tpl create mode 100644 nova/tests/functional/api_sample_tests/api_samples/os-server-groups/v2.64/server-groups-list-resp.json.tpl create mode 100644 nova/tests/functional/api_sample_tests/api_samples/os-server-groups/v2.64/server-groups-post-req.json.tpl create mode 100644 nova/tests/functional/api_sample_tests/api_samples/os-server-groups/v2.64/server-groups-post-resp.json.tpl create mode 100644 releasenotes/notes/complex-anti-affinity-policies-dcf4719e859093be.yaml diff --git a/api-ref/source/os-server-groups.inc b/api-ref/source/os-server-groups.inc index 295e11bf38..761d89da72 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 f129505115..5a55eb2fa9 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 0000000000..2dba808f39 --- /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 0000000000..6e4ec47a43 --- /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 0000000000..a6defffdfb --- /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 0000000000..2dba808f39 --- /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 308f43aaf3..c362dafd50 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 ec1ecbe1c5..7e4cc0aab0 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 27053fcdbc..bb544e4af1 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 8ffa6ec2fe..586395904c 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 629fcb8911..607dd55f74 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 c2771ba4d2..d8401e1583 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 025bca8ada..14fdd485f4 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 0000000000..185db9e048 --- /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 0000000000..44f3cdf5e0 --- /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 0000000000..4d766b21a1 --- /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 0000000000..185db9e048 --- /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 b7ef598faf..aff6e8c206 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 1386269951..8c04193ebd 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 8fa7546f14..f87fe589b6 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 c2aaa521e7..0a05875e3a 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 0000000000..f4c1b0e81d --- /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.