diff --git a/heat/engine/resources/security_group.py b/heat/engine/resources/security_group.py index c62dffadd0..ac934d9ef9 100644 --- a/heat/engine/resources/security_group.py +++ b/heat/engine/resources/security_group.py @@ -24,16 +24,81 @@ logger = logging.getLogger(__name__) class SecurityGroup(resource.Resource): properties_schema = {'GroupDescription': {'Type': 'String', 'Required': True}, - 'VpcId': {'Type': 'String', - 'Implemented': False}, + 'VpcId': {'Type': 'String'}, 'SecurityGroupIngress': {'Type': 'List'}, - 'SecurityGroupEgress': {'Type': 'List', - 'Implemented': False}} + 'SecurityGroupEgress': {'Type': 'List'}} def __init__(self, name, json_snippet, stack): super(SecurityGroup, self).__init__(name, json_snippet, stack) def handle_create(self): + if self.properties['VpcId'] and clients.quantumclient is not None: + self._handle_create_quantum() + else: + self._handle_create_nova() + + def _handle_create_quantum(self): + from quantumclient.common.exceptions import QuantumClientException + client = self.quantum() + + sec = client.create_security_group({'security_group': { + 'name': self.physical_resource_name(), + 'description': self.properties['GroupDescription']} + })['security_group'] + + self.resource_id_set(sec['id']) + if self.properties['SecurityGroupIngress']: + for i in self.properties['SecurityGroupIngress']: + # Quantum only accepts positive ints + if int(i['FromPort']) < 0: + i['FromPort'] = None + if int(i['ToPort']) < 0: + i['ToPort'] = None + if i['FromPort'] is None and i['ToPort'] is None: + i['CidrIp'] = None + + try: + rule = client.create_security_group_rule({ + 'security_group_rule': { + 'direction': 'ingress', + 'remote_ip_prefix': i['CidrIp'], + 'port_range_min': i['FromPort'], + 'ethertype': 'IPv4', + 'port_range_max': i['ToPort'], + 'protocol': i['IpProtocol'], + 'security_group_id': sec['id'] + } + }) + except QuantumClientException as ex: + if ex.status_code == 409: + # no worries, the rule is already there + pass + else: + # unexpected error + raise + if self.properties['SecurityGroupEgress']: + for i in self.properties['SecurityGroupEgress']: + try: + rule = client.create_security_group_rule({ + 'security_group_rule': { + 'direction': 'egress', + 'remote_ip_prefix': i['CidrIp'], + 'port_range_min': i['FromPort'], + 'ethertype': 'IPv4', + 'port_range_max': i['ToPort'], + 'protocol': i['IpProtocol'], + 'security_group_id': sec['id'] + } + }) + except QuantumClientException as ex: + if ex.status_code == 409: + # no worries, the rule is already there + pass + else: + # unexpected error + raise + + def _handle_create_nova(self): sec = None groups = self.nova().security_groups.list() @@ -69,6 +134,12 @@ class SecurityGroup(resource.Resource): return self.UPDATE_REPLACE def handle_delete(self): + if self.properties['VpcId'] and clients.quantumclient is not None: + self._handle_delete_quantum() + else: + self._handle_delete_nova() + + def _handle_delete_nova(self): if self.resource_id is not None: try: sec = self.nova().security_groups.get(self.resource_id) @@ -84,6 +155,32 @@ class SecurityGroup(resource.Resource): self.nova().security_groups.delete(self.resource_id) self.resource_id = None + def _handle_delete_quantum(self): + from quantumclient.common.exceptions import QuantumClientException + client = self.quantum() + + if self.resource_id is not None: + try: + sec = client.show_security_group( + self.resource_id)['security_group'] + except QuantumClientException as ex: + if ex.status_code != 404: + raise + else: + for rule in sec['security_group_rules']: + try: + client.delete_security_group_rule(rule['id']) + except QuantumClientException as ex: + if ex.status_code != 404: + raise + + try: + client.delete_security_group(self.resource_id) + except QuantumClientException as ex: + if ex.status_code != 404: + raise + self.resource_id = None + def FnGetRefId(self): return unicode(self.name) diff --git a/heat/tests/test_security_group.py b/heat/tests/test_security_group.py index 913248cde1..22ad385793 100644 --- a/heat/tests/test_security_group.py +++ b/heat/tests/test_security_group.py @@ -1,6 +1,6 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 -# Licensed under the Apache License, Version 2.0 (the 'License"); you may +# 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 # @@ -21,9 +21,12 @@ from heat.engine import parser from heat.tests.common import HeatTestCase from heat.tests.utils import setup_dummy_db from heat.tests.v1_1 import fakes +from heat.tests.utils import stack_delete_after from novaclient.v1_1 import security_groups as nova_sg from novaclient.v1_1 import security_group_rules as nova_sgr +from quantumclient.common.exceptions import QuantumClientException +from quantumclient.v2_0 import client as quantumclient NovaSG = collections.namedtuple('NovaSG', ' '.join([ @@ -54,6 +57,30 @@ Resources: CidrIp : 0.0.0.0/0 ''' + test_template_quantum = ''' +HeatTemplateFormatVersion: '2012-12-12' +Resources: + the_sg: + Type: AWS::EC2::SecurityGroup + Properties: + GroupDescription: HTTP and SSH access + VpcId: aaaa + SecurityGroupIngress: + - IpProtocol: tcp + FromPort: 22 + ToPort: 22 + CidrIp: 0.0.0.0/0 + - IpProtocol: tcp + FromPort : 80 + ToPort : 80 + CidrIp : 0.0.0.0/0 + SecurityGroupEgress: + - IpProtocol: tcp + FromPort: 22 + ToPort: 22 + CidrIp: 10.0.1.0/24 +''' + def setUp(self): super(SecurityGroupTest, self).setUp() self.fc = fakes.FakeClient() @@ -65,12 +92,22 @@ Resources: self.m.StubOutWithMock(nova_sg.SecurityGroupManager, 'get') self.m.StubOutWithMock(nova_sg.SecurityGroupManager, 'list') setup_dummy_db() + self.m.StubOutWithMock(quantumclient.Client, 'create_security_group') + self.m.StubOutWithMock( + quantumclient.Client, 'create_security_group_rule') + self.m.StubOutWithMock(quantumclient.Client, 'show_security_group') + self.m.StubOutWithMock( + quantumclient.Client, 'delete_security_group_rule') + self.m.StubOutWithMock(quantumclient.Client, 'delete_security_group') + + def tearDown(self): + super(SecurityGroupTest, self).tearDown() def create_stack(self, template): t = template_format.parse(template) - stack = self.parse_stack(t) - self.assertEqual(None, stack.create()) - return stack + self.stack = self.parse_stack(t) + self.assertEqual(None, self.stack.create()) + return self.stack def parse_stack(self, t): ctx = context.RequestContext.from_dict({ @@ -91,6 +128,7 @@ Resources: self.assertEqual(ref_id, resource.FnGetRefId()) self.assertEqual(metadata, dict(resource.metadata)) + @stack_delete_after def test_security_group_nova(self): #create script clients.OpenStackClients.nova('compute').AndReturn(self.fc) @@ -130,17 +168,17 @@ Resources: "ip_range": { "cidr": "0.0.0.0/0" }, - "id": 130 + 'id': 130 }, { - "from_port": 80, - "group": {}, - "ip_protocol": "tcp", - "to_port": 80, - "parent_group_id": 2, - "ip_range": { - "cidr": "0.0.0.0/0" + 'from_port': 80, + 'group': {}, + 'ip_protocol': 'tcp', + 'to_port': 80, + 'parent_group_id': 2, + 'ip_range': { + 'cidr': '0.0.0.0/0' }, - "id": 131 + 'id': 131 }] )) clients.OpenStackClients.nova('compute').AndReturn(self.fc) @@ -161,6 +199,7 @@ Resources: stack.delete() self.m.VerifyAll() + @stack_delete_after def test_security_group_nova_exception(self): #create script clients.OpenStackClients.nova('compute').AndReturn(self.fc) @@ -196,17 +235,17 @@ Resources: "ip_range": { "cidr": "0.0.0.0/0" }, - "id": 130 + 'id': 130 }, { - "from_port": 80, - "group": {}, - "ip_protocol": "tcp", - "to_port": 80, - "parent_group_id": 2, - "ip_range": { - "cidr": "0.0.0.0/0" + 'from_port': 80, + 'group': {}, + 'ip_protocol': 'tcp', + 'to_port': 80, + 'parent_group_id': 2, + 'ip_range': { + 'cidr': '0.0.0.0/0' }, - "id": 131 + 'id': 131 }] )) clients.OpenStackClients.nova('compute').AndReturn(self.fc) @@ -237,3 +276,263 @@ Resources: stack.delete() self.m.VerifyAll() + + @stack_delete_after + def test_security_group_quantum(self): + #create script + quantumclient.Client.create_security_group({ + 'security_group': { + 'name': 'test_stack.the_sg', + 'description': 'HTTP and SSH access' + } + }).AndReturn({ + 'security_group': { + 'tenant_id': 'f18ca530cc05425e8bac0a5ff92f7e88', + 'name': 'test_stack.the_sg', + 'description': 'HTTP and SSH access', + 'security_group_rules': [], + 'id': 'aaaa' + } + }) + + quantumclient.Client.create_security_group_rule({ + 'security_group_rule': { + 'direction': 'ingress', + 'remote_ip_prefix': '0.0.0.0/0', + 'port_range_min': 22, + 'ethertype': 'IPv4', + 'port_range_max': 22, + 'protocol': 'tcp', + 'security_group_id': 'aaaa' + } + }).AndReturn({ + 'security_group_rule': { + 'direction': 'ingress', + 'remote_ip_prefix': '0.0.0.0/0', + 'port_range_min': 22, + 'ethertype': 'IPv4', + 'port_range_max': 22, + 'protocol': 'tcp', + 'security_group_id': 'aaaa', + 'id': 'bbbb' + } + }) + quantumclient.Client.create_security_group_rule({ + 'security_group_rule': { + 'direction': 'ingress', + 'remote_ip_prefix': '0.0.0.0/0', + 'port_range_min': 80, + 'ethertype': 'IPv4', + 'port_range_max': 80, + 'protocol': 'tcp', + 'security_group_id': 'aaaa' + } + }).AndReturn({ + 'security_group_rule': { + 'direction': 'ingress', + 'remote_ip_prefix': '0.0.0.0/0', + 'port_range_min': 80, + 'ethertype': 'IPv4', + 'port_range_max': 80, + 'protocol': 'tcp', + 'security_group_id': 'aaaa', + 'id': 'cccc' + } + }) + quantumclient.Client.create_security_group_rule({ + 'security_group_rule': { + 'direction': 'egress', + 'remote_ip_prefix': '10.0.1.0/24', + 'port_range_min': 22, + 'ethertype': 'IPv4', + 'port_range_max': 22, + 'protocol': 'tcp', + 'security_group_id': 'aaaa' + } + }).AndReturn({ + 'security_group_rule': { + 'direction': 'egress', + 'remote_ip_prefix': '10.0.1.0/24', + 'port_range_min': 22, + 'ethertype': 'IPv4', + 'port_range_max': 22, + 'protocol': 'tcp', + 'security_group_id': 'aaaa', + 'id': 'dddd' + } + }) + + # delete script + quantumclient.Client.show_security_group('aaaa').AndReturn({ + 'security_group': { + 'tenant_id': 'f18ca530cc05425e8bac0a5ff92f7e88', + 'name': 'sc1', + 'description': '', + 'security_group_rules': [{ + 'direction': 'ingress', + 'protocol': 'tcp', + 'port_range_max': 22, + 'id': 'bbbb', + 'ethertype': 'IPv4', + 'security_group_id': 'aaaa', + 'remote_ip_prefix': '0.0.0.0/0', + 'tenant_id': 'f18ca530cc05425e8bac0a5ff92f7e88', + 'port_range_min': 22 + }, { + 'direction': 'ingress', + 'protocol': 'tcp', + 'port_range_max': 80, + 'id': 'cccc', + 'ethertype': 'IPv4', + 'security_group_id': 'aaaa', + 'remote_ip_prefix': '0.0.0.0/0', + 'tenant_id': 'f18ca530cc05425e8bac0a5ff92f7e88', + 'port_range_min': 80 + }, { + 'direction': 'egress', + 'protocol': 'tcp', + 'port_range_max': 22, + 'id': 'dddd', + 'ethertype': 'IPv4', + 'security_group_id': 'aaaa', + 'remote_ip_prefix': '10.0.1.0/24', + 'tenant_id': 'f18ca530cc05425e8bac0a5ff92f7e88', + 'port_range_min': 22 + }], + 'id': 'aaaa'}}) + quantumclient.Client.delete_security_group_rule('bbbb').AndReturn(None) + quantumclient.Client.delete_security_group_rule('cccc').AndReturn(None) + quantumclient.Client.delete_security_group_rule('dddd').AndReturn(None) + quantumclient.Client.delete_security_group('aaaa').AndReturn(None) + + self.m.ReplayAll() + stack = self.create_stack(self.test_template_quantum) + + sg = stack['the_sg'] + self.assertEqual(sg.UPDATE_REPLACE, sg.handle_update({})) + + self.assertResourceState(sg, 'the_sg') + + stack.delete() + self.m.VerifyAll() + + @stack_delete_after + def test_security_group_quantum_exception(self): + #create script + quantumclient.Client.create_security_group({ + 'security_group': { + 'name': 'test_stack.the_sg', + 'description': 'HTTP and SSH access' + } + }).AndReturn({ + 'security_group': { + 'tenant_id': 'f18ca530cc05425e8bac0a5ff92f7e88', + 'name': 'test_stack.the_sg', + 'description': 'HTTP and SSH access', + 'security_group_rules': [], + 'id': 'aaaa' + } + }) + + quantumclient.Client.create_security_group_rule({ + 'security_group_rule': { + 'direction': 'ingress', + 'remote_ip_prefix': '0.0.0.0/0', + 'port_range_min': 22, + 'ethertype': 'IPv4', + 'port_range_max': 22, + 'protocol': 'tcp', + 'security_group_id': 'aaaa' + } + }).AndRaise( + QuantumClientException(status_code=409)) + quantumclient.Client.create_security_group_rule({ + 'security_group_rule': { + 'direction': 'ingress', + 'remote_ip_prefix': '0.0.0.0/0', + 'port_range_min': 80, + 'ethertype': 'IPv4', + 'port_range_max': 80, + 'protocol': 'tcp', + 'security_group_id': 'aaaa' + } + }).AndRaise( + QuantumClientException(status_code=409)) + quantumclient.Client.create_security_group_rule({ + 'security_group_rule': { + 'direction': 'egress', + 'remote_ip_prefix': '10.0.1.0/24', + 'port_range_min': 22, + 'ethertype': 'IPv4', + 'port_range_max': 22, + 'protocol': 'tcp', + 'security_group_id': 'aaaa' + } + }).AndRaise( + QuantumClientException(status_code=409)) + + # delete script + quantumclient.Client.show_security_group('aaaa').AndReturn({ + 'security_group': { + 'tenant_id': 'f18ca530cc05425e8bac0a5ff92f7e88', + 'name': 'sc1', + 'description': '', + 'security_group_rules': [{ + 'direction': 'ingress', + 'protocol': 'tcp', + 'port_range_max': 22, + 'id': 'bbbb', + 'ethertype': 'IPv4', + 'security_group_id': 'aaaa', + 'remote_ip_prefix': '0.0.0.0/0', + 'tenant_id': 'f18ca530cc05425e8bac0a5ff92f7e88', + 'port_range_min': 22 + }, { + 'direction': 'ingress', + 'protocol': 'tcp', + 'port_range_max': 80, + 'id': 'cccc', + 'ethertype': 'IPv4', + 'security_group_id': 'aaaa', + 'remote_ip_prefix': '0.0.0.0/0', + 'tenant_id': 'f18ca530cc05425e8bac0a5ff92f7e88', + 'port_range_min': 80 + }, { + 'direction': 'egress', + 'protocol': 'tcp', + 'port_range_max': 22, + 'id': 'dddd', + 'ethertype': 'IPv4', + 'security_group_id': 'aaaa', + 'remote_ip_prefix': '10.0.1.0/24', + 'tenant_id': 'f18ca530cc05425e8bac0a5ff92f7e88', + 'port_range_min': 22 + }], + 'id': 'aaaa'}}) + quantumclient.Client.delete_security_group_rule('bbbb').AndRaise( + QuantumClientException(status_code=404)) + quantumclient.Client.delete_security_group_rule('cccc').AndRaise( + QuantumClientException(status_code=404)) + quantumclient.Client.delete_security_group_rule('dddd').AndRaise( + QuantumClientException(status_code=404)) + quantumclient.Client.delete_security_group('aaaa').AndRaise( + QuantumClientException(status_code=404)) + + quantumclient.Client.show_security_group('aaaa').AndRaise( + QuantumClientException(status_code=404)) + + self.m.ReplayAll() + stack = self.create_stack(self.test_template_quantum) + + sg = stack['the_sg'] + self.assertEqual(sg.UPDATE_REPLACE, sg.handle_update({})) + + self.assertResourceState(sg, 'the_sg') + + self.assertEqual(None, sg.delete()) + + sg.state_set(sg.CREATE_COMPLETE, 'to delete again') + sg.resource_id = 'aaaa' + stack.delete() + + self.m.VerifyAll()