]> review.fuel-infra Code Review - openstack-build/heat-build.git/commitdiff
Implement the SubnetId property in the Instance resource
authorWinson Chan <winson.c.chan@intel.com>
Wed, 8 May 2013 06:13:49 +0000 (23:13 -0700)
committerWinson Chan <winson.c.chan@intel.com>
Fri, 10 May 2013 15:00:40 +0000 (08:00 -0700)
If the SubnetId parameter is given in the template, the handle_create method
in the Instance resource verifies the subnet in quantum and configures the
NetworkInterfaces for the subnet. If no NetworkInterface is specified, a port
is created for the subnet and the nic is configured to use that port.

Change-Id: I5a05cbe0798e6b181bf9dd3b547cda525e18480f
Fixes: bug #1163952
heat/engine/resources/instance.py
heat/engine/resources/network_interface.py
heat/tests/test_instance.py
heat/tests/test_instance_network.py [new file with mode: 0644]
heat/tests/test_vpc.py

index a83c49889eb6d44c8e2767e0e85112c473ac0b07..9ebe9ed485e1cf8ab6e948afbf2f1ee1e0da592e 100644 (file)
@@ -26,6 +26,7 @@ from oslo.config import cfg
 from heat.engine import clients
 from heat.engine import resource
 from heat.common import exception
+from heat.engine.resources.network_interface import NetworkInterface
 
 from heat.openstack.common import log as logging
 
@@ -90,8 +91,7 @@ class Instance(resource.Resource):
                          'NetworkInterfaces': {'Type': 'List'},
                          'SourceDestCheck': {'Type': 'Boolean',
                                              'Implemented': False},
-                         'SubnetId': {'Type': 'String',
-                                      'Implemented': False},
+                         'SubnetId': {'Type': 'String'},
                          'Tags': {'Type': 'List',
                                   'Schema': {'Type': 'Map',
                                              'Schema': tags_schema}},
@@ -231,22 +231,42 @@ class Instance(resource.Resource):
 
         return self.mime_string
 
-    @staticmethod
-    def _build_nics(network_interfaces):
-        if not network_interfaces:
-            return None
-
-        nics = []
-        for nic in network_interfaces:
-            if isinstance(nic, basestring):
-                nics.append({
-                    'NetworkInterfaceId': nic,
-                    'DeviceIndex': len(nics)})
-            else:
-                nics.append(nic)
-        sorted_nics = sorted(nics, key=lambda nic: int(nic['DeviceIndex']))
-
-        return [{'port-id': nic['NetworkInterfaceId']} for nic in sorted_nics]
+    def _build_nics(self, network_interfaces, subnet_id=None):
+
+        nics = None
+
+        if network_interfaces:
+            unsorted_nics = []
+            for entry in network_interfaces:
+                nic = (entry
+                       if not isinstance(entry, basestring)
+                       else {'NetworkInterfaceId': entry,
+                             'DeviceIndex': len(unsorted_nics)})
+                unsorted_nics.append(nic)
+            sorted_nics = sorted(unsorted_nics,
+                                 key=lambda nic: int(nic['DeviceIndex']))
+            nics = [{'port-id': nic['NetworkInterfaceId']}
+                    for nic in sorted_nics]
+        else:
+            # if SubnetId property in Instance, ensure subnet exists
+            if subnet_id:
+                quantumclient = self.quantum()
+                network_id = NetworkInterface.network_id_from_subnet_id(
+                    quantumclient, subnet_id)
+                # if subnet verified, create a port to use this subnet
+                # if port is not created explicitly, nova will choose
+                # the first subnet in the given network.
+                if network_id:
+                    fixed_ip = {'subnet_id': subnet_id}
+                    props = {
+                        'admin_state_up': True,
+                        'network_id': network_id,
+                        'fixed_ips': [fixed_ip]
+                    }
+                    port = quantumclient.create_port({'port': props})['port']
+                    nics = [{'port-id': port['id']}]
+
+        return nics
 
     def handle_create(self):
         if self.properties.get('SecurityGroups') is None:
@@ -299,7 +319,8 @@ class Instance(resource.Resource):
         else:
             scheduler_hints = None
 
-        nics = self._build_nics(self.properties['NetworkInterfaces'])
+        nics = self._build_nics(self.properties['NetworkInterfaces'],
+                                subnet_id=self.properties['SubnetId'])
 
         server_userdata = self._build_userdata(userdata)
         server = None
index c1601e62c80a13b0a61c32e624e32bd90c49de4e..eee083ec83e9dde40c6ebf34c5fb9f5f3d6bee05 100644 (file)
@@ -46,15 +46,21 @@ class NetworkInterface(resource.Resource):
     def __init__(self, name, json_snippet, stack):
         super(NetworkInterface, self).__init__(name, json_snippet, stack)
 
+    @staticmethod
+    def network_id_from_subnet_id(quantumclient, subnet_id):
+        subnet_info = quantumclient.show_subnet(subnet_id)
+        return subnet_info['subnet']['network_id']
+
     def handle_create(self):
         client = self.quantum()
 
-        subnet = self.stack.resource_by_refid(self.properties['SubnetId'])
-        fixed_ip = {'subnet_id': self.properties['SubnetId']}
+        subnet_id = self.properties['SubnetId']
+        network_id = self.network_id_from_subnet_id(client, subnet_id)
+
+        fixed_ip = {'subnet_id': subnet_id}
         if self.properties['PrivateIpAddress']:
             fixed_ip['ip_address'] = self.properties['PrivateIpAddress']
 
-        network_id = subnet.properties.get('VpcId')
         props = {
             'name': self.physical_resource_name(),
             'admin_state_up': True,
index c032aef0c4f1899b4e152fa55800db2f86dee1d4..bfb59e3e96c25929ab6df41d8a5fb786adf112a6 100644 (file)
@@ -212,16 +212,20 @@ class instancesTest(HeatTestCase):
         self.assertEqual(instance.state, instance.CREATE_COMPLETE)
 
     def test_build_nics(self):
-        self.assertEqual(None, instances.Instance._build_nics([]))
-        self.assertEqual(None, instances.Instance._build_nics(None))
+        return_server = self.fc.servers.list()[1]
+        instance = self._create_test_instance(return_server,
+                                              'test_build_nics')
+
+        self.assertEqual(None, instance._build_nics([]))
+        self.assertEqual(None, instance._build_nics(None))
         self.assertEqual([
             {'port-id': 'id3'}, {'port-id': 'id1'}, {'port-id': 'id2'}],
-            instances.Instance._build_nics([
+            instance._build_nics([
                 'id3', 'id1', 'id2']))
         self.assertEqual([
             {'port-id': 'id1'},
             {'port-id': 'id2'},
-            {'port-id': 'id3'}], instances.Instance._build_nics([
+            {'port-id': 'id3'}], instance._build_nics([
                 {'NetworkInterfaceId': 'id3', 'DeviceIndex': '3'},
                 {'NetworkInterfaceId': 'id1', 'DeviceIndex': '1'},
                 {'NetworkInterfaceId': 'id2', 'DeviceIndex': 2},
@@ -232,7 +236,7 @@ class instancesTest(HeatTestCase):
             {'port-id': 'id3'},
             {'port-id': 'id4'},
             {'port-id': 'id5'}
-        ], instances.Instance._build_nics([
+        ], instance._build_nics([
             {'NetworkInterfaceId': 'id3', 'DeviceIndex': '3'},
             {'NetworkInterfaceId': 'id1', 'DeviceIndex': '1'},
             {'NetworkInterfaceId': 'id2', 'DeviceIndex': 2},
diff --git a/heat/tests/test_instance_network.py b/heat/tests/test_instance_network.py
new file mode 100644 (file)
index 0000000..f643db8
--- /dev/null
@@ -0,0 +1,271 @@
+# 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.tests.v1_1 import fakes
+from heat.engine.resources import instance as instances
+from heat.engine.resources import network_interface as network_interfaces
+from heat.common import template_format
+from heat.engine import parser
+from heat.engine import scheduler
+from heat.openstack.common import uuidutils
+from heat.tests.common import HeatTestCase
+from heat.tests.utils import setup_dummy_db
+
+
+wp_template = '''
+{
+  "AWSTemplateFormatVersion" : "2010-09-09",
+  "Description" : "WordPress",
+  "Parameters" : {
+    "KeyName" : {
+      "Description" : "KeyName",
+      "Type" : "String",
+      "Default" : "test"
+    },
+    "InstanceType": {
+      "Type": "String",
+      "Description": "EC2 instance type",
+      "Default": "m1.small",
+      "AllowedValues": [ "m1.small", "m1.large" ]
+    },
+    "SubnetId": {
+      "Type" : "String",
+      "Description" : "SubnetId of an existing subnet in your VPC"
+    },
+  },
+  "Resources" : {
+    "WebServer": {
+      "Type": "AWS::EC2::Instance",
+      "Properties": {
+        "ImageId"        : "F17-x86_64-gold",
+        "InstanceType"   : { "Ref" : "InstanceType" },
+        "SubnetId"       : { "Ref" : "SubnetId" },
+        "KeyName"        : { "Ref" : "KeyName" },
+        "UserData"       : "wordpress"
+      }
+    }
+  }
+}
+'''
+
+
+wp_template_with_nic = '''
+{
+  "AWSTemplateFormatVersion" : "2010-09-09",
+  "Description" : "WordPress",
+  "Parameters" : {
+    "KeyName" : {
+      "Description" : "KeyName",
+      "Type" : "String",
+      "Default" : "test"
+    },
+    "InstanceType": {
+      "Type": "String",
+      "Description": "EC2 instance type",
+      "Default": "m1.small",
+      "AllowedValues": [ "m1.small", "m1.large" ]
+    },
+    "SubnetId": {
+      "Type" : "String",
+      "Description" : "SubnetId of an existing subnet in your VPC"
+    },
+  },
+  "Resources" : {
+
+    "nic1": {
+        "Type": "AWS::EC2::NetworkInterface",
+        "Properties": {
+            "SubnetId": { "Ref": "SubnetId" }
+        }
+    },
+
+    "WebServer": {
+      "Type": "AWS::EC2::Instance",
+      "Properties": {
+        "ImageId"        : "F17-x86_64-gold",
+        "InstanceType"   : { "Ref" : "InstanceType" },
+        "NetworkInterfaces": [ { "NetworkInterfaceId" : {"Ref": "nic1"},
+                                 "DeviceIndex" : "0"  } ],
+        "KeyName"        : { "Ref" : "KeyName" },
+        "UserData"       : "wordpress"
+      }
+    }
+  }
+}
+'''
+
+
+class FakeQuantum(object):
+
+    def show_subnet(self, subnet, **_params):
+        return {
+            'subnet': {
+                'name': 'name',
+                'network_id': 'fc68ea2c-b60b-4b4f-bd82-94ec81110766',
+                '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': '4156c7a5-e8c4-4aff-a6e1-8f3c7bc83861',
+                'enable_dhcp': False,
+            }}
+
+    def create_port(self, body=None):
+        return {
+            'port': {
+                'admin_state_up': True,
+                'device_id': '',
+                'device_owner': '',
+                'fixed_ips': [{
+                    'ip_address': '10.0.3.3',
+                    'subnet_id': '4156c7a5-e8c4-4aff-a6e1-8f3c7bc83861'}],
+                'id': '64d913c1-bcb1-42d2-8f0a-9593dbcaf251',
+                'mac_address': 'fa:16:3e:25:32:5d',
+                'name': '',
+                'network_id': 'fc68ea2c-b60b-4b4f-bd82-94ec81110766',
+                'status': 'ACTIVE',
+                'tenant_id': 'c1210485b2424d48804aad5d39c61b8f'
+            }}
+
+
+class instancesTest(HeatTestCase):
+    def setUp(self):
+        super(instancesTest, self).setUp()
+        self.fc = fakes.FakeClient()
+        setup_dummy_db()
+
+    def _create_test_instance(self, return_server, name):
+        stack_name = '%s_stack' % name
+        t = template_format.parse(wp_template)
+        template = parser.Template(t)
+        kwargs = {'KeyName': 'test',
+                  'InstanceType': 'm1.large',
+                  'SubnetId': '4156c7a5-e8c4-4aff-a6e1-8f3c7bc83861'}
+        params = parser.Parameters(stack_name, template, kwargs)
+        stack = parser.Stack(None, stack_name, template, params,
+                             stack_id=uuidutils.generate_uuid())
+
+        t['Resources']['WebServer']['Properties']['ImageId'] = 'CentOS 5.2'
+        instance = instances.Instance('%s_name' % name,
+                                      t['Resources']['WebServer'], stack)
+
+        self.m.StubOutWithMock(instance, 'nova')
+        instance.nova().MultipleTimes().AndReturn(self.fc)
+
+        self.m.StubOutWithMock(instance, 'quantum')
+        instance.quantum().MultipleTimes().AndReturn(FakeQuantum())
+
+        instance.t = instance.stack.resolve_runtime_data(instance.t)
+
+        # need to resolve the template functions
+        server_userdata = instance._build_userdata(
+            instance.t['Properties']['UserData'])
+
+        self.m.StubOutWithMock(self.fc.servers, 'create')
+        self.fc.servers.create(
+            image=1, flavor=3, key_name='test',
+            name='%s.%s' % (stack_name, instance.name),
+            security_groups=None,
+            userdata=server_userdata, scheduler_hints=None, meta=None,
+            nics=[{'port-id': '64d913c1-bcb1-42d2-8f0a-9593dbcaf251'}],
+            availability_zone=None).AndReturn(
+                return_server)
+        self.m.ReplayAll()
+
+        scheduler.TaskRunner(instance.create)()
+        return instance
+
+    def _create_test_instance_with_nic(self, return_server, name):
+        stack_name = '%s_stack' % name
+        t = template_format.parse(wp_template_with_nic)
+        template = parser.Template(t)
+        kwargs = {'KeyName': 'test',
+                  'InstanceType': 'm1.large',
+                  'SubnetId': '4156c7a5-e8c4-4aff-a6e1-8f3c7bc83861'}
+        params = parser.Parameters(stack_name, template, kwargs)
+        stack = parser.Stack(None, stack_name, template, params,
+                             stack_id=uuidutils.generate_uuid())
+
+        t['Resources']['WebServer']['Properties']['ImageId'] = 'CentOS 5.2'
+
+        nic = network_interfaces.NetworkInterface('%s_nic' % name,
+                                                  t['Resources']['nic1'],
+                                                  stack)
+
+        instance = instances.Instance('%s_name' % name,
+                                      t['Resources']['WebServer'], stack)
+
+        self.m.StubOutWithMock(nic, 'quantum')
+        nic.quantum().MultipleTimes().AndReturn(FakeQuantum())
+
+        self.m.StubOutWithMock(instance, 'nova')
+        instance.nova().MultipleTimes().AndReturn(self.fc)
+
+        nic.t = nic.stack.resolve_runtime_data(nic.t)
+        instance.t = instance.stack.resolve_runtime_data(instance.t)
+
+        # need to resolve the template functions
+        server_userdata = instance._build_userdata(
+            instance.t['Properties']['UserData'])
+        self.m.StubOutWithMock(self.fc.servers, 'create')
+        self.fc.servers.create(
+            image=1, flavor=3, key_name='test',
+            name='%s.%s' % (stack_name, instance.name),
+            security_groups=None,
+            userdata=server_userdata, scheduler_hints=None, meta=None,
+            nics=[{'port-id': '64d913c1-bcb1-42d2-8f0a-9593dbcaf251'}],
+            availability_zone=None).AndReturn(
+                return_server)
+        self.m.ReplayAll()
+
+        # create network interface
+        scheduler.TaskRunner(nic.create)()
+        stack.resources["nic1"] = nic
+
+        scheduler.TaskRunner(instance.create)()
+        return instance
+
+    def test_instance_create(self):
+        return_server = self.fc.servers.list()[1]
+        instance = self._create_test_instance(return_server,
+                                              'test_instance_create')
+        # this makes sure the auto increment worked on instance creation
+        self.assertTrue(instance.id > 0)
+
+        expected_ip = return_server.networks['public'][0]
+        self.assertEqual(instance.FnGetAtt('PublicIp'), expected_ip)
+        self.assertEqual(instance.FnGetAtt('PrivateIp'), expected_ip)
+        self.assertEqual(instance.FnGetAtt('PrivateDnsName'), expected_ip)
+        self.assertEqual(instance.FnGetAtt('PrivateDnsName'), expected_ip)
+
+        self.m.VerifyAll()
+
+    def test_instance_create_with_nic(self):
+        return_server = self.fc.servers.list()[1]
+        instance = self._create_test_instance_with_nic(
+            return_server, 'test_instance_create_with_network_interface')
+
+        # this makes sure the auto increment worked on instance creation
+        self.assertTrue(instance.id > 0)
+
+        expected_ip = return_server.networks['public'][0]
+        self.assertEqual(instance.FnGetAtt('PublicIp'), expected_ip)
+        self.assertEqual(instance.FnGetAtt('PrivateIp'), expected_ip)
+        self.assertEqual(instance.FnGetAtt('PrivateDnsName'), expected_ip)
+        self.assertEqual(instance.FnGetAtt('PrivateDnsName'), expected_ip)
+
+        self.m.VerifyAll()
index 022039aa3a3667a7ce43248625fe5408333fdde6..f2523a07db110fb6ad16052d7bdce7a418d8c760 100644 (file)
@@ -115,6 +115,21 @@ class VPCTestBase(HeatTestCase):
             u'bbbb',
             {'subnet_id': 'cccc'}).AndReturn(None)
 
+    def mock_show_subnet(self):
+        quantumclient.Client.show_subnet('cccc').AndReturn({
+            'subnet': {
+                'name': 'test_stack.the_subnet',
+                'network_id': 'aaaa',
+                'tenant_id': 'c1210485b2424d48804aad5d39c61b8f',
+                'allocation_pools': [{'start': '10.0.0.2',
+                                      'end': '10.0.0.254'}],
+                'gateway_ip': '10.0.0.1',
+                'ip_version': 4,
+                'cidr': '10.0.0.0/24',
+                'id': 'cccc',
+                'enable_dhcp': False,
+            }})
+
     def mock_create_security_group(self):
         quantumclient.Client.create_security_group({
             'security_group': {
@@ -372,6 +387,7 @@ Resources:
         self.mock_create_security_group()
         self.mock_create_network()
         self.mock_create_subnet()
+        self.mock_show_subnet()
         self.mock_create_network_interface()
         self.mock_delete_network_interface()
         self.mock_delete_subnet()