# # 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 oslo.utils import excutils import six from heat.common import exception from heat.common.i18n import _ from heat.common.i18n import _LE from heat.common.i18n import _LI from heat.common.i18n import _LW from heat.engine import attributes from heat.engine import constraints from heat.engine import properties from heat.engine import resource from heat.engine.resources import vpc from heat.openstack.common import log as logging LOG = logging.getLogger(__name__) class ElasticIp(resource.Resource): PROPERTIES = ( DOMAIN, INSTANCE_ID, ) = ( 'Domain', 'InstanceId', ) ATTRIBUTES = ( ALLOCATION_ID, ) = ( 'AllocationId', ) properties_schema = { DOMAIN: properties.Schema( properties.Schema.STRING, _('Set to "vpc" to have IP address allocation associated to your ' 'VPC.'), constraints=[ constraints.AllowedValues(['vpc']), ] ), INSTANCE_ID: properties.Schema( properties.Schema.STRING, _('Instance ID to associate with EIP.'), update_allowed=True, constraints=[ constraints.CustomConstraint('nova.server') ] ), } attributes_schema = { ALLOCATION_ID: attributes.Schema( _('ID that AWS assigns to represent the allocation of the address ' 'for use with Amazon VPC. Returned only for VPC elastic IP ' 'addresses.') ), } def __init__(self, name, json_snippet, stack): super(ElasticIp, self).__init__(name, json_snippet, stack) self.ipaddress = None def _ipaddress(self): if self.ipaddress is None and self.resource_id is not None: if self.properties[self.DOMAIN]: try: ips = self.neutron().show_floatingip(self.resource_id) except Exception as ex: self.client_plugin('neutron').ignore_not_found(ex) else: self.ipaddress = ips['floatingip']['floating_ip_address'] else: try: ips = self.nova().floating_ips.get(self.resource_id) except Exception as e: self.client_plugin('nova').ignore_not_found(e) else: self.ipaddress = ips.ip return self.ipaddress or '' def handle_create(self): """Allocate a floating IP for the current tenant.""" ips = None if self.properties[self.DOMAIN]: from heat.engine.resources import internet_gateway ext_net = internet_gateway.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']) LOG.info(_LI('ElasticIp create %s'), str(ips)) else: try: ips = self.nova().floating_ips.create() except Exception as e: with excutils.save_and_reraise_exception(): if self.client_plugin('nova').is_not_found(e): LOG.error(_LE("No default floating IP pool configured." " Set 'default_floating_pool' in " "nova.conf.")) if ips: self.ipaddress = ips.ip self.resource_id_set(ips.id) LOG.info(_LI('ElasticIp create %s'), str(ips)) instance_id = self.properties[self.INSTANCE_ID] if instance_id: server = self.nova().servers.get(instance_id) server.add_floating_ip(self._ipaddress()) def handle_delete(self): if self.resource_id is None: return # may be just create an eip when creation, or create the association # failed when creation, there will no association, if we attempt to # disassociate, an exception will raised, we need # to catch and ignore it, and then to deallocate the eip instance_id = self.properties[self.INSTANCE_ID] if instance_id: try: server = self.nova().servers.get(instance_id) if server: server.remove_floating_ip(self._ipaddress()) except Exception as e: is_not_found = self.client_plugin('nova').is_not_found(e) is_unprocessable_entity = self.client_plugin( 'nova').is_unprocessable_entity(e) if (not is_not_found and not is_unprocessable_entity): raise # deallocate the eip if self.properties[self.DOMAIN]: try: self.neutron().delete_floatingip(self.resource_id) except Exception as ex: self.client_plugin('neutron').ignore_not_found(ex) else: try: self.nova().floating_ips.delete(self.resource_id) except Exception as e: self.client_plugin('nova').ignore_not_found(e) def handle_update(self, json_snippet, tmpl_diff, prop_diff): if prop_diff: if self.INSTANCE_ID in prop_diff: instance_id = prop_diff.get(self.INSTANCE_ID) if instance_id: # no need to remove the floating ip from the old instance, # nova does this automatically when calling # add_floating_ip(). server = self.nova().servers.get(instance_id) server.add_floating_ip(self._ipaddress()) else: # to remove the floating_ip from the old instance instance_id_old = self.properties[self.INSTANCE_ID] if instance_id_old: server = self.nova().servers.get(instance_id_old) server.remove_floating_ip(self._ipaddress()) def FnGetRefId(self): return six.text_type(self._ipaddress()) def _resolve_attribute(self, name): if name == self.ALLOCATION_ID: return six.text_type(self.resource_id) class ElasticIpAssociation(resource.Resource): PROPERTIES = ( INSTANCE_ID, EIP, ALLOCATION_ID, NETWORK_INTERFACE_ID, ) = ( 'InstanceId', 'EIP', 'AllocationId', 'NetworkInterfaceId', ) properties_schema = { INSTANCE_ID: properties.Schema( properties.Schema.STRING, _('Instance ID to associate with EIP specified by EIP property.'), update_allowed=True, constraints=[ constraints.CustomConstraint('nova.server') ] ), EIP: properties.Schema( properties.Schema.STRING, _('EIP address to associate with instance.'), update_allowed=True ), ALLOCATION_ID: properties.Schema( properties.Schema.STRING, _('Allocation ID for VPC EIP address.'), update_allowed=True ), NETWORK_INTERFACE_ID: properties.Schema( properties.Schema.STRING, _('Network interface ID to associate with EIP.'), update_allowed=True ), } def FnGetRefId(self): return self.physical_resource_name_or_FnGetRefId() def validate(self): ''' Validate any of the provided parameters ''' super(ElasticIpAssociation, self).validate() eip = self.properties[self.EIP] allocation_id = self.properties[self.ALLOCATION_ID] instance_id = self.properties[self.INSTANCE_ID] ni_id = self.properties[self.NETWORK_INTERFACE_ID] # to check EIP and ALLOCATION_ID, should provide one of if bool(eip) == bool(allocation_id): msg = _("Either 'EIP' or 'AllocationId' must be provided.") raise exception.StackValidationFailed(message=msg) # to check if has EIP, also should specify InstanceId if eip and not instance_id: msg = _("Must specify 'InstanceId' if you specify 'EIP'.") raise exception.StackValidationFailed(message=msg) # to check InstanceId and NetworkInterfaceId, should provide # at least one if not instance_id and not ni_id: raise exception.PropertyUnspecifiedError('InstanceId', 'NetworkInterfaceId') def _get_port_info(self, ni_id=None, instance_id=None): port_id = None port_rsrc = None if ni_id: port_rsrc = self.neutron().list_ports(id=ni_id)['ports'][0] port_id = ni_id elif instance_id: ports = self.neutron().list_ports(device_id=instance_id) port_rsrc = ports['ports'][0] port_id = port_rsrc['id'] return port_id, port_rsrc def _neutron_add_gateway_router(self, float_id, network_id): router = vpc.VPC.router_for_vpc(self.neutron(), network_id) if router is not None: 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}) def _neutron_update_floating_ip(self, allocationId, port_id=None, ignore_not_found=False): try: self.neutron().update_floatingip( allocationId, {'floatingip': {'port_id': port_id}}) except Exception as e: if ignore_not_found: self.client_plugin('neutron').ignore_not_found(e) else: raise def _nova_remove_floating_ip(self, instance_id, eip, ignore_not_found=False): server = None try: server = self.nova().servers.get(instance_id) server.remove_floating_ip(eip) except Exception as e: is_not_found = self.client_plugin('nova').is_not_found(e) iue = self.client_plugin('nova').is_unprocessable_entity(e) if ((not ignore_not_found and is_not_found) or (not is_not_found and not iue)): raise 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(_LE('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(_LE('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]: server = self.nova().servers.get(self.properties[self.INSTANCE_ID]) server.add_floating_ip(self.properties[self.EIP]) self.resource_id_set(self.properties[self.EIP]) LOG.debug('ElasticIpAssociation ' '%(instance)s.add_floating_ip(%(eip)s)', {'instance': self.properties[self.INSTANCE_ID], 'eip': self.properties[self.EIP]}) elif self.properties[self.ALLOCATION_ID]: ni_id = self.properties[self.NETWORK_INTERFACE_ID] instance_id = self.properties[self.INSTANCE_ID] port_id, port_rsrc = self._get_port_info(ni_id, instance_id) if not port_id or not port_rsrc: LOG.warn(_LW('Skipping association, resource not specified')) return float_id = self.properties[self.ALLOCATION_ID] network_id = port_rsrc['network_id'] self._neutron_add_gateway_router(float_id, network_id) self._neutron_update_floating_ip(float_id, port_id) self.resource_id_set(float_id) def handle_delete(self): """Remove a floating IP address from a server or port.""" if self.resource_id is None: return if self.properties[self.EIP]: instance_id = self.properties[self.INSTANCE_ID] eip = self.properties[self.EIP] self._nova_remove_floating_ip(instance_id, eip, ignore_not_found=True) elif self.properties[self.ALLOCATION_ID]: float_id = self.properties[self.ALLOCATION_ID] self._neutron_update_floating_ip(float_id, 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 { 'AWS::EC2::EIP': ElasticIp, 'AWS::EC2::EIPAssociation': ElasticIpAssociation, }