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',