Porting SecurityGroup related controller into v2.1

This patch port SecurityGroup, ServerSecurityGroup and SecurityGroupAction
controller into v2.1 security_groups extension.

This patch just move the v2 code into v2.1 and share the unittest. The other
v2.1 related fix and improvement will be addressed by subsequent patchset.

Partially implements blueprint v2-on-v3-api

Change-Id: I93951ce677b74ebed7db1b2fff1f788344806dba
This commit is contained in:
He Jie Xu 2014-09-18 16:53:40 +08:00
parent 45c2205ef1
commit a9d446d627
22 changed files with 539 additions and 39 deletions

View File

@ -0,0 +1,5 @@
{
"addSecurityGroup": {
"name": "test"
}
}

View File

@ -0,0 +1,6 @@
{
"security_group": {
"name": "test",
"description": "description"
}
}

View File

@ -0,0 +1,5 @@
{
"removeSecurityGroup": {
"name": "test"
}
}

View File

@ -0,0 +1,9 @@
{
"security_group": {
"description": "default",
"id": 1,
"name": "default",
"rules": [],
"tenant_id": "openstack"
}
}

View File

@ -0,0 +1,9 @@
{
"security_group": {
"description": "default",
"id": 1,
"name": "default",
"rules": [],
"tenant_id": "openstack"
}
}

View File

@ -0,0 +1,11 @@
{
"security_groups": [
{
"description": "default",
"id": 1,
"name": "default",
"rules": [],
"tenant_id": "openstack"
}
]
}

View File

@ -10,7 +10,7 @@
}
]
},
"created": "2013-09-25T03:29:13Z",
"created": "2014-09-18T10:13:33Z",
"flavor": {
"id": "1",
"links": [
@ -20,8 +20,8 @@
}
]
},
"hostId": "0e312d6763795d572ccd716973fd078290d9ec446517b222d3395660",
"id": "f6961f7a-0133-4f27-94cd-901dca4ba426",
"hostId": "24451d49cba30e60300a5b928ebc93a2d0b43c084a677b0a14fd678b",
"id": "b08eb8d8-db43-44fb-bd89-dfe3302b84ef",
"image": {
"id": "70a599e0-31e7-49b7-b260-868f441e862b",
"links": [
@ -34,11 +34,11 @@
"key_name": null,
"links": [
{
"href": "http://openstack.example.com/v3/servers/f6961f7a-0133-4f27-94cd-901dca4ba426",
"href": "http://openstack.example.com/v3/servers/b08eb8d8-db43-44fb-bd89-dfe3302b84ef",
"rel": "self"
},
{
"href": "http://openstack.example.com/servers/f6961f7a-0133-4f27-94cd-901dca4ba426",
"href": "http://openstack.example.com/servers/b08eb8d8-db43-44fb-bd89-dfe3302b84ef",
"rel": "bookmark"
}
],
@ -54,7 +54,7 @@
],
"status": "ACTIVE",
"tenant_id": "openstack",
"updated": "2013-09-25T03:29:14Z",
"updated": "2014-09-18T10:13:34Z",
"user_id": "fake"
}
}

View File

@ -8,4 +8,4 @@
},
"security_groups": [{"name": "test"}]
}
}
}

View File

@ -1,14 +1,14 @@
{
"server": {
"adminPass": "ki8cbWeZdxH6",
"id": "2dabdd93-ced7-4607-a542-2516de84e0e5",
"adminPass": "xhS2khTdkRkT",
"id": "60874907-c72b-4a01-805d-54c992510e47",
"links": [
{
"href": "http://openstack.example.com/v3/servers/2dabdd93-ced7-4607-a542-2516de84e0e5",
"href": "http://openstack.example.com/v3/servers/60874907-c72b-4a01-805d-54c992510e47",
"rel": "self"
},
{
"href": "http://openstack.example.com/servers/2dabdd93-ced7-4607-a542-2516de84e0e5",
"href": "http://openstack.example.com/servers/60874907-c72b-4a01-805d-54c992510e47",
"rel": "bookmark"
}
],
@ -18,4 +18,4 @@
}
]
}
}
}

View File

@ -0,0 +1,11 @@
{
"security_groups": [
{
"description": "default",
"id": 1,
"name": "default",
"rules": [],
"tenant_id": "openstack"
}
]
}

View File

@ -11,7 +11,7 @@
}
]
},
"created": "2013-09-25T03:29:11Z",
"created": "2014-09-18T10:13:33Z",
"flavor": {
"id": "1",
"links": [
@ -21,8 +21,8 @@
}
]
},
"hostId": "afeeb125d4d37d0a2123e3144a20a6672fda5d4b6cb85ec193430d82",
"id": "1b94e3fc-1b1c-431a-a077-6b280fb720ce",
"hostId": "2ab794bccd321fe64f9f8b679266aa2c96825f467434bbdd71b09b1d",
"id": "d182742c-6f20-479c-8e32-f79f9c9df6e3",
"image": {
"id": "70a599e0-31e7-49b7-b260-868f441e862b",
"links": [
@ -35,11 +35,11 @@
"key_name": null,
"links": [
{
"href": "http://openstack.example.com/v3/servers/1b94e3fc-1b1c-431a-a077-6b280fb720ce",
"href": "http://openstack.example.com/v3/servers/d182742c-6f20-479c-8e32-f79f9c9df6e3",
"rel": "self"
},
{
"href": "http://openstack.example.com/servers/1b94e3fc-1b1c-431a-a077-6b280fb720ce",
"href": "http://openstack.example.com/servers/d182742c-6f20-479c-8e32-f79f9c9df6e3",
"rel": "bookmark"
}
],
@ -55,7 +55,7 @@
],
"status": "ACTIVE",
"tenant_id": "openstack",
"updated": "2013-09-25T03:29:12Z",
"updated": "2014-09-18T10:13:34Z",
"user_id": "fake"
}
]

View File

@ -15,9 +15,12 @@
# under the License.
"""The security groups extension."""
import contextlib
from oslo.serialization import jsonutils
from webob import exc
from nova.api.openstack import common
from nova.api.openstack.compute.schemas.v3 import security_groups as \
schema_security_groups
from nova.api.openstack import extensions
@ -25,10 +28,13 @@ from nova.api.openstack import wsgi
from nova import compute
from nova.compute import api as compute_api
from nova import exception
from nova.i18n import _
from nova.network.security_group import neutron_driver
from nova.network.security_group import openstack_driver
from nova.openstack.common import log as logging
LOG = logging.getLogger(__name__)
ALIAS = 'os-security-groups'
ATTRIBUTE_NAME = 'security_groups'
authorize = extensions.extension_authorizer('compute', 'v3:' + ALIAS)
@ -41,6 +47,263 @@ def _authorize_context(req):
return context
@contextlib.contextmanager
def translate_exceptions():
"""Translate nova exceptions to http exceptions."""
try:
yield
except exception.Invalid as exp:
msg = exp.format_message()
raise exc.HTTPBadRequest(explanation=msg)
except exception.SecurityGroupNotFound as exp:
msg = exp.format_message()
raise exc.HTTPNotFound(explanation=msg)
except exception.InstanceNotFound as exp:
msg = exp.format_message()
raise exc.HTTPNotFound(explanation=msg)
except exception.SecurityGroupLimitExceeded as exp:
msg = exp.format_message()
raise exc.HTTPForbidden(explanation=msg)
except exception.NoUniqueMatch as exp:
msg = exp.format_message()
raise exc.HTTPConflict(explanation=msg)
class SecurityGroupControllerBase(wsgi.Controller):
"""Base class for Security Group controllers."""
def __init__(self):
self.security_group_api = (
openstack_driver.get_openstack_security_group_driver())
self.compute_api = compute.API(
security_group_api=self.security_group_api)
def _format_security_group_rule(self, context, rule, group_rule_data=None):
"""Return a secuity group rule in desired API response format.
If group_rule_data is passed in that is used rather than querying
for it.
"""
sg_rule = {}
sg_rule['id'] = rule['id']
sg_rule['parent_group_id'] = rule['parent_group_id']
sg_rule['ip_protocol'] = rule['protocol']
sg_rule['from_port'] = rule['from_port']
sg_rule['to_port'] = rule['to_port']
sg_rule['group'] = {}
sg_rule['ip_range'] = {}
if rule['group_id']:
with translate_exceptions():
try:
source_group = self.security_group_api.get(
context, id=rule['group_id'])
except exception.SecurityGroupNotFound:
# NOTE(arosen): There is a possible race condition that can
# occur here if two api calls occur concurrently: one that
# lists the security groups and another one that deletes a
# security group rule that has a group_id before the
# group_id is fetched. To handle this if
# SecurityGroupNotFound is raised we return None instead
# of the rule and the caller should ignore the rule.
LOG.debug("Security Group ID %s does not exist",
rule['group_id'])
return
sg_rule['group'] = {'name': source_group.get('name'),
'tenant_id': source_group.get('project_id')}
elif group_rule_data:
sg_rule['group'] = group_rule_data
else:
sg_rule['ip_range'] = {'cidr': rule['cidr']}
return sg_rule
def _format_security_group(self, context, group):
security_group = {}
security_group['id'] = group['id']
security_group['description'] = group['description']
security_group['name'] = group['name']
security_group['tenant_id'] = group['project_id']
security_group['rules'] = []
for rule in group['rules']:
formatted_rule = self._format_security_group_rule(context, rule)
if formatted_rule:
security_group['rules'] += [formatted_rule]
return security_group
def _from_body(self, body, key):
if not body:
raise exc.HTTPBadRequest(
explanation=_("The request body can't be empty"))
value = body.get(key, None)
if value is None:
raise exc.HTTPBadRequest(
explanation=_("Missing parameter %s") % key)
return value
class SecurityGroupController(SecurityGroupControllerBase):
"""The Security group API controller for the OpenStack API."""
def show(self, req, id):
"""Return data about the given security group."""
context = _authorize_context(req)
with translate_exceptions():
id = self.security_group_api.validate_id(id)
security_group = self.security_group_api.get(context, None, id,
map_exception=True)
return {'security_group': self._format_security_group(context,
security_group)}
@wsgi.response(202)
def delete(self, req, id):
"""Delete a security group."""
context = _authorize_context(req)
with translate_exceptions():
id = self.security_group_api.validate_id(id)
security_group = self.security_group_api.get(context, None, id,
map_exception=True)
self.security_group_api.destroy(context, security_group)
def index(self, req):
"""Returns a list of security groups."""
context = _authorize_context(req)
search_opts = {}
search_opts.update(req.GET)
with translate_exceptions():
project_id = context.project_id
raw_groups = self.security_group_api.list(context,
project=project_id,
search_opts=search_opts)
limited_list = common.limited(raw_groups, req)
result = [self._format_security_group(context, group)
for group in limited_list]
return {'security_groups':
list(sorted(result,
key=lambda k: (k['tenant_id'], k['name'])))}
def create(self, req, body):
"""Creates a new security group."""
context = _authorize_context(req)
security_group = self._from_body(body, 'security_group')
group_name = security_group.get('name', None)
group_description = security_group.get('description', None)
with translate_exceptions():
self.security_group_api.validate_property(group_name, 'name', None)
self.security_group_api.validate_property(group_description,
'description', None)
group_ref = self.security_group_api.create_security_group(
context, group_name, group_description)
return {'security_group': self._format_security_group(context,
group_ref)}
def update(self, req, id, body):
"""Update a security group."""
context = _authorize_context(req)
with translate_exceptions():
id = self.security_group_api.validate_id(id)
security_group = self.security_group_api.get(context, None, id,
map_exception=True)
security_group_data = self._from_body(body, 'security_group')
group_name = security_group_data.get('name', None)
group_description = security_group_data.get('description', None)
with translate_exceptions():
self.security_group_api.validate_property(group_name, 'name', None)
self.security_group_api.validate_property(group_description,
'description', None)
group_ref = self.security_group_api.update_security_group(
context, security_group, group_name, group_description)
return {'security_group': self._format_security_group(context,
group_ref)}
class ServerSecurityGroupController(SecurityGroupControllerBase):
def index(self, req, server_id):
"""Returns a list of security groups for the given instance."""
context = _authorize_context(req)
self.security_group_api.ensure_default(context)
with translate_exceptions():
instance = self.compute_api.get(context, server_id)
groups = self.security_group_api.get_instance_security_groups(
context, instance['uuid'], True)
result = [self._format_security_group(context, group)
for group in groups]
return {'security_groups':
list(sorted(result,
key=lambda k: (k['tenant_id'], k['name'])))}
class SecurityGroupActionController(wsgi.Controller):
def __init__(self, *args, **kwargs):
super(SecurityGroupActionController, self).__init__(*args, **kwargs)
self.security_group_api = (
openstack_driver.get_openstack_security_group_driver())
self.compute_api = compute.API(
security_group_api=self.security_group_api)
def _parse(self, body, action):
try:
body = body[action]
group_name = body['name']
except TypeError:
msg = _("Missing parameter dict")
raise exc.HTTPBadRequest(explanation=msg)
except KeyError:
msg = _("Security group not specified")
raise exc.HTTPBadRequest(explanation=msg)
if not group_name or group_name.strip() == '':
msg = _("Security group name cannot be empty")
raise exc.HTTPBadRequest(explanation=msg)
return group_name
def _invoke(self, method, context, id, group_name):
with translate_exceptions():
instance = self.compute_api.get(context, id)
method(context, instance, group_name)
@wsgi.response(202)
@wsgi.action('addSecurityGroup')
def _addSecurityGroup(self, req, id, body):
context = req.environ['nova.context']
authorize(context)
group_name = self._parse(body, 'addSecurityGroup')
return self._invoke(self.security_group_api.add_to_instance,
context, id, group_name)
@wsgi.response(202)
@wsgi.action('removeSecurityGroup')
def _removeSecurityGroup(self, req, id, body):
context = req.environ['nova.context']
authorize(context)
group_name = self._parse(body, 'removeSecurityGroup')
return self._invoke(self.security_group_api.remove_from_instance,
context, id, group_name)
class SecurityGroupsOutputController(wsgi.Controller):
def __init__(self, *args, **kwargs):
super(SecurityGroupsOutputController, self).__init__(*args, **kwargs)
@ -115,12 +378,20 @@ class SecurityGroups(extensions.V3APIExtensionBase):
version = 1
def get_controller_extensions(self):
controller = SecurityGroupsOutputController()
output = extensions.ControllerExtension(self, 'servers', controller)
return [output]
secgrp_output_ext = extensions.ControllerExtension(
self, 'servers', SecurityGroupsOutputController())
secgrp_act_ext = extensions.ControllerExtension(
self, 'servers', SecurityGroupActionController())
return [secgrp_output_ext, secgrp_act_ext]
def get_resources(self):
return []
secgrp_ext = extensions.ResourceExtension('os-security-groups',
SecurityGroupController())
server_secgrp_ext = extensions.ResourceExtension(
'os-security-groups',
controller=ServerSecurityGroupController(),
parent=dict(member_name='server', collection_name='servers'))
return [secgrp_ext, server_secgrp_ext]
# NOTE(gmann): This function is not supposed to use 'body_deprecated_param'
# parameter as this is placed to handle scheduler_hint extension for V2.1.

View File

@ -51,8 +51,8 @@ class TestNeutronSecurityGroupsTestCase(test.TestCase):
super(TestNeutronSecurityGroupsTestCase, self).tearDown()
class TestNeutronSecurityGroups(
test_security_groups.TestSecurityGroups,
class TestNeutronSecurityGroupsV21(
test_security_groups.TestSecurityGroupsV21,
TestNeutronSecurityGroupsTestCase):
def _create_sg_template(self, **kwargs):
@ -400,6 +400,12 @@ class TestNeutronSecurityGroups(
device_id=test_security_groups.FAKE_UUID1)
class TestNeutronSecurityGroupsV2(TestNeutronSecurityGroupsV21):
controller_cls = security_groups.SecurityGroupController
server_secgrp_ctl_cls = security_groups.ServerSecurityGroupController
secgrp_act_ctl_cls = security_groups.SecurityGroupActionController
class TestNeutronSecurityGroupRulesTestCase(TestNeutronSecurityGroupsTestCase):
def setUp(self):
super(TestNeutronSecurityGroupRulesTestCase, self).setUp()

View File

@ -21,6 +21,8 @@ from oslo.serialization import jsonutils
import webob
from nova.api.openstack.compute.contrib import security_groups as secgroups_v2
from nova.api.openstack.compute.plugins.v3 import security_groups as \
secgroups_v21
from nova.api.openstack import wsgi
from nova.api.openstack import xmlutil
from nova import compute
@ -121,17 +123,17 @@ def return_server_nonexistent(context, server_id, columns_to_join=None):
raise exception.InstanceNotFound(instance_id=server_id)
# NOTE(oomichi): v2.1 API does not support security group management (create/
# update/delete a security group). We don't need to test this class against
# v2.1 API.
class TestSecurityGroups(test.TestCase):
def setUp(self):
super(TestSecurityGroups, self).setUp()
class TestSecurityGroupsV21(test.TestCase):
secgrp_ctl_cls = secgroups_v21.SecurityGroupController
server_secgrp_ctl_cls = secgroups_v21.ServerSecurityGroupController
secgrp_act_ctl_cls = secgroups_v21.SecurityGroupActionController
self.controller = secgroups_v2.SecurityGroupController()
self.server_controller = (
secgroups_v2.ServerSecurityGroupController())
self.manager = secgroups_v2.SecurityGroupActionController()
def setUp(self):
super(TestSecurityGroupsV21, self).setUp()
self.controller = self.secgrp_ctl_cls()
self.server_controller = self.server_secgrp_ctl_cls()
self.manager = self.secgrp_act_ctl_cls()
# This needs to be done here to set fake_id because the derived
# class needs to be called first if it wants to set
@ -807,6 +809,12 @@ class TestSecurityGroups(test.TestCase):
self.manager._removeSecurityGroup(req, '1', body)
class TestSecurityGroupsV2(TestSecurityGroupsV21):
controller_cls = secgroups_v2.SecurityGroupController
server_secgrp_ctl_cls = secgroups_v2.ServerSecurityGroupController
secgrp_act_ctl_cls = secgroups_v2.SecurityGroupActionController
# NOTE(oomichi): v2.1 API does not support security group management (create/
# update/delete a security group). We don't need to test this class against
# v2.1 API.

View File

@ -0,0 +1,5 @@
{
"addSecurityGroup": {
"name": "%(group_name)s"
}
}

View File

@ -0,0 +1,6 @@
{
"security_group": {
"name": "%(group_name)s",
"description": "description"
}
}

View File

@ -0,0 +1,5 @@
{
"removeSecurityGroup": {
"name": "%(group_name)s"
}
}

View File

@ -0,0 +1,9 @@
{
"security_group": {
"description": "%(description)s",
"id": 1,
"name": "%(group_name)s",
"rules": [],
"tenant_id": "openstack"
}
}

View File

@ -0,0 +1,9 @@
{
"security_group": {
"description": "default",
"id": 1,
"name": "default",
"rules": [],
"tenant_id": "openstack"
}
}

View File

@ -0,0 +1,11 @@
{
"security_groups": [
{
"description": "default",
"id": 1,
"name": "default",
"rules": [],
"tenant_id": "openstack"
}
]
}

View File

@ -0,0 +1,11 @@
{
"security_groups": [
{
"description": "default",
"id": 1,
"name": "default",
"rules": [],
"tenant_id": "openstack"
}
]
}

View File

@ -19,21 +19,44 @@ from nova.tests.integrated.v3 import test_servers
def fake_get(*args, **kwargs):
nova_group = {}
nova_group['id'] = 'fake'
nova_group['description'] = ''
nova_group['name'] = 'test'
nova_group['project_id'] = 'fake'
nova_group['id'] = 1
nova_group['description'] = 'default'
nova_group['name'] = 'default'
nova_group['project_id'] = 'openstack'
nova_group['rules'] = []
return nova_group
def fake_get_instances_security_groups_bindings(self, context, servers):
def fake_get_instances_security_groups_bindings(self, context, servers,
detailed=False):
result = {}
for s in servers:
result[s.get('id')] = [{'name': 'test'}]
return result
def fake_add_to_instance(self, context, instance, security_group_name):
pass
def fake_remove_from_instance(self, context, instance, security_group_name):
pass
def fake_list(self, context, names=None, ids=None, project=None,
search_opts=None):
return [fake_get()]
def fake_get_instance_security_groups(self, context, instance_uuid,
detailed=False):
return [fake_get()]
def fake_create_security_group(self, context, name, description):
return fake_get()
class SecurityGroupsJsonTest(test_servers.ServersSampleBase):
extension_name = 'os-security-groups'
@ -44,6 +67,21 @@ class SecurityGroupsJsonTest(test_servers.ServersSampleBase):
self.stubs.Set(neutron_driver.SecurityGroupAPI,
'get_instances_security_groups_bindings',
fake_get_instances_security_groups_bindings)
self.stubs.Set(neutron_driver.SecurityGroupAPI,
'add_to_instance',
fake_add_to_instance)
self.stubs.Set(neutron_driver.SecurityGroupAPI,
'remove_from_instance',
fake_remove_from_instance)
self.stubs.Set(neutron_driver.SecurityGroupAPI,
'list',
fake_list)
self.stubs.Set(neutron_driver.SecurityGroupAPI,
'get_instance_security_groups',
fake_get_instance_security_groups)
self.stubs.Set(neutron_driver.SecurityGroupAPI,
'create_security_group',
fake_create_security_group)
def test_server_create(self):
self._post_server()
@ -61,3 +99,68 @@ class SecurityGroupsJsonTest(test_servers.ServersSampleBase):
subs = self._get_regexes()
subs['hostid'] = '[a-f0-9]+'
self._verify_response('servers-detail-resp', subs, response, 200)
def _get_create_subs(self):
return {
'group_name': 'default',
"description": "default",
}
def _create_security_group(self):
subs = self._get_create_subs()
return self._do_post('os-security-groups',
'security-group-post-req', subs)
def _add_group(self, uuid):
subs = {
'group_name': 'test'
}
return self._do_post('servers/%s/action' % uuid,
'security-group-add-post-req', subs)
def test_security_group_create(self):
response = self._create_security_group()
subs = self._get_create_subs()
self._verify_response('security-groups-create-resp', subs,
response, 200)
def test_security_groups_list(self):
# Get api sample of security groups get list request.
response = self._do_get('os-security-groups')
subs = self._get_regexes()
self._verify_response('security-groups-list-get-resp',
subs, response, 200)
def test_security_groups_get(self):
# Get api sample of security groups get request.
security_group_id = '11111111-1111-1111-1111-111111111111'
response = self._do_get('os-security-groups/%s' % security_group_id)
subs = self._get_regexes()
self._verify_response('security-groups-get-resp', subs, response, 200)
def test_security_groups_list_server(self):
# Get api sample of security groups for a specific server.
uuid = self._post_server()
response = self._do_get('servers/%s/os-security-groups' % uuid)
subs = self._get_regexes()
self._verify_response('server-security-groups-list-resp',
subs, response, 200)
def test_security_groups_add(self):
self._create_security_group()
uuid = self._post_server()
response = self._add_group(uuid)
self.assertEqual(response.status_code, 202)
self.assertEqual(response.content, '')
def test_security_groups_remove(self):
self._create_security_group()
uuid = self._post_server()
self._add_group(uuid)
subs = {
'group_name': 'test'
}
response = self._do_post('servers/%s/action' % uuid,
'security-group-remove-post-req', subs)
self.assertEqual(response.status_code, 202)
self.assertEqual(response.content, '')