]> review.fuel-infra Code Review - openstack-build/heat-build.git/commitdiff
Implement Internet Gateway and VPC attachment
authorSteve Baker <sbaker@redhat.com>
Wed, 13 Feb 2013 21:05:11 +0000 (10:05 +1300)
committerSteve Baker <sbaker@redhat.com>
Sun, 17 Feb 2013 20:13:58 +0000 (09:13 +1300)
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 [new file with mode: 0644]
heat/engine/resources/network_interface.py
heat/engine/resources/subnet.py
heat/engine/resources/vpc.py
heat/tests/test_vpc.py

diff --git a/heat/engine/resources/internet_gateway.py b/heat/engine/resources/internet_gateway.py
new file mode 100644 (file)
index 0000000..49a68d0
--- /dev/null
@@ -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,
+    }
index e3683a39f26c7dee12f3ff28d34aabbdcbcb7819..2c28d16481cb77a195dc965707d4117bb0df96d6 100644 (file)
@@ -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
index 66fcab52d5292061adb527a6a27ebba309e2beec..b2d7d7151b46efa63ece0f53bc7131acd6402305 100644 (file)
@@ -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
index de5738cbb551fa4f371864037ca7d57eb3b51ea4..745942692e188eed9996cbf5c73a23b2cfc0c4a8 100644 (file)
@@ -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:
index c2d40d192053eb57085ae82296c2f3237e4fafab..ca37a0fcbf6588d9671792d16e5f5808d4ca1ded 100644 (file)
@@ -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()