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'