From 0c2ee06320ad9540b3f5dcf77f406d5f2471626d Mon Sep 17 00:00:00 2001 From: huangtianhua Date: Wed, 3 Sep 2014 14:59:48 +0800 Subject: [PATCH] Implement AWS::EC2::EIPAssociation updatable Change-Id: Iedb9a1a42e134e9c524311747d6253a0199ac1d4 --- heat/engine/resources/eip.py | 108 ++++++++++++++++++++++++- heat/tests/test_eip.py | 153 ++++++++++++++++++++++++++++++++++- 2 files changed, 255 insertions(+), 6 deletions(-) diff --git a/heat/engine/resources/eip.py b/heat/engine/resources/eip.py index c108773d9..be78c803a 100644 --- a/heat/engine/resources/eip.py +++ b/heat/engine/resources/eip.py @@ -184,19 +184,23 @@ class ElasticIpAssociation(resource.Resource): properties_schema = { INSTANCE_ID: properties.Schema( properties.Schema.STRING, - _('Instance ID to associate with EIP specified by EIP property.') + _('Instance ID to associate with EIP specified by EIP property.'), + update_allowed=True ), EIP: properties.Schema( properties.Schema.STRING, - _('EIP address to associate with instance.') + _('EIP address to associate with instance.'), + update_allowed=True ), ALLOCATION_ID: properties.Schema( properties.Schema.STRING, - _('Allocation ID for VPC EIP address.') + _('Allocation ID for VPC EIP address.'), + update_allowed=True ), NETWORK_INTERFACE_ID: properties.Schema( properties.Schema.STRING, - _('Network interface ID to associate with EIP.') + _('Network interface ID to associate with EIP.'), + update_allowed=True ), } @@ -276,6 +280,93 @@ class ElasticIpAssociation(resource.Resource): return server + def _floatingIp_detach(self, + nova_ignore_not_found=False, + neutron_ignore_not_found=False): + eip = self.properties[self.EIP] + allocation_id = self.properties[self.ALLOCATION_ID] + instance_id = self.properties[self.INSTANCE_ID] + server = None + if eip: + # if has eip_old, to remove the eip_old from the instance + server = self._nova_remove_floating_ip(instance_id, + eip, + nova_ignore_not_found) + else: + # if hasn't eip_old, to update neutron floatingIp + self._neutron_update_floating_ip(allocation_id, + None, + neutron_ignore_not_found) + + return server + + def _handle_update_eipInfo(self, prop_diff): + eip_update = prop_diff.get(self.EIP) + allocation_id_update = prop_diff.get(self.ALLOCATION_ID) + instance_id = self.properties[self.INSTANCE_ID] + ni_id = self.properties[self.NETWORK_INTERFACE_ID] + if eip_update: + server = self._floatingIp_detach(neutron_ignore_not_found=True) + if server: + # then to attach the eip_update to the instance + server.add_floating_ip(eip_update) + self.resource_id_set(eip_update) + elif allocation_id_update: + self._floatingIp_detach(nova_ignore_not_found=True) + port_id, port_rsrc = self._get_port_info(ni_id, instance_id) + if not port_id or not port_rsrc: + LOG.error(_('Port not specified.')) + raise exception.NotFound(_('Failed to update, can not found ' + 'port info.')) + + network_id = port_rsrc['network_id'] + self._neutron_add_gateway_router(allocation_id_update, network_id) + self._neutron_update_floating_ip(allocation_id_update, port_id) + self.resource_id_set(allocation_id_update) + + def _handle_update_portInfo(self, prop_diff): + instance_id_update = prop_diff.get(self.INSTANCE_ID) + ni_id_update = prop_diff.get(self.NETWORK_INTERFACE_ID) + eip = self.properties[self.EIP] + allocation_id = self.properties[self.ALLOCATION_ID] + # if update portInfo, no need to detach the port from + # old instance/floatingip. + if eip: + server = self.nova().servers.get(instance_id_update) + server.add_floating_ip(eip) + else: + port_id, port_rsrc = self._get_port_info(ni_id_update, + instance_id_update) + if not port_id or not port_rsrc: + LOG.error(_('Port not specified.')) + raise exception.NotFound(_('Failed to update, can not found ' + 'port info.')) + + network_id = port_rsrc['network_id'] + self._neutron_add_gateway_router(allocation_id, network_id) + self._neutron_update_floating_ip(allocation_id, port_id) + + def _validate_update_properties(self, prop_diff): + # according to aws doc, when update allocation_id or eip, + # if you also change the InstanceId or NetworkInterfaceId, + # should go to Replacement flow + if self.ALLOCATION_ID in prop_diff or self.EIP in prop_diff: + instance_id = prop_diff.get(self.INSTANCE_ID) + ni_id = prop_diff.get(self.NETWORK_INTERFACE_ID) + + if instance_id or ni_id: + raise resource.UpdateReplace(self.name) + + # according to aws doc, when update the instance_id or + # network_interface_id, if you also change the EIP or + # ALLOCATION_ID, should go to Replacement flow + if (self.INSTANCE_ID in prop_diff or + self.NETWORK_INTERFACE_ID in prop_diff): + eip = prop_diff.get(self.EIP) + allocation_id = prop_diff.get(self.ALLOCATION_ID) + if eip or allocation_id: + raise resource.UpdateReplace(self.name) + def handle_create(self): """Add a floating IP address to a server.""" if self.properties[self.EIP]: @@ -319,6 +410,15 @@ class ElasticIpAssociation(resource.Resource): port_id=None, ignore_not_found=True) + def handle_update(self, json_snippet, tmpl_diff, prop_diff): + if prop_diff: + self._validate_update_properties(prop_diff) + if self.ALLOCATION_ID in prop_diff or self.EIP in prop_diff: + self._handle_update_eipInfo(prop_diff) + elif (self.INSTANCE_ID in prop_diff or + self.NETWORK_INTERFACE_ID in prop_diff): + self._handle_update_portInfo(prop_diff) + def resource_mapping(): return { diff --git a/heat/tests/test_eip.py b/heat/tests/test_eip.py index af41d45f4..c00913d25 100644 --- a/heat/tests/test_eip.py +++ b/heat/tests/test_eip.py @@ -482,8 +482,8 @@ class AllocTest(HeatTestCase): id = 'fc68ea2c-b60b-4b4f-bd82-94ec81110766' neutronclient.Client.delete_floatingip(id).AndReturn(None) - def mock_list_ports(self): - neutronclient.Client.list_ports(id='the_nic').AndReturn( + def mock_list_ports(self, id='the_nic'): + neutronclient.Client.list_ports(id=id).AndReturn( {"ports": [{ "status": "DOWN", "binding:host_id": "null", @@ -689,3 +689,152 @@ class AllocTest(HeatTestCase): self.assertEqual((rsrc.DELETE, rsrc.COMPLETE), rsrc.state) self.m.VerifyAll() + + def test_update_association_with_InstanceId(self): + nova.NovaClientPlugin._create().AndReturn(self.fc) + server = self.fc.servers.list()[0] + self.fc.servers.get('WebServer').MultipleTimes() \ + .AndReturn(server) + server_update = self.fc.servers.list()[1] + self.fc.servers.get('5678').AndReturn(server_update) + + self.m.ReplayAll() + + t = template_format.parse(eip_template_ipassoc) + stack = utils.parse_stack(t) + self.create_eip(t, stack, 'IPAddress') + ass = self.create_association(t, stack, 'IPAssoc') + self.assertEqual('11.0.0.1', ass.properties['EIP']) + + # update with the new InstanceId + props = copy.deepcopy(ass.properties.data) + update_server_id = '5678' + props['InstanceId'] = update_server_id + update_snippet = rsrc_defn.ResourceDefinition(ass.name, ass.type(), + stack.t.parse(stack, + props)) + scheduler.TaskRunner(ass.update, update_snippet)() + self.assertEqual((ass.UPDATE, ass.COMPLETE), ass.state) + + self.m.VerifyAll() + + def test_update_association_with_EIP(self): + nova.NovaClientPlugin._create().AndReturn(self.fc) + server = self.fc.servers.list()[0] + self.fc.servers.get('WebServer').MultipleTimes() \ + .AndReturn(server) + + self.m.ReplayAll() + + t = template_format.parse(eip_template_ipassoc) + stack = utils.parse_stack(t) + self.create_eip(t, stack, 'IPAddress') + ass = self.create_association(t, stack, 'IPAssoc') + + # update with the new EIP + props = copy.deepcopy(ass.properties.data) + update_eip = '11.0.0.2' + props['EIP'] = update_eip + update_snippet = rsrc_defn.ResourceDefinition(ass.name, ass.type(), + stack.t.parse(stack, + props)) + scheduler.TaskRunner(ass.update, update_snippet)() + self.assertEqual((ass.UPDATE, ass.COMPLETE), ass.state) + + self.m.VerifyAll() + + def test_update_association_with_AllocationId_or_EIP(self): + nova.NovaClientPlugin._create().AndReturn(self.fc) + server = self.fc.servers.list()[0] + self.fc.servers.get('WebServer').MultipleTimes()\ + .AndReturn(server) + + self.mock_list_instance_ports('WebServer') + self.mock_show_network() + self.mock_no_router_for_vpc() + self.mock_update_floatingip( + port='a000228d-b40b-4124-8394-a4082ae1b76c') + + self.mock_update_floatingip(port=None) + self.m.ReplayAll() + + t = template_format.parse(eip_template_ipassoc) + stack = utils.parse_stack(t) + self.create_eip(t, stack, 'IPAddress') + ass = self.create_association(t, stack, 'IPAssoc') + self.assertEqual('11.0.0.1', ass.properties['EIP']) + + # change EIP to AllocationId + props = copy.deepcopy(ass.properties.data) + update_allocationId = 'fc68ea2c-b60b-4b4f-bd82-94ec81110766' + props['AllocationId'] = update_allocationId + props.pop('EIP') + update_snippet = rsrc_defn.ResourceDefinition(ass.name, ass.type(), + stack.t.parse(stack, + props)) + scheduler.TaskRunner(ass.update, update_snippet)() + self.assertEqual((ass.UPDATE, ass.COMPLETE), ass.state) + + # change AllocationId to EIP + props = copy.deepcopy(ass.properties.data) + update_eip = '11.0.0.2' + props['EIP'] = update_eip + props.pop('AllocationId') + update_snippet = rsrc_defn.ResourceDefinition(ass.name, ass.type(), + stack.t.parse(stack, + props)) + scheduler.TaskRunner(ass.update, update_snippet)() + self.assertEqual((ass.UPDATE, ass.COMPLETE), ass.state) + + self.m.VerifyAll() + + def test_update_association_with_NetworkInterfaceId_or_InstanceId(self): + self.mock_create_floatingip() + self.mock_list_ports() + self.mock_show_network() + self.mock_no_router_for_vpc() + self.mock_update_floatingip() + + self.mock_list_ports(id='a000228d-b40b-4124-8394-a4082ae1b76b') + self.mock_show_network() + self.mock_no_router_for_vpc() + self.mock_update_floatingip( + port='a000228d-b40b-4124-8394-a4082ae1b76b') + + self.mock_list_instance_ports('5678') + self.mock_show_network() + self.mock_no_router_for_vpc() + self.mock_update_floatingip( + port='a000228d-b40b-4124-8394-a4082ae1b76c') + + self.m.ReplayAll() + + t = template_format.parse(eip_template_ipassoc2) + stack = utils.parse_stack(t) + self.create_eip(t, stack, 'the_eip') + ass = self.create_association(t, stack, 'IPAssoc') + + # update with the new NetworkInterfaceId + props = copy.deepcopy(ass.properties.data) + update_networkInterfaceId = 'a000228d-b40b-4124-8394-a4082ae1b76b' + props['NetworkInterfaceId'] = update_networkInterfaceId + + update_snippet = rsrc_defn.ResourceDefinition(ass.name, ass.type(), + stack.t.parse(stack, + props)) + scheduler.TaskRunner(ass.update, update_snippet)() + self.assertEqual((ass.UPDATE, ass.COMPLETE), ass.state) + + # update with the InstanceId + props = copy.deepcopy(ass.properties.data) + instance_id = '5678' + props.pop('NetworkInterfaceId') + props['InstanceId'] = instance_id + + update_snippet = rsrc_defn.ResourceDefinition(ass.name, ass.type(), + stack.t.parse(stack, + props)) + scheduler.TaskRunner(ass.update, update_snippet)() + self.assertEqual((ass.UPDATE, ass.COMPLETE), ass.state) + + self.m.VerifyAll()