From d0b994858c5a22f488131074d497ba7544e1523a Mon Sep 17 00:00:00 2001 From: Oleksii Chuprykov Date: Thu, 16 Apr 2015 09:56:29 -0400 Subject: [PATCH] Add share network resource for manila Implement create, delete, update methods for resource. Implements: partial blueprint add-manila-resources Change-Id: Icc51b5ffd4b2256d2552cb00c0e51078049a96aa --- heat/engine/clients/os/manila.py | 7 + .../openstack/manila/share_network.py | 172 +++++++++++++++ heat/tests/test_manila_client.py | 3 + heat/tests/test_share_network.py | 204 ++++++++++++++++++ 4 files changed, 386 insertions(+) create mode 100644 heat/engine/resources/openstack/manila/share_network.py create mode 100644 heat/tests/test_share_network.py diff --git a/heat/engine/clients/os/manila.py b/heat/engine/clients/os/manila.py index 71c5d0f779..0d91a4174e 100644 --- a/heat/engine/clients/os/manila.py +++ b/heat/engine/clients/os/manila.py @@ -105,6 +105,13 @@ class ManilaClientPlugin(client_plugin.ClientPlugin): "share snapshot" ) + def get_security_service(self, service_identity): + return self._find_resource_by_id_or_name( + service_identity, + self.client().security_services.list(), + 'security service' + ) + class ManilaShareBaseConstraint(constraints.BaseCustomConstraint): # check that exceptions module has been loaded. Without this check diff --git a/heat/engine/resources/openstack/manila/share_network.py b/heat/engine/resources/openstack/manila/share_network.py new file mode 100644 index 0000000000..2d165c5b86 --- /dev/null +++ b/heat/engine/resources/openstack/manila/share_network.py @@ -0,0 +1,172 @@ +# +# 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 heat.common import exception +from heat.common.i18n import _ +from heat.engine import attributes +from heat.engine import constraints +from heat.engine import properties +from heat.engine import resource +from heat.engine import support + + +class ManilaShareNetwork(resource.Resource): + """ + Stores network information that will be used by share servers, + where shares are hosted. + """ + + support_status = support.SupportStatus(version='5.0.0') + + PROPERTIES = ( + NAME, NEUTRON_NETWORK, NEUTRON_SUBNET, NOVA_NETWORK, + DESCRIPTION, SECURITY_SERVICES, + ) = ( + 'name', 'neutron_network', 'neutron_subnet', 'nova_network', + 'description', 'security_services', + ) + + ATTRIBUTES = ( + SEGMENTATION_ID, CIDR, IP_VERSION, NETWORK_TYPE, + ) = ( + 'segmentation_id', 'cidr', 'ip_version', 'network_type', + ) + + properties_schema = { + NAME: properties.Schema( + properties.Schema.STRING, + _('Name of the share network.'), + update_allowed=True + ), + NEUTRON_NETWORK: properties.Schema( + properties.Schema.STRING, + _('Neutron network id.'), + update_allowed=True, + constraints=[constraints.CustomConstraint('neutron.network')] + ), + NEUTRON_SUBNET: properties.Schema( + properties.Schema.STRING, + _('Neutron subnet id.'), + update_allowed=True, + constraints=[constraints.CustomConstraint('neutron.subnet')] + ), + NOVA_NETWORK: properties.Schema( + properties.Schema.STRING, + _('Nova network id.'), + update_allowed=True + ), + DESCRIPTION: properties.Schema( + properties.Schema.STRING, + _('Share network description.'), + update_allowed=True + ), + SECURITY_SERVICES: properties.Schema( + properties.Schema.LIST, + _('A list of security services IDs or names.'), + schema=properties.Schema( + properties.Schema.STRING + ), + update_allowed=True, + default=[] + ) + } + + attributes_schema = { + SEGMENTATION_ID: attributes.Schema( + _('VLAN ID for VLAN networks or tunnel-id for GRE/VXLAN ' + 'networks.'), + type=attributes.Schema.STRING + ), + CIDR: attributes.Schema( + _('CIDR of subnet.'), + type=attributes.Schema.STRING + ), + IP_VERSION: attributes.Schema( + _('Version of IP address.'), + type=attributes.Schema.STRING + ), + NETWORK_TYPE: attributes.Schema( + _('The physical mechanism by which the virtual network is ' + 'implemented.'), + type=attributes.Schema.STRING + ), + } + + default_client_name = 'manila' + + def _request_network(self): + return self.client().share_networks.get(self.resource_id) + + def _resolve_attribute(self, name): + network = self._request_network() + return getattr(network, name, None) + + def validate(self): + super(ManilaShareNetwork, self).validate() + if (self.properties[self.NEUTRON_NETWORK] and + self.properties[self.NOVA_NETWORK]): + raise exception.ResourcePropertyConflict(self.NEUTRON_NETWORK, + self.NOVA_NETWORK) + + if (self.properties[self.NOVA_NETWORK] and + self.properties[self.NEUTRON_SUBNET]): + raise exception.ResourcePropertyConflict(self.NEUTRON_SUBNET, + self.NOVA_NETWORK) + + def handle_create(self): + network = self.client().share_networks.create( + name=self.properties[self.NAME], + neutron_net_id=self.properties[self.NEUTRON_NETWORK], + neutron_subnet_id=self.properties[self.NEUTRON_SUBNET], + nova_net_id=self.properties[self.NOVA_NETWORK], + description=self.properties[self.DESCRIPTION]) + self.resource_id_set(network.id) + + for service in self.properties.get(self.SECURITY_SERVICES): + self.client().share_networks.add_security_service( + self.resource_id, + self.client_plugin().get_security_service(service).id) + + def handle_update(self, json_snippet=None, tmpl_diff=None, prop_diff=None): + if self.SECURITY_SERVICES in prop_diff: + services = prop_diff.pop(self.SECURITY_SERVICES) + s_curr = set([self.client_plugin().get_security_service(s).id + for s in self.properties.get( + self.SECURITY_SERVICES)]) + s_new = set([self.client_plugin().get_security_service(s).id + for s in services]) + for service in s_curr - s_new: + self.client().share_networks.remove_security_service( + self.resource_id, service) + for service in s_new - s_curr: + self.client().share_networks.add_security_service( + self.resource_id, service) + + if prop_diff: + self.client().share_networks.update( + self.resource_id, + name=prop_diff.get(self.NAME), + neutron_net_id=prop_diff.get(self.NEUTRON_NETWORK), + neutron_subnet_id=prop_diff.get(self.NEUTRON_SUBNET), + nova_net_id=prop_diff.get(self.NOVA_NETWORK), + description=prop_diff.get(self.DESCRIPTION)) + + def handle_delete(self): + try: + self.client().share_networks.delete(self.resource_id) + except Exception as ex: + self.client_plugin().ignore_not_found(ex) + + +def resource_mapping(): + return {'OS::Manila::ShareNetwork': ManilaShareNetwork} diff --git a/heat/tests/test_manila_client.py b/heat/tests/test_manila_client.py index ad6cec6226..54575541b4 100644 --- a/heat/tests/test_manila_client.py +++ b/heat/tests/test_manila_client.py @@ -30,6 +30,9 @@ class ManilaClientPluginTests(common.HeatTestCase): ('share_snapshot', dict(manager_name="share_snapshots", method_name="get_share_snapshot")), + ('security_service', + dict(manager_name="security_services", + method_name="get_security_service")), ] def setUp(self): diff --git a/heat/tests/test_share_network.py b/heat/tests/test_share_network.py new file mode 100644 index 0000000000..9678d1e163 --- /dev/null +++ b/heat/tests/test_share_network.py @@ -0,0 +1,204 @@ +# +# 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 copy + +import mock + +from heat.common import exception +from heat.common import template_format +from heat.engine import properties +from heat.engine.resources.openstack.manila import share_network +from heat.engine import scheduler +from heat.tests import common +from heat.tests import utils + + +stack_template = """ +heat_template_version: 2015-04-30 +resources: + share_network: + type: OS::Manila::ShareNetwork + properties: + name: 1 + description: 2 + neutron_network: 3 + neutron_subnet: 4 + security_services: [6, 7] +""" + + +class DummyShareNetwork(object): + def __init__(self): + self.id = '42' + self.segmentation_id = '2' + self.cidr = '3' + self.ip_version = '5' + self.network_type = '6' + + +class ManilaShareNetworkTest(common.HeatTestCase): + + def setUp(self): + super(ManilaShareNetworkTest, self).setUp() + utils.setup_dummy_db() + self.ctx = utils.dummy_context() + + t = template_format.parse(stack_template) + self.stack = utils.parse_stack(t) + resource_defns = self.stack.t.resource_definitions(self.stack) + self.rsrc_defn = resource_defns['share_network'] + + self.client = mock.Mock() + self.patchobject(share_network.ManilaShareNetwork, 'client', + return_value=self.client) + self.client_plugin = mock.Mock() + self.patchobject(share_network.ManilaShareNetwork, 'client_plugin', + return_value=self.client_plugin) + self.patchobject(properties.Properties, 'validate', + return_value=mock.Mock) + + def _create_network(self, name, snippet, stack): + net = share_network.ManilaShareNetwork(name, snippet, stack) + self.client.share_networks.create.return_value = DummyShareNetwork() + self.client.share_networks.get.return_value = DummyShareNetwork() + + def get_security_service(id): + return mock.Mock(id=id) + + self.client_plugin.get_security_service.side_effect = ( + get_security_service) + + scheduler.TaskRunner(net.create)() + return net + + def test_create(self, rsrc_defn=None, stack=None): + if rsrc_defn is None: + rsrc_defn = self.rsrc_defn + if stack is None: + stack = self.stack + net = self._create_network('share_network', rsrc_defn, stack) + self.assertEqual((net.CREATE, net.COMPLETE), net.state) + self.assertEqual('42', net.resource_id) + net.client().share_networks.create.assert_called_with( + name='1', description='2', neutron_net_id='3', + neutron_subnet_id='4', nova_net_id=None) + calls = [mock.call('42', '6'), mock.call('42', '7')] + net.client().share_networks.add_security_service.assert_has_calls( + calls, any_order=True) + + def test_create_fail(self): + self.client.share_networks.add_security_service.side_effect = ( + Exception()) + self.assertRaises( + exception.ResourceFailure, + self._create_network, 'share_network', self.rsrc_defn, self.stack) + + def test_update(self): + net = self._create_network('share_network', self.rsrc_defn, self.stack) + update_template = copy.deepcopy(net.t) + update_template['Properties']['name'] = 'a' + update_template['Properties']['description'] = 'b' + update_template['Properties']['neutron_network'] = 'c' + update_template['Properties']['neutron_subnet'] = 'd' + update_template['Properties']['security_services'] = ['7', '8'] + scheduler.TaskRunner(net.update, update_template)() + self.assertEqual((net.UPDATE, net.COMPLETE), net.state) + + exp_args = { + 'name': 'a', + 'description': 'b', + 'neutron_net_id': 'c', + 'neutron_subnet_id': 'd', + 'nova_net_id': None + } + net.client().share_networks.update.assert_called_with('42', **exp_args) + net.client().share_networks.add_security_service.assert_called_with( + '42', '8') + net.client().share_networks.remove_security_service.assert_called_with( + '42', '6') + + def test_update_security_services(self): + net = self._create_network('share_network', self.rsrc_defn, self.stack) + update_template = copy.deepcopy(net.t) + update_template['Properties']['security_services'] = ['7', '8'] + scheduler.TaskRunner(net.update, update_template)() + self.assertEqual((net.UPDATE, net.COMPLETE), net.state) + called = net.client().share_networks.update.called + self.assertFalse(called) + net.client().share_networks.add_security_service.assert_called_with( + '42', '8') + net.client().share_networks.remove_security_service.assert_called_with( + '42', '6') + + def test_update_fail(self): + net = self._create_network('share_network', self.rsrc_defn, self.stack) + self.client.share_networks.remove_security_service.side_effect = ( + Exception()) + update_template = copy.deepcopy(net.t) + update_template['Properties']['security_services'] = [] + run = scheduler.TaskRunner(net.update, update_template) + self.assertRaises(exception.ResourceFailure, run) + + def test_delete(self): + net = self._create_network('share_network', self.rsrc_defn, self.stack) + scheduler.TaskRunner(net.delete)() + self.assertEqual((net.DELETE, net.COMPLETE), net.state) + self.client.share_networks.delete.assert_called_once_with( + net.resource_id) + + def test_delete_not_found(self): + net = self._create_network('share_network', self.rsrc_defn, self.stack) + self.client.share_networks.delete.side_effect = ( + self.client.exceptions.NotFound()) + scheduler.TaskRunner(net.delete)() + self.assertEqual((net.DELETE, net.COMPLETE), net.state) + self.client.share_networks.delete.assert_called_once_with( + net.resource_id) + + def test_nova_net_neutron_net_conflict(self): + t = template_format.parse(stack_template) + t['resources']['share_network']['properties']['nova_network'] = 1 + stack = utils.parse_stack(t) + rsrc_defn = stack.t.resource_definitions(stack)['share_network'] + net = self._create_network('share_network', rsrc_defn, stack) + msg = ('Cannot define the following properties at the same time: ' + 'neutron_network, nova_network.') + self.assertRaisesRegexp(exception.ResourcePropertyConflict, msg, + net.validate) + + def test_nova_net_neutron_subnet_conflict(self): + t = template_format.parse(stack_template) + t['resources']['share_network']['properties']['nova_network'] = 1 + del t['resources']['share_network']['properties']['neutron_network'] + stack = utils.parse_stack(t) + rsrc_defn = stack.t.resource_definitions(stack)['share_network'] + net = self._create_network('share_network', rsrc_defn, stack) + msg = ('Cannot define the following properties at the same time: ' + 'neutron_subnet, nova_network.') + self.assertRaisesRegexp(exception.ResourcePropertyConflict, msg, + net.validate) + + def test_attributes(self): + net = self._create_network('share_network', self.rsrc_defn, + self.stack) + self.assertEqual('2', net.FnGetAtt('segmentation_id')) + self.assertEqual('3', net.FnGetAtt('cidr')) + self.assertEqual('5', net.FnGetAtt('ip_version')) + self.assertEqual('6', net.FnGetAtt('network_type')) + + def test_resource_mapping(self): + mapping = share_network.resource_mapping() + self.assertEqual(1, len(mapping)) + self.assertEqual(share_network.ManilaShareNetwork, + mapping['OS::Manila::ShareNetwork'])