# Copyright 2014 # The Cloudscaling Group, Inc. # # 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 collections import netaddr from neutronclient.common import exceptions as neutron_exception from oslo_config import cfg from oslo_log import log as logging from ec2api.api import address as address_api from ec2api.api import common from ec2api.api import dhcp_options from ec2api.api import ec2utils from ec2api.api import security_group as security_group_api from ec2api import clients from ec2api.db import api as db_api from ec2api import exception from ec2api.i18n import _ CONF = cfg.CONF LOG = logging.getLogger(__name__) """Network interface related API implementation """ Validator = common.Validator def create_network_interface(context, subnet_id, private_ip_address=None, private_ip_addresses=None, secondary_private_ip_address_count=None, description=None, security_group_id=None, client_token=None): if client_token: result = describe_network_interfaces(context, filter=[{'name': 'client-token', 'value': [client_token]}]) if result['networkInterfaceSet']: if len(result['networkInterfaceSet']) > 1: LOG.error('describe_network_interfaces returns %s ' 'network_interfaces, but 1 is expected.', len(result['networkInterfaceSet'])) LOG.error('Requested client token: %s', client_token) LOG.error('Result: %s', result) return result['networkInterfaceSet'][0] subnet = ec2utils.get_db_item(context, subnet_id) if subnet is None: raise exception.InvalidSubnetIDNotFound(id=subnet_id) neutron = clients.neutron(context) os_subnet = neutron.show_subnet(subnet['os_id'])['subnet'] # NOTE(Alex): Combine and check ip addresses. Neutron will accept # ip_address as a parameter for specified address and subnet_id for # address to auto-allocate. # TODO(Alex): Implement better diagnostics. subnet_ipnet = netaddr.IPNetwork(os_subnet['cidr']) if not private_ip_addresses: private_ip_addresses = [] if private_ip_address is not None: private_ip_addresses.insert(0, {'private_ip_address': private_ip_address, 'primary': True}) primary_ip = None fixed_ips = [] for ip in private_ip_addresses: ip_address = netaddr.IPAddress(ip['private_ip_address']) if ip_address not in subnet_ipnet: raise exception.InvalidParameterValue( value=str(ip_address), parameter='PrivateIpAddresses', reason='IP address is out of the subnet range') if ip.get('primary', False): if primary_ip is not None: raise exception.InvalidParameterValue( value=str(ip_address), parameter='PrivateIpAddresses', reason='More than one primary ip is supplied') else: primary_ip = str(ip_address) fixed_ips.insert(0, {'ip_address': primary_ip}) else: fixed_ips.append({'ip_address': str(ip_address)}) if not fixed_ips and not secondary_private_ip_address_count: secondary_private_ip_address_count = 1 if secondary_private_ip_address_count is None: secondary_private_ip_address_count = 0 if secondary_private_ip_address_count > 0: for _i in range(secondary_private_ip_address_count): fixed_ips.append({'subnet_id': os_subnet['id']}) vpc = db_api.get_item_by_id(context, subnet['vpc_id']) vpc_id = vpc['id'] dhcp_options_id = vpc.get('dhcp_options_id', None) if not security_group_id: default_groups = security_group_api.describe_security_groups( context, filter=[{'name': 'vpc-id', 'value': [vpc_id]}, {'name': 'group-name', 'value': ['default']}] )['securityGroupInfo'] security_group_id = [default_group['groupId'] for default_group in default_groups] security_groups = db_api.get_items_by_ids(context, security_group_id) if any(security_group['vpc_id'] != vpc['id'] for security_group in security_groups): msg = _('You have specified two resources that belong to ' 'different networks.') raise exception.InvalidGroupNotFound(msg) os_groups = [security_group['os_id'] for security_group in security_groups] with common.OnCrashCleaner() as cleaner: os_port_body = {'port': {'network_id': os_subnet['network_id'], 'security_groups': os_groups}} os_port_body['port']['fixed_ips'] = fixed_ips try: os_port = neutron.create_port(os_port_body)['port'] except (neutron_exception.IpAddressGenerationFailureClient, neutron_exception.OverQuotaClient): raise exception.InsufficientFreeAddressesInSubnet() except (neutron_exception.IpAddressInUseClient, neutron_exception.BadRequest) as ex: # NOTE(ft): AWS returns InvalidIPAddress.InUse for a primary IP # address, but InvalidParameterValue for secondary one. # AWS returns PrivateIpAddressLimitExceeded, but Neutron does # general InvalidInput (converted to BadRequest) in the same case. msg = _('Specified network interface parameters are invalid. ' 'Reason: %(reason)s') % {'reason': ex.message} raise exception.InvalidParameterValue(msg) cleaner.addCleanup(neutron.delete_port, os_port['id']) if primary_ip is None: primary_ip = os_port['fixed_ips'][0]['ip_address'] network_interface = db_api.add_item(context, 'eni', {'os_id': os_port['id'], 'vpc_id': subnet['vpc_id'], 'subnet_id': subnet['id'], 'description': description, 'private_ip_address': primary_ip}) cleaner.addCleanup(db_api.delete_item, context, network_interface['id']) network_interface_id = network_interface['id'] neutron.update_port(os_port['id'], {'port': {'name': network_interface_id}}) if dhcp_options_id: dhcp_options._add_dhcp_opts_to_port( context, db_api.get_item_by_id(context, dhcp_options_id), network_interface, os_port) security_groups = security_group_api._format_security_groups_ids_names( context) return {'networkInterface': _format_network_interface(context, network_interface, os_port, security_groups=security_groups)} def delete_network_interface(context, network_interface_id): network_interface = ec2utils.get_db_item(context, network_interface_id) if 'instance_id' in network_interface: msg = _("Network interface '%(eni_id)s' is currently in use.") msg = msg % {'eni_id': network_interface_id} raise exception.InvalidParameterValue(msg) for address in db_api.get_items(context, 'eipalloc'): if address.get('network_interface_id') == network_interface['id']: address_api._disassociate_address_item(context, address) neutron = clients.neutron(context) with common.OnCrashCleaner() as cleaner: db_api.delete_item(context, network_interface['id']) cleaner.addCleanup(db_api.restore_item, context, 'eni', network_interface) try: neutron.delete_port(network_interface['os_id']) except neutron_exception.PortNotFoundClient: pass return True class NetworkInterfaceDescriber(common.TaggableItemsDescriber): KIND = 'eni' FILTER_MAP = {'addresses.private-ip-address': ['privateIpAddressesSet', 'privateIpAddress'], 'addresses.primary': ['privateIpAddressesSet', 'primary'], 'addresses.association.public-ip': ['privateIpAddressesSet', ('association', 'publicIp')], 'addresses.association.owner-id': ['privateIpAddressesSet', ('association', 'ipOwnerId')], 'association.association-id': ('association', 'associationId'), 'association.allocation-id': ('association', 'allocationId'), 'association.ip-owner-id': ('association', 'ipOwnerId'), 'association.public-ip': ('association', 'publicIp'), 'attachment.attachment-id': ('attachment', 'attachmentId'), 'attachment.instance-id': ('attachment', 'instanceId'), 'attachment.instance-owner-id': ('attachment', 'instanceOwnerId'), 'attachment.device-index': ('attachment', 'deviceIndex'), 'attachment.status': ('attachment', 'status'), 'attachment.attach.time': ('attachment', 'attachTime'), 'attachment.delete-on-termination': ('attachment', 'deleteOnTermination'), 'client-token': 'clientToken', 'description': 'description', 'group-id': ['groupSet', 'groupId'], 'group-name': ['groupSet', 'groupName'], 'mac-address': 'macAddress', 'network-interface-id': 'networkInterfaceId', 'owner-id': 'ownerId', 'private-ip-address': 'privateIpAddress', 'requester-managed': 'requesterManaged', 'source-dest-check': 'sourceDestCheck', 'status': 'status', 'vpc-id': 'vpcId', 'subnet-id': 'subnetId'} def format(self, network_interface, os_port): if not network_interface: return None return _format_network_interface( self.context, network_interface, os_port, self.ec2_addresses[network_interface['id']], self.security_groups) def get_os_items(self): addresses = address_api.describe_addresses(self.context) self.ec2_addresses = collections.defaultdict(list) for address in addresses['addressesSet']: if 'networkInterfaceId' in address: self.ec2_addresses[ address['networkInterfaceId']].append(address) self.security_groups = ( security_group_api._format_security_groups_ids_names(self.context)) neutron = clients.neutron(self.context) return neutron.list_ports(tenant_id=self.context.project_id)['ports'] def get_name(self, os_item): return '' def describe_network_interfaces(context, network_interface_id=None, filter=None): formatted_network_interfaces = NetworkInterfaceDescriber().describe( context, ids=network_interface_id, filter=filter) return {'networkInterfaceSet': formatted_network_interfaces} def assign_private_ip_addresses(context, network_interface_id, private_ip_address=None, secondary_private_ip_address_count=None, allow_reassignment=False): # TODO(Alex): allow_reassignment is not supported at the moment network_interface = ec2utils.get_db_item(context, network_interface_id) subnet = db_api.get_item_by_id(context, network_interface['subnet_id']) neutron = clients.neutron(context) os_subnet = neutron.show_subnet(subnet['os_id'])['subnet'] os_port = neutron.show_port(network_interface['os_id'])['port'] subnet_ipnet = netaddr.IPNetwork(os_subnet['cidr']) fixed_ips = os_port['fixed_ips'] or [] if private_ip_address is not None: for ip_address in private_ip_address: if netaddr.IPAddress(ip_address) not in subnet_ipnet: raise exception.InvalidParameterValue( value=str(ip_address), parameter='PrivateIpAddress', reason='IP address is out of the subnet range') fixed_ips.append({'ip_address': str(ip_address)}) elif secondary_private_ip_address_count > 0: for _i in range(secondary_private_ip_address_count): fixed_ips.append({'subnet_id': os_subnet['id']}) try: neutron.update_port(os_port['id'], {'port': {'fixed_ips': fixed_ips}}) except neutron_exception.IpAddressGenerationFailureClient: raise exception.InsufficientFreeAddressesInSubnet() except neutron_exception.IpAddressInUseClient: msg = _('Some of %(addresses)s is assigned, but move is not ' 'allowed.') % {'addresses': private_ip_address} raise exception.InvalidParameterValue(msg) except neutron_exception.BadRequest as ex: # NOTE(ft):AWS returns PrivateIpAddressLimitExceeded, but Neutron does # general InvalidInput (converted to BadRequest) in the same case. msg = _('Specified network interface parameters are invalid. ' 'Reason: %(reason)s') % {'reason': ex.message} raise exception.InvalidParameterValue(msg) return True def unassign_private_ip_addresses(context, network_interface_id, private_ip_address): network_interface = ec2utils.get_db_item(context, network_interface_id) if network_interface['private_ip_address'] in private_ip_address: raise exception.InvalidParameterValue( value=str(network_interface['private_ip_address']), parameter='PrivateIpAddresses', reason='Primary IP address cannot be unassigned') neutron = clients.neutron(context) os_port = neutron.show_port(network_interface['os_id'])['port'] fixed_ips = os_port['fixed_ips'] or [] new_fixed_ips = [ip for ip in fixed_ips if ip['ip_address'] not in private_ip_address] if len(new_fixed_ips) + len(private_ip_address) != len(fixed_ips): msg = _('Some of the specified addresses are not assigned to ' 'interface %(id)s') % {'id': network_interface_id} raise exception.InvalidParameterValue(msg) os_port = neutron.update_port(os_port['id'], {'port': {'fixed_ips': new_fixed_ips}}) return True def describe_network_interface_attribute(context, network_interface_id, attribute=None): if attribute is None: raise exception.InvalidParameterCombination( _('No attributes specified.')) network_interface = ec2utils.get_db_item(context, network_interface_id) def _format_attr_description(result): result['description'] = { 'value': network_interface.get('description', '')} def _format_attr_source_dest_check(result): result['sourceDestCheck'] = { 'value': network_interface.get('source_dest_check', True)} def _format_attr_group_set(result): ec2_network_interface = describe_network_interfaces(context, network_interface_id=[network_interface_id] )['networkInterfaceSet'][0] result['groupSet'] = ec2_network_interface['groupSet'] def _format_attr_attachment(result): ec2_network_interface = describe_network_interfaces(context, network_interface_id=[network_interface_id] )['networkInterfaceSet'][0] if 'attachment' in ec2_network_interface: result['attachment'] = ec2_network_interface['attachment'] attribute_formatter = { 'description': _format_attr_description, 'sourceDestCheck': _format_attr_source_dest_check, 'groupSet': _format_attr_group_set, 'attachment': _format_attr_attachment, } fn = attribute_formatter.get(attribute) if fn is None: raise exception.InvalidParameterValue(value=attribute, parameter='attribute', reason='Unknown attribute.') result = {'networkInterfaceId': network_interface['id']} fn(result) return result def modify_network_interface_attribute(context, network_interface_id, description=None, source_dest_check=None, security_group_id=None, attachment=None): params_count = ( int(description is not None) + int(source_dest_check is not None) + int(security_group_id is not None) + int(attachment is not None)) if params_count != 1: raise exception.InvalidParameterCombination( 'Multiple attributes specified') network_interface = ec2utils.get_db_item(context, network_interface_id) if description is not None: network_interface['description'] = description db_api.update_item(context, network_interface) neutron = clients.neutron(context) if security_group_id is not None: os_groups = [sg['os_id'] for sg in ec2utils.get_db_items(context, 'sg', security_group_id)] neutron.update_port(network_interface['os_id'], {'port': {'security_groups': os_groups}}) if source_dest_check is not None: allowed = [] if source_dest_check else [{'ip_address': '0.0.0.0/0'}] neutron.update_port(network_interface['os_id'], {'port': {'allowed_address_pairs': allowed}}) network_interface['source_dest_check'] = source_dest_check db_api.update_item(context, network_interface) if attachment: attachment_id = attachment.get('attachment_id') delete_on_termination = attachment.get('delete_on_termination') if attachment_id is None or delete_on_termination is None: raise exception.MissingParameter( _('The request must contain the parameter attachment ' 'deleteOnTermination')) attachment_id_own = ec2utils.change_ec2_id_kind( network_interface['id'], 'eni-attach') if ('instance_id' not in network_interface or attachment_id_own != attachment_id): raise exception.InvalidAttachmentIDNotFound(id=attachment_id) network_interface['delete_on_termination'] = delete_on_termination db_api.update_item(context, network_interface) return True def reset_network_interface_attribute(context, network_interface_id, attribute): # TODO(Alex) This is only a stub because it's not supported by # Openstack. True will be returned for now in any case. # NOTE(Alex) There is a bug in the AWS doc about this method - # "sourceDestCheck" should be used instead of "SourceDestCheck". # Also aws cli doesn't work with it because it doesn't comply with # the API. if attribute == 'sourceDestCheck': return modify_network_interface_attribute(context, network_interface_id, source_dest_check=True) return True def attach_network_interface(context, network_interface_id, instance_id, device_index): network_interface = ec2utils.get_db_item(context, network_interface_id) if 'instance_id' in network_interface: raise exception.InvalidParameterValue( _("Network interface '%(id)s' is currently in use.") % {'id': network_interface_id}) os_instance_id = ec2utils.get_db_item(context, instance_id)['os_id'] # TODO(Alex) Check that the instance is not yet attached to another VPC # TODO(Alex) Check that the instance is "our", not created via nova # (which means that it doesn't belong to any VPC and can't be attached) if any(eni['device_index'] == device_index for eni in db_api.get_items(context, 'eni') if eni.get('instance_id') == instance_id): raise exception.InvalidParameterValue( _("Instance '%(id)s' already has an interface attached at " "device index '%(index)s'.") % {'id': instance_id, 'index': device_index}) neutron = clients.neutron(context) os_port = neutron.show_port(network_interface['os_id'])['port'] nova = clients.nova(context) with common.OnCrashCleaner() as cleaner: # TODO(Alex) nova inserts compute:%availability_zone into device_owner # 'device_owner': 'compute:None'}}) _attach_network_interface_item(context, network_interface, instance_id, device_index) cleaner.addCleanup(_detach_network_interface_item, context, network_interface) nova.servers.interface_attach(os_instance_id, os_port['id'], None, None) return {'attachmentId': ec2utils.change_ec2_id_kind( network_interface['id'], 'eni-attach')} def detach_network_interface(context, attachment_id, force=None): network_interface = db_api.get_item_by_id( context, ec2utils.change_ec2_id_kind(attachment_id, 'eni')) if not network_interface or 'instance_id' not in network_interface: raise exception.InvalidAttachmentIDNotFound(id=attachment_id) if network_interface['device_index'] == 0: raise exception.OperationNotPermitted( _('The network interface at device index 0 cannot be detached.')) neutron = clients.neutron(context) os_port = neutron.show_port(network_interface['os_id'])['port'] with common.OnCrashCleaner() as cleaner: instance_id = network_interface['instance_id'] device_index = network_interface['device_index'] attach_time = network_interface['attach_time'] delete_on_termination = network_interface['delete_on_termination'] _detach_network_interface_item(context, network_interface) cleaner.addCleanup(_attach_network_interface_item, context, network_interface, instance_id, device_index, attach_time, delete_on_termination) neutron.update_port(os_port['id'], {'port': {'device_id': '', 'device_owner': ''}}) return True def _format_network_interface(context, network_interface, os_port, associated_ec2_addresses=[], security_groups={}): ec2_network_interface = {} ec2_network_interface['networkInterfaceId'] = network_interface['id'] ec2_network_interface['subnetId'] = network_interface['subnet_id'] ec2_network_interface['vpcId'] = network_interface['vpc_id'] ec2_network_interface['description'] = network_interface['description'] ec2_network_interface['sourceDestCheck'] = ( network_interface.get('source_dest_check', True)) ec2_network_interface['requesterManaged'] = ( os_port.get('device_owner', '').startswith('network:')) ec2_network_interface['ownerId'] = context.project_id security_group_set = [] for sg_id in os_port['security_groups']: if security_groups.get(sg_id): security_group_set.append(security_groups[sg_id]) ec2_network_interface['groupSet'] = security_group_set if 'instance_id' in network_interface: ec2_network_interface['status'] = 'in-use' ec2_network_interface['attachment'] = { 'attachmentId': ec2utils.change_ec2_id_kind( network_interface['id'], 'eni-attach'), 'instanceId': network_interface['instance_id'], 'deviceIndex': network_interface['device_index'], 'status': 'attached', 'deleteOnTermination': network_interface['delete_on_termination'], 'attachTime': network_interface['attach_time'], 'instanceOwnerId': context.project_id } else: ec2_network_interface['status'] = 'available' ec2_network_interface['macAddress'] = os_port['mac_address'] if os_port['fixed_ips']: ipsSet = [] for ip in os_port['fixed_ips']: primary = ( network_interface.get('private_ip_address', '') == ip['ip_address']) item = {'privateIpAddress': ip['ip_address'], 'primary': primary} ec2_address = next( (addr for addr in associated_ec2_addresses if addr['privateIpAddress'] == ip['ip_address']), None) if ec2_address: item['association'] = { 'associationId': ec2utils.change_ec2_id_kind( ec2_address['allocationId'], 'eipassoc'), 'allocationId': ec2_address['allocationId'], 'ipOwnerId': context.project_id, 'publicDnsName': None, 'publicIp': ec2_address['publicIp'], } if primary: ipsSet.insert(0, item) else: ipsSet.append(item) ec2_network_interface['privateIpAddressesSet'] = ipsSet primary_ip = ipsSet[0] ec2_network_interface['privateIpAddress'] = ( primary_ip['privateIpAddress']) if 'association' in primary_ip: ec2_network_interface['association'] = primary_ip['association'] # NOTE(ft): AWS returns empty tag set for a network interface # if no tag exists ec2_network_interface['tagSet'] = [] return ec2_network_interface def _attach_network_interface_item(context, network_interface, instance_id, device_index, attach_time=None, delete_on_termination=False): if not attach_time: attach_time = ec2utils.isotime(None, True) network_interface.update({ 'instance_id': instance_id, 'device_index': device_index, 'attach_time': attach_time, 'delete_on_termination': delete_on_termination}) db_api.update_item(context, network_interface) def _detach_network_interface_item(context, network_interface): network_interface.pop('instance_id', None) network_interface.pop('device_index', None) network_interface.pop('attach_time', None) network_interface.pop('delete_on_termination', None) db_api.update_item(context, network_interface)