From d22734c32600fbe121a50734e2ee30bf7bdf15ad Mon Sep 17 00:00:00 2001 From: Steve Baker Date: Thu, 14 Feb 2013 10:05:11 +1300 Subject: [PATCH] Implement Internet Gateway and VPC attachment Please note that this takes a different approach for all VPC resources types. Previously the resource_id matched the underlying quantum resource UUID (or a composite of the underlying UUIDs) This had some problems, including: - it was resource_id abuse - it required extra quantum _list and _show calls to get enough data to wire up new resources - it made the already difficult job of mapping VPC <-> quantum even harder The new approach is to store all quantum UUIDs in resource metadata. The resource id is just the resource name, so resources can easily fetch other resources from the stack by ref/name then look up their metadata to find the underlying quantum UUIDs to perform operations with. Implements blueprint resource-type-internetgateway Change-Id: I69937254566b74f173439b3c5ce4c4e8d8c49afa --- heat/engine/resources/internet_gateway.py | 109 ++++++++ heat/engine/resources/network_interface.py | 13 +- heat/engine/resources/subnet.py | 24 +- heat/engine/resources/vpc.py | 11 +- heat/tests/test_vpc.py | 299 +++++++++++++-------- 5 files changed, 327 insertions(+), 129 deletions(-) create mode 100644 heat/engine/resources/internet_gateway.py diff --git a/heat/engine/resources/internet_gateway.py b/heat/engine/resources/internet_gateway.py new file mode 100644 index 00000000..49a68d07 --- /dev/null +++ b/heat/engine/resources/internet_gateway.py @@ -0,0 +1,109 @@ +# 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 quantumclient.common.exceptions import QuantumClientException + +from heat.common import exception +from heat.openstack.common import log as logging +from heat.engine import resource + +logger = logging.getLogger(__name__) + + +class InternetGateway(resource.Resource): + tags_schema = {'Key': {'Type': 'String', + 'Required': True}, + 'Value': {'Type': 'String', + 'Required': True}} + + properties_schema = { + 'Tags': {'Type': 'List', 'Schema': { + 'Type': 'Map', + 'Implemented': False, + 'Schema': tags_schema}} + } + + def __init__(self, name, json_snippet, stack): + super(InternetGateway, self).__init__(name, json_snippet, stack) + + def handle_create(self): + client = self.quantum() + + ext_filter = {'router:external': True} + ext_nets = client.list_networks(**ext_filter)['networks'] + if len(ext_nets) != 1: + # TODO sbaker if there is more than one external network + # add a heat configuration variable to set the ID of + # the default one + raise exception.Error( + 'Expected 1 external network, found %d' % len(ext_nets)) + + external_network_id = ext_nets[0]['id'] + md = { + 'external_network_id': external_network_id + } + self.metadata = md + + def handle_delete(self): + pass + + def handle_update(self, json_snippet): + return self.UPDATE_REPLACE + + +class VPCGatewayAttachment(resource.Resource): + + properties_schema = { + 'VpcId': { + 'Type': 'String', + 'Required': True}, + 'InternetGatewayId': {'Type': 'String'}, + 'VpnGatewayId': { + 'Type': 'String', + 'Implemented': False} + } + + def __init__(self, name, json_snippet, stack): + super(VPCGatewayAttachment, self).__init__(name, json_snippet, stack) + + def handle_create(self): + client = self.quantum() + gateway = self.stack[self.properties.get('InternetGatewayId')] + vpc = self.stack[self.properties.get('VpcId')] + external_network_id = gateway.metadata['external_network_id'] + + for router_id in vpc.metadata['all_router_ids']: + client.add_gateway_router(router_id, { + 'network_id': external_network_id}) + + def handle_delete(self): + client = self.quantum() + vpc = self.stack[self.properties.get('VpcId')] + for router_id in vpc.metadata['all_router_ids']: + try: + client.remove_gateway_router(router_id) + except QuantumClientException as ex: + if ex.status_code != 404: + raise ex + + def handle_update(self, json_snippet): + return self.UPDATE_REPLACE + + +def resource_mapping(): + return { + 'AWS::EC2::InternetGateway': InternetGateway, + 'AWS::EC2::VPCGatewayAttachment': VPCGatewayAttachment, + } diff --git a/heat/engine/resources/network_interface.py b/heat/engine/resources/network_interface.py index e3683a39..2c28d164 100644 --- a/heat/engine/resources/network_interface.py +++ b/heat/engine/resources/network_interface.py @@ -50,12 +50,12 @@ class NetworkInterface(resource.Resource): def handle_create(self): client = self.quantum() - fixed_ip = {'subnet_id': self.properties['SubnetId']} + subnet = self.stack[self.properties['SubnetId']] + fixed_ip = {'subnet_id': subnet.metadata['subnet_id']} if self.properties['PrivateIpAddress']: fixed_ip['ip_address'] = self.properties['PrivateIpAddress'] - subnet = client.show_subnet(self.properties['SubnetId']) - network_id = subnet['subnet']['network_id'] + network_id = subnet.metadata['network_id'] props = { 'name': self.physical_resource_name(), 'admin_state_up': True, @@ -66,12 +66,15 @@ class NetworkInterface(resource.Resource): if self.properties['GroupSet']: props['security_groups'] = self.properties['GroupSet'] port = client.create_port({'port': props})['port'] - self.resource_id_set(port['id']) + md = { + 'port_id': port['id'] + } + self.metadata = md def handle_delete(self): client = self.quantum() try: - client.delete_port(self.resource_id) + client.delete_port(self.metadata['port_id']) except QuantumClientException as ex: if ex.status_code != 404: raise ex diff --git a/heat/engine/resources/subnet.py b/heat/engine/resources/subnet.py index 66fcab52..b2d7d715 100644 --- a/heat/engine/resources/subnet.py +++ b/heat/engine/resources/subnet.py @@ -48,7 +48,10 @@ class Subnet(resource.Resource): def handle_create(self): client = self.quantum() # TODO sbaker Verify that this CidrBlock is within the vpc CidrBlock - (network_id, router_id) = self.properties.get('VpcId').split(':') + vpc = self.stack[self.properties.get('VpcId')] + network_id = vpc.metadata['network_id'] + router_id = vpc.metadata['router_id'] + props = { 'network_id': network_id, 'cidr': self.properties.get('CidrBlock'), @@ -57,24 +60,35 @@ class Subnet(resource.Resource): } subnet = client.create_subnet({'subnet': props})['subnet'] + #TODO sbaker check for a non-default router for this network + # and use that instead if it exists client.add_interface_router( router_id, {'subnet_id': subnet['id']}) - self.resource_id_set(subnet['id']) + md = { + 'network_id': network_id, + 'router_id': router_id, + 'subnet_id': subnet['id'] + } + self.metadata = md def handle_delete(self): client = self.quantum() - (network_id, router_id) = self.properties.get('VpcId').split(':') + router_id = self.metadata['router_id'] + subnet_id = self.metadata['subnet_id'] + + #TODO sbaker check for a non-default router for this network + # and remove that instead if it exists try: client.remove_interface_router( router_id, - {'subnet_id': self.resource_id}) + {'subnet_id': subnet_id}) except QuantumClientException as ex: if ex.status_code != 404: raise ex try: - client.delete_subnet(self.resource_id) + client.delete_subnet(subnet_id) except QuantumClientException as ex: if ex.status_code != 404: raise ex diff --git a/heat/engine/resources/vpc.py b/heat/engine/resources/vpc.py index de5738cb..74594269 100644 --- a/heat/engine/resources/vpc.py +++ b/heat/engine/resources/vpc.py @@ -50,12 +50,17 @@ class VPC(resource.Resource): # Creates a network with an implicit router net = client.create_network({'network': props})['network'] router = client.create_router({'router': props})['router'] - id = '%s:%s' % (net['id'], router['id']) - self.resource_id_set(id) + md = { + 'network_id': net['id'], + 'router_id': router['id'], + 'all_router_ids': [router['id']] + } + self.metadata = md def handle_delete(self): client = self.quantum() - (network_id, router_id) = self.resource_id.split(':') + network_id = self.metadata['network_id'] + router_id = self.metadata['router_id'] try: client.delete_router(router_id) except QuantumClientException as ex: diff --git a/heat/tests/test_vpc.py b/heat/tests/test_vpc.py index c2d40d19..ca37a0fc 100644 --- a/heat/tests/test_vpc.py +++ b/heat/tests/test_vpc.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,79 +21,43 @@ from nose.plugins.attrib import attr from heat.common import context from heat.common import exception from heat.common import template_format +from heat.engine import parser from heat.engine.resources import network_interface from heat.engine.resources import subnet from heat.engine.resources import vpc -from heat.engine import parser +from heat.engine.resources import internet_gateway from quantumclient.common.exceptions import QuantumClientException from quantumclient.v2_0 import client as quantumclient -test_template_vpc = ''' -HeatTemplateFormatVersion: '2012-12-12' -Resources: - the_vpc: - Type: AWS::EC2::VPC - Properties: {CidrBlock: '10.0.0.0/16'} -''' - -test_template_subnet = ''' -HeatTemplateFormatVersion: '2012-12-12' -Resources: - the_vpc: - Type: AWS::EC2::VPC - Properties: {CidrBlock: '10.0.0.0/16'} - the_subnet: - Type: AWS::EC2::Subnet - Properties: - CidrBlock: 10.0.0.0/24 - VpcId: {Ref: the_vpc} - AvailabilityZone: moon -''' - -test_template_nic = ''' -HeatTemplateFormatVersion: '2012-12-12' -Resources: - the_vpc: - Type: AWS::EC2::VPC - Properties: {CidrBlock: '10.0.0.0/16'} - the_subnet: - Type: AWS::EC2::Subnet - Properties: - CidrBlock: 10.0.0.0/24 - VpcId: {Ref: the_vpc} - AvailabilityZone: moon - the_nic: - Type: AWS::EC2::NetworkInterface - Properties: - PrivateIpAddress: 10.0.0.100 - SubnetId: {Ref: the_subnet} -''' - class VPCTestBase(unittest.TestCase): def setUp(self): self.m = mox.Mox() + self.m.StubOutWithMock(quantumclient.Client, 'add_interface_router') + self.m.StubOutWithMock(quantumclient.Client, 'add_gateway_router') self.m.StubOutWithMock(quantumclient.Client, 'create_network') + self.m.StubOutWithMock(quantumclient.Client, 'create_port') self.m.StubOutWithMock(quantumclient.Client, 'create_router') self.m.StubOutWithMock(quantumclient.Client, 'create_subnet') - self.m.StubOutWithMock(quantumclient.Client, 'show_subnet') - self.m.StubOutWithMock(quantumclient.Client, 'create_port') - self.m.StubOutWithMock(quantumclient.Client, 'add_interface_router') - self.m.StubOutWithMock(quantumclient.Client, 'remove_interface_router') self.m.StubOutWithMock(quantumclient.Client, 'delete_network') + self.m.StubOutWithMock(quantumclient.Client, 'delete_port') self.m.StubOutWithMock(quantumclient.Client, 'delete_router') self.m.StubOutWithMock(quantumclient.Client, 'delete_subnet') - self.m.StubOutWithMock(quantumclient.Client, 'delete_port') + self.m.StubOutWithMock(quantumclient.Client, 'list_networks') + self.m.StubOutWithMock(quantumclient.Client, 'remove_gateway_router') + self.m.StubOutWithMock(quantumclient.Client, 'remove_interface_router') + self.m.StubOutWithMock(quantumclient.Client, 'show_subnet') + self.m.StubOutWithMock(quantumclient.Client, 'show_network') def tearDown(self): self.m.UnsetStubs() - def create_stack(self, temlate): - t = template_format.parse(temlate) + def create_stack(self, template): + t = template_format.parse(template) stack = self.parse_stack(t) - stack.create() + self.assertEqual(None, stack.create()) return stack def parse_stack(self, t): @@ -148,51 +112,6 @@ class VPCTestBase(unittest.TestCase): u'bbbb', {'subnet_id': 'cccc'}).AndReturn(None) - def mock_create_network_interface(self): - quantumclient.Client.show_subnet('cccc').AndReturn({ - 'subnet': { - 'name': 'test_stack.the_subnet', - 'network_id': 'aaaa', - 'tenant_id': 'c1210485b2424d48804aad5d39c61b8f', - 'allocation_pools': [{ - 'start': '10.10.0.2', 'end': '10.10.0.254'}], - 'gateway_ip': '10.10.0.1', - 'ip_version': 4, - 'cidr': '10.10.0.0/24', - 'id': 'cccc', - 'enable_dhcp': False} - }) - quantumclient.Client.create_port({ - 'port': { - 'network_id': 'aaaa', 'fixed_ips': [{ - 'subnet_id': u'cccc', - 'ip_address': u'10.0.0.100' - }], - 'name': u'test_stack.the_nic', - 'admin_state_up': True - }}).AndReturn({ - 'port': { - 'admin_state_up': True, - 'device_id': '', - 'device_owner': '', - 'fixed_ips': [ - { - 'ip_address': '10.0.0.100', - 'subnet_id': 'cccc' - } - ], - 'id': 'dddd', - 'mac_address': 'fa:16:3e:25:32:5d', - 'name': 'test_stack.the_nic', - 'network_id': 'aaaa', - 'status': 'ACTIVE', - 'tenant_id': 'c1210485b2424d48804aad5d39c61b8f' - } - }) - - def mock_delete_network_interface(self): - quantumclient.Client.delete_port('dddd').AndReturn(None) - def mock_delete_network(self): quantumclient.Client.delete_router('bbbb').AndReturn(None) quantumclient.Client.delete_network('aaaa').AndReturn(None) @@ -203,24 +122,37 @@ class VPCTestBase(unittest.TestCase): {'subnet_id': 'cccc'}).AndReturn(None) quantumclient.Client.delete_subnet('cccc').AndReturn(None) + def assertResourceState(self, resource, ref_id, metadata={}): + self.assertEqual(None, resource.validate()) + self.assertEqual(resource.CREATE_COMPLETE, resource.state) + self.assertEqual(ref_id, resource.FnGetRefId()) + self.assertEqual(metadata, dict(resource.metadata)) + @attr(tag=['unit', 'resource']) @attr(speed='fast') class VPCTest(VPCTestBase): + test_template = ''' +HeatTemplateFormatVersion: '2012-12-12' +Resources: + the_vpc: + Type: AWS::EC2::VPC + Properties: {CidrBlock: '10.0.0.0/16'} +''' + def test_vpc(self): self.mock_create_network() self.mock_delete_network() self.m.ReplayAll() - stack = self.create_stack(test_template_vpc) - resource = stack['the_vpc'] - resource.validate() - - ref_id = resource.FnGetRefId() - self.assertEqual('aaaa:bbbb', ref_id) - - self.assertEqual(vpc.VPC.UPDATE_REPLACE, resource.handle_update({})) + stack = self.create_stack(self.test_template) + resource = stack['the_vpc'] + self.assertResourceState(resource, 'the_vpc', { + 'network_id': 'aaaa', + 'router_id': 'bbbb', + 'all_router_ids': ['bbbb']}) + self.assertEqual(resource.UPDATE_REPLACE, resource.handle_update({})) self.assertEqual(None, resource.delete()) self.m.VerifyAll() @@ -230,6 +162,20 @@ class VPCTest(VPCTestBase): @attr(speed='fast') class SubnetTest(VPCTestBase): + test_template = ''' +HeatTemplateFormatVersion: '2012-12-12' +Resources: + the_vpc: + Type: AWS::EC2::VPC + Properties: {CidrBlock: '10.0.0.0/16'} + the_subnet: + Type: AWS::EC2::Subnet + Properties: + CidrBlock: 10.0.0.0/24 + VpcId: {Ref: the_vpc} + AvailabilityZone: moon +''' + def test_subnet(self): self.mock_create_network() self.mock_create_subnet() @@ -245,16 +191,15 @@ class SubnetTest(VPCTestBase): QuantumClientException(status_code=404)) self.m.ReplayAll() - stack = self.create_stack(test_template_subnet) + stack = self.create_stack(self.test_template) resource = stack['the_subnet'] + self.assertResourceState(resource, 'the_subnet', { + 'network_id': 'aaaa', + 'router_id': 'bbbb', + 'subnet_id': 'cccc'}) - resource.validate() - - ref_id = resource.FnGetRefId() - self.assertEqual('cccc', ref_id) - - self.assertEqual(vpc.VPC.UPDATE_REPLACE, resource.handle_update({})) + self.assertEqual(resource.UPDATE_REPLACE, resource.handle_update({})) self.assertRaises( exception.InvalidTemplateAttribute, resource.FnGetAtt, @@ -273,6 +218,57 @@ class SubnetTest(VPCTestBase): @attr(speed='fast') class NetworkInterfaceTest(VPCTestBase): + test_template = ''' +HeatTemplateFormatVersion: '2012-12-12' +Resources: + the_vpc: + Type: AWS::EC2::VPC + Properties: {CidrBlock: '10.0.0.0/16'} + the_subnet: + Type: AWS::EC2::Subnet + Properties: + CidrBlock: 10.0.0.0/24 + VpcId: {Ref: the_vpc} + AvailabilityZone: moon + the_nic: + Type: AWS::EC2::NetworkInterface + Properties: + PrivateIpAddress: 10.0.0.100 + SubnetId: {Ref: the_subnet} +''' + + def mock_create_network_interface(self): + quantumclient.Client.create_port({ + 'port': { + 'network_id': 'aaaa', 'fixed_ips': [{ + 'subnet_id': u'cccc', + 'ip_address': u'10.0.0.100' + }], + 'name': u'test_stack.the_nic', + 'admin_state_up': True + }}).AndReturn({ + 'port': { + 'admin_state_up': True, + 'device_id': '', + 'device_owner': '', + 'fixed_ips': [ + { + 'ip_address': '10.0.0.100', + 'subnet_id': 'cccc' + } + ], + 'id': 'dddd', + 'mac_address': 'fa:16:3e:25:32:5d', + 'name': 'test_stack.the_nic', + 'network_id': 'aaaa', + 'status': 'ACTIVE', + 'tenant_id': 'c1210485b2424d48804aad5d39c61b8f' + } + }) + + def mock_delete_network_interface(self): + quantumclient.Client.delete_port('dddd').AndReturn(None) + def test_network_interface(self): self.mock_create_network() self.mock_create_subnet() @@ -283,15 +279,86 @@ class NetworkInterfaceTest(VPCTestBase): self.m.ReplayAll() - stack = self.create_stack(test_template_nic) + stack = self.create_stack(self.test_template) resource = stack['the_nic'] + self.assertResourceState(resource, 'the_nic', { + 'port_id': 'dddd'}) + + self.assertEqual(resource.UPDATE_REPLACE, resource.handle_update({})) + + stack.delete() + self.m.VerifyAll() + + +@attr(tag=['unit', 'resource']) +@attr(speed='fast') +class InternetGatewayTest(VPCTestBase): + + test_template = ''' +HeatTemplateFormatVersion: '2012-12-12' +Resources: + the_gateway: + Type: AWS::EC2::InternetGateway + the_vpc: + Type: AWS::EC2::VPC + Properties: + DependsOn : the_gateway + CidrBlock: '10.0.0.0/16' + the_subnet: + Type: AWS::EC2::Subnet + Properties: + CidrBlock: 10.0.0.0/24 + VpcId: {Ref: the_vpc} + AvailabilityZone: moon + the_attachment: + Type: AWS::EC2::VPCGatewayAttachment + Properties: + VpcId: {Ref: the_vpc} + DependsOn : the_subnet + InternetGatewayId: {Ref: the_gateway} +''' + + def mock_create_internet_gateway(self): + quantumclient.Client.list_networks( + **{'router:external': True}).AndReturn({'networks': [{ + 'status': 'ACTIVE', + 'subnets': [], + 'name': 'nova', + 'router:external': True, + 'tenant_id': 'c1210485b2424d48804aad5d39c61b8f', + 'admin_state_up': True, + 'shared': True, + 'id': 'eeee' + }]}) + + def mock_create_gateway_attachment(self): + quantumclient.Client.add_gateway_router( + 'bbbb', {'network_id': 'eeee'}).AndReturn(None) + + def mock_delete_gateway_attachment(self): + quantumclient.Client.remove_gateway_router('bbbb').AndReturn(None) + + def test_internet_gateway(self): + self.mock_create_internet_gateway() + self.mock_create_network() + self.mock_create_subnet() + self.mock_create_gateway_attachment() + self.mock_delete_gateway_attachment() + self.mock_delete_subnet() + self.mock_delete_network() + + self.m.ReplayAll() - resource.validate() + stack = self.create_stack(self.test_template) - ref_id = resource.FnGetRefId() - self.assertEqual('dddd', ref_id) + gateway = stack['the_gateway'] + self.assertResourceState(gateway, 'the_gateway', { + 'external_network_id': 'eeee'}) + self.assertEqual(gateway.UPDATE_REPLACE, gateway.handle_update({})) - self.assertEqual(vpc.VPC.UPDATE_REPLACE, resource.handle_update({})) + attachment = stack['the_attachment'] + self.assertResourceState(attachment, 'the_attachment') + self.assertEqual(gateway.UPDATE_REPLACE, attachment.handle_update({})) stack.delete() self.m.VerifyAll() -- 2.45.2