From c81ad66e7e9cda6034b31722bad54c924233d3ad Mon Sep 17 00:00:00 2001 From: Aleks Chirko Date: Tue, 14 Jan 2014 16:28:06 +0200 Subject: [PATCH] Add share's networks API Add server side for share's networks. Implemented controller will carry user requests to the DB and thus will allow user to manage share's networks data. Add share's networks support to the share API. Partially implements bp: join-tenant-network Change-Id: Ie4f3945255a049e80083f08a39d7f703a5c75c5e --- manila/api/v1/router.py | 7 + manila/api/v1/security_service.py | 42 ++-- manila/api/v1/share_networks.py | 227 +++++++++++++++++++ manila/api/v1/shares.py | 2 + manila/api/views/share_networks.py | 51 +++++ manila/api/views/shares.py | 1 + manila/share/api.py | 4 +- manila/tests/api/contrib/stubs.py | 1 + manila/tests/api/v1/test_share_networks.py | 248 +++++++++++++++++++++ manila/tests/api/v1/test_shares.py | 2 + manila/tests/test_share_api.py | 1 + 11 files changed, 567 insertions(+), 19 deletions(-) create mode 100644 manila/api/v1/share_networks.py create mode 100644 manila/api/views/share_networks.py create mode 100644 manila/tests/api/v1/test_share_networks.py diff --git a/manila/api/v1/router.py b/manila/api/v1/router.py index 77fbcd2b88..09ea1ff5af 100644 --- a/manila/api/v1/router.py +++ b/manila/api/v1/router.py @@ -28,6 +28,7 @@ from manila.api import versions from manila.api.v1 import security_service from manila.api.v1 import share_metadata +from manila.api.v1 import share_networks from manila.api.v1 import share_snapshots from manila.api.v1 import shares @@ -86,3 +87,9 @@ class APIRouter(manila.api.openstack.APIRouter): security_service.create_resource() mapper.resource("security-service", "security-services", controller=self.resources['security_services']) + + self.resources['share_networks'] = share_networks.create_resource() + mapper.resource(share_networks.RESOURCE_NAME, + 'share-networks', + controller=self.resources['share_networks'], + member={'action': 'POST'}) diff --git a/manila/api/v1/security_service.py b/manila/api/v1/security_service.py index 8b12bfff86..37541b23a1 100644 --- a/manila/api/v1/security_service.py +++ b/manila/api/v1/security_service.py @@ -113,25 +113,31 @@ class SecurityServiceController(wsgi.Controller): search_opts = {} search_opts.update(req.GET) - common.remove_invalid_options( - context, search_opts, self._get_security_services_search_options()) - if 'all_tenants' in search_opts: - security_services = db.security_service_get_all(context) - del search_opts['all_tenants'] + if 'share_network_id' in search_opts: + share_nw = db.share_network_get(context, + search_opts['share_network_id']) + security_services = share_nw['security_services'] else: - security_services = db.security_service_get_all_by_project( - context, context.project_id) - - if search_opts: - results = [] - not_found = object() - for service in security_services: - for opt, value in search_opts.iteritems(): - if service.get(opt, not_found) != value: - break - else: - results.append(service) - security_services = results + common.remove_invalid_options( + context, + search_opts, + self._get_security_services_search_options()) + if 'all_tenants' in search_opts: + security_services = db.security_service_get_all(context) + del search_opts['all_tenants'] + else: + security_services = db.security_service_get_all_by_project( + context, context.project_id) + if search_opts: + results = [] + not_found = object() + for service in security_services: + for opt, value in search_opts.iteritems(): + if service.get(opt, not_found) != value: + break + else: + results.append(service) + security_services = results limited_list = common.limited(security_services, req) diff --git a/manila/api/v1/share_networks.py b/manila/api/v1/share_networks.py new file mode 100644 index 0000000000..cdc147b238 --- /dev/null +++ b/manila/api/v1/share_networks.py @@ -0,0 +1,227 @@ +# Copyright 2014 NetApp +# 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 shares api.""" + +import webob +from webob import exc + +from manila.api.openstack import wsgi +from manila.api.views import share_networks as share_networks_views +from manila.api import xmlutil +from manila.common import constants +from manila.db import api as db_api +from manila import exception +from manila.openstack.common import log as logging + +RESOURCE_NAME = 'share_network' +RESOURCES_NAME = 'share_networks' +LOG = logging.getLogger(__name__) +SHARE_NETWORK_ATTRS = ('id', + 'project_id', + 'created_at', + 'updated_at', + 'neutron_net_id', + 'neutron_subnet_id', + 'network_type', + 'segmentation_id', + 'cidr', + 'ip_version', + 'name', + 'description', + 'status') + + +def _make_share_network(elem): + for attr in SHARE_NETWORK_ATTRS: + elem.set(attr) + + +class ShareNetworkTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement(RESOURCE_NAME, selector=RESOURCE_NAME) + _make_share_network(root) + return xmlutil.MasterTemplate(root, 1) + + +class ShareNetworksTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement(RESOURCES_NAME) + elem = xmlutil.SubTemplateElement(root, RESOURCE_NAME, + selector=RESOURCES_NAME) + _make_share_network(elem) + return xmlutil.MasterTemplate(root, 1) + + +class ShareNetworkController(wsgi.Controller): + """The Share Network API controller for the OpenStack API.""" + + _view_builder_class = share_networks_views.ViewBuilder + + @wsgi.serializers(xml=ShareNetworkTemplate) + def show(self, req, id): + """Return data about the requested network info.""" + context = req.environ['manila.context'] + + try: + share_network = db_api.share_network_get(context, id) + except exception.ShareNetworkNotFound as e: + msg = "%s" % e + raise exc.HTTPNotFound(explanation=msg) + + return self._view_builder.build_share_network(share_network) + + def delete(self, req, id): + """Delete specified share network.""" + context = req.environ['manila.context'] + + try: + share_network = db_api.share_network_get(context, id) + except exception.ShareNetworkNotFound as e: + msg = "%s" % e + raise exc.HTTPNotFound(explanation=msg) + + if share_network['status'] == constants.STATUS_ACTIVE: + msg = "Network %s is in use" % id + raise exc.HTTPBadRequest(explanation=msg) + + db_api.share_network_delete(context, id) + + return webob.Response(status_int=202) + + @wsgi.serializers(xml=ShareNetworksTemplate) + def index(self, req): + """Returns a summary list of share's networks.""" + context = req.environ['manila.context'] + + search_opts = {} + search_opts.update(req.GET) + + if search_opts.pop('all_tenants', None): + networks = db_api.share_network_get_all(context) + else: + networks = db_api.share_network_get_all_by_project( + context, + context.project_id) + + if search_opts: + for key, value in search_opts.iteritems(): + networks = [network for network in networks + if network[key] == value] + return self._view_builder.build_share_networks(networks) + + @wsgi.serializers(xml=ShareNetworkTemplate) + def update(self, req, id, body): + """Update specified share network.""" + context = req.environ['manila.context'] + + if not body or RESOURCE_NAME not in body: + raise exc.HTTPUnprocessableEntity() + + try: + share_network = db_api.share_network_get(context, id) + except exception.ShareNetworkNotFound as e: + msg = "%s" % e + raise exc.HTTPNotFound(explanation=msg) + + if share_network['status'] == constants.STATUS_ACTIVE: + msg = "Network %s is in use" % id + raise exc.HTTPBadRequest(explanation=msg) + + update_values = body[RESOURCE_NAME] + + try: + share_network = db_api.share_network_update(context, + id, + update_values) + except exception.DBError: + msg = "Could not save supplied data due to database error" + raise exc.HTTPBadRequest(explanation=msg) + + return self._view_builder.build_share_network(share_network) + + @wsgi.serializers(xml=ShareNetworkTemplate) + def create(self, req, body): + """Creates a new share network.""" + context = req.environ['manila.context'] + + if not body or RESOURCE_NAME not in body: + raise exc.HTTPUnprocessableEntity() + + values = body[RESOURCE_NAME] + values['project_id'] = context.project_id + + try: + share_network = db_api.share_network_create(context, values) + except exception.DBError: + msg = "Could not save supplied data due to database error" + raise exc.HTTPBadRequest(explanation=msg) + + return self._view_builder.build_share_network(share_network) + + @wsgi.serializers(xml=ShareNetworkTemplate) + def action(self, req, id, body): + _actions = { + 'add_security_service': self._add_security_service, + 'remove_security_service': self._remove_security_service, + } + for action, data in body.iteritems(): + try: + return _actions[action](req, id, data) + except KeyError: + msg = _("Share networks does not have %s action") % action + raise exc.HTTPBadRequest(explanation=msg) + + def _add_security_service(self, req, id, data): + context = req.environ['manila.context'] + try: + share_network = db_api.share_network_add_security_service( + context, + id, + data['security_service_id']) + except KeyError: + msg = "Malformed request body" + raise exc.HTTPBadRequest(explanation=msg) + except exception.NotFound as e: + msg = "%s" % e + raise exc.HTTPNotFound(explanation=msg) + except exception.ShareNetworkSecurityServiceAssociationError as e: + msg = "%s" % e + raise exc.HTTPBadRequest(explanation=msg) + + return self._view_builder.build_share_network(share_network) + + def _remove_security_service(self, req, id, data): + context = req.environ['manila.context'] + try: + share_network = db_api.share_network_remove_security_service( + context, + id, + data['security_service_id']) + except KeyError: + msg = "Malformed request body" + raise exc.HTTPBadRequest(explanation=msg) + except exception.NotFound as e: + msg = "%s" % e + raise exc.HTTPNotFound(explanation=msg) + except exception.ShareNetworkSecurityServiceDissociationError as e: + msg = "%s" % e + raise exc.HTTPBadRequest(explanation=msg) + + return self._view_builder.build_share_network(share_network) + + +def create_resource(): + return wsgi.Resource(ShareNetworkController()) diff --git a/manila/api/v1/shares.py b/manila/api/v1/shares.py index 279c6cb672..443dcff5eb 100644 --- a/manila/api/v1/shares.py +++ b/manila/api/v1/shares.py @@ -198,6 +198,8 @@ class ShareController(wsgi.Controller): else: kwargs['snapshot'] = None + kwargs['share_network_id'] = share.get('share_network_id') + display_name = share.get('display_name') display_description = share.get('display_description') new_share = self.share_api.create(context, diff --git a/manila/api/views/share_networks.py b/manila/api/views/share_networks.py new file mode 100644 index 0000000000..4009a83657 --- /dev/null +++ b/manila/api/views/share_networks.py @@ -0,0 +1,51 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2014 OpenStack LLC. +# 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 manila.api import common + + +class ViewBuilder(common.ViewBuilder): + """Model a server API response as a python dictionary.""" + + _collection_name = 'share_networks' + + def build_share_network(self, share_network): + """View of a share network.""" + + return {'share_network': self._build_share_network_view(share_network)} + + def build_share_networks(self, share_networks): + return {'share_networks': + [self._build_share_network_view(share_network) + for share_network in share_networks]} + + def _build_share_network_view(self, share_network): + return { + 'id': share_network.get('id'), + 'project_id': share_network.get('project_id'), + 'created_at': share_network.get('created_at'), + 'updated_at': share_network.get('updated_at'), + 'neutron_net_id': share_network.get('neutron_net_id'), + 'neutron_subnet_id': share_network.get('neutron_subnet_id'), + 'network_type': share_network.get('network_type'), + 'segmentation_id': share_network.get('segmentation_id'), + 'cidr': share_network.get('cidr'), + 'ip_version': share_network.get('ip_version'), + 'name': share_network.get('name'), + 'description': share_network.get('description'), + 'status': share_network.get('status'), + } diff --git a/manila/api/views/shares.py b/manila/api/views/shares.py index 89a5719385..dc716d59c1 100644 --- a/manila/api/views/shares.py +++ b/manila/api/views/shares.py @@ -60,6 +60,7 @@ class ViewBuilder(common.ViewBuilder): 'name': share.get('display_name'), 'description': share.get('display_description'), 'snapshot_id': share.get('snapshot_id'), + 'share_network_id': share.get('share_network_id'), 'share_proto': share.get('share_proto'), 'export_location': share.get('export_location'), 'metadata': metadata, diff --git a/manila/share/api.py b/manila/share/api.py index 490d3f8ef6..21061231f6 100644 --- a/manila/share/api.py +++ b/manila/share/api.py @@ -49,7 +49,8 @@ class API(base.Base): super(API, self).__init__(db_driver) def create(self, context, share_proto, size, name, description, - snapshot=None, availability_zone=None, metadata=None): + snapshot=None, availability_zone=None, metadata=None, + share_network_id=None): """Create new share.""" policy.check_policy(context, 'create') @@ -125,6 +126,7 @@ class API(base.Base): 'user_id': context.user_id, 'project_id': context.project_id, 'snapshot_id': snapshot_id, + 'share_network_id': share_network_id, 'availability_zone': availability_zone, 'metadata': metadata, 'status': "creating", diff --git a/manila/tests/api/contrib/stubs.py b/manila/tests/api/contrib/stubs.py index 7b808c7d62..d155ac4ce3 100644 --- a/manila/tests/api/contrib/stubs.py +++ b/manila/tests/api/contrib/stubs.py @@ -39,6 +39,7 @@ def stub_share(id, **kwargs): 'display_description': 'displaydesc', 'created_at': datetime.datetime(1, 1, 1, 1, 1, 1), 'snapshot_id': '2', + 'share_network_id': None } share.update(kwargs) return share diff --git a/manila/tests/api/v1/test_share_networks.py b/manila/tests/api/v1/test_share_networks.py new file mode 100644 index 0000000000..4395a26275 --- /dev/null +++ b/manila/tests/api/v1/test_share_networks.py @@ -0,0 +1,248 @@ +# Copyright 2014 NetApp +# 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. + +import mock +import unittest +from webob import exc as webob_exc + +from manila.api.v1 import share_networks +from manila.common import constants +from manila.db import api as db_api +from manila import exception +from manila.tests.api import fakes + + +fake_share_network = {'id': 'fake network id', + 'project_id': 'fake project', + 'created_at': None, + 'updated_at': None, + 'neutron_net_id': 'fake net id', + 'neutron_subnet_id': 'fake subnet id', + 'network_type': 'vlan', + 'segmentation_id': 1000, + 'cidr': '10.0.0.0/24', + 'ip_version': 4, + 'name': 'fake name', + 'description': 'fake description', + 'status': constants.STATUS_INACTIVE, + 'shares': [], + 'network_allocations': [], + 'security_services': [] + } + + +class ShareNetworkAPITest(unittest.TestCase): + + def __init__(self, *args, **kwargs): + super(ShareNetworkAPITest, self).__init__(*args, **kwargs) + self.controller = share_networks.ShareNetworkController() + self.req = fakes.HTTPRequest.blank('/share-networks') + self.context = self.req.environ['manila.context'] + self.body = {share_networks.RESOURCE_NAME: {'name': 'fake name'}} + + def _check_share_network_view(self, view, share_nw): + self.assertEqual(view['id'], share_nw['id']) + self.assertEqual(view['project_id'], share_nw['project_id']) + self.assertEqual(view['created_at'], share_nw['created_at']) + self.assertEqual(view['updated_at'], share_nw['updated_at']) + self.assertEqual(view['neutron_net_id'], + share_nw['neutron_net_id']) + self.assertEqual(view['neutron_subnet_id'], + share_nw['neutron_subnet_id']) + self.assertEqual(view['network_type'], share_nw['network_type']) + self.assertEqual(view['segmentation_id'], + share_nw['segmentation_id']) + self.assertEqual(view['cidr'], share_nw['cidr']) + self.assertEqual(view['ip_version'], share_nw['ip_version']) + self.assertEqual(view['name'], share_nw['name']) + self.assertEqual(view['description'], share_nw['description']) + self.assertEqual(view['status'], share_nw['status']) + + self.assertEqual(view['created_at'], None) + self.assertEqual(view['updated_at'], None) + self.assertFalse('shares' in view) + self.assertFalse('network_allocations' in view) + self.assertFalse('security_services' in view) + + def test_create_nominal(self): + with mock.patch.object(db_api, + 'share_network_create', + mock.Mock(return_value=fake_share_network)): + + result = self.controller.create(self.req, self.body) + + db_api.share_network_create.assert_called_once_with( + self.req.environ['manila.context'], + self.body[share_networks.RESOURCE_NAME]) + + self._check_share_network_view( + result[share_networks.RESOURCE_NAME], + fake_share_network) + + def test_create_db_api_exception(self): + with mock.patch.object(db_api, + 'share_network_create', + mock.Mock(side_effect=exception.DBError)): + self.assertRaises(webob_exc.HTTPBadRequest, + self.controller.create, + self.req, + self.body) + + def test_create_wrong_body(self): + body = None + self.assertRaises(webob_exc.HTTPUnprocessableEntity, + self.controller.create, + self.req, + body) + + @mock.patch.object(db_api, 'share_network_get', + mock.Mock(return_value=fake_share_network)) + def test_delete_nominal(self): + share_nw = 'fake network id' + + with mock.patch.object(db_api, 'share_network_delete'): + self.controller.delete(self.req, share_nw) + db_api.share_network_delete.assert_called_once_with( + self.req.environ['manila.context'], + share_nw) + + @mock.patch.object(db_api, 'share_network_get', mock.Mock()) + def test_delete_not_found(self): + share_nw = 'fake network id' + db_api.share_network_get.side_effect = exception.ShareNetworkNotFound( + share_network_id=share_nw) + + self.assertRaises(webob_exc.HTTPNotFound, + self.controller.delete, + self.req, + share_nw) + + @mock.patch.object(db_api, 'share_network_get', mock.Mock()) + def test_delete_in_use(self): + share_nw = fake_share_network.copy() + share_nw['status'] = constants.STATUS_ACTIVE + + db_api.share_network_get.return_value = share_nw + + self.assertRaises(webob_exc.HTTPBadRequest, + self.controller.delete, + self.req, + share_nw['id']) + + def test_show_nominal(self): + share_nw = 'fake network id' + with mock.patch.object(db_api, + 'share_network_get', + mock.Mock(return_value=fake_share_network)): + result = self.controller.show(self.req, share_nw) + + db_api.share_network_get.assert_called_once_with( + self.req.environ['manila.context'], + share_nw) + + self._check_share_network_view( + result[share_networks.RESOURCE_NAME], + fake_share_network) + + def test_show_not_found(self): + share_nw = 'fake network id' + test_exception = exception.ShareNetworkNotFound() + with mock.patch.object(db_api, + 'share_network_get', + mock.Mock(side_effect=test_exception)): + self.assertRaises(webob_exc.HTTPNotFound, + self.controller.show, + self.req, + share_nw) + + def test_index_no_filters(self): + networks = [fake_share_network] + with mock.patch.object(db_api, + 'share_network_get_all_by_project', + mock.Mock(return_value=networks)): + + result = self.controller.index(self.req) + + db_api.share_network_get_all_by_project.assert_called_once_with( + self.context, + self.context.project_id) + + self.assertEqual(len(result[share_networks.RESOURCES_NAME]), 1) + self._check_share_network_view( + result[share_networks.RESOURCES_NAME][0], + fake_share_network) + + @mock.patch.object(db_api, 'share_network_get', mock.Mock()) + def test_update_nominal(self): + share_nw = 'fake network id' + db_api.share_network_get.return_value = fake_share_network + + body = {share_networks.RESOURCE_NAME: {'name': 'new name'}} + + with mock.patch.object(db_api, + 'share_network_update', + mock.Mock(return_value=fake_share_network)): + result = self.controller.update(self.req, share_nw, body) + + db_api.share_network_update.assert_called_once_with( + self.req.environ['manila.context'], + share_nw, + body[share_networks.RESOURCE_NAME]) + + self._check_share_network_view( + result[share_networks.RESOURCE_NAME], + fake_share_network) + + @mock.patch.object(db_api, 'share_network_get', mock.Mock()) + def test_update_not_found(self): + share_nw = 'fake network id' + db_api.share_network_get.side_effect = exception.ShareNetworkNotFound( + share_network_id=share_nw) + + self.assertRaises(webob_exc.HTTPNotFound, + self.controller.update, + self.req, + share_nw, + self.body) + + @mock.patch.object(db_api, 'share_network_get', mock.Mock()) + def test_update_in_use(self): + share_nw = fake_share_network.copy() + share_nw['status'] = constants.STATUS_ACTIVE + + db_api.share_network_get.return_value = share_nw + + self.assertRaises(webob_exc.HTTPBadRequest, + self.controller.update, + self.req, + share_nw['id'], + self.body) + + @mock.patch.object(db_api, 'share_network_get', mock.Mock()) + def test_update_db_api_exception(self): + share_nw = 'fake network id' + db_api.share_network_get.return_value = fake_share_network + + body = {share_networks.RESOURCE_NAME: {'neutron_subnet_id': + 'new subnet'}} + + with mock.patch.object(db_api, + 'share_network_update', + mock.Mock(side_effect=exception.DBError)): + self.assertRaises(webob_exc.HTTPBadRequest, + self.controller.update, + self.req, + share_nw, + body) diff --git a/manila/tests/api/v1/test_shares.py b/manila/tests/api/v1/test_shares.py index a8ac869ea8..ece8ab46ce 100644 --- a/manila/tests/api/v1/test_shares.py +++ b/manila/tests/api/v1/test_shares.py @@ -145,6 +145,7 @@ class ShareApiTest(test.TestCase): 'metadata': {}, 'size': 1, 'snapshot_id': '2', + 'share_network_id': None, 'status': 'fakestatus', 'links': [{'href': 'http://localhost/v1/fake/shares/1', 'rel': 'self'}, @@ -246,6 +247,7 @@ class ShareApiTest(test.TestCase): 'metadata': {}, 'id': '1', 'snapshot_id': '2', + 'share_network_id': None, 'created_at': datetime.datetime(1, 1, 1, 1, 1, 1), 'size': 1, 'links': [ diff --git a/manila/tests/test_share_api.py b/manila/tests/test_share_api.py index 4d584671ae..c2a80bddb2 100644 --- a/manila/tests/test_share_api.py +++ b/manila/tests/test_share_api.py @@ -40,6 +40,7 @@ def fake_share(id, **kwargs): 'user_id': 'fakeuser', 'project_id': 'fakeproject', 'snapshot_id': None, + 'share_network_id': None, 'availability_zone': 'fakeaz', 'status': 'fakestatus', 'display_name': 'fakename',