diff --git a/manila/api/v1/share_networks.py b/manila/api/v1/share_networks.py index 9d314b83e1..8fe46b215f 100644 --- a/manila/api/v1/share_networks.py +++ b/manila/api/v1/share_networks.py @@ -45,6 +45,7 @@ SHARE_NETWORK_ATTRS = ( 'user_id', 'created_at', 'updated_at', + 'nova_net_id', 'neutron_net_id', 'neutron_subnet_id', 'network_type', @@ -208,6 +209,23 @@ class ShareNetworkController(wsgi.Controller): 'detail') return self._get_share_networks(req) + @staticmethod + def _verify_no_mutually_exclusive_data(share_network, update_data=None): + update_data = update_data or dict() + neutron_net_id = ( + share_network.get('neutron_net_id') or + update_data.get('neutron_net_id')) + neutron_subnet_id = ( + share_network.get('neutron_subnet_id') or + update_data.get('neutron_subnet_id')) + nova_net_id = ( + share_network.get('nova_net_id') or + update_data.get('nova_net_id')) + if nova_net_id and (neutron_net_id or neutron_subnet_id): + msg = _("Neutron net data and Nova net data are mutually " + "exclusive. Only one of these are allowed at a time.") + raise exc.HTTPBadRequest(explanation=msg) + @wsgi.serializers(xml=ShareNetworkTemplate) def update(self, req, id, body): """Update specified share network.""" @@ -224,6 +242,7 @@ class ShareNetworkController(wsgi.Controller): update_values = body[RESOURCE_NAME] + self._verify_no_mutually_exclusive_data(share_network, update_values) if share_network['share_servers']: for value in update_values: if value not in ['name', 'description']: @@ -254,6 +273,7 @@ class ShareNetworkController(wsgi.Controller): values = body[RESOURCE_NAME] values['project_id'] = context.project_id + self._verify_no_mutually_exclusive_data(values) try: reservations = QUOTAS.reserve(context, share_networks=1) diff --git a/manila/api/views/share_networks.py b/manila/api/views/share_networks.py index 6a1c9caf79..cf8fbef29c 100644 --- a/manila/api/views/share_networks.py +++ b/manila/api/views/share_networks.py @@ -43,6 +43,7 @@ class ViewBuilder(common.ViewBuilder): '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'), + 'nova_net_id': share_network.get('nova_net_id'), 'network_type': share_network.get('network_type'), 'segmentation_id': share_network.get('segmentation_id'), 'cidr': share_network.get('cidr'), diff --git a/manila/db/migrations/alembic/versions/17115072e1c3_add_nova_net_id_column_to_share_networks.py b/manila/db/migrations/alembic/versions/17115072e1c3_add_nova_net_id_column_to_share_networks.py new file mode 100644 index 0000000000..d38c2c65bd --- /dev/null +++ b/manila/db/migrations/alembic/versions/17115072e1c3_add_nova_net_id_column_to_share_networks.py @@ -0,0 +1,39 @@ +# Copyright 2015 Mirantis 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. + +"""add_nova_net_id_column_to_share_networks + +Revision ID: 17115072e1c3 +Revises: 38e632621e5a +Create Date: 2015-02-05 18:07:19.062995 + +""" + +# revision identifiers, used by Alembic. +revision = '17115072e1c3' +down_revision = '38e632621e5a' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + op.add_column( + 'share_networks', + sa.Column('nova_net_id', sa.String(36), nullable=True)) + + +def downgrade(): + op.drop_column('share_networks', 'nova_net_id') diff --git a/manila/db/sqlalchemy/models.py b/manila/db/sqlalchemy/models.py index 9dacc13f43..7c3c457551 100644 --- a/manila/db/sqlalchemy/models.py +++ b/manila/db/sqlalchemy/models.py @@ -326,6 +326,7 @@ class ShareNetwork(BASE, ManilaBase): deleted = Column(String(36), default='False') project_id = Column(String(36), nullable=False) user_id = Column(String(36), nullable=False) + nova_net_id = Column(String(36), nullable=True) neutron_net_id = Column(String(36), nullable=True) neutron_subnet_id = Column(String(36), nullable=True) network_type = Column(String(32), nullable=True) diff --git a/manila/tests/api/v1/test_share_networks.py b/manila/tests/api/v1/test_share_networks.py index b41b590466..6d2cf6d37a 100644 --- a/manila/tests/api/v1/test_share_networks.py +++ b/manila/tests/api/v1/test_share_networks.py @@ -13,6 +13,7 @@ # License for the specific language governing permissions and limitations # under the License. +import ddt import mock from oslo_db import exception as db_exception from oslo_utils import timeutils @@ -74,6 +75,7 @@ fake_sn_with_ss_shortened = { QUOTAS = quota.QUOTAS +@ddt.ddt class ShareNetworkAPITest(test.TestCase): def setUp(self): @@ -110,6 +112,64 @@ class ShareNetworkAPITest(test.TestCase): self.assertFalse('network_allocations' in view) self.assertFalse('security_services' in view) + @ddt.data( + {'nova_net_id': 'fake_nova_net_id'}, + {'neutron_net_id': 'fake_neutron_net_id'}, + {'neutron_subnet_id': 'fake_neutron_subnet_id'}, + {'neutron_net_id': 'fake', 'neutron_subnet_id': 'fake'}) + def test_create_valid_cases(self, data): + data.update({'user_id': 'fake_user_id'}) + body = {share_networks.RESOURCE_NAME: data} + result = self.controller.create(self.req, body) + data.pop('user_id', None) + for k, v in data.items(): + self.assertIn(data[k], result['share_network'][k]) + + @ddt.data( + {'nova_net_id': 'foo', 'neutron_net_id': 'bar'}, + {'nova_net_id': 'foo', 'neutron_subnet_id': 'quuz'}, + {'nova_net_id': 'foo', 'neutron_net_id': 'bar', + 'neutron_subnet_id': 'quuz'}) + def test_create_invalid_cases(self, data): + data.update({'user_id': 'fake_user_id'}) + body = {share_networks.RESOURCE_NAME: data} + self.assertRaises( + webob_exc.HTTPBadRequest, self.controller.create, self.req, body) + + @ddt.data( + {'nova_net_id': 'fake_nova_net_id'}, + {'neutron_net_id': 'fake_neutron_net_id'}, + {'neutron_subnet_id': 'fake_neutron_subnet_id'}, + {'neutron_net_id': 'fake', 'neutron_subnet_id': 'fake'}) + def test_update_valid_cases(self, data): + body = {share_networks.RESOURCE_NAME: {'user_id': 'fake_user'}} + created = self.controller.create(self.req, body) + + body = {share_networks.RESOURCE_NAME: data} + result = self.controller.update( + self.req, created['share_network']['id'], body) + + for k, v in data.items(): + self.assertIn(data[k], result['share_network'][k]) + + self._check_share_network_view( + result[share_networks.RESOURCE_NAME], + result['share_network']) + + @ddt.data( + {'nova_net_id': 'foo', 'neutron_net_id': 'bar'}, + {'nova_net_id': 'foo', 'neutron_subnet_id': 'quuz'}, + {'nova_net_id': 'foo', 'neutron_net_id': 'bar', + 'neutron_subnet_id': 'quuz'}) + def test_update_invalid_cases(self, data): + body = {share_networks.RESOURCE_NAME: {'user_id': 'fake_user'}} + created = self.controller.create(self.req, body) + body = {share_networks.RESOURCE_NAME: data} + self.assertRaises( + webob_exc.HTTPBadRequest, + self.controller.update, + self.req, created['share_network']['id'], body) + def test_create_nominal(self): with mock.patch.object(db_api, 'share_network_create', diff --git a/manila/tests/api/views/__init__.py b/manila/tests/api/views/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/manila/tests/api/views/test_share_networks.py b/manila/tests/api/views/test_share_networks.py new file mode 100644 index 0000000000..171d8d0c36 --- /dev/null +++ b/manila/tests/api/views/test_share_networks.py @@ -0,0 +1,109 @@ +# Copyright (c) 2015 Mirantis, 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. + +import ddt + +from manila.api.views import share_networks +from manila import test + + +@ddt.ddt +class ViewBuilderTestCase(test.TestCase): + + def setUp(self): + super(ViewBuilderTestCase, self).setUp() + self.builder = share_networks.ViewBuilder() + + def test__collection_name(self): + self.assertEqual('share_networks', self.builder._collection_name) + + @ddt.data( + {'id': 'fake_sn_id', 'name': 'fake_sn_name'}, + {'id': 'fake_sn_id', 'name': 'fake_sn_name', 'fake_extra_key': 'foo'}, + ) + def test_build_share_network(self, sn): + expected_keys = ( + 'id', 'name', 'project_id', 'created_at', 'updated_at', + 'neutron_net_id', 'neutron_subnet_id', 'nova_net_id', + 'network_type', 'segmentation_id', 'cidr', 'ip_version', + 'description') + + result = self.builder.build_share_network(sn) + + self.assertEqual(1, len(result)) + self.assertIn('share_network', result) + self.assertEqual(sn['id'], result['share_network']['id']) + self.assertEqual(sn['name'], result['share_network']['name']) + self.assertEqual(len(expected_keys), len(result['share_network'])) + for key in expected_keys: + self.assertIn(key, result['share_network']) + + @ddt.data( + [], + [dict(id='fake_id', + name='fake_name', + project_id='fake_project_id', + created_at='fake_created_at', + updated_at='fake_updated_at', + neutron_net_id='fake_neutron_net_id', + neutron_subnet_id='fake_neutron_subnet_id', + nova_net_id='fake_nova_net_id', + network_type='fake_network_type', + segmentation_id='fake_segmentation_id', + cidr='fake_cidr', + ip_version='fake_ip_version', + description='fake_description'), + dict(id='fake_id2', name='fake_name2')], + ) + def test_build_share_networks_with_details(self, share_networks): + expected = [] + for share_network in share_networks: + expected.append(dict( + id=share_network.get('id'), + name=share_network.get('name'), + 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'), + nova_net_id=share_network.get('nova_net_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'), + description=share_network.get('description'))) + expected = {'share_networks': expected} + + result = self.builder.build_share_networks(share_networks, True) + + self.assertEqual(expected, result) + + @ddt.data( + [], + [{'id': 'foo', 'name': 'bar'}], + [{'id': 'id1', 'name': 'name1'}, {'id': 'id2', 'name': 'name2'}], + [{'id': 'id1', 'name': 'name1'}, + {'id': 'id2', 'name': 'name2', 'fake': 'I should not be returned'}], + ) + def test_build_share_networks_without_details(self, share_networks): + expected = [] + for share_network in share_networks: + expected.append(dict( + id=share_network.get('id'), name=share_network.get('name'))) + expected = {'share_networks': expected} + + result = self.builder.build_share_networks(share_networks, False) + + self.assertEqual(expected, result)