From: Jeff Peeler Date: Wed, 3 Jul 2013 14:22:25 +0000 (-0400) Subject: Add new attributes to EIPAssociation resource X-Git-Tag: 2014.1~183^2 X-Git-Url: https://review.fuel-infra.org/gitweb?a=commitdiff_plain;h=29d1e2472dd07af3160a71ca6abfe172c6b9ad45;p=openstack-build%2Fheat-build.git Add new attributes to EIPAssociation resource AllocationId NetworkInterfaceId The EIP resource also now supports the Domain property, which when set creates the resource using neutron networking. (Neutron networking is required for all VPC operations.) The EIP test covers the new functionality as well as some of the old using both nova and neutron. Fixes bug #1164865 Fixes bug #1164866 Change-Id: I7ad7fba8ed6f8a584fc2ca00aaf80144c5274134 --- diff --git a/heat/engine/resources/eip.py b/heat/engine/resources/eip.py index d6c9e5c9..b8e8df4b 100644 --- a/heat/engine/resources/eip.py +++ b/heat/engine/resources/eip.py @@ -15,6 +15,8 @@ from heat.engine import clients from heat.engine import resource +from heat.engine.resources.vpc import VPC +from heat.common import exception from heat.openstack.common import excutils from heat.openstack.common import log as logging @@ -24,7 +26,7 @@ logger = logging.getLogger(__name__) class ElasticIp(resource.Resource): properties_schema = {'Domain': {'Type': 'String', - 'Implemented': False}, + 'AllowedValues': ['vpc']}, 'InstanceId': {'Type': 'String'}} attributes_schema = { "AllocationId": ("ID that AWS assigns to represent the allocation of" @@ -37,8 +39,17 @@ class ElasticIp(resource.Resource): self.ipaddress = None def _ipaddress(self): - if self.ipaddress is None: - if self.resource_id is not None: + if self.ipaddress is None and self.resource_id is not None: + if self.properties['Domain'] and clients.neutronclient: + ne = clients.neutronclient.exceptions.NeutronClientException + try: + ips = self.neutron().show_floatingip(self.resource_id) + except ne as e: + if e.status_code == 404: + logger.warn("Floating IPs not found: %s" % str(e)) + else: + self.ipaddress = ips['floatingip']['floating_ip_address'] + else: try: ips = self.nova().floating_ips.get(self.resource_id) except clients.novaclient.exceptions.NotFound as ex: @@ -49,18 +60,34 @@ class ElasticIp(resource.Resource): def handle_create(self): """Allocate a floating IP for the current tenant.""" - try: - ips = self.nova().floating_ips.create() - except clients.novaclient.exceptions.NotFound: - with excutils.save_and_reraise_exception(): - msg = ("No default floating IP pool configured." - "Set 'default_floating_pool' in nova.conf.") - logger.error(msg) - - if ips: + ips = None + if self.properties['Domain'] and clients.neutronclient: + from heat.engine.resources.internet_gateway import InternetGateway + + ext_net = InternetGateway.get_external_network_id(self.neutron()) + props = {'floating_network_id': ext_net} + ips = self.neutron().create_floatingip({ + 'floatingip': props})['floatingip'] + self.ipaddress = ips['floating_ip_address'] + self.resource_id_set(ips['id']) logger.info('ElasticIp create %s' % str(ips)) - self.ipaddress = ips.ip - self.resource_id_set(ips.id) + else: + if self.properties['Domain']: + raise exception.Error('Domain property can not be set on ' + 'resource %s without Neutron available' % + self.name) + try: + ips = self.nova().floating_ips.create() + except clients.novaclient.exceptions.NotFound: + with excutils.save_and_reraise_exception(): + msg = ("No default floating IP pool configured." + "Set 'default_floating_pool' in nova.conf.") + logger.error(msg) + + if ips: + self.ipaddress = ips.ip + self.resource_id_set(ips.id) + logger.info('ElasticIp create %s' % str(ips)) if self.properties['InstanceId']: server = self.nova().servers.get(self.properties['InstanceId']) @@ -77,7 +104,15 @@ class ElasticIp(resource.Resource): """De-allocate a floating IP.""" if self.resource_id is not None: - self.nova().floating_ips.delete(self.resource_id) + if self.properties['Domain'] and clients.neutronclient: + ne = clients.neutronclient.exceptions.NeutronClientException + try: + self.neutron().delete_floatingip(self.resource_id) + except ne as e: + if e.status_code != 404: + raise e + else: + self.nova().floating_ips.delete(self.resource_id) def FnGetRefId(self): return unicode(self._ipaddress()) @@ -91,32 +126,82 @@ class ElasticIpAssociation(resource.Resource): properties_schema = {'InstanceId': {'Type': 'String', 'Required': False}, 'EIP': {'Type': 'String'}, - 'AllocationId': {'Type': 'String', - 'Implemented': False}} + 'AllocationId': {'Type': 'String'}, + 'NetworkInterfaceId': {'Type': 'String'}} def FnGetRefId(self): - return unicode(self.properties.get('EIP', '0.0.0.0')) + return unicode(self.physical_resource_name()) def handle_create(self): """Add a floating IP address to a server.""" - logger.debug('ElasticIpAssociation %s.add_floating_ip(%s)' % - (self.properties['InstanceId'], - self.properties['EIP'])) - - if self.properties['InstanceId']: + if self.properties['EIP'] is not None \ + and self.properties['AllocationId'] is not None: + raise exception.ResourcePropertyConflict('EIP', + 'AllocationId') + + if self.properties['EIP']: + if not self.properties['InstanceId']: + logger.warn('Skipping association, InstanceId not specified') + return server = self.nova().servers.get(self.properties['InstanceId']) server.add_floating_ip(self.properties['EIP']) - self.resource_id_set(self.properties['EIP']) + self.resource_id_set(self.properties['EIP']) + logger.debug('ElasticIpAssociation %s.add_floating_ip(%s)' % + (self.properties['InstanceId'], + self.properties['EIP'])) + elif self.properties['AllocationId']: + assert clients.neutronclient, "Neutron required for VPC operations" + port_id = None + port_rsrc = None + if self.properties['NetworkInterfaceId']: + port_id = self.properties['NetworkInterfaceId'] + port_rsrc = self.neutron().list_ports(id=port_id)['ports'][0] + elif self.properties['InstanceId']: + instance_id = self.properties['InstanceId'] + ports = self.neutron().list_ports(device_id=instance_id) + port_rsrc = ports['ports'][0] + port_id = port_rsrc['id'] + else: + logger.warn('Skipping association, resource not specified') + return + + float_id = self.properties['AllocationId'] + self.resource_id_set(float_id) + + # assuming only one fixed_ip + subnet_id = port_rsrc['fixed_ips'][0]['subnet_id'] + subnets = self.neutron().list_subnets(id=subnet_id) + subnet_rsrc = subnets['subnets'][0] + netid = subnet_rsrc['network_id'] + + router_id = VPC.router_for_vpc(self.neutron(), netid)['id'] + floatingip = self.neutron().show_floatingip(float_id) + floating_net_id = floatingip['floatingip']['floating_network_id'] + + self.neutron().add_gateway_router( + router_id, {'network_id': floating_net_id}) + + self.neutron().update_floatingip( + float_id, {'floatingip': {'port_id': port_id}}) def handle_delete(self): - """Remove a floating IP address from a server.""" - if self.properties['InstanceId']: + """Remove a floating IP address from a server or port.""" + if self.properties['EIP']: try: server = self.nova().servers.get(self.properties['InstanceId']) if server: server.remove_floating_ip(self.properties['EIP']) except clients.novaclient.exceptions.NotFound as ex: pass + elif self.properties['AllocationId']: + float_id = self.properties['AllocationId'] + ne = clients.neutronclient.exceptions.NeutronClientException + try: + self.neutron().update_floatingip( + float_id, {'floatingip': {'port_id': None}}) + except ne as e: + if e.status_code != 404: + raise e def resource_mapping(): diff --git a/heat/tests/test_eip.py b/heat/tests/test_eip.py index 75f4c666..728af887 100644 --- a/heat/tests/test_eip.py +++ b/heat/tests/test_eip.py @@ -12,6 +12,7 @@ # License for the specific language governing permissions and limitations # under the License. +from testtools import skipIf from heat.common import exception from heat.common import template_format @@ -19,8 +20,10 @@ from heat.engine.resources import eip from heat.engine import clients from heat.engine import resource from heat.engine import scheduler +from heat.engine import parser from heat.tests.common import HeatTestCase from heat.tests.v1_1 import fakes +from heat.tests import fakes as fakec from heat.tests import utils @@ -66,9 +69,63 @@ eip_template_ipassoc = ''' } ''' +eip_template_ipassoc2 = ''' +{ + "AWSTemplateFormatVersion" : "2010-09-09", + "Description" : "EIP Test", + "Parameters" : {}, + "Resources" : { + "the_eip" : { + "Type" : "AWS::EC2::EIP", + "Properties" : { + "Domain": "vpc" + } + }, + "IPAssoc" : { + "Type" : "AWS::EC2::EIPAssociation", + "Properties" : { + "AllocationId" : 'fc68ea2c-b60b-4b4f-bd82-94ec81110766', + "NetworkInterfaceId" : { "Ref" : "the_nic" } + } + }, + "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" } + } + }, + "the_nic" : { + "Type" : "AWS::EC2::NetworkInterface", + "Properties" : { + "PrivateIpAddress": "10.0.0.100", + "SubnetId": { "Ref": "the_subnet" } + } + }, + } +} +''' + + +def force_networking(mode): + if mode == 'nova': + force_networking.client = clients.neutronclient + clients.neutronclient = None + if mode == 'neutron': + clients.neutronclient = force_networking.client +force_networking.client = None + class EIPTest(HeatTestCase): def setUp(self): + # force Nova, will test Neutron below + force_networking('nova') super(EIPTest, self).setUp() self.fc = fakes.FakeClient() self.m.StubOutWithMock(eip.ElasticIp, 'nova') @@ -76,6 +133,10 @@ class EIPTest(HeatTestCase): self.m.StubOutWithMock(self.fc.servers, 'get') utils.setup_dummy_db() + def tearDown(self): + super(EIPTest, self).tearDown() + force_networking('neutron') + def create_eip(self, t, stack, resource_name): rsrc = eip.ElasticIp(resource_name, t['Resources'][resource_name], @@ -95,7 +156,6 @@ class EIPTest(HeatTestCase): return rsrc def test_eip(self): - eip.ElasticIp.nova().MultipleTimes().AndReturn(self.fc) self.fc.servers.get('WebServer').AndReturn(self.fc.servers.list()[0]) self.fc.servers.get('WebServer') @@ -109,7 +169,7 @@ class EIPTest(HeatTestCase): try: self.assertEqual('11.0.0.1', rsrc.FnGetRefId()) - rsrc.ipaddress = None + rsrc.refid = None self.assertEqual('11.0.0.1', rsrc.FnGetRefId()) self.assertEqual('1', rsrc.FnGetAtt('AllocationId')) @@ -125,6 +185,26 @@ class EIPTest(HeatTestCase): self.m.VerifyAll() + def test_association_eip(self): + eip.ElasticIp.nova().AndReturn(self.fc) + eip.ElasticIp.nova().AndReturn(self.fc) + + self.m.ReplayAll() + + t = template_format.parse(eip_template_ipassoc) + stack = utils.parse_stack(t) + + rsrc = self.create_eip(t, stack, 'IPAddress') + association = self.create_association(t, stack, 'IPAssoc') + + # TODO(sbaker), figure out why this is an empty string + #self.assertEqual('', association.FnGetRefId()) + + association.delete() + rsrc.delete() + + self.m.VerifyAll() + def test_eip_with_exception(self): self.m.StubOutWithMock(self.fc.floating_ips, 'create') eip.ElasticIp.nova().MultipleTimes().AndReturn(self.fc) @@ -143,24 +223,230 @@ class EIPTest(HeatTestCase): rsrc.handle_create) self.m.VerifyAll() - def test_association(self): - eip.ElasticIp.nova().AndReturn(self.fc) - eip.ElasticIpAssociation.nova().AndReturn(self.fc) - self.fc.servers.get('WebServer').AndReturn(self.fc.servers.list()[0]) - eip.ElasticIpAssociation.nova().AndReturn(self.fc) + +class AllocTest(HeatTestCase): + + @skipIf(clients.neutronclient is None, 'neutronclient unavailable') + def setUp(self): + super(AllocTest, self).setUp() + + self.fc = fakes.FakeClient() + self.m.StubOutWithMock(eip.ElasticIp, 'nova') + self.m.StubOutWithMock(eip.ElasticIpAssociation, 'nova') + self.m.StubOutWithMock(self.fc.servers, 'get') + + self.m.StubOutWithMock(parser.Stack, 'resource_by_refid') + self.m.StubOutWithMock(clients.neutronclient.Client, + 'create_floatingip') + self.m.StubOutWithMock(clients.neutronclient.Client, + 'show_floatingip') + self.m.StubOutWithMock(clients.neutronclient.Client, + 'update_floatingip') + self.m.StubOutWithMock(clients.neutronclient.Client, + 'delete_floatingip') + self.m.StubOutWithMock(clients.neutronclient.Client, + 'add_gateway_router') + self.m.StubOutWithMock(clients.neutronclient.Client, 'list_networks') + self.m.StubOutWithMock(clients.neutronclient.Client, 'list_ports') + self.m.StubOutWithMock(clients.neutronclient.Client, 'list_subnets') + self.m.StubOutWithMock(clients.neutronclient.Client, 'show_network') + self.m.StubOutWithMock(clients.neutronclient.Client, 'list_routers') + self.m.StubOutWithMock(clients.neutronclient.Client, + 'remove_gateway_router') + self.m.StubOutWithMock(clients.OpenStackClients, 'keystone') + + utils.setup_dummy_db() + + def mock_show_network(self): + vpc_name = utils.PhysName('test_stack', 'the_vpc') + clients.neutronclient.Client.show_network( + 'aaaa-netid' + ).AndReturn({"network": { + "status": "BUILD", + "subnets": [], + "name": vpc_name, + "admin_state_up": False, + "shared": False, + "tenant_id": "c1210485b2424d48804aad5d39c61b8f", + "id": "aaaa-netid" + }}) + + def create_eip(self, t, stack, resource_name): + rsrc = eip.ElasticIp(resource_name, + t['Resources'][resource_name], + stack) + self.assertEqual(None, rsrc.validate()) + scheduler.TaskRunner(rsrc.create)() + self.assertEqual((rsrc.CREATE, rsrc.COMPLETE), rsrc.state) + return rsrc + + def create_association(self, t, stack, resource_name): + rsrc = eip.ElasticIpAssociation(resource_name, + t['Resources'][resource_name], + stack) + self.assertEqual(None, rsrc.validate()) + scheduler.TaskRunner(rsrc.create)() + self.assertEqual((rsrc.CREATE, rsrc.COMPLETE), rsrc.state) + return rsrc + + def mock_update_floatingip(self, port='the_nic'): + clients.neutronclient.Client.update_floatingip( + 'fc68ea2c-b60b-4b4f-bd82-94ec81110766', + {'floatingip': {'port_id': port}}).AndReturn(None) + + def mock_create_gateway_attachment(self): + clients.neutronclient.Client.add_gateway_router( + 'bbbb', {'network_id': 'eeee'}).AndReturn(None) + + def mock_create_floatingip(self): + clients.neutronclient.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' + }]}) + + clients.neutronclient.Client.create_floatingip({ + 'floatingip': {'floating_network_id': u'eeee'} + }).AndReturn({'floatingip': { + "status": "ACTIVE", + "id": "fc68ea2c-b60b-4b4f-bd82-94ec81110766", + "floating_ip_address": "192.168.9.3" + }}) + + def mock_show_floatingip(self, refid): + clients.neutronclient.Client.show_floatingip( + refid, + ).AndReturn({'floatingip': { + 'router_id': None, + 'tenant_id': 'e936e6cd3e0b48dcb9ff853a8f253257', + 'floating_network_id': 'eeee', + 'fixed_ip_address': None, + 'floating_ip_address': '172.24.4.227', + 'port_id': None, + 'id': 'ffff' + }}) + + def mock_delete_floatingip(self): + id = 'fc68ea2c-b60b-4b4f-bd82-94ec81110766' + clients.neutronclient.Client.delete_floatingip(id).AndReturn(None) + + def mock_list_ports(self): + clients.neutronclient.Client.list_ports(id='the_nic').AndReturn( + {"ports": [{ + "status": "DOWN", + "binding:host_id": "null", + "name": "wp-NIC-yu7fc7l4g5p6", + "admin_state_up": True, + "network_id": "22c26451-cf27-4d48-9031-51f5e397b84e", + "tenant_id": "ecf538ec1729478fa1f97f1bf4fdcf7b", + "binding:vif_type": "ovs", + "device_owner": "", + "binding:capabilities": {"port_filter": True}, + "mac_address": "fa:16:3e:62:2d:4f", + "fixed_ips": [{"subnet_id": "mysubnetid-70ec", + "ip_address": "192.168.9.2"}], + "id": "a000228d-b40b-4124-8394-a4082ae1b76b", + "security_groups": ["5c6f529d-3186-4c36-84c0-af28b8daac7b"], + "device_id": "" + }]}) + + def mock_list_subnets(self): + clients.neutronclient.Client.list_subnets( + id='mysubnetid-70ec').AndReturn( + {'subnets': [{ + u'name': u'wp-Subnet-pyjm7bvoi4xw', + u'enable_dhcp': True, + u'network_id': u'aaaa-netid', + u'tenant_id': u'ecf538ec1729478fa1f97f1bf4fdcf7b', + u'dns_nameservers': [], + u'allocation_pools': [{u'start': u'192.168.9.2', + u'end': u'192.168.9.254'}], + u'host_routes': [], + u'ip_version': 4, + u'gateway_ip': u'192.168.9.1', + u'cidr': u'192.168.9.0/24', + u'id': u'2c339ccd-734a-4acc-9f64-6f0dfe427e2d' + }]}) + + def mock_router_for_vpc(self): + vpc_name = utils.PhysName('test_stack', 'the_vpc') + clients.neutronclient.Client.list_routers(name=vpc_name).AndReturn({ + "routers": [{ + "status": "ACTIVE", + "external_gateway_info": { + "network_id": "zzzz", + "enable_snat": True}, + "name": vpc_name, + "admin_state_up": True, + "tenant_id": "3e21026f2dc94372b105808c0e721661", + "routes": [], + "id": "bbbb" + }] + }) + + def mock_keystone(self): + clients.OpenStackClients.keystone().AndReturn( + fakec.FakeKeystoneClient()) + + def test_neutron_eip(self): + eip.ElasticIp.nova().MultipleTimes().AndReturn(self.fc) self.fc.servers.get('WebServer').AndReturn(self.fc.servers.list()[0]) - eip.ElasticIp.nova().AndReturn(self.fc) + self.fc.servers.get('WebServer') self.m.ReplayAll() - t = template_format.parse(eip_template_ipassoc) + t = template_format.parse(eip_template) stack = utils.parse_stack(t) rsrc = self.create_eip(t, stack, 'IPAddress') - association = self.create_association(t, stack, 'IPAssoc') - # TODO(sbaker), figure out why this is an empty string - #self.assertEqual('', association.FnGetRefId()) + try: + self.assertEqual('11.0.0.1', rsrc.FnGetRefId()) + rsrc.refid = None + self.assertEqual('11.0.0.1', rsrc.FnGetRefId()) + + self.assertEqual('1', rsrc.FnGetAtt('AllocationId')) + + self.assertRaises(resource.UpdateReplace, + rsrc.handle_update, {}, {}, {}) + + self.assertRaises(exception.InvalidTemplateAttribute, + rsrc.FnGetAtt, 'Foo') + + finally: + rsrc.destroy() + + self.m.VerifyAll() + + def test_association_allocationid(self): + self.mock_keystone() + self.mock_create_gateway_attachment() + self.mock_show_network() + self.mock_router_for_vpc() + + self.mock_create_floatingip() + self.mock_list_ports() + self.mock_list_subnets() + + self.mock_show_floatingip('fc68ea2c-b60b-4b4f-bd82-94ec81110766') + self.mock_update_floatingip() + + self.mock_update_floatingip(port=None) + self.mock_delete_floatingip() + + self.m.ReplayAll() + + t = template_format.parse(eip_template_ipassoc2) + stack = utils.parse_stack(t) + + rsrc = self.create_eip(t, stack, 'the_eip') + association = self.create_association(t, stack, 'IPAssoc') association.delete() rsrc.delete()