From 109d01c50fec51cb77ed78c9a9dac0f43ff30fdb Mon Sep 17 00:00:00 2001 From: =?utf8?q?Bartosz=20G=C3=B3rski?= Date: Mon, 2 Sep 2013 18:33:05 -0700 Subject: [PATCH] Adding VPN Service to Heat resources Adds Neutron VPN service component to resources supported by Heat with unit tests. Change-Id: I3a626166253bcb33d8875cf406b293688f53ffa3 Implements: blueprint vpnaas-support --- heat/engine/resources/neutron/vpnservice.py | 92 ++++++++++ heat/tests/test_neutron_vpnservice.py | 189 ++++++++++++++++++++ 2 files changed, 281 insertions(+) create mode 100644 heat/engine/resources/neutron/vpnservice.py create mode 100644 heat/tests/test_neutron_vpnservice.py diff --git a/heat/engine/resources/neutron/vpnservice.py b/heat/engine/resources/neutron/vpnservice.py new file mode 100644 index 00000000..8c44204e --- /dev/null +++ b/heat/engine/resources/neutron/vpnservice.py @@ -0,0 +1,92 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# +# 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.engine import clients +from heat.engine.resources.neutron import neutron +from heat.engine import scheduler + +if clients.neutronclient is not None: + from neutronclient.common.exceptions import NeutronClientException + +from heat.openstack.common import log as logging + +logger = logging.getLogger(__name__) + + +class VPNService(neutron.NeutronResource): + """ + A resource for VPN service in Neutron. + """ + + properties_schema = {'name': {'Type': 'String'}, + 'description': {'Type': 'String'}, + 'admin_state_up': {'Type': 'Boolean', + 'Default': True}, + 'subnet_id': {'Type': 'String', + 'Required': True}, + 'router_id': {'Type': 'String', + 'Required': True}} + + attributes_schema = { + 'admin_state_up': 'the administrative state of the vpn service', + 'description': 'description of the vpn service', + 'id': 'unique identifier for the vpn service', + 'name': 'name for the vpn service', + 'router_id': 'unique identifier for router used to create the vpn' + ' service', + 'status': 'the status of the vpn service', + 'subnet_id': 'unique identifier for subnet used to create the vpn' + ' service', + 'tenant_id': 'tenant owning the vpn service' + } + + update_allowed_keys = ('Properties',) + + update_allowed_properties = ('name', 'description', 'admin_state_up',) + + def _show_resource(self): + return self.neutron().show_vpnservice(self.resource_id)['vpnservice'] + + def handle_create(self): + props = self.prepare_properties( + self.properties, + self.physical_resource_name()) + vpnservice = self.neutron().create_vpnservice({'vpnservice': props})[ + 'vpnservice'] + self.resource_id_set(vpnservice['id']) + + def handle_update(self, json_snippet, tmpl_diff, prop_diff): + if prop_diff: + self.neutron().update_vpnservice(self.resource_id, + {'vpnservice': prop_diff}) + + def handle_delete(self): + client = self.neutron() + try: + client.delete_vpnservice(self.resource_id) + except NeutronClientException as ex: + if ex.status_code != 404: + raise ex + else: + return scheduler.TaskRunner(self._confirm_delete)() + + +def resource_mapping(): + if clients.neutronclient is None: + return {} + + return { + 'OS::Neutron::VPNService': VPNService, + } diff --git a/heat/tests/test_neutron_vpnservice.py b/heat/tests/test_neutron_vpnservice.py new file mode 100644 index 00000000..d53775ee --- /dev/null +++ b/heat/tests/test_neutron_vpnservice.py @@ -0,0 +1,189 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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 + +from testtools import skipIf + +from heat.common import exception +from heat.common import template_format +from heat.engine import clients +from heat.engine import scheduler +from heat.engine.resources.neutron import vpnservice +from heat.openstack.common.importutils import try_import +from heat.tests import fakes +from heat.tests import utils +from heat.tests.common import HeatTestCase + + +neutronclient = try_import('neutronclient.v2_0.client') + +vpnservice_template = ''' +{ + "AWSTemplateFormatVersion" : "2010-09-09", + "Description" : "Template to test VPN service resource", + "Parameters" : {}, + "Resources" : { + "VPNService" : { + "Type" : "OS::Neutron::VPNService", + "Properties" : { + "name" : "VPNService", + "description" : "My new VPN service", + "admin_state_up" : true, + "router_id" : "rou123", + "subnet_id" : "sub123" + } + } + } +} +''' + + +@skipIf(neutronclient is None, 'neutronclient unavailable') +class VPNServiceTest(HeatTestCase): + + VPN_SERVICE_CONF = { + 'vpnservice': { + 'name': 'VPNService', + 'description': 'My new VPN service', + 'admin_state_up': True, + 'router_id': 'rou123', + 'subnet_id': 'sub123' + } + } + + def setUp(self): + super(VPNServiceTest, self).setUp() + self.m.StubOutWithMock(neutronclient.Client, 'create_vpnservice') + self.m.StubOutWithMock(neutronclient.Client, 'delete_vpnservice') + self.m.StubOutWithMock(neutronclient.Client, 'show_vpnservice') + self.m.StubOutWithMock(neutronclient.Client, 'update_vpnservice') + self.m.StubOutWithMock(clients.OpenStackClients, 'keystone') + utils.setup_dummy_db() + + def create_vpnservice(self): + clients.OpenStackClients.keystone().AndReturn( + fakes.FakeKeystoneClient()) + neutronclient.Client.create_vpnservice( + self.VPN_SERVICE_CONF).AndReturn({'vpnservice': {'id': 'vpn123'}}) + snippet = template_format.parse(vpnservice_template) + self.stack = utils.parse_stack(snippet) + return vpnservice.VPNService('vpnservice', + snippet['Resources']['VPNService'], + self.stack) + + @utils.stack_delete_after + def test_create(self): + rsrc = self.create_vpnservice() + self.m.ReplayAll() + scheduler.TaskRunner(rsrc.create)() + self.assertEqual((rsrc.CREATE, rsrc.COMPLETE), rsrc.state) + self.m.VerifyAll() + + @utils.stack_delete_after + def test_create_failed(self): + clients.OpenStackClients.keystone().AndReturn( + fakes.FakeKeystoneClient()) + neutronclient.Client.create_vpnservice(self.VPN_SERVICE_CONF).AndRaise( + vpnservice.NeutronClientException()) + self.m.ReplayAll() + snippet = template_format.parse(vpnservice_template) + self.stack = utils.parse_stack(snippet) + rsrc = vpnservice.VPNService('vpnservice', + snippet['Resources']['VPNService'], + self.stack) + error = self.assertRaises(exception.ResourceFailure, + scheduler.TaskRunner(rsrc.create)) + self.assertEqual( + 'NeutronClientException: An unknown exception occurred.', + str(error)) + self.assertEqual((rsrc.CREATE, rsrc.FAILED), rsrc.state) + self.m.VerifyAll() + + @utils.stack_delete_after + def test_delete(self): + neutronclient.Client.delete_vpnservice('vpn123') + neutronclient.Client.show_vpnservice('vpn123').AndRaise( + vpnservice.NeutronClientException(status_code=404)) + rsrc = self.create_vpnservice() + self.m.ReplayAll() + scheduler.TaskRunner(rsrc.create)() + scheduler.TaskRunner(rsrc.delete)() + self.assertEqual((rsrc.DELETE, rsrc.COMPLETE), rsrc.state) + self.m.VerifyAll() + + @utils.stack_delete_after + def test_delete_already_gone(self): + neutronclient.Client.delete_vpnservice('vpn123').AndRaise( + vpnservice.NeutronClientException(status_code=404)) + rsrc = self.create_vpnservice() + self.m.ReplayAll() + scheduler.TaskRunner(rsrc.create)() + scheduler.TaskRunner(rsrc.delete)() + self.assertEqual((rsrc.DELETE, rsrc.COMPLETE), rsrc.state) + self.m.VerifyAll() + + @utils.stack_delete_after + def test_delete_failed(self): + neutronclient.Client.delete_vpnservice('vpn123').AndRaise( + vpnservice.NeutronClientException(status_code=400)) + rsrc = self.create_vpnservice() + self.m.ReplayAll() + scheduler.TaskRunner(rsrc.create)() + error = self.assertRaises(exception.ResourceFailure, + scheduler.TaskRunner(rsrc.delete)) + self.assertEqual( + 'NeutronClientException: An unknown exception occurred.', + str(error)) + self.assertEqual((rsrc.DELETE, rsrc.FAILED), rsrc.state) + self.m.VerifyAll() + + @utils.stack_delete_after + def test_attribute(self): + rsrc = self.create_vpnservice() + neutronclient.Client.show_vpnservice('vpn123').MultipleTimes( + ).AndReturn(self.VPN_SERVICE_CONF) + self.m.ReplayAll() + scheduler.TaskRunner(rsrc.create)() + self.assertEqual('VPNService', rsrc.FnGetAtt('name')) + self.assertEqual('My new VPN service', rsrc.FnGetAtt('description')) + self.assertEqual(True, rsrc.FnGetAtt('admin_state_up')) + self.assertEqual('rou123', rsrc.FnGetAtt('router_id')) + self.assertEqual('sub123', rsrc.FnGetAtt('subnet_id')) + self.m.VerifyAll() + + @utils.stack_delete_after + def test_attribute_failed(self): + rsrc = self.create_vpnservice() + self.m.ReplayAll() + scheduler.TaskRunner(rsrc.create)() + error = self.assertRaises(exception.InvalidTemplateAttribute, + rsrc.FnGetAtt, 'non-existent_property') + self.assertEqual( + 'The Referenced Attribute (vpnservice non-existent_property) is ' + 'incorrect.', + str(error)) + self.m.VerifyAll() + + @utils.stack_delete_after + def test_update(self): + rsrc = self.create_vpnservice() + neutronclient.Client.update_vpnservice( + 'vpn123', {'vpnservice': {'admin_state_up': False}}) + self.m.ReplayAll() + scheduler.TaskRunner(rsrc.create)() + update_template = copy.deepcopy(rsrc.t) + update_template['Properties']['admin_state_up'] = False + scheduler.TaskRunner(rsrc.update, update_template)() + self.m.VerifyAll() -- 2.45.2