Merge "Microversion 2.64 - Use new format policy in server group"

This commit is contained in:
Zuul 2018-07-16 15:41:56 +00:00 committed by Gerrit Code Review
commit ae40af621f
22 changed files with 520 additions and 34 deletions

View File

@ -38,13 +38,15 @@ Response
- name: name_server_group - name: name_server_group
- policies: policies - policies: policies
- members: members - members: members
- metadata: metadata_object - metadata: metadata_server_group_max_2_63
- project_id: project_id_server_group - project_id: project_id_server_group
- user_id: user_id_server_group - user_id: user_id_server_group
- policy: policy_name
- rules: policy_rules
**Example List Server Groups: JSON response** **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 :language: javascript
Create Server Group Create Server Group
@ -56,7 +58,7 @@ Creates a server group.
Normal response codes: 200 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 Request
------- -------
@ -66,10 +68,12 @@ Request
- server_group: server_group - server_group: server_group
- name: name_server_group - name: name_server_group
- policies: policies - policies: policies
- policy: policy_name
- rules: policy_rules_optional
**Example Create Server Group: JSON request** **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 :language: javascript
Response Response
@ -82,13 +86,15 @@ Response
- name: name_server_group - name: name_server_group
- policies: policies - policies: policies
- members: members - members: members
- metadata: metadata_object - metadata: metadata_server_group_max_2_63
- project_id: project_id_server_group - project_id: project_id_server_group
- user_id: user_id_server_group - user_id: user_id_server_group
- policy: policy_name
- rules: policy_rules
**Example Create Server Group: JSON response** **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 :language: javascript
Show Server Group Details Show Server Group Details
@ -119,13 +125,15 @@ Response
- name: name_server_group - name: name_server_group
- policies: policies - policies: policies
- members: members - members: members
- metadata: metadata_object - metadata: metadata_server_group_max_2_63
- project_id: project_id_server_group - project_id: project_id_server_group
- user_id: user_id_server_group - user_id: user_id_server_group
- policy: policy_name
- rules: policy_rules
**Example Show Server Group Details: JSON response** **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 :language: javascript
Delete Server Group Delete Server Group

View File

@ -4299,6 +4299,14 @@ metadata_object:
in: body in: body
required: true required: true
type: object 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: migrate:
description: | description: |
The action to cold migrate a server. The action to cold migrate a server.
@ -5111,6 +5119,50 @@ policies:
in: body in: body
required: true required: true
type: array 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: pool:
description: | description: |
Pool from which to allocate the IP address. If you omit this parameter, the call Pool from which to allocate the IP address. If you omit this parameter, the call

View File

@ -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"
}
}

View File

@ -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"
}
]
}

View File

@ -0,0 +1,7 @@
{
"server_group": {
"name": "test",
"policy": "anti-affinity",
"rules": {"max_server_per_host": 3}
}
}

View File

@ -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"
}
}

View File

@ -19,7 +19,7 @@
} }
], ],
"status": "CURRENT", "status": "CURRENT",
"version": "2.63", "version": "2.64",
"min_version": "2.1", "min_version": "2.1",
"updated": "2013-07-23T11:33:21Z" "updated": "2013-07-23T11:33:21Z"
} }

View File

@ -22,7 +22,7 @@
} }
], ],
"status": "CURRENT", "status": "CURRENT",
"version": "2.63", "version": "2.64",
"min_version": "2.1", "min_version": "2.1",
"updated": "2013-07-23T11:33:21Z" "updated": "2013-07-23T11:33:21Z"
} }

View File

@ -11,7 +11,7 @@
"anti-affinity" "anti-affinity"
], ],
"policy": "anti-affinity", "policy": "anti-affinity",
"rules": {}, "rules": {"max_server_per_host": "3"},
"members": [], "members": [],
"hosts": null "hosts": null
} }

View File

@ -150,6 +150,12 @@ REST_API_VERSION_HISTORY = """REST API Version History:
responses. responses.
* 2.63 - Add support for applying trusted certificates when creating or * 2.63 - Add support for applying trusted certificates when creating or
rebuilding a server. 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 # 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 # Note(cyeoh): This only applies for the v2.1 API once microversions
# support is fully merged. It does not affect the V2 API. # support is fully merged. It does not affect the V2 API.
_MIN_API_VERSION = "2.1" _MIN_API_VERSION = "2.1"
_MAX_API_VERSION = "2.63" _MAX_API_VERSION = "2.64"
DEFAULT_API_VERSION = _MIN_API_VERSION DEFAULT_API_VERSION = _MIN_API_VERSION
# Almost all proxy APIs which are related to network, images and baremetal # Almost all proxy APIs which are related to network, images and baremetal

View File

@ -818,3 +818,21 @@ the following APIs:
* ``GET /servers/{server_id}`` * ``GET /servers/{server_id}``
* ``PUT /servers/{server_id}`` * ``PUT /servers/{server_id}``
* ``POST /servers/{server_id}/action (rebuild)`` * ``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.

View File

@ -51,6 +51,25 @@ create_v215 = copy.deepcopy(create)
policies = create_v215['properties']['server_group']['properties']['policies'] policies = create_v215['properties']['server_group']['properties']['policies']
policies['items'][0]['enum'].extend(['soft-anti-affinity', 'soft-affinity']) 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 = { server_groups_query_param = {
'type': 'object', 'type': 'object',

View File

@ -31,6 +31,7 @@ from nova import context as nova_context
import nova.exception import nova.exception
from nova.i18n import _ from nova.i18n import _
from nova import objects from nova import objects
from nova.objects import service
from nova.policies import server_groups as sg_policies from nova.policies import server_groups as sg_policies
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
@ -38,6 +39,9 @@ LOG = logging.getLogger(__name__)
CONF = nova.conf.CONF CONF = nova.conf.CONF
GROUP_POLICY_OBJ_MICROVERSION = "2.64"
def _authorize_context(req, action): def _authorize_context(req, action):
context = req.environ['nova.context'] context = req.environ['nova.context']
context.can(sg_policies.POLICY_ROOT % action) context.can(sg_policies.POLICY_ROOT % action)
@ -78,6 +82,15 @@ def _get_not_deleted(context, uuids):
return found_inst_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): class ServerGroupController(wsgi.Controller):
"""The Server group API controller for the OpenStack API.""" """The Server group API controller for the OpenStack API."""
@ -89,9 +102,14 @@ class ServerGroupController(wsgi.Controller):
server_group = {} server_group = {}
server_group['id'] = group.uuid server_group['id'] = group.uuid
server_group['name'] = group.name server_group['name'] = group.name
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 [] server_group['policies'] = group.policies or []
# NOTE(danms): This has been exposed to the user, but never used. # NOTE(yikun): Before v2.64, a empty metadata is exposed to the
# Since we can't remove it, just make sure it's always empty. # user, and it is removed since v2.64.
server_group['metadata'] = {} server_group['metadata'] = {}
members = [] members = []
if group.members: if group.members:
@ -146,9 +164,10 @@ class ServerGroupController(wsgi.Controller):
return {'server_groups': result} return {'server_groups': result}
@wsgi.Controller.api_version("2.1") @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, "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): def create(self, req, body):
"""Creates a new server group.""" """Creates a new server group."""
context = _authorize_context(req, 'create') context = _authorize_context(req, 'create')
@ -161,13 +180,28 @@ class ServerGroupController(wsgi.Controller):
raise exc.HTTPForbidden(explanation=msg) raise exc.HTTPForbidden(explanation=msg)
vals = body['server_group'] vals = body['server_group']
sg = objects.InstanceGroup(context)
sg.project_id = context.project_id if api_version_request.is_supported(
sg.user_id = context.user_id 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: try:
sg.name = vals.get('name') sg.name = vals.get('name')
policies = vals.get('policies') sg.project_id = context.project_id
sg.policy = policies[0] sg.user_id = context.user_id
sg.create() sg.create()
except ValueError as e: except ValueError as e:
raise exc.HTTPBadRequest(explanation=e) raise exc.HTTPBadRequest(explanation=e)

View File

@ -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"
}
}

View File

@ -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"
}
]
}

View File

@ -0,0 +1,7 @@
{
"server_group": {
"name": "%(name)s",
"policy": "anti-affinity",
"rules": {"max_server_per_host": 3}
}
}

View File

@ -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"
}
}

View File

@ -68,3 +68,13 @@ class ServerGroupsV213SampleJsonTest(ServerGroupsSampleJsonTest):
def setUp(self): def setUp(self):
super(ServerGroupsV213SampleJsonTest, self).setUp() super(ServerGroupsV213SampleJsonTest, self).setUp()
self.api.microversion = self.microversion 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

View File

@ -27,7 +27,9 @@ class TestServerGroupNotificationSample(
def test_server_group_create_delete(self): def test_server_group_create_delete(self):
group_req = { group_req = {
"name": "test-server-group", "name": "test-server-group",
"policies": ["anti-affinity"]} "policy": "anti-affinity",
"rules": {"max_server_per_host": 3}
}
group = self.api.post_server_groups(group_req) group = self.api.post_server_groups(group_req)
self.assertEqual(1, len(fake_notifier.VERSIONED_NOTIFICATIONS)) self.assertEqual(1, len(fake_notifier.VERSIONED_NOTIFICATIONS))
@ -48,7 +50,9 @@ class TestServerGroupNotificationSample(
def test_server_group_add_member(self): def test_server_group_add_member(self):
group_req = { group_req = {
"name": "test-server-group", "name": "test-server-group",
"policies": ["anti-affinity"]} "policy": "anti-affinity",
"rules": {"max_server_per_host": 3}
}
group = self.api.post_server_groups(group_req) group = self.api.post_server_groups(group_req)
fake_notifier.reset() fake_notifier.reset()

View File

@ -93,8 +93,9 @@ class ServerGroupTestBase(test.TestCase,
def _boot_a_server_to_group(self, group, def _boot_a_server_to_group(self, group,
expected_status='ACTIVE', flavor=None): expected_status='ACTIVE', flavor=None):
server = self._build_minimal_create_server_request(self.api, server = self._build_minimal_create_server_request(
'some-server') self.api, 'some-server',
image_uuid='a2459075-d96c-40d5-893e-577ff92e721c', networks=[])
if flavor: if flavor:
server['flavorRef'] = ('http://fake.server/%s' server['flavorRef'] = ('http://fake.server/%s'
% flavor['id']) % flavor['id'])
@ -686,6 +687,11 @@ class ServerGroupTestV215(ServerGroupTestV21):
host.start() 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): def test_create_and_delete_groups(self):
groups = [self.anti_affinity, groups = [self.anti_affinity,
self.affinity, self.affinity,
@ -698,9 +704,8 @@ class ServerGroupTestV215(ServerGroupTestV21):
created_group = self.api.post_server_groups(group) created_group = self.api.post_server_groups(group)
created_groups.append(created_group) created_groups.append(created_group)
self.assertEqual(group['name'], created_group['name']) 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['members'])
self.assertEqual({}, created_group['metadata'])
self.assertIn('id', created_group) self.assertIn('id', created_group)
group_details = self.api.get_server_group(created_group['id']) group_details = self.api.get_server_group(created_group['id'])
@ -845,3 +850,47 @@ class ServerGroupTestV215(ServerGroupTestV21):
def test_soft_affinity_not_supported(self): def test_soft_affinity_not_supported(self):
pass 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))

View File

@ -13,10 +13,13 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import copy
import mock import mock
from oslo_utils import uuidutils from oslo_utils import uuidutils
import six
import webob 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.api.openstack.compute import server_groups as sg_v21
from nova import context from nova import context
from nova import exception from nova import exception
@ -43,13 +46,14 @@ def server_group_template(**kwargs):
def server_group_resp_template(**kwargs): def server_group_resp_template(**kwargs):
sgroup = kwargs.copy() sgroup = kwargs.copy()
sgroup.setdefault('name', 'test') sgroup.setdefault('name', 'test')
if 'policy' not in kwargs:
sgroup.setdefault('policies', []) sgroup.setdefault('policies', [])
sgroup.setdefault('members', []) sgroup.setdefault('members', [])
return sgroup return sgroup
def server_group_db(sg): def server_group_db(sg):
attrs = sg.copy() attrs = copy.deepcopy(sg)
if 'id' in attrs: if 'id' in attrs:
attrs['uuid'] = attrs.pop('id') attrs['uuid'] = attrs.pop('id')
if 'policies' in attrs: if 'policies' in attrs:
@ -57,6 +61,8 @@ def server_group_db(sg):
attrs['policies'] = policies attrs['policies'] = policies
else: else:
attrs['policies'] = [] attrs['policies'] = []
if 'policy' in attrs:
del attrs['policies']
if 'members' in attrs: if 'members' in attrs:
members = attrs.pop('members') members = attrs.pop('members')
attrs['members'] = members attrs['members'] = members
@ -111,7 +117,8 @@ class ServerGroupTestV21(test.NoDBTestCase):
self.assertRaises(self.validation_error, self.controller.create, self.assertRaises(self.validation_error, self.controller.create,
self.req, body={'server_group': sgroup}) 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 = server_group_template()
sgroup['policies'] = policies sgroup['policies'] = policies
res_dict = self.controller.create(self.req, 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.assertTrue(uuidutils.is_uuid_like(res_dict['server_group']['id']))
self.assertEqual(res_dict['server_group']['policies'], policies) 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): def test_create_server_group(self):
policies = ['affinity', 'anti-affinity'] policies = ['affinity', 'anti-affinity']
for policy in policies: 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): def test_create_server_group_rbac_default(self):
sgroup = server_group_template() 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, def _test_list_server_group(self, mock_get_all, mock_get_by_project,
path, api_version='2.1', limited=None): path, api_version='2.1', limited=None):
policies = ['anti-affinity'] policies = ['anti-affinity']
policy = "anti-affinity"
members = [] members = []
metadata = {} # always empty metadata = {} # always empty
names = ['default-x', 'test'] names = ['default-x', 'test']
p_id = fakes.FAKE_PROJECT_ID p_id = fakes.FAKE_PROJECT_ID
u_id = fakes.FAKE_USER_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, sg1 = server_group_resp_template(id=uuidsentinel.sg1_id,
name=names[0], name=names[0],
policies=policies, policies=policies,
@ -664,3 +711,140 @@ class ServerGroupTestV213(ServerGroupTestV21):
def test_list_server_group_by_tenant(self): def test_list_server_group_by_tenant(self):
self._test_list_server_group_by_tenant(api_version='2.13') 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})

View File

@ -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.