From aeda1f6e64f77cd8e54b434be8083f5f6e66237c Mon Sep 17 00:00:00 2001 From: Debo Dutta Date: Thu, 15 Aug 2013 02:39:17 -0700 Subject: [PATCH] Add REST API for instance group api extension Support the Creation, Read, Delete, and List of server groups. Refactored the code to use objects (https://review.openstack.org/#/c/38979/ Renamed from "instance group" to "server group". Implements: blueprint instance-group-api-extension Change-Id: I650a8f191dea5eab5b4b1828f0b9f65e33edea2a --- .../all_extensions/extensions-get-resp.json | 8 + .../all_extensions/extensions-get-resp.xml | 3 + .../server-groups-get-resp.json | 9 + .../server-groups-get-resp.xml | 8 + .../server-groups-list-resp.json | 11 + .../server-groups-list-resp.xml | 10 + .../server-groups-post-req.json | 6 + .../server-groups-post-req.xml | 5 + .../server-groups-post-resp.json | 9 + .../server-groups-post-resp.xml | 8 + etc/nova/policy.json | 1 + .../compute/contrib/server_groups.py | 263 ++++++++++++ .../compute/contrib/test_server_groups.py | 389 ++++++++++++++++++ nova/tests/fake_policy.py | 1 + .../extensions-get-resp.json.tpl | 8 + .../extensions-get-resp.xml.tpl | 3 + .../server-groups-get-resp.json.tpl | 9 + .../server-groups-get-resp.xml.tpl | 8 + .../server-groups-list-resp.json.tpl | 11 + .../server-groups-list-resp.xml.tpl | 10 + .../server-groups-post-req.json.tpl | 6 + .../server-groups-post-req.xml.tpl | 5 + .../server-groups-post-resp.json.tpl | 10 + .../server-groups-post-resp.xml.tpl | 8 + nova/tests/integrated/test_api_samples.py | 55 +++ 25 files changed, 864 insertions(+) create mode 100644 doc/api_samples/os-server-groups/server-groups-get-resp.json create mode 100644 doc/api_samples/os-server-groups/server-groups-get-resp.xml create mode 100644 doc/api_samples/os-server-groups/server-groups-list-resp.json create mode 100644 doc/api_samples/os-server-groups/server-groups-list-resp.xml create mode 100644 doc/api_samples/os-server-groups/server-groups-post-req.json create mode 100644 doc/api_samples/os-server-groups/server-groups-post-req.xml create mode 100644 doc/api_samples/os-server-groups/server-groups-post-resp.json create mode 100644 doc/api_samples/os-server-groups/server-groups-post-resp.xml create mode 100644 nova/api/openstack/compute/contrib/server_groups.py create mode 100644 nova/tests/api/openstack/compute/contrib/test_server_groups.py create mode 100644 nova/tests/integrated/api_samples/os-server-groups/server-groups-get-resp.json.tpl create mode 100644 nova/tests/integrated/api_samples/os-server-groups/server-groups-get-resp.xml.tpl create mode 100644 nova/tests/integrated/api_samples/os-server-groups/server-groups-list-resp.json.tpl create mode 100644 nova/tests/integrated/api_samples/os-server-groups/server-groups-list-resp.xml.tpl create mode 100644 nova/tests/integrated/api_samples/os-server-groups/server-groups-post-req.json.tpl create mode 100644 nova/tests/integrated/api_samples/os-server-groups/server-groups-post-req.xml.tpl create mode 100644 nova/tests/integrated/api_samples/os-server-groups/server-groups-post-resp.json.tpl create mode 100644 nova/tests/integrated/api_samples/os-server-groups/server-groups-post-resp.xml.tpl diff --git a/doc/api_samples/all_extensions/extensions-get-resp.json b/doc/api_samples/all_extensions/extensions-get-resp.json index 4af4a8a144..c95789a642 100644 --- a/doc/api_samples/all_extensions/extensions-get-resp.json +++ b/doc/api_samples/all_extensions/extensions-get-resp.json @@ -432,6 +432,14 @@ "namespace": "http://docs.openstack.org/compute/ext/server-external-events/api/v2", "updated": "2014-02-18T00:00:00-00:00" }, + { + "alias": "os-server-groups", + "description": "Server group support.", + "links": [], + "name": "ServerGroups", + "namespace": "http://docs.openstack.org/compute/ext/servergroups/api/v2", + "updated": "2013-06-01T00:00:00+00:00" + }, { "alias": "os-instance_usage_audit_log", "description": "Admin-only Task Log Monitoring.", diff --git a/doc/api_samples/all_extensions/extensions-get-resp.xml b/doc/api_samples/all_extensions/extensions-get-resp.xml index 94a49ec5ac..5683362ffb 100644 --- a/doc/api_samples/all_extensions/extensions-get-resp.xml +++ b/doc/api_samples/all_extensions/extensions-get-resp.xml @@ -182,6 +182,9 @@ Server External Event Triggers. + + Server group support. + Admin-only Task Log Monitoring. diff --git a/doc/api_samples/os-server-groups/server-groups-get-resp.json b/doc/api_samples/os-server-groups/server-groups-get-resp.json new file mode 100644 index 0000000000..53c923f7db --- /dev/null +++ b/doc/api_samples/os-server-groups/server-groups-get-resp.json @@ -0,0 +1,9 @@ +{ + "server_group": { + "id": "5bbcc3c4-1da2-4437-a48a-66f15b1b13f9", + "name": "test", + "policies": ["test_policy"], + "members": [], + "metadata": {} + } +} diff --git a/doc/api_samples/os-server-groups/server-groups-get-resp.xml b/doc/api_samples/os-server-groups/server-groups-get-resp.xml new file mode 100644 index 0000000000..db8b7712b2 --- /dev/null +++ b/doc/api_samples/os-server-groups/server-groups-get-resp.xml @@ -0,0 +1,8 @@ + + + + test_policy + + + + diff --git a/doc/api_samples/os-server-groups/server-groups-list-resp.json b/doc/api_samples/os-server-groups/server-groups-list-resp.json new file mode 100644 index 0000000000..4707b60edc --- /dev/null +++ b/doc/api_samples/os-server-groups/server-groups-list-resp.json @@ -0,0 +1,11 @@ +{ + "server_groups": [ + { + "id": "616fb98f-46ca-475e-917e-2563e5a8cd19", + "name": "test", + "policies": ["test_policy"], + "members": [], + "metadata": {} + } + ] +} diff --git a/doc/api_samples/os-server-groups/server-groups-list-resp.xml b/doc/api_samples/os-server-groups/server-groups-list-resp.xml new file mode 100644 index 0000000000..f6fc487009 --- /dev/null +++ b/doc/api_samples/os-server-groups/server-groups-list-resp.xml @@ -0,0 +1,10 @@ + + + + + test_policy + + + + + diff --git a/doc/api_samples/os-server-groups/server-groups-post-req.json b/doc/api_samples/os-server-groups/server-groups-post-req.json new file mode 100644 index 0000000000..211c2a7c0e --- /dev/null +++ b/doc/api_samples/os-server-groups/server-groups-post-req.json @@ -0,0 +1,6 @@ +{ + "server_group": { + "name": "test", + "policies": ["test_policy"] + } +} diff --git a/doc/api_samples/os-server-groups/server-groups-post-req.xml b/doc/api_samples/os-server-groups/server-groups-post-req.xml new file mode 100644 index 0000000000..628585973f --- /dev/null +++ b/doc/api_samples/os-server-groups/server-groups-post-req.xml @@ -0,0 +1,5 @@ + + + test_policy + + diff --git a/doc/api_samples/os-server-groups/server-groups-post-resp.json b/doc/api_samples/os-server-groups/server-groups-post-resp.json new file mode 100644 index 0000000000..53c923f7db --- /dev/null +++ b/doc/api_samples/os-server-groups/server-groups-post-resp.json @@ -0,0 +1,9 @@ +{ + "server_group": { + "id": "5bbcc3c4-1da2-4437-a48a-66f15b1b13f9", + "name": "test", + "policies": ["test_policy"], + "members": [], + "metadata": {} + } +} diff --git a/doc/api_samples/os-server-groups/server-groups-post-resp.xml b/doc/api_samples/os-server-groups/server-groups-post-resp.xml new file mode 100644 index 0000000000..db8b7712b2 --- /dev/null +++ b/doc/api_samples/os-server-groups/server-groups-post-resp.xml @@ -0,0 +1,8 @@ + + + + test_policy + + + + diff --git a/etc/nova/policy.json b/etc/nova/policy.json index 90d90fe1b2..b889af3e35 100644 --- a/etc/nova/policy.json +++ b/etc/nova/policy.json @@ -211,6 +211,7 @@ "compute_extension:server_diagnostics": "rule:admin_api", "compute_extension:v3:os-server-diagnostics": "rule:admin_api", "compute_extension:v3:os-server-diagnostics:discoverable": "", + "compute_extension:server_groups": "", "compute_extension:server_password": "", "compute_extension:v3:os-server-password": "", "compute_extension:v3:os-server-password:discoverable": "", diff --git a/nova/api/openstack/compute/contrib/server_groups.py b/nova/api/openstack/compute/contrib/server_groups.py new file mode 100644 index 0000000000..2c3d86d391 --- /dev/null +++ b/nova/api/openstack/compute/contrib/server_groups.py @@ -0,0 +1,263 @@ +# Copyright (c) 2014 Cisco Systems, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""The Server Group API Extension.""" + +import webob +from webob import exc + +from nova.api.openstack import common +from nova.api.openstack import extensions +from nova.api.openstack import wsgi +from nova.api.openstack import xmlutil +import nova.exception +from nova.objects import instance_group as instance_group_obj +from nova.openstack.common.gettextutils import _ +from nova.openstack.common import log as logging +from nova import utils + +LOG = logging.getLogger(__name__) + +authorize = extensions.extension_authorizer('compute', 'server_groups') + + +def make_policy(elem): + elem.text = str + + +def make_member(elem): + elem.text = str + + +def make_group(elem): + elem.set('name') + elem.set('id') + policies = xmlutil.SubTemplateElement(elem, 'policies') + policy = xmlutil.SubTemplateElement(policies, 'policy', + selector='policies') + make_policy(policy) + members = xmlutil.SubTemplateElement(elem, 'members') + member = xmlutil.SubTemplateElement(members, 'member', + selector='members') + make_member(member) + elem.append(common.MetadataTemplate()) + + +server_group_nsmap = {None: xmlutil.XMLNS_V11, 'atom': xmlutil.XMLNS_ATOM} + + +def _authorize_context(req): + context = req.environ['nova.context'] + authorize(context) + return context + + +class ServerGroupTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('server_group', + selector='server_group') + make_group(root) + return xmlutil.MasterTemplate(root, 1, nsmap=server_group_nsmap) + + +class ServerGroupsTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('server_groups') + elem = xmlutil.SubTemplateElement(root, 'server_group', + selector='server_groups') + # Note: listing server groups only shows name and uuid + make_group(elem) + return xmlutil.MasterTemplate(root, 1, nsmap=server_group_nsmap) + + +class ServerGroupXMLDeserializer(wsgi.MetadataXMLDeserializer): + """Deserializer to handle xml-formatted server group requests.""" + + metadata_deserializer = common.MetadataXMLDeserializer() + + def default(self, string): + """Deserialize an xml-formatted server group create request.""" + dom = xmlutil.safe_minidom_parse_string(string) + server_group = self._extract_server_group(dom) + return {'body': {'server_group': server_group}} + + def _extract_server_group(self, node): + """Marshal the instance attribute of a parsed request.""" + server_group = {} + sg_node = self.find_first_child_named(node, 'server_group') + if sg_node is not None: + if sg_node.hasAttribute('name'): + server_group['name'] = sg_node.getAttribute('name') + + if sg_node.hasAttribute('id'): + server_group['id'] = sg_node.getAttribute('id') + + policies = self._extract_policies(sg_node) + server_group['policies'] = policies or [] + + return server_group + + def _extract_policies(self, server_group_node): + """Marshal the server group policies element of a parsed request.""" + policies_node = self.find_first_child_named(server_group_node, + 'policies') + if policies_node is not None: + policy_nodes = self.find_children_named(policies_node, + 'policy') + policies = [] + if policy_nodes is not None: + for node in policy_nodes: + policies.append(node.firstChild.nodeValue) + return policies + + def _extract_members(self, server_group_node): + """Marshal the server group members element of a parsed request.""" + members_node = self.find_first_child_named(server_group_node, + 'members') + if members_node is not None: + member_nodes = self.find_children_named(members_node, + 'member') + + members = [] + if member_nodes is not None: + for node in member_nodes: + members.append(node.firstChild.nodeValue) + return members + + +class ServerGroupController(wsgi.Controller): + """The Server group API controller for the OpenStack API.""" + + def _format_server_group(self, context, group): + # the id field has its value as the uuid of the server group + # There is no 'uuid' key in server_group seen by clients. + # In addition, clients see policies as a ["policy-name"] list; + # and they see members as a ["server-id"] list. + server_group = {} + server_group['id'] = group.uuid + server_group['name'] = group.name + server_group['policies'] = group.policies or [] + server_group['members'] = group.members or [] + server_group['metadata'] = group.metadetails or {} + return server_group + + def _validate_input_body(self, body, entity_name): + if not self.is_valid_body(body, entity_name): + msg = _("the body is invalid.") + raise nova.exception.InvalidInput(reason=msg) + + subbody = dict(body[entity_name]) + + expected_fields = ['name', 'policies'] + for field in expected_fields: + value = subbody.pop(field, None) + if not value: + msg = _("'%s' is either missing or empty.") % field + raise nova.exception.InvalidInput(reason=msg) + if isinstance(value, basestring): + utils.check_string_length(value, field, + min_length=1, max_length=255) + elif isinstance(value, list): + [utils.check_string_length(v, field, + min_length=1, max_length=255) for v in value] + + if subbody: + msg = _("unsupported fields: %s") % subbody.keys() + raise nova.exception.InvalidInput(reason=msg) + + @wsgi.serializers(xml=ServerGroupTemplate) + def show(self, req, id): + """Return data about the given server group.""" + context = _authorize_context(req) + try: + sg = instance_group_obj.InstanceGroup.get_by_uuid(context, id) + except nova.exception.InstanceGroupNotFound as e: + raise webob.exc.HTTPNotFound(explanation=e.format_message()) + return {'server_group': self._format_server_group(context, sg)} + + def delete(self, req, id): + """Delete an server group.""" + context = _authorize_context(req) + try: + sg = instance_group_obj.InstanceGroup.get_by_uuid(context, id) + sg.destroy(context) + except nova.exception.InstanceGroupNotFound as e: + raise webob.exc.HTTPNotFound(explanation=e.format_message()) + return webob.Response(status_int=204) + + @wsgi.serializers(xml=ServerGroupsTemplate) + def index(self, req): + """Returns a list of server groups.""" + context = _authorize_context(req) + project_id = context.project_id + if 'all_projects' in req.GET and context.is_admin: + sgs = instance_group_obj.InstanceGroupList.get_all(context) + else: + sgs = instance_group_obj.InstanceGroupList.get_by_project_id( + context, project_id) + limited_list = common.limited(sgs.objects, req) + result = [self._format_server_group(context, group) + for group in limited_list] + return {'server_groups': result} + + @wsgi.serializers(xml=ServerGroupTemplate) + @wsgi.deserializers(xml=ServerGroupXMLDeserializer) + def create(self, req, body): + """Creates a new server group.""" + context = _authorize_context(req) + + try: + self._validate_input_body(body, 'server_group') + except nova.exception.InvalidInput as e: + raise exc.HTTPBadRequest(explanation=e.format_message()) + + vals = body['server_group'] + sg = instance_group_obj.InstanceGroup() + sg.project_id = context.project_id + sg.user_id = context.user_id + try: + sg.name = vals.get('name') + sg.policies = vals.get('policies') + sg.create(context) + except ValueError as e: + raise exc.HTTPBadRequest(explanation=e) + + return {'server_group': self._format_server_group(context, sg)} + + +class ServerGroupsTemplateElement(xmlutil.TemplateElement): + def will_render(self, datum): + return "server_groups" in datum + + +class Server_groups(extensions.ExtensionDescriptor): + """Server group support.""" + name = "ServerGroups" + alias = "os-server-groups" + namespace = ("http://docs.openstack.org/compute/ext/" + "servergroups/api/v2") + updated = "2013-06-20T00:00:00+00:00" + + def get_resources(self): + resources = [] + + res = extensions.ResourceExtension( + 'os-server-groups', + controller=ServerGroupController(), + member_actions={"action": "POST", }) + + resources.append(res) + + return resources diff --git a/nova/tests/api/openstack/compute/contrib/test_server_groups.py b/nova/tests/api/openstack/compute/contrib/test_server_groups.py new file mode 100644 index 0000000000..1dee4682d9 --- /dev/null +++ b/nova/tests/api/openstack/compute/contrib/test_server_groups.py @@ -0,0 +1,389 @@ +# Copyright (c) 2014 Cisco Systems, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from lxml import etree +import webob + +from nova.api.openstack.compute.contrib import server_groups +from nova.api.openstack import wsgi +import nova.db +from nova import exception +from nova.openstack.common import uuidutils +from nova import test +from nova.tests.api.openstack import fakes +from nova.tests import utils + +FAKE_UUID1 = 'a47ae74e-ab08-447f-8eee-ffd43fc46c16' +FAKE_UUID2 = 'c6e6430a-6563-4efa-9542-5e93c9e97d18' +FAKE_UUID3 = 'b8713410-9ba3-e913-901b-13410ca90121' + + +class AttrDict(dict): + def __getattr__(self, k): + return self[k] + + +def server_group_template(**kwargs): + sgroup = kwargs.copy() + sgroup.setdefault('name', 'test') + return sgroup + + +def server_group_resp_template(**kwargs): + sgroup = kwargs.copy() + sgroup.setdefault('name', 'test') + sgroup.setdefault('policies', []) + sgroup.setdefault('members', []) + sgroup.setdefault('metadata', {}) + return sgroup + + +def server_group_db(sg): + attrs = sg.copy() + if 'id' in attrs: + attrs['uuid'] = attrs.pop('id') + if 'policies' in attrs: + policies = attrs.pop('policies') + attrs['policies'] = policies + else: + attrs['policies'] = [] + if 'members' in attrs: + members = attrs.pop('members') + attrs['members'] = members + else: + attrs['members'] = [] + if 'metadata' in attrs: + attrs['metadetails'] = attrs.pop('metadata') + else: + attrs['metadetails'] = {} + attrs['deleted'] = 0 + attrs['deleted_at'] = None + attrs['created_at'] = None + attrs['updated_at'] = None + if 'user_id' not in attrs: + attrs['user_id'] = 'user_id' + if 'project_id' not in attrs: + attrs['project_id'] = 'project_id' + attrs['id'] = 7 + + return AttrDict(attrs) + + +class ServerGroupTest(test.TestCase): + def setUp(self): + super(ServerGroupTest, self).setUp() + self.controller = server_groups.ServerGroupController() + self.app = fakes.wsgi_app(init_only=('os-server-groups',)) + + def test_create_server_group_with_no_policies(self): + req = fakes.HTTPRequest.blank('/v2/fake/os-server-groups') + sgroup = server_group_template() + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, + req, {'server_group': sgroup}) + + def test_create_server_group_normal(self): + req = fakes.HTTPRequest.blank('/v2/fake/os-server-groups') + sgroup = server_group_template() + policies = ['test_policy'] + sgroup['policies'] = policies + res_dict = self.controller.create(req, {'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']['policies'], policies) + + def test_create_server_group_with_illegal_name(self): + # blank name + sgroup = server_group_template(name='') + req = fakes.HTTPRequest.blank('/v2/fake/os-server-groups') + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, + req, {'server_group': sgroup}) + + # name with length 256 + sgroup = server_group_template(name='1234567890' * 26) + req = fakes.HTTPRequest.blank('/v2/fake/os-server-groups') + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, + req, {'server_group': sgroup}) + + # non-string name + sgroup = server_group_template(name=12) + req = fakes.HTTPRequest.blank('/v2/fake/os-server-groups') + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, + req, {'server_group': sgroup}) + + def test_create_server_group_with_no_body(self): + req = fakes.HTTPRequest.blank('/v2/fake/os-server-groups') + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.create, req, None) + + def test_create_server_group_with_no_server_group(self): + body = {'no-instanceGroup': None} + req = fakes.HTTPRequest.blank('/v2/fake/os-server-groups') + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.create, req, body) + + def test_list_server_group_by_tenant(self): + groups = [] + policies = ['test_policy'] + members = ['1', '2'] + metadata = {'key1': 'value1'} + names = ['default-x', 'test'] + sg1 = server_group_resp_template(id=str(1345), + name=names[0], + policies=policies, + members=members, + metadata=metadata) + sg2 = server_group_resp_template(id=str(891), + name=names[1], + policies=policies, + members=members, + metadata=metadata) + groups = [sg1, sg2] + expected = {'server_groups': groups} + + def return_server_groups(context, project_id): + return [server_group_db(sg) for sg in groups] + + self.stubs.Set(nova.db, 'instance_group_get_all_by_project_id', + return_server_groups) + + req = fakes.HTTPRequest.blank('/v2/fake/os-server-groups') + res_dict = self.controller.index(req) + self.assertEqual(res_dict, expected) + + def test_list_server_group_all(self): + all_groups = [] + tenant_groups = [] + policies = ['test_policy'] + members = ['1', '2'] + metadata = {'key1': 'value1'} + names = ['default-x', 'test'] + sg1 = server_group_resp_template(id=str(1345), + name=names[0], + policies=[], + members=members, + metadata=metadata) + sg2 = server_group_resp_template(id=str(891), + name=names[1], + policies=policies, + members=members, + metadata={}) + tenant_groups = [sg2] + all_groups = [sg1, sg2] + + all = {'server_groups': all_groups} + tenant_specific = {'server_groups': tenant_groups} + + def return_all_server_groups(context): + return [server_group_db(sg) for sg in all_groups] + + self.stubs.Set(nova.db, 'instance_group_get_all', + return_all_server_groups) + + def return_tenant_server_groups(context, project_id): + return [server_group_db(sg) for sg in tenant_groups] + + self.stubs.Set(nova.db, 'instance_group_get_all_by_project_id', + return_tenant_server_groups) + + path = '/v2/fake/os-server-groups?all_projects=True' + + req = fakes.HTTPRequest.blank(path, use_admin_context=True) + res_dict = self.controller.index(req) + self.assertEqual(res_dict, all) + req = fakes.HTTPRequest.blank(path) + res_dict = self.controller.index(req) + self.assertEqual(res_dict, tenant_specific) + + def test_delete_server_group_by_id(self): + sg = server_group_template(id='123') + + self.called = False + + def server_group_delete(context, id): + self.called = True + + def return_server_group(context, group_id): + self.assertEqual(sg['id'], group_id) + return server_group_db(sg) + + self.stubs.Set(nova.db, 'instance_group_delete', + server_group_delete) + self.stubs.Set(nova.db, 'instance_group_get', + return_server_group) + + req = fakes.HTTPRequest.blank('/v2/fake/os-server-groups/123') + resp = self.controller.delete(req, '123') + self.assertTrue(self.called) + self.assertEqual(resp.status_int, 204) + + def test_delete_non_existing_server_group(self): + req = fakes.HTTPRequest.blank('/v2/fake/os-server-groups/invalid') + self.assertRaises(webob.exc.HTTPNotFound, self.controller.delete, + req, 'invalid') + + +class TestServerGroupXMLDeserializer(test.TestCase): + + def setUp(self): + super(TestServerGroupXMLDeserializer, self).setUp() + self.deserializer = server_groups.ServerGroupXMLDeserializer() + + def test_create_request(self): + serial_request = """ + +""" + request = self.deserializer.deserialize(serial_request) + expected = { + "server_group": { + "name": "test", + "policies": [] + }, + } + self.assertEqual(request['body'], expected) + + def test_update_request(self): + serial_request = """ + + +policy-1 +policy-2 + +""" + request = self.deserializer.deserialize(serial_request) + expected = { + "server_group": { + "name": 'test', + "policies": ['policy-1', 'policy-2'] + }, + } + self.assertEqual(request['body'], expected) + + def test_create_request_no_name(self): + serial_request = """ + +""" + request = self.deserializer.deserialize(serial_request) + expected = { + "server_group": { + "policies": [] + }, + } + self.assertEqual(request['body'], expected) + + def test_corrupt_xml(self): + """Should throw a 400 error on corrupt xml.""" + self.assertRaises( + exception.MalformedRequestBody, + self.deserializer.deserialize, + utils.killer_xml_body()) + + +class TestServerGroupXMLSerializer(test.TestCase): + def setUp(self): + super(TestServerGroupXMLSerializer, self).setUp() + self.namespace = wsgi.XMLNS_V11 + self.index_serializer = server_groups.ServerGroupsTemplate() + self.default_serializer = server_groups.ServerGroupTemplate() + + def _tag(self, elem): + tagname = elem.tag + self.assertEqual(tagname[0], '{') + tmp = tagname.partition('}') + namespace = tmp[0][1:] + self.assertEqual(namespace, self.namespace) + return tmp[2] + + def _verify_server_group(self, raw_group, tree): + policies = raw_group['policies'] + members = raw_group['members'] + metadata = raw_group['metadata'] + self.assertEqual('server_group', self._tag(tree)) + self.assertEqual(raw_group['id'], tree.get('id')) + self.assertEqual(raw_group['name'], tree.get('name')) + self.assertEqual(3, len(tree)) + for child in tree: + child_tag = self._tag(child) + if child_tag == 'policies': + self.assertEqual(len(policies), len(child)) + for idx, gr_child in enumerate(child): + self.assertEqual(self._tag(gr_child), 'policy') + self.assertEqual(policies[idx], + gr_child.text) + elif child_tag == 'members': + self.assertEqual(len(members), len(child)) + for idx, gr_child in enumerate(child): + self.assertEqual(self._tag(gr_child), 'member') + self.assertEqual(members[idx], + gr_child.text) + elif child_tag == 'metadata': + self.assertEqual(len(metadata), len(child)) + metas = {} + for idx, gr_child in enumerate(child): + self.assertEqual(self._tag(gr_child), 'meta') + key = gr_child.get('key') + self.assertTrue(key in ['key1', 'key2']) + metas[key] = gr_child.text + self.assertEqual(len(metas), len(metadata)) + for k in metadata: + self.assertEqual(metadata[k], metas[k]) + + def _verify_server_group_brief(self, raw_group, tree): + self.assertEqual('server_group', self._tag(tree)) + self.assertEqual(raw_group['id'], tree.get('id')) + self.assertEqual(raw_group['name'], tree.get('name')) + + def test_group_serializer(self): + policies = ["policy-1", "policy-2"] + members = ["1", "2"] + metadata = dict(key1="value1", key2="value2") + raw_group = dict( + id='890', + name='name', + policies=policies, + members=members, + metadata=metadata) + sg_group = dict(server_group=raw_group) + text = self.default_serializer.serialize(sg_group) + + tree = etree.fromstring(text) + + self._verify_server_group(raw_group, tree) + + def test_groups_serializer(self): + policies = ["policy-1", "policy-2", + "policy-3"] + members = ["1", "2", "3"] + metadata = dict(key1="value1", key2="value2") + groups = [dict( + id='890', + name='test', + policies=policies[0:2], + members=members[0:2], + metadata=metadata), + dict( + id='123', + name='default', + policies=policies[2:], + members=members[2:], + metadata=metadata)] + sg_groups = dict(server_groups=groups) + text = self.index_serializer.serialize(sg_groups) + + tree = etree.fromstring(text) + + self.assertEqual('server_groups', self._tag(tree)) + self.assertEqual(len(groups), len(tree)) + for idx, child in enumerate(tree): + self._verify_server_group_brief(groups[idx], child) diff --git a/nova/tests/fake_policy.py b/nova/tests/fake_policy.py index 67fa0410c2..986518b7a7 100644 --- a/nova/tests/fake_policy.py +++ b/nova/tests/fake_policy.py @@ -255,6 +255,7 @@ policy_data = """ "compute_extension:v3:os-security-groups": "", "compute_extension:server_diagnostics": "", "compute_extension:v3:os-server-diagnostics": "", + "compute_extension:server_groups": "", "compute_extension:server_password": "", "compute_extension:v3:os-server-password": "", "compute_extension:server_usage": "", diff --git a/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.json.tpl b/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.json.tpl index 5fbca8c885..70992ba252 100644 --- a/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.json.tpl +++ b/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.json.tpl @@ -647,6 +647,14 @@ "name": "ExtendedServicesDelete", "namespace": "http://docs.openstack.org/compute/ext/extended_services_delete/api/v2", "updated": "%(timestamp)s" + }, + { + "alias": "os-server-groups", + "description": "%(text)s", + "links": [], + "name": "ServerGroups", + "namespace": "http://docs.openstack.org/compute/ext/servergroups/api/v2", + "updated": "%(timestamp)s" } ] } diff --git a/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.xml.tpl b/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.xml.tpl index 2aa684a9c0..f4be29ae30 100644 --- a/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.xml.tpl +++ b/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.xml.tpl @@ -242,4 +242,7 @@ %(text)s + + %(text)s + diff --git a/nova/tests/integrated/api_samples/os-server-groups/server-groups-get-resp.json.tpl b/nova/tests/integrated/api_samples/os-server-groups/server-groups-get-resp.json.tpl new file mode 100644 index 0000000000..213464121c --- /dev/null +++ b/nova/tests/integrated/api_samples/os-server-groups/server-groups-get-resp.json.tpl @@ -0,0 +1,9 @@ +{ + "server_group": { + "id": "%(id)s", + "name": "%(name)s", + "policies": ["test_policy"], + "members": [], + "metadata": {} + } +} diff --git a/nova/tests/integrated/api_samples/os-server-groups/server-groups-get-resp.xml.tpl b/nova/tests/integrated/api_samples/os-server-groups/server-groups-get-resp.xml.tpl new file mode 100644 index 0000000000..7283e9d003 --- /dev/null +++ b/nova/tests/integrated/api_samples/os-server-groups/server-groups-get-resp.xml.tpl @@ -0,0 +1,8 @@ + + + + test_policy + + + + diff --git a/nova/tests/integrated/api_samples/os-server-groups/server-groups-list-resp.json.tpl b/nova/tests/integrated/api_samples/os-server-groups/server-groups-list-resp.json.tpl new file mode 100644 index 0000000000..d55d8c6fa0 --- /dev/null +++ b/nova/tests/integrated/api_samples/os-server-groups/server-groups-list-resp.json.tpl @@ -0,0 +1,11 @@ +{ + "server_groups": [ + { + "id": "%(id)s", + "name": "test", + "policies": ["test_policy"], + "members": [], + "metadata": {} + } + ] +} diff --git a/nova/tests/integrated/api_samples/os-server-groups/server-groups-list-resp.xml.tpl b/nova/tests/integrated/api_samples/os-server-groups/server-groups-list-resp.xml.tpl new file mode 100644 index 0000000000..ca512db784 --- /dev/null +++ b/nova/tests/integrated/api_samples/os-server-groups/server-groups-list-resp.xml.tpl @@ -0,0 +1,10 @@ + + + + + test_policy + + + + + diff --git a/nova/tests/integrated/api_samples/os-server-groups/server-groups-post-req.json.tpl b/nova/tests/integrated/api_samples/os-server-groups/server-groups-post-req.json.tpl new file mode 100644 index 0000000000..894511e11c --- /dev/null +++ b/nova/tests/integrated/api_samples/os-server-groups/server-groups-post-req.json.tpl @@ -0,0 +1,6 @@ +{ + "server_group": { + "name": "%(name)s", + "policies": ["test_policy"] + } +} diff --git a/nova/tests/integrated/api_samples/os-server-groups/server-groups-post-req.xml.tpl b/nova/tests/integrated/api_samples/os-server-groups/server-groups-post-req.xml.tpl new file mode 100644 index 0000000000..628585973f --- /dev/null +++ b/nova/tests/integrated/api_samples/os-server-groups/server-groups-post-req.xml.tpl @@ -0,0 +1,5 @@ + + + test_policy + + diff --git a/nova/tests/integrated/api_samples/os-server-groups/server-groups-post-resp.json.tpl b/nova/tests/integrated/api_samples/os-server-groups/server-groups-post-resp.json.tpl new file mode 100644 index 0000000000..a642e9166d --- /dev/null +++ b/nova/tests/integrated/api_samples/os-server-groups/server-groups-post-resp.json.tpl @@ -0,0 +1,10 @@ +{ + "server_group": { + "id": "%(id)s", + "name": "%(name)s", + "policies": ["test_policy"], + "members": [], + "metadata": {} + } +} + diff --git a/nova/tests/integrated/api_samples/os-server-groups/server-groups-post-resp.xml.tpl b/nova/tests/integrated/api_samples/os-server-groups/server-groups-post-resp.xml.tpl new file mode 100644 index 0000000000..7283e9d003 --- /dev/null +++ b/nova/tests/integrated/api_samples/os-server-groups/server-groups-post-resp.xml.tpl @@ -0,0 +1,8 @@ + + + + test_policy + + + + diff --git a/nova/tests/integrated/test_api_samples.py b/nova/tests/integrated/test_api_samples.py index 1dc812403e..2233b877b4 100644 --- a/nova/tests/integrated/test_api_samples.py +++ b/nova/tests/integrated/test_api_samples.py @@ -4160,3 +4160,58 @@ class ServerExternalEventsJsonTest(ServersSampleBase): class ServerExternalEventsXmlTest(ServerExternalEventsJsonTest): ctype = 'xml' + + +class ServerGroupsSampleJsonTest(ServersSampleBase): + extension_name = ("nova.api.openstack.compute.contrib" + ".server_groups.Server_groups") + + def _get_create_subs(self): + return {'name': 'test'} + + def _post_server_group(self): + """Verify the response status and returns the UUID of the + newly created server group. + """ + subs = self._get_create_subs() + response = self._do_post('os-server-groups', + 'server-groups-post-req', subs) + subs = self._get_regexes() + subs['name'] = 'test' + return self._verify_response('server-groups-post-resp', + subs, response, 200) + + def _create_server_group(self): + subs = self._get_create_subs() + return self._do_post('os-server-groups', + 'server-groups-post-req', subs) + + def test_server_groups_post(self): + return self._post_server_group() + + def test_server_groups_list(self): + subs = self._get_create_subs() + uuid = self._post_server_group() + response = self._do_get('os-server-groups') + subs.update(self._get_regexes()) + subs['id'] = uuid + self._verify_response('server-groups-list-resp', + subs, response, 200) + + def test_server_groups_get(self): + # Get api sample of server groups get request. + subs = {'name': 'test'} + uuid = self._post_server_group() + subs['id'] = uuid + response = self._do_get('os-server-groups/%s' % uuid) + + self._verify_response('server-groups-get-resp', subs, response, 200) + + def test_server_groups_delete(self): + uuid = self._post_server_group() + response = self._do_delete('os-server-groups/%s' % uuid) + self.assertEqual(response.status, 204) + + +class ServerGroupsSampleXmlTest(ServerGroupsSampleJsonTest): + ctype = 'xml'