diff --git a/heat/engine/resources/instance.py b/heat/engine/resources/instance.py index d35ce89068..4c04003f3e 100644 --- a/heat/engine/resources/instance.py +++ b/heat/engine/resources/instance.py @@ -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}}, @@ -221,22 +221,41 @@ class Instance(resource.Resource): return self.mime_string - @staticmethod - def _build_nics(network_interfaces): - if not network_interfaces: - return None + def _build_nics(self, network_interfaces, subnet_id=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'])) + 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 [{'port-id': nic['NetworkInterfaceId']} for nic in sorted_nics] + return nics def handle_create(self): if self.properties.get('SecurityGroups') is None: @@ -289,7 +308,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 diff --git a/heat/engine/resources/network_interface.py b/heat/engine/resources/network_interface.py index 03e5ac59a0..60f9b0620b 100644 --- a/heat/engine/resources/network_interface.py +++ b/heat/engine/resources/network_interface.py @@ -45,15 +45,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, diff --git a/heat/tests/test_instance.py b/heat/tests/test_instance.py index fcc22c863f..5e99ca6629 100644 --- a/heat/tests/test_instance.py +++ b/heat/tests/test_instance.py @@ -27,6 +27,31 @@ from heat.common import template_format from heat.engine import parser from heat.openstack.common import uuidutils +wp_template = ''' +{ + "AWSTemplateFormatVersion" : "2010-09-09", + "Description" : "WordPress", + "Parameters" : { + "KeyName" : { + "Description" : "KeyName", + "Type" : "String", + "Default" : "test" + } + }, + "Resources" : { + "WebServer": { + "Type": "AWS::EC2::Instance", + "Properties": { + "ImageId" : "F17-x86_64-gold", + "InstanceType" : "m1.large", + "KeyName" : "test", + "UserData" : "wordpress" + } + } + } +} +''' + @attr(tag=['unit', 'resource', 'instance']) @attr(speed='fast') @@ -41,6 +66,61 @@ class instancesTest(unittest.TestCase): self.m.UnsetStubs() print "instancesTest teardown complete" + def _setup_test_stack(self, stack_name): + t = template_format.parse(wp_template) + template = parser.Template(t) + stack = parser.Stack(utils.dummy_context(), stack_name, template, + stack_id=uuidutils.generate_uuid()) + return (t, stack) + + def _setup_test_instance(self, return_server, name, image_id=None): + stack_name = '%s_stack' % name + (t, stack) = self._setup_test_stack(stack_name) + + t['Resources']['WebServer']['Properties']['ImageId'] =\ + image_id or 'CentOS 5.2' + t['Resources']['WebServer']['Properties']['InstanceType'] =\ + '256 MB Server' + instance = instances.Instance('%s_name' % name, + t['Resources']['WebServer'], stack) + + self.m.StubOutWithMock(instance, 'nova') + instance.nova().MultipleTimes().AndReturn(self.fc) + + instance.t = instance.stack.resolve_runtime_data(instance.t) + + # need to resolve the template functions + server_userdata = instance._build_userdata( + instance.t['Properties']['UserData']) + instance.mime_string = "wordpress" + self.m.StubOutWithMock(self.fc.servers, 'create') + self.fc.servers.create( + image=1, flavor=1, key_name='test', + name='%s.%s' % (stack_name, instance.name), + security_groups=None, + userdata=server_userdata, scheduler_hints=None, + meta=None, nics=None, availability_zone=None).AndReturn( + return_server) + + return instance + + def _create_test_instance(self, return_server, name): + t = template_format.parse(wp_template) + + stack_name = 'instance_create_test_stack' + template = parser.Template(t) + params = parser.Parameters(stack_name, template, {'KeyName': 'test'}) + stack = parser.Stack(None, stack_name, template, params, + stack_id=uuidutils.generate_uuid()) + + t['Resources']['WebServer']['Properties']['ImageId'] = 'CentOS 5.2' + t['Resources']['WebServer']['Properties']['InstanceType'] =\ + '256 MB Server' + + instance = instances.Instance('create_instance_name', + t['Resources']['WebServer'], stack) + return instance + def test_instance_create(self): f = open("%s/WordPress_Single_Instance_gold.template" % self.path) t = template_format.parse(f.read()) @@ -183,16 +263,20 @@ class instancesTest(unittest.TestCase): self.assertEqual(instance.metadata, {'test': 123}) 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}, @@ -203,7 +287,7 @@ class instancesTest(unittest.TestCase): {'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 index 0000000000..4f0ffabf36 --- /dev/null +++ b/heat/tests/test_instance_network.py @@ -0,0 +1,272 @@ +# 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 unittest +import mox + +from nose.plugins.attrib import attr + +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.openstack.common import uuidutils + +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' + }} + + +@attr(tag=['unit', 'resource', 'instance']) +@attr(speed='fast') +class instancesTest(unittest.TestCase): + def setUp(self): + self.m = mox.Mox() + self.fc = fakes.FakeClient() + + 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() + + self.assertEqual(instance.create(), None) + 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 + self.assertEqual(nic.create(), None) + stack.resources["nic1"] = nic + + self.assertEqual(instance.create(), None) + 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() diff --git a/heat/tests/test_vpc.py b/heat/tests/test_vpc.py index 53ec1b8620..cbda39a763 100644 --- a/heat/tests/test_vpc.py +++ b/heat/tests/test_vpc.py @@ -113,6 +113,21 @@ class VPCTestBase(unittest.TestCase): 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_delete_network(self): quantumclient.Client.delete_router('bbbb').AndReturn(None) quantumclient.Client.delete_network('aaaa').AndReturn(None) @@ -271,6 +286,7 @@ Resources: def test_network_interface(self): 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()