diff --git a/gbpservice/neutron/plugins/ml2plus/drivers/apic_aim/config.py b/gbpservice/neutron/plugins/ml2plus/drivers/apic_aim/config.py index b0dd010e6..7ab6e8c05 100644 --- a/gbpservice/neutron/plugins/ml2plus/drivers/apic_aim/config.py +++ b/gbpservice/neutron/plugins/ml2plus/drivers/apic_aim/config.py @@ -53,10 +53,15 @@ apic_opts = [ "plugin, formatted as a dictionary mapping Neutron external " "network IDs (UUIDs) to ACI external network distinguished " "names."), + # REVISIT: Eliminate the following two options, leaving a single + # RPC implementation. cfg.BoolOpt('enable_raw_sql_for_device_rpc', default=False, help=("This will use those raw SQL statements to speed " "up the calculation of the EP file.")), + cfg.BoolOpt('enable_new_rpc', + default=False, + help=("Enable new RPC handler.")), cfg.IntOpt('apic_nova_vm_name_cache_update_interval', default=60, help=("How many seconds for the polling thread on each " "controller should wait before it updates the nova vm " diff --git a/gbpservice/neutron/plugins/ml2plus/drivers/apic_aim/constants.py b/gbpservice/neutron/plugins/ml2plus/drivers/apic_aim/constants.py index 1c02fdc4c..0aac2ab91 100644 --- a/gbpservice/neutron/plugins/ml2plus/drivers/apic_aim/constants.py +++ b/gbpservice/neutron/plugins/ml2plus/drivers/apic_aim/constants.py @@ -10,8 +10,20 @@ # License for the specific language governing permissions and limitations # under the License. +from neutron_lib import constants as n_constants + GBP_FLOW_CLASSIFIER = 'gbp_flowclassifier' GBP_PORT = 'gbp_port' GBP_NETWORK_VRF = 'gbp_network_vrf' GBP_NETWORK_EPG = 'gbp_network_epg' GBP_NETWORK_LINK = 'gbp_network_link' + +DEVICE_OWNER_SNAT_PORT = 'apic:snat-pool' +DEVICE_OWNER_SVI_PORT = 'apic:svi' + +IPV4_ANY_CIDR = '0.0.0.0/0' +IPV4_METADATA_CIDR = '169.254.169.254/16' + +PROMISCUOUS_TYPES = [n_constants.DEVICE_OWNER_DHCP, + n_constants.DEVICE_OWNER_LOADBALANCER] +PROMISCUOUS_SUFFIX = 'promiscuous' diff --git a/gbpservice/neutron/plugins/ml2plus/drivers/apic_aim/mechanism_driver.py b/gbpservice/neutron/plugins/ml2plus/drivers/apic_aim/mechanism_driver.py index c23d18b0a..5578e600b 100644 --- a/gbpservice/neutron/plugins/ml2plus/drivers/apic_aim/mechanism_driver.py +++ b/gbpservice/neutron/plugins/ml2plus/drivers/apic_aim/mechanism_driver.py @@ -84,18 +84,22 @@ from gbpservice.neutron.plugins.ml2plus.drivers.apic_aim import config # noqa from gbpservice.neutron.plugins.ml2plus.drivers.apic_aim import db from gbpservice.neutron.plugins.ml2plus.drivers.apic_aim import exceptions from gbpservice.neutron.plugins.ml2plus.drivers.apic_aim import extension_db +from gbpservice.neutron.plugins.ml2plus.drivers.apic_aim import rpc from gbpservice.neutron.plugins.ml2plus.drivers.apic_aim import trunk_driver from gbpservice.neutron.services.grouppolicy.drivers.cisco.apic import ( nova_client as nclient) +# REVISIT: We need the aim_mapping policy driver's config until +# advertise_mtu and nested_host_vlan are moved to the mechanism +# driver's own config. Also, the noqa comment has to be on the same +# line as the entire import. +from gbpservice.neutron.services.grouppolicy.drivers.cisco.apic import config as pd_config # noqa + LOG = log.getLogger(__name__) BAKERY = baked.bakery(500, _size_alert=lambda c: LOG.warning( "sqlalchemy baked query cache size exceeded in %s" % __name__)) -DEVICE_OWNER_SNAT_PORT = 'apic:snat-pool' -DEVICE_OWNER_SVI_PORT = 'apic:svi' - ANY_FILTER_NAME = 'AnyFilter' ANY_FILTER_ENTRY_NAME = 'AnyFilterEntry' DEFAULT_VRF_NAME = 'DefaultVRF' @@ -112,8 +116,6 @@ SUPPORTED_VNIC_TYPES = [portbindings.VNIC_NORMAL, AGENT_TYPE_DVS = 'DVS agent' VIF_TYPE_DVS = 'dvs' -PROMISCUOUS_TYPES = [n_constants.DEVICE_OWNER_DHCP, - n_constants.DEVICE_OWNER_LOADBALANCER] VIF_TYPE_FABRIC = 'fabric' FABRIC_HOST_ID = 'fabric' @@ -199,7 +201,8 @@ class KeystoneNotificationEndpoint(object): class ApicMechanismDriver(api_plus.MechanismDriver, db.DbMixin, - extension_db.ExtensionDbMixin): + extension_db.ExtensionDbMixin, + rpc.ApicRpcHandlerMixin): NIC_NAME_LEN = 14 class TopologyRpcEndpoint(object): @@ -239,6 +242,10 @@ class ApicMechanismDriver(api_plus.MechanismDriver, cfg.CONF.ml2_apic_aim.enable_optimized_metadata) self.enable_dhcp_opt = ( cfg.CONF.ml2_apic_aim.enable_optimized_dhcp) + # REVISIT: The following 2 items should be moved to + # the ml2_apic_aim group. + self.nested_host_vlan = cfg.CONF.aim_mapping.nested_host_vlan + self.advertise_mtu = cfg.CONF.aim_mapping.advertise_mtu self.ap_name = 'OpenStack' self.apic_system_id = cfg.CONF.apic_system_id self.notifier = ofrpc.AgentNotifierApi(n_topics.AGENT) @@ -261,8 +268,11 @@ class ApicMechanismDriver(api_plus.MechanismDriver, self.enable_iptables_firewall = (cfg.CONF.ml2_apic_aim. enable_iptables_firewall) self.l3_domain_dn = cfg.CONF.ml2_apic_aim.l3_domain_dn + # REVISIT: Eliminate the following two variables, leaving a + # single RPC implementation. self.enable_raw_sql_for_device_rpc = (cfg.CONF.ml2_apic_aim. enable_raw_sql_for_device_rpc) + self.enable_new_rpc = cfg.CONF.ml2_apic_aim.enable_new_rpc self.apic_nova_vm_name_cache_update_interval = (cfg.CONF.ml2_apic_aim. apic_nova_vm_name_cache_update_interval) self._setup_nova_vm_update() @@ -663,7 +673,8 @@ class ApicMechanismDriver(api_plus.MechanismDriver, aim_ext_subnet_ipv4 = aim_resource.ExternalSubnet( tenant_name=tenant_aname, l3out_name=aname, - external_network_name=L3OUT_EXT_EPG, cidr='0.0.0.0/0', + external_network_name=L3OUT_EXT_EPG, + cidr=aim_cst.IPV4_ANY_CIDR, scope=scope, aggregate=aggregate) self.aim.create(aim_ctx, aim_ext_subnet_ipv4) @@ -3553,7 +3564,8 @@ class ApicMechanismDriver(api_plus.MechanismDriver, snat_port_query = ("SELECT id FROM ports " "WHERE network_id = '" + ext_network['id'] + "' " "AND device_id = '" + host_or_vrf + "' AND " - "device_owner = '" + DEVICE_OWNER_SNAT_PORT + "'") + "device_owner = '" + aim_cst.DEVICE_OWNER_SNAT_PORT + + "'") snat_port = session.execute(snat_port_query).first() if snat_port: snat_port = dict(snat_port) @@ -3572,7 +3584,7 @@ class ApicMechanismDriver(api_plus.MechanismDriver, query += lambda q: q.filter( models_v2.Port.network_id == sa.bindparam('network_id'), models_v2.Port.device_id == sa.bindparam('device_id'), - models_v2.Port.device_owner == DEVICE_OWNER_SNAT_PORT) + models_v2.Port.device_owner == aim_cst.DEVICE_OWNER_SNAT_PORT) snat_port = query(session).params( network_id=ext_network['id'], device_id=host_or_vrf).first() @@ -3611,7 +3623,7 @@ class ApicMechanismDriver(api_plus.MechanismDriver, for snat_subnet in snat_subnets: try: attrs = {'device_id': host_or_vrf, - 'device_owner': DEVICE_OWNER_SNAT_PORT, + 'device_owner': aim_cst.DEVICE_OWNER_SNAT_PORT, 'tenant_id': ext_network['tenant_id'], 'name': 'snat-pool-port:%s' % host_or_vrf, 'network_id': ext_network['id'], @@ -3658,7 +3670,7 @@ class ApicMechanismDriver(api_plus.MechanismDriver, query += lambda q: q.filter( models_v2.IPAllocation.subnet_id == sa.bindparam('subnet_id')) query += lambda q: q.filter( - models_v2.Port.device_owner == DEVICE_OWNER_SNAT_PORT) + models_v2.Port.device_owner == aim_cst.DEVICE_OWNER_SNAT_PORT) return query(session).params( subnet_id=subnet_id).first() @@ -3683,7 +3695,7 @@ class ApicMechanismDriver(api_plus.MechanismDriver, models_v2.Port.id)) query += lambda q: q.filter( models_v2.Port.network_id == sa.bindparam('ext_network_id'), - models_v2.Port.device_owner == DEVICE_OWNER_SNAT_PORT) + models_v2.Port.device_owner == aim_cst.DEVICE_OWNER_SNAT_PORT) snat_ports = query(session).params( ext_network_id=ext_network_id).all() @@ -3893,7 +3905,7 @@ class ApicMechanismDriver(api_plus.MechanismDriver, primary_ips.append(ip + '/' + mask) else: attrs = {'device_id': '', - 'device_owner': DEVICE_OWNER_SVI_PORT, + 'device_owner': aim_cst.DEVICE_OWNER_SVI_PORT, 'tenant_id': network['tenant_id'], 'name': 'apic-svi-port:node-%s' % node, 'network_id': network['id'], @@ -4994,7 +5006,7 @@ class ApicMechanismDriver(api_plus.MechanismDriver, # using other values requires deleting and re-creating the # external network. res_dict[cisco_apic.NAT_TYPE] = 'distributed' - res_dict[cisco_apic.EXTERNAL_CIDRS] = ['0.0.0.0/0'] + res_dict[cisco_apic.EXTERNAL_CIDRS] = [aim_cst.IPV4_ANY_CIDR] self.set_network_extn_db(mgr.actual_session, net_db.id, res_dict) def _missing_subnet_extension_mapping(self, mgr, subnet_db): diff --git a/gbpservice/neutron/plugins/ml2plus/drivers/apic_aim/rpc.py b/gbpservice/neutron/plugins/ml2plus/drivers/apic_aim/rpc.py new file mode 100644 index 000000000..ce23fd04a --- /dev/null +++ b/gbpservice/neutron/plugins/ml2plus/drivers/apic_aim/rpc.py @@ -0,0 +1,1072 @@ +# Copyright (c) 2019 Cisco Systems Inc. +# All Rights Reserved. +# +# 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 collections import defaultdict +from collections import namedtuple +import netaddr +import sqlalchemy as sa +from sqlalchemy.ext import baked + +from neutron.db import api as db_api +from neutron.db.extra_dhcp_opt import models as dhcp_models +from neutron.db.models import allowed_address_pair as aap_models +from neutron.db.models import dns as dns_models +from neutron.db.models import l3 as l3_models +from neutron.db.models import securitygroup as sg_models +from neutron.db.models import segment as segment_models +from neutron.db import models_v2 +from neutron.db.port_security import models as psec_models +from neutron.plugins.ml2 import models as ml2_models +from neutron.services.trunk import models as trunk_models +from neutron_lib.api.definitions import portbindings +from neutron_lib import constants as n_constants +from neutron_lib import context as n_context +from oslo_log import log +from oslo_serialization import jsonutils + +from gbpservice.neutron.plugins.ml2plus.drivers.apic_aim import constants +from gbpservice.neutron.plugins.ml2plus.drivers.apic_aim import db +from gbpservice.neutron.plugins.ml2plus.drivers.apic_aim import extension_db + +# REVISIT: This should be moved to the mechanism driver. +from gbpservice.neutron.services.grouppolicy.drivers.cisco.apic import ( + port_ha_ipaddress_binding as ha_ip_db) + +LOG = log.getLogger(__name__) + +BAKERY = baked.bakery(_size_alert=lambda c: LOG.warning( + "sqlalchemy baked query cache size exceeded in %s" % __name__)) + +EndpointPortInfo = namedtuple( + 'EndpointPortInfo', + ['project_id', + 'port_id', + 'port_name', + 'network_id', + 'mac_address', + 'admin_state_up', + 'device_id', + 'device_owner', + 'host', + 'vif_type', + 'vif_details', + 'psec_enabled', + 'trunk_id', + 'subport_trunk_id', + 'net_mtu', + 'net_dns_domain', + 'nested_domain_name', + 'nested_domain_type', + 'nested_domain_infra_vlan', + 'nested_domain_service_vlan', + 'nested_domain_node_network_vlan', + 'epg_name', + 'epg_app_profile_name', + 'epg_tenant_name', + 'vrf_name', + 'vrf_tenant_name', + 'vm_name']) + +EndpointFixedIpInfo = namedtuple( + 'EndpointFixedIpInfo', + ['ip_address', + 'subnet_id', + 'ip_version', + 'cidr', + 'gateway_ip', + 'enable_dhcp', + 'dns_nameserver', + 'route_destination', + 'route_nexthop']) + +EndpointBindingInfo = namedtuple( + 'EndpointBindingInfo', + ['host', + 'level', + 'network_type', + 'physical_network']) + +EndpointSecurityGroupInfo = namedtuple( + 'EndpointSecurityGroupInfo', + ['sg_id', + 'project_id']) + +EndpointDhcpIpInfo = namedtuple( + 'EndpointDhcpIpInfo', + ['mac_address', + 'ip_address', + 'subnet_id']) + +EndpointAapInfo = namedtuple( + 'EndpointAapInfo', + ['mac_address', + 'ip_address']) + +EndpointOwnedIpInfo = namedtuple( + 'EndpointOwnedIpInfo', + ['ip_address', + 'actual_port_id']) + +EndpointExternalNetworkInfo = namedtuple( + 'EndpointExternalNetworkInfo', + ['network_id', + 'project_id', + 'epg_name', + 'epg_app_profile_name', + 'epg_tenant_name', + 'external_network_dn', + 'nat_type']) + +EndpointFipInfo = namedtuple( + 'EndpointFipInfo', + ['floating_ip_id', + 'floating_ip_address', + 'floating_network_id', + 'fixed_ip_address']) + +EndpointSnatInfo = namedtuple( + 'EndpointSnatInfo', + ['network_id', + 'ip_address', + 'cidr', + 'gateway_ip']) + +EndpointTrunkInfo = namedtuple( + 'EndpointTrunkInfo', + ['master_port_id', + 'subport_port_id', + 'segmentation_type', + 'segmentation_id']) + + +class ApicRpcHandlerMixin(object): + + # The following five methods handle RPCs from the Opflex agent. + # + # REVISIT: These handler methods are currently called by + # corresponding handler methods in the aim_mapping_rpc + # module. Once these RPC handlers are all fully implemented and + # tested, move the instantiation of the + # opflexagent.rpc.GBPServerRpcCallback class from aim_mapping_rpc + # to this module and eliminate the other RPC handler + # implementations. + + def get_gbp_details(self, context, **kwargs): + LOG.debug("APIC AIM MD handling get_gbp_details for: %s", kwargs) + + # REVISIT: This RPC is no longer invoked by the Opflex agent, + # and should be eliminated or should simply log an error, but + # it is used extensively in unit tests. + + request = {'device': kwargs.get('device')} + host = kwargs.get('host') + response = self.request_endpoint_details( + context, request=request, host=host) + gbp_details = response.get('gbp_details') + return gbp_details or response + + def get_vrf_details(self, context, **kwargs): + LOG.debug("APIC AIM MD handling get_vrf_details for: %s", kwargs) + + vrf_id = kwargs.get('vrf_id') + if not vrf_id: + LOG.error("Missing vrf_id in get_vrf_details RPC: %s", + kwargs) + return + + try: + return self._get_vrf_details(context, vrf_id) + except Exception as e: + LOG.error("An exception occurred while processing " + "get_vrf_details RPC: %s", kwargs) + LOG.exception(e) + return {'l3_policy_id': vrf_id} + + def request_endpoint_details(self, context, **kwargs): + LOG.debug("APIC AIM MD handling request_endpoint_details for: %s", + kwargs) + + request = kwargs.get('request') + if not request: + LOG.error("Missing request in request_endpoint_details RPC: %s", + kwargs) + return + + device = request.get('device') + if not device: + LOG.error("Missing device in request_endpoint_details RPC: %s", + kwargs) + return + + host = kwargs.get('host') + if not host: + LOG.error("Missing host in request_endpoint_details RPC: %s", + kwargs) + return + + try: + return self._request_endpoint_details(context, request, host) + except Exception as e: + LOG.error("An exception occurred while processing " + "request_endpoint_details RPC: %s", kwargs) + LOG.exception(e) + return {'device': device} + + def request_vrf_details(self, context, kwargs): + LOG.debug("APIC AIM MD handling request_vrf_details for: %s", kwargs) + + # REVISIT: This RPC is not currently invoked by the Opflex + # agent, but that may be planned. Once it is, move the handler + # implementation from get_vrf_details() to this method. + return self.get_vrf_details(context, kwargs) + + # REVISIT: def ip_address_owner_update(self, context, **kwargs): + + @db_api.retry_if_session_inactive() + def _get_vrf_details(self, context, vrf_id): + vrf_tenant_name, vrf_name = vrf_id.split(' ') + with db_api.context_manager.reader.using(context) as session: + vrf_subnets = self._query_vrf_subnets( + session, vrf_tenant_name, vrf_name) + return { + 'l3_policy_id': vrf_id, + 'vrf_tenant': vrf_tenant_name, + 'vrf_name': vrf_name, + 'vrf_subnets': vrf_subnets + } + + @db_api.retry_if_session_inactive() + def _request_endpoint_details(self, context, request, host): + device = request['device'] + info = {'device': device} + response = { + 'device': device, + 'request_id': request.get('request_id'), + 'timestamp': request.get('timestamp') + } + + # Loop so we can bind the port, if necessary, outside the + # transaction in which we query the endpoint's state, and then + # retry. + while True: + # Start a read-only transaction. Separate read-write + # transactions will be used if needed to bind the port or + # assign SNAT IPs. + with db_api.context_manager.reader.using(context) as session: + # Extract possibly truncated port ID from device. + # + # REVISIT: If device identifies the port by its MAC + # address instead of its UUID, _device_to_port_id() + # will query for the entire port DB object. So + # consider not calling _device_to_port_id() and + # instead removing any device prefix here and + # conditionally filtering in + # _query_endpoint_port_info() below on either the + # port's UUID or its mac_address. + port_id = self.plugin._device_to_port_id(context, device) + + # Query for all the needed scalar (non-list) state + # associated with the port. + port_infos = self._query_endpoint_port_info(session, port_id) + if not port_infos: + LOG.info("Nonexistent port %s in requent_endpoint_details " + "RPC from host %s", port_id, host) + return response + if len(port_infos) > 1: + LOG.info("Multiple ports start with %s in " + "requent_endpoint_details RPC from host %s", + port_id, host) + return response + port_info = port_infos[0] + info['port_info'] = port_info + + # If port is bound, check host and do remaining + # queries. + if port_info.vif_type not in [ + portbindings.VIF_TYPE_UNBOUND, + portbindings.VIF_TYPE_BINDING_FAILED]: + + # Check that port is bound to host making the RPC + # request. + if port_info.host != host: + LOG.warning("Port %s bound to host %s, but " + "request_endpoint_details RPC made from " + "host %s", + port_info.port_id, port_info.host, host) + return response + + # Query for all needed state associated with each + # of the port's static IPs. + info['ip_info'] = self._query_endpoint_fixed_ip_info( + session, port_info.port_id) + + # Query for list of state associated with each of + # the port's binding levels, sorted by level. + info['binding_info'] = self._query_endpoint_binding_info( + session, port_info.port_id) + + # Query for list of the port's security groups. + info['sg_info'] = self._query_endpoint_sg_info( + session, port_info.port_id) + + # Query for list of state associated with each + # DHCP IP on the port's network. + info['dhcp_ip_info'] = self._query_endpoint_dhcp_ip_info( + session, port_info.network_id) + + # Query for the port's allowed address pairs. + info['aap_info'] = self._query_endpoint_aap_info( + session, port_info.port_id) + + # Query for list of state associated with each of + # the port's HAIP owned IP addresses. + info['owned_ip_info'] = ( + self._query_endpoint_haip_owned_ip_info( + session, port_info.port_id, port_info.network_id)) + + # Query for dict of state associated with the + # external networks to which the port's subnets + # are routed. + subnet_ids = set([ip.subnet_id for ip in info['ip_info']]) + info['ext_net_info'] = self._query_endpoint_ext_net_info( + session, subnet_ids) + + # Query for list of floating IPs for both this + # port and all the other ports on which this + # port's HAIP owned addresses are actually + # defined. + fip_port_ids = ( + [port_info.port_id] + + [x.actual_port_id for x in info['owned_ip_info']]) + info['fip_info'] = self._query_endpoint_fip_info( + session, fip_port_ids) + + # Query for dict of state associated with the SNAT + # ports on this host of the endpoint port's + # external networks. + info['snat_info'] = self._query_endpoint_snat_info( + session, host, info['ext_net_info'].keys()) + + # Query for list of trunk subports for a trunk + # that the endpoint's port is associated with, + # either as the master port or as a subport. + trunk_id = port_info.trunk_id or port_info.subport_trunk_id + if trunk_id: + info['trunk_info'] = self._query_endpoint_trunk_info( + session, trunk_id) + + # Query for the port's extra DHCP options. + info['extra_dhcp_opts'] = ( + self._query_endpoint_extra_dhcp_opts( + session, port_info.port_id)) + + # Query for nested domain allowed VLANs for the + # port's network. + info['nested_domain_allowed_vlans'] = ( + self._query_endpoint_nested_domain_allowed_vlans( + session, port_info.network_id)) + + # Query for VRF subnets. + info['vrf_subnets'] = self._query_vrf_subnets( + session, port_info.vrf_tenant_name, port_info.vrf_name) + + # Let the GBP policy driver do its queries and add + # its info. + if self.gbp_driver: + self.gbp_driver.query_endpoint_rpc_info(session, info) + + # Done with queries, so exit transaction and retry loop. + break + + # Attempt to bind port outside transaction. + pc = self.plugin.get_bound_port_context(context, port_id, host) + if (pc.vif_type == portbindings.VIF_TYPE_BINDING_FAILED or + pc.vif_type == portbindings.VIF_TYPE_UNBOUND): + LOG.warning("The request_endpoint_details RPC handler is " + "unable to bind port %s on host %s", + port_id, pc.host) + return response + + # Successfully bound port, so loop to retry queries. + + # Completed queries, so build up the response. + response['neutron_details'] = self._build_endpoint_neutron_details( + info) + response['gbp_details'] = self._build_endpoint_gbp_details(info) + response['trunk_details'] = self._build_endpoint_trunk_details(info) + + # Let the GBP policy driver add/update its details in the response. + if self.gbp_driver: + self.gbp_driver.update_endpoint_rpc_details(info, response) + + # Return the response. + return response + + def _query_endpoint_port_info(self, session, port_id): + query = BAKERY(lambda s: s.query( + models_v2.Port.project_id, + models_v2.Port.id, + models_v2.Port.name, + models_v2.Port.network_id, + models_v2.Port.mac_address, + models_v2.Port.admin_state_up, + models_v2.Port.device_id, + models_v2.Port.device_owner, + ml2_models.PortBinding.host, + ml2_models.PortBinding.vif_type, + ml2_models.PortBinding.vif_details, + psec_models.PortSecurityBinding.port_security_enabled, + trunk_models.Trunk.id, + trunk_models.SubPort.trunk_id, + models_v2.Network.mtu, + dns_models.NetworkDNSDomain.dns_domain, + extension_db.NetworkExtensionDb.nested_domain_name, + extension_db.NetworkExtensionDb.nested_domain_type, + extension_db.NetworkExtensionDb.nested_domain_infra_vlan, + extension_db.NetworkExtensionDb.nested_domain_service_vlan, + extension_db.NetworkExtensionDb. + nested_domain_node_network_vlan, + db.NetworkMapping.epg_name, + db.NetworkMapping.epg_app_profile_name, + db.NetworkMapping.epg_tenant_name, + db.NetworkMapping.vrf_name, + db.NetworkMapping.vrf_tenant_name, + db.VMName.vm_name, + )) + query += lambda q: q.outerjoin( + ml2_models.PortBinding, + ml2_models.PortBinding.port_id == models_v2.Port.id) + query += lambda q: q.outerjoin( + psec_models.PortSecurityBinding, + psec_models.PortSecurityBinding.port_id == models_v2.Port.id) + query += lambda q: q.outerjoin( + trunk_models.Trunk, + trunk_models.Trunk.port_id == models_v2.Port.id) + query += lambda q: q.outerjoin( + trunk_models.SubPort, + trunk_models.SubPort.port_id == models_v2.Port.id) + query += lambda q: q.outerjoin( + models_v2.Network, + models_v2.Network.id == models_v2.Port.network_id) + query += lambda q: q.outerjoin( + dns_models.NetworkDNSDomain, + dns_models.NetworkDNSDomain.network_id == + models_v2.Port.network_id) + query += lambda q: q.outerjoin( + extension_db.NetworkExtensionDb, + extension_db.NetworkExtensionDb.network_id == + models_v2.Port.network_id) + query += lambda q: q.outerjoin( + db.NetworkMapping, + db.NetworkMapping.network_id == models_v2.Port.network_id) + query += lambda q: q.outerjoin( + db.VMName, + db.VMName.device_id == models_v2.Port.device_id) + query += lambda q: q.filter( + models_v2.Port.id.startswith(sa.bindparam('port_id'))) + return [EndpointPortInfo._make(row) for row in + query(session).params( + port_id=port_id)] + + def _query_endpoint_fixed_ip_info(self, session, port_id): + # In this query, IPAllocations are outerjoined with + # DNSNameServers and SubnetRoutes. This avoids needing to make + # separate queries for DNSNameServers and for SubnetRoutes, + # but results in rows being returned for the cross product of + # the DNSNameServer rows and SubnetRoute rows associated with + # each fixed IP. Unless there are use cases where large + # numbers of rows in both these tables exist for the same + # fixed IP, this approach is expected to provide better + # latency and scalability than using separate + # queries. Redundant information must be ignored when + # processing the rows returned from this query. + query = BAKERY(lambda s: s.query( + models_v2.IPAllocation.ip_address, + models_v2.IPAllocation.subnet_id, + models_v2.Subnet.ip_version, + models_v2.Subnet.cidr, + models_v2.Subnet.gateway_ip, + models_v2.Subnet.enable_dhcp, + models_v2.DNSNameServer.address, + models_v2.SubnetRoute.destination, + models_v2.SubnetRoute.nexthop, + )) + query += lambda q: q.join( + models_v2.Subnet, + models_v2.Subnet.id == models_v2.IPAllocation.subnet_id) + query += lambda q: q.outerjoin( + models_v2.DNSNameServer, + models_v2.DNSNameServer.subnet_id == + models_v2.IPAllocation.subnet_id) + query += lambda q: q.outerjoin( + models_v2.SubnetRoute, + models_v2.SubnetRoute.subnet_id == + models_v2.IPAllocation.subnet_id) + query += lambda q: q.filter( + models_v2.IPAllocation.port_id == sa.bindparam('port_id')) + query += lambda q: q.order_by( + models_v2.DNSNameServer.order) + return [EndpointFixedIpInfo._make(row) for row in + query(session).params( + port_id=port_id)] + + def _query_endpoint_binding_info(self, session, port_id): + query = BAKERY(lambda s: s.query( + ml2_models.PortBindingLevel.host, + ml2_models.PortBindingLevel.level, + segment_models.NetworkSegment.network_type, + segment_models.NetworkSegment.physical_network, + )) + query += lambda q: q.join( + segment_models.NetworkSegment, + segment_models.NetworkSegment.id == + ml2_models.PortBindingLevel.segment_id) + query += lambda q: q.filter( + ml2_models.PortBindingLevel.port_id == sa.bindparam('port_id')) + query += lambda q: q.order_by( + ml2_models.PortBindingLevel.level) + return [EndpointBindingInfo._make(row) for row in + query(session).params( + port_id=port_id)] + + def _query_endpoint_sg_info(self, session, port_id): + query = BAKERY(lambda s: s.query( + sg_models.SecurityGroup.id, + sg_models.SecurityGroup.project_id, + )) + query += lambda q: q.join( + sg_models.SecurityGroupPortBinding, + sg_models.SecurityGroupPortBinding.security_group_id == + sg_models.SecurityGroup.id) + query += lambda q: q.filter( + sg_models.SecurityGroupPortBinding.port_id == + sa.bindparam('port_id')) + return [EndpointSecurityGroupInfo._make(row) for row in + query(session).params( + port_id=port_id)] + + def _query_endpoint_dhcp_ip_info(self, session, network_id): + query = BAKERY(lambda s: s.query( + models_v2.Port.mac_address, + models_v2.IPAllocation.ip_address, + models_v2.IPAllocation.subnet_id, + )) + query += lambda q: q.join( + models_v2.IPAllocation, + models_v2.IPAllocation.port_id == models_v2.Port.id) + query += lambda q: q.filter( + models_v2.Port.network_id == sa.bindparam('network_id'), + models_v2.Port.device_owner == n_constants.DEVICE_OWNER_DHCP) + return [EndpointDhcpIpInfo._make(row) for row in + query(session).params( + network_id=network_id)] + + def _query_endpoint_aap_info(self, session, port_id): + query = BAKERY(lambda s: s.query( + aap_models.AllowedAddressPair.mac_address, + aap_models.AllowedAddressPair.ip_address, + )) + query += lambda q: q.filter( + aap_models.AllowedAddressPair.port_id == + sa.bindparam('port_id')) + return [EndpointAapInfo._make(row) for row in + query(session).params( + port_id=port_id)] + + def _query_endpoint_haip_owned_ip_info(self, session, port_id, network_id): + query = BAKERY(lambda s: s.query( + ha_ip_db.HAIPAddressToPortAssocation.ha_ip_address, + models_v2.IPAllocation.port_id, + )) + query += lambda q: q.outerjoin( + models_v2.IPAllocation, + models_v2.IPAllocation.ip_address == + ha_ip_db.HAIPAddressToPortAssocation.ha_ip_address and + models_v2.IPAllocation.network_id == + sa.bindparam('network_id')) + query += lambda q: q.filter( + ha_ip_db.HAIPAddressToPortAssocation.port_id == + sa.bindparam('port_id')) + return [EndpointOwnedIpInfo._make(row) for row in + query(session).params( + port_id=port_id, + network_id=network_id)] + + def _query_endpoint_ext_net_info(self, session, subnet_ids): + # REVISIT: Consider replacing this query with additional joins + # in _query_endpoint_fixed_ip_info to eliminate a round-trip + # to the DB server. This would require using aliases to + # disambiguate between the endpoint's port's IPAllocation and + # the router port's IPAllocation, and its not obvious if + # aliases can be used with baked queries. + if not subnet_ids: + return {} + query = BAKERY(lambda s: s.query( + models_v2.Network.id, + models_v2.Network.project_id, + db.NetworkMapping.epg_name, + db.NetworkMapping.epg_app_profile_name, + db.NetworkMapping.epg_tenant_name, + extension_db.NetworkExtensionDb.external_network_dn, + extension_db.NetworkExtensionDb.nat_type, + )) + query += lambda q: q.join( + models_v2.Port, # router's gw_port + models_v2.Port.network_id == models_v2.Network.id) + query += lambda q: q.join( + l3_models.Router, + l3_models.Router.gw_port_id == models_v2.Port.id) + query += lambda q: q.join( + l3_models.RouterPort, + l3_models.RouterPort.router_id == l3_models.Router.id and + l3_models.RouterPort.port_type == + n_constants.DEVICE_OWNER_ROUTER_INTF) + query += lambda q: q.join( + models_v2.IPAllocation, # router interface IP + models_v2.IPAllocation.port_id == l3_models.RouterPort.port_id) + query += lambda q: q.join( + db.NetworkMapping, # mapping of gw_port's network + db.NetworkMapping.network_id == models_v2.Port.network_id) + query += lambda q: q.outerjoin( + extension_db.NetworkExtensionDb, + extension_db.NetworkExtensionDb.network_id == + models_v2.Port.network_id) + query += lambda q: q.filter( + models_v2.IPAllocation.subnet_id.in_( + sa.bindparam('subnet_ids', expanding=True))) + query += lambda q: q.distinct() + return {row[0]: EndpointExternalNetworkInfo._make(row) for row in + query(session).params( + subnet_ids=list(subnet_ids))} + + def _query_endpoint_fip_info(self, session, port_ids): + if not port_ids: + return [] + query = BAKERY(lambda s: s.query( + l3_models.FloatingIP.id, + l3_models.FloatingIP.floating_ip_address, + l3_models.FloatingIP.floating_network_id, + l3_models.FloatingIP.fixed_ip_address, + )) + query += lambda q: q.filter( + l3_models.FloatingIP.fixed_port_id.in_(sa.bindparam( + 'port_ids', expanding=True))) + return [EndpointFipInfo._make(row) for row in + query(session).params( + port_ids=port_ids)] + + def _query_endpoint_snat_info(self, session, host, ext_net_ids): + # REVISIT: Consider replacing this query with additional joins + # in _query_endpoint_ext_net_info to eliminate a round-trip to + # the DB server. This would require using aliases to + # disambiguate tables appearing multiple times in the query, + # and its not obvious if aliases can be used with baked + # queries. + if not ext_net_ids: + return {} + query = BAKERY(lambda s: s.query( + models_v2.Port.network_id, + models_v2.IPAllocation.ip_address, + models_v2.Subnet.cidr, + models_v2.Subnet.gateway_ip, + )) + query += lambda q: q.join( + models_v2.IPAllocation, + models_v2.IPAllocation.port_id == models_v2.Port.id) + query += lambda q: q.join( + models_v2.Subnet, + models_v2.Subnet.id == models_v2.IPAllocation.subnet_id) + query += lambda q: q.filter( + models_v2.Port.network_id.in_(sa.bindparam( + 'ext_net_ids', expanding=True)), + models_v2.Port.device_id == sa.bindparam('host'), + models_v2.Port.device_owner == constants.DEVICE_OWNER_SNAT_PORT) + return {row[0]: EndpointSnatInfo._make(row) for row in + query(session).params( + host=host, + ext_net_ids=ext_net_ids)} + + def _query_endpoint_trunk_info(self, session, trunk_id): + query = BAKERY(lambda s: s.query( + trunk_models.Trunk.port_id, + trunk_models.SubPort.port_id, + trunk_models.SubPort.segmentation_type, + trunk_models.SubPort.segmentation_id, + )) + query += lambda q: q.join( + trunk_models.SubPort, + trunk_models.SubPort.trunk_id == trunk_models.Trunk.id) + query += lambda q: q.filter( + trunk_models.Trunk.id == sa.bindparam('trunk_id')) + return [EndpointTrunkInfo._make(row) for row in + query(session).params( + trunk_id=trunk_id)] + + def _query_endpoint_extra_dhcp_opts(self, session, port_id): + query = BAKERY(lambda s: s.query( + dhcp_models.ExtraDhcpOpt.opt_name, + dhcp_models.ExtraDhcpOpt.opt_value, + )) + query += lambda q: q.filter( + dhcp_models.ExtraDhcpOpt.port_id == sa.bindparam('port_id')) + return {k: v for k, v in query(session).params( + port_id=port_id)} + + def _query_endpoint_nested_domain_allowed_vlans(self, session, network_id): + query = BAKERY(lambda s: s.query( + extension_db.NetworkExtNestedDomainAllowedVlansDb.vlan, + )) + query += lambda q: q.filter( + extension_db.NetworkExtNestedDomainAllowedVlansDb.network_id == + sa.bindparam('network_id')) + return [x for x, in query(session).params( + network_id=network_id)] + + def _query_vrf_subnets(self, session, vrf_tenant_name, vrf_name): + # A VRF mapped from one or two (IPv4 and/or IPv6) + # address_scopes cannot be associated with unscoped + # subnets. So first see if the VRF is mapped from + # address_scopes, and if so, return the subnetpool CIDRs + # associated with those address_scopes. + query = BAKERY(lambda s: s.query( + models_v2.SubnetPoolPrefix.cidr)) + query += lambda q: q.join( + models_v2.SubnetPool, + models_v2.SubnetPool.id == + models_v2.SubnetPoolPrefix.subnetpool_id) + query += lambda q: q.join( + db.AddressScopeMapping, + db.AddressScopeMapping.scope_id == + models_v2.SubnetPool.address_scope_id) + query += lambda q: q.filter( + db.AddressScopeMapping.vrf_name == + sa.bindparam('vrf_name'), + db.AddressScopeMapping.vrf_tenant_name == + sa.bindparam('vrf_tenant_name')) + result = [x for x, in query(session).params( + vrf_name=vrf_name, + vrf_tenant_name=vrf_tenant_name)] + if result: + return result + + # If the VRF is not mapped from address_scopes, return the + # CIDRs of all the subnets on all the networks associated with + # the VRF. + # + # REVISIT: Consider combining these two queries into a single + # query, using outerjoins to SubnetPool and + # AddressScopeMapping. But that would result in all the + # subnets' CIDRs being returned, even for the scoped case + # where they are not needed, so it may not be a win. + query = BAKERY(lambda s: s.query( + models_v2.Subnet.cidr)) + query += lambda q: q.join( + db.NetworkMapping, + db.NetworkMapping.network_id == + models_v2.Subnet.network_id) + query += lambda q: q.filter( + db.NetworkMapping.vrf_name == + sa.bindparam('vrf_name'), + db.NetworkMapping.vrf_tenant_name == + sa.bindparam('vrf_tenant_name')) + return [x for x, in query(session).params( + vrf_name=vrf_name, + vrf_tenant_name=vrf_tenant_name)] + + def _build_endpoint_neutron_details(self, info): + port_info = info['port_info'] + binding_info = info['binding_info'] + + details = {} + details['admin_state_up'] = port_info.admin_state_up + details['device_owner'] = port_info.device_owner + details['fixed_ips'] = self._build_fixed_ips(info) + details['network_id'] = port_info.network_id + details['network_type'] = binding_info[-1].network_type + details['physical_network'] = binding_info[-1].physical_network + details['port_id'] = port_info.port_id + + return details + + def _build_fixed_ips(self, info): + ip_info = info['ip_info'] + + # Build dict of unique fixed IPs, ignoring duplicates due to + # joins between Port and DNSNameServers and Routes. + fixed_ips = {} + for ip in ip_info: + if ip.ip_address not in fixed_ips: + fixed_ips[ip.ip_address] = {'subnet_id': ip.subnet_id, + 'ip_address': ip.ip_address} + + return fixed_ips.values() + + def _build_endpoint_gbp_details(self, info): + port_info = info['port_info'] + + # Note that the GBP policy driver will replace these + # app_profile_name, endpoint_group_name, ptg_tenant, + # ... values if the port belongs to a GBP PolicyTarget. + + details = {} + details['allowed_address_pairs'] = self._build_aaps(info) + details['app_profile_name'] = port_info.epg_app_profile_name + details['device'] = info['device'] # Redundant. + if self.apic_optimized_dhcp_lease_time > 0: + details['dhcp_lease_time'] = self.apic_optimized_dhcp_lease_time + details['dns_domain'] = port_info.net_dns_domain or '' + details['enable_dhcp_optimization'] = self.enable_dhcp_opt + details['enable_metadata_optimization'] = self.enable_metadata_opt + details['endpoint_group_name'] = port_info.epg_name + details['floating_ip'] = self._build_fips(info) + details['host'] = port_info.host + details['host_snat_ips'] = self._build_host_snat_ips(info) + mtu = self._get_interface_mtu(info) + if mtu: + details['interface_mtu'] = mtu + details['ip_mapping'] = self._build_ipms(info) + details['l3_policy_id'] = ("%s %s" % + (port_info.vrf_tenant_name, + port_info.vrf_name)) + details['mac_address'] = port_info.mac_address + details['nested_domain_allowed_vlans'] = ( + info['nested_domain_allowed_vlans']) + details['nested_domain_infra_vlan'] = ( + port_info.nested_domain_infra_vlan) + details['nested_domain_name'] = port_info.nested_domain_name + details['nested_domain_node_network_vlan'] = ( + port_info.nested_domain_node_network_vlan) + details['nested_domain_service_vlan'] = ( + port_info.nested_domain_service_vlan) + details['nested_domain_type'] = port_info.nested_domain_type + details['nested_host_vlan'] = ( + self.nested_host_vlan if port_info.nested_domain_infra_vlan + else None) + details['port_id'] = port_info.port_id # Redundant. + details['promiscuous_mode'] = self._get_promiscuous_mode(info) + details['ptg_tenant'] = port_info.epg_tenant_name + if info['sg_info']: + # Only add security group details if the port has SGs and + # it doesn't belong to a legacy VM using iptables. + vif_details = (port_info.vif_details and + jsonutils.loads(port_info.vif_details)) + if not (vif_details and vif_details.get('port_filter') and + vif_details.get('ovs_hybrid_plug')): + details['security_group'] = self._build_sg_details(info) + details['subnets'] = self._build_subnet_details(info) + details['vm-name'] = (port_info.vm_name if + port_info.device_owner.startswith('compute:') and + port_info.vm_name else port_info.device_id) + details['vrf_name'] = port_info.vrf_name + details['vrf_subnets'] = info['vrf_subnets'] + details['vrf_tenant'] = port_info.vrf_tenant_name + + return details + + def _build_aaps(self, info): + owned_ips = set(ip.ip_address for ip in info['owned_ip_info']) + aaps = {} + for allowed in info['aap_info']: + aaps[allowed.ip_address] = {'ip_address': allowed.ip_address, + 'mac_address': allowed.mac_address} + cidr = netaddr.IPNetwork(allowed.ip_address) + if ((cidr.version == 4 and cidr.prefixlen != 32) or + (cidr.version == 6 and cidr.prefixlen != 128)): + # Never mark CIDRs as "active", but + # look for owned addresses in this CIDR, and + # if present, add them to the allowed-address-pairs + # list, and mark those as "active". + for ip in owned_ips: + if ip in cidr and ip not in aaps: + aaps[ip] = {'ip_address': ip, + 'mac_address': allowed.mac_address, + 'active': True} + elif allowed.ip_address in owned_ips: + aaps[allowed.ip_address]['active'] = True + return aaps.values() + + def _build_fips(self, info): + ext_net_info = info['ext_net_info'] + fips = [] + for fip in info['fip_info']: + details = {'id': fip.floating_ip_id, + 'fixed_ip_address': fip.fixed_ip_address, + 'floating_ip_address': fip.floating_ip_address} + ext_net = ext_net_info.get(fip.floating_network_id) + if (ext_net and ext_net.external_network_dn and + ext_net.nat_type == 'distributed'): + details['nat_epg_app_profile'] = ext_net.epg_app_profile_name + details['nat_epg_name'] = ext_net.epg_name + details['nat_epg_tenant'] = ext_net.epg_tenant_name + fips.append(details) + return fips + + def _build_host_snat_ips(self, info): + snat_info = info['snat_info'] + host = info['port_info'].host + ext_nets_with_fips = {fip.floating_network_id + for fip in info['fip_info']} + host_snat_ips = [] + for ext_net in info['ext_net_info'].values(): + if ext_net in ext_nets_with_fips: + # No need for SNAT IP. + continue + snat = snat_info.get(ext_net.network_id) + if snat: + snat_ip = {'host_snat_ip': snat.ip_address, + 'gateway_ip': snat.gateway_ip, + 'prefixlen': int(snat.cidr.split('/')[1])} + else: + # No existing SNAT IP for this external network on + # this host, so allocate one. + # + # REVISIT: Should this have a retry loop/decorator so + # that we don't have to retry the entire RPC handler + # if we get a retriable exception? + ctx = n_context.get_admin_context() + with db_api.context_manager.writer.using(ctx): + snat_ip = self.get_or_allocate_snat_ip( + ctx, host, {'id': ext_net.network_id, + 'tenant_id': ext_net.project_id}) + if snat_ip: + snat_ip['external_segment_name'] = ( + ext_net.external_network_dn.replace('/', ':')) + host_snat_ips.append(snat_ip) + return host_snat_ips + + def _get_interface_mtu(self, info): + if self.advertise_mtu: + opts = info['extra_dhcp_opts'] + opt_value = opts.get('interface-mtu') or opts.get('26') + if opt_value: + try: + return int(opt_value) + except ValueError: + pass + return info['port_info'].net_mtu + + def _build_ipms(self, info): + ext_nets_with_fips = {fip.floating_network_id + for fip in info['fip_info']} + return [{'external_segment_name': + ext_net.external_network_dn.replace('/', ':'), + 'nat_epg_app_profile': ext_net.epg_app_profile_name, + 'nat_epg_name': ext_net.epg_name, + 'nat_epg_tenant': ext_net.epg_tenant_name} + for ext_net in info['ext_net_info'].values() + if ext_net.external_network_dn and + ext_net.nat_type == 'distributed' and + ext_net.network_id not in ext_nets_with_fips] + + def _get_promiscuous_mode(self, info): + port_info = info['port_info'] + # REVISIT: Replace PROMISCUOUS_SUFFIX with a proper API + # attribute if really needed, but why not just have + # applications use port_security_enabled=False? + return (port_info.device_owner in constants.PROMISCUOUS_TYPES or + port_info.port_name.endswith(constants.PROMISCUOUS_SUFFIX) or + not port_info.psec_enabled) + + def _build_sg_details(self, info): + return ( + [{'policy-space': self.name_mapper.project(None, sg.project_id), + 'name': sg.sg_id} for sg in info['sg_info']] + + [{'policy-space': 'common', 'name': self._default_sg_name}]) + + def _build_subnet_details(self, info): + ip_info = info['ip_info'] + dhcp_ip_info = info['dhcp_ip_info'] + + # Build dict of subnets with basic subnet details, and collect + # joined DNSNameServer and Route info. Order must be preserved + # among DNSNameServer entries for a subnet. + subnets = {} + subnet_dns_nameservers = defaultdict(list) + subnet_routes = defaultdict(set) + for ip in ip_info: + if ip.subnet_id not in subnets: + subnet = {} + subnet['cidr'] = ip.cidr + subnet['enable_dhcp'] = ip.enable_dhcp + subnet['gateway_ip'] = ip.gateway_ip + subnet['id'] = ip.subnet_id + subnet['ip_version'] = ip.ip_version + subnets[ip.subnet_id] = subnet + if ip.dns_nameserver: + dns_nameservers = subnet_dns_nameservers[ip.subnet_id] + if ip.dns_nameserver not in dns_nameservers: + dns_nameservers.append(ip.dns_nameserver) + if ip.route_destination: + subnet_routes[ip.subnet_id].add( + (ip.route_destination, ip.route_nexthop)) + + # Add remaining details to each subnet. + for subnet_id, subnet in subnets.items(): + dhcp_ips = set() + dhcp_ports = defaultdict(list) + for ip in dhcp_ip_info: + if ip.subnet_id == subnet_id: + dhcp_ips.add(ip.ip_address) + dhcp_ports[ip.mac_address].append(ip.ip_address) + dhcp_ips = list(dhcp_ips) + + routes = subnet_routes[subnet_id] + if subnet['ip_version'] == 4: + # Find default and metadata routes. + default_routes = set() + metadata_routes = set() + for route in routes: + destination = route[0] + if destination == constants.IPV4_ANY_CIDR: + default_routes.add(route) + elif destination == constants.IPV4_METADATA_CIDR: + metadata_routes.add(route) + # Add gateway_ip and missing routes. Note that these + # might get removed by the GBP PD if the L2P's + # inject_default_route attribute is False. + gateway_ip = subnet['gateway_ip'] + if not default_routes and gateway_ip: + routes.add((constants.IPV4_ANY_CIDR, gateway_ip)) + # REVISIT: We need to decide if we should provide + # host-routes for all of the DHCP agents. For now + # use the first DHCP agent in our list for the + # metadata host-route next-hop IPs. + if (not metadata_routes and dhcp_ports and + (not self.enable_metadata_opt or + (self.enable_metadata_opt and not default_routes))): + for ip in dhcp_ports[dhcp_ports.keys()[0]]: + routes.add((constants.IPV4_METADATA_CIDR, ip)) + + subnet['dhcp_server_ips'] = dhcp_ips + subnet['dhcp_server_ports'] = dhcp_ports + subnet['dns_nameservers'] = (subnet_dns_nameservers[subnet_id] or + dhcp_ips) + subnet['host_routes'] = [ + {'destination': destination, 'nexthop': nexthop} + for destination, nexthop in routes] + + return subnets.values() + + def _build_endpoint_trunk_details(self, info): + trunk_info = info.get('trunk_info') + if not trunk_info: + return + port_info = info.get('port_info') + return {'trunk_id': port_info.trunk_id or port_info.subport_trunk_id, + 'master_port_id': trunk_info[0].master_port_id, + 'subports': [{'port_id': sp.subport_port_id, + 'segmentation_type': sp.segmentation_type, + 'segmentation_id': sp.segmentation_id} + for sp in trunk_info]} diff --git a/gbpservice/neutron/services/grouppolicy/drivers/cisco/apic/aim_mapping.py b/gbpservice/neutron/services/grouppolicy/drivers/cisco/apic/aim_mapping.py index f9e02044e..c10680675 100644 --- a/gbpservice/neutron/services/grouppolicy/drivers/cisco/apic/aim_mapping.py +++ b/gbpservice/neutron/services/grouppolicy/drivers/cisco/apic/aim_mapping.py @@ -60,6 +60,7 @@ from gbpservice.neutron.services.grouppolicy.drivers.cisco.apic import ( apic_mapping_lib as alib) from gbpservice.neutron.services.grouppolicy.drivers.cisco.apic import ( nova_client as nclient) +from gbpservice.neutron.services.grouppolicy.drivers.cisco.apic import config # noqa from gbpservice.neutron.services.grouppolicy import plugin as gbp_plugin LOG = logging.getLogger(__name__) @@ -98,48 +99,6 @@ COMMON_TENANT_AIM_RESOURCES = [aim_resource.Contract.__name__, # REVISIT: override add_router_interface L3 API check for now NO_VALIDATE = cisco_apic_l3.OVERRIDE_NETWORK_ROUTING_TOPOLOGY_VALIDATION -# REVISIT: Auto-PTG is currently config driven to align with the -# config driven behavior of the older driver but is slated for -# removal. -opts = [ - cfg.BoolOpt('create_auto_ptg', - default=True, - help=_("Automatically create a PTG when a L2 Policy " - "gets created. This is currently an aim_mapping " - "policy driver specific feature.")), - cfg.BoolOpt('create_per_l3p_implicit_contracts', - default=True, - help=_("This configuration is set to True to migrate a " - "deployment that has l3_policies without implicit " - "AIM contracts (these are deployments which have " - "AIM implicit contracts per tenant). A Neutron server " - "restart is required for this configuration to take " - "effect. The creation of the implicit contracts " - "happens at the time of the AIM policy driver " - "initialization. The configuration can be set to " - "False to avoid recreating the implicit contracts " - "on subsequent Neutron server restarts. This " - "option will be removed in the O release")), - cfg.BoolOpt('advertise_mtu', - default=True, - help=_('If True, advertise network MTU values if core plugin ' - 'calculates them. MTU is advertised to running ' - 'instances via DHCP and RA MTU options.')), - cfg.IntOpt('nested_host_vlan', - default=4094, - help=_("This is a locally siginificant VLAN used to provide " - "connectivity to the OpenStack VM when configured " - "to host the nested domain (Kubernetes/OpenShift). " - "Any traffic originating from the VM and intended " - "to go on the Neutron network, is tagged with this " - "VLAN. The VLAN is stripped by the Opflex installed " - "flows on the integration bridge and the traffic is " - "forwarded on the Neutron network.")), -] - - -cfg.CONF.register_opts(opts, "aim_mapping") - class InvalidVrfForDualStackAddressScopes(exc.GroupPolicyBadRequest): message = _("User-specified address scopes for both address families, " diff --git a/gbpservice/neutron/services/grouppolicy/drivers/cisco/apic/aim_mapping_rpc.py b/gbpservice/neutron/services/grouppolicy/drivers/cisco/apic/aim_mapping_rpc.py index 40152a07d..1c2c2300e 100644 --- a/gbpservice/neutron/services/grouppolicy/drivers/cisco/apic/aim_mapping_rpc.py +++ b/gbpservice/neutron/services/grouppolicy/drivers/cisco/apic/aim_mapping_rpc.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +from collections import namedtuple import sqlalchemy as sa from sqlalchemy.ext import baked @@ -25,6 +26,13 @@ from neutron_lib.api.definitions import portbindings from opflexagent import rpc as o_rpc from oslo_log import log +from gbpservice.neutron.db.grouppolicy.extensions import ( + apic_auto_ptg_db as auto_ptg_db) +from gbpservice.neutron.db.grouppolicy.extensions import ( + apic_segmentation_label_db as seg_label_db) +from gbpservice.neutron.db.grouppolicy import group_policy_mapping_db as gpmdb +from gbpservice.neutron.plugins.ml2plus.drivers.apic_aim import ( + constants as md_const) from gbpservice.neutron.plugins.ml2plus.drivers.apic_aim import ( mechanism_driver as md) from gbpservice.neutron.services.grouppolicy.drivers.cisco.apic import ( @@ -36,6 +44,15 @@ LOG = log.getLogger(__name__) BAKERY = baked.bakery(_size_alert=lambda c: LOG.warning( "sqlalchemy baked query cache size exceeded in %s" % __name__)) +EndpointPtInfo = namedtuple( + 'EndpointPtInfo', + ['pt_id', + 'ptg_id', + 'apg_id', + 'inject_default_route', + 'l3p_project_id', + 'is_auto_ptg']) + class AIMMappingRPCMixin(ha_ip_db.HAIPOwnerDbMixin): """RPC mixin for AIM mapping. @@ -80,13 +97,28 @@ class AIMMappingRPCMixin(ha_ip_db.HAIPOwnerDbMixin): return {'l3_policy_id': vrf} def get_vrf_details(self, context, **kwargs): + if self.aim_mech_driver.enable_new_rpc: + # REVISIT: Eliminate other RPC implementations and + # move this handler directly to the mechanism driver. + return self.aim_mech_driver.get_vrf_details( + context, **kwargs) return self._get_vrf_details(context, **kwargs) def request_vrf_details(self, context, **kwargs): + if self.aim_mech_driver.enable_new_rpc: + # REVISIT: Eliminate other RPC implementations and + # move this handler directly to the mechanism driver. + return self.aim_mech_driver.request_vrf_details( + context, **kwargs) return self._get_vrf_details(context, **kwargs) def get_gbp_details(self, context, **kwargs): LOG.debug("APIC AIM handling get_gbp_details for: %s", kwargs) + if self.aim_mech_driver.enable_new_rpc: + # REVISIT: Eliminate other RPC implementations and + # move this handler directly to the mechanism driver. + return self.aim_mech_driver.get_gbp_details( + context, **kwargs) try: return self._get_gbp_details(context, kwargs, kwargs.get('host')) except Exception as e: @@ -98,6 +130,11 @@ class AIMMappingRPCMixin(ha_ip_db.HAIPOwnerDbMixin): def request_endpoint_details(self, context, **kwargs): LOG.debug("APIC AIM handling get_endpoint_details for: %s", kwargs) + if self.aim_mech_driver.enable_new_rpc: + # REVISIT: Eliminate other RPC implementations and + # move this handler directly to the mechanism driver. + return self.aim_mech_driver.request_endpoint_details( + context, **kwargs) request = kwargs.get('request') try: return self._request_endpoint_details(context, **kwargs) @@ -857,3 +894,118 @@ class AIMMappingRPCMixin(ha_ip_db.HAIPOwnerDbMixin): # What is an "End of the Chain" port for Neutron? pass + + # The query_endpoint_rpc_info and update_endpoint_rpc_details + # methods below are called by the apic_aim mechanism driver while + # handling the request_endpoint_details (aka get_gbp_details) RPC + # from the agent. + + def query_endpoint_rpc_info(self, session, info): + # This method is called within a transaction from the apic_aim + # MD's request_endpoint_details RPC handler to retrieve GBP + # state needed to build the RPC response, after the info param + # has already been populated with the data available within + # Neutron itself. + + # Query for all needed scalar (non-list) state for the + # policies associated with the port, and make sure the port is + # owned by a policy target before continuing. + pt_infos = self._query_pt_info( + session, info['port_info'].port_id) + if not pt_infos: + return + + # A list was returned by the PT info query, like all the other + # endpoint RPC queries, here and in the mechanism + # driver. Currently, there will be at most a single item in + # this list, but a join may later be added to this query in + # order to eliminate another query's round-trip to the DB + # server, resulting in multiple rows being returned. For now, + # we just need that single row. + pt_info = pt_infos[0] + info['gbp_pt_info'] = pt_info + + # Query for policy target's segmentation labels. + info['gbp_segmentation_labels'] = self._query_segmentation_labels( + session, pt_info.pt_id) + + def _query_pt_info(self, session, port_id): + query = BAKERY(lambda s: s.query( + gpmdb.PolicyTargetMapping.id, + gpmdb.PolicyTargetMapping.policy_target_group_id, + gpmdb.PolicyTargetGroupMapping.application_policy_group_id, + gpmdb.L2PolicyMapping.inject_default_route, + gpmdb.L3PolicyMapping.project_id, + auto_ptg_db.ApicAutoPtgDB.is_auto_ptg, + )) + query += lambda q: q.join( + gpmdb.PolicyTargetGroupMapping, + gpmdb.PolicyTargetGroupMapping.id == + gpmdb.PolicyTargetMapping.policy_target_group_id) + query += lambda q: q.join( + gpmdb.L2PolicyMapping, + gpmdb.L2PolicyMapping.id == + gpmdb.PolicyTargetGroupMapping.l2_policy_id) + query += lambda q: q.join( + gpmdb.L3PolicyMapping, + gpmdb.L3PolicyMapping.id == + gpmdb.L2PolicyMapping.l3_policy_id) + query += lambda q: q.outerjoin( + auto_ptg_db.ApicAutoPtgDB, + auto_ptg_db.ApicAutoPtgDB.policy_target_group_id == + gpmdb.PolicyTargetMapping.policy_target_group_id) + query += lambda q: q.filter( + gpmdb.PolicyTargetMapping.port_id == sa.bindparam('port_id')) + return [EndpointPtInfo._make(row) for row in + query(session).params( + port_id=port_id)] + + def _query_segmentation_labels(self, session, pt_id): + query = BAKERY(lambda s: s.query( + seg_label_db.ApicSegmentationLabelDB.segmentation_label)) + query += lambda q: q.filter( + seg_label_db.ApicSegmentationLabelDB.policy_target_id == + sa.bindparam('pt_id')) + return [x for x, in query(session).params( + pt_id=pt_id)] + + def update_endpoint_rpc_details(self, info, details): + # This method is called outside a transaction from the + # apic_aim MD's request_endpoint_details RPC handler to add or + # update details within the RPC response, using data stored in + # info by query_endpoint_rpc_info. + + # First, make sure the port is owned by a PolicyTarget before + # continuing. + pt_info = info.get('gbp_pt_info') + if not pt_info: + return + gbp_details = details['gbp_details'] + + # Replace EPG identity if not auto_ptg. + if not pt_info.is_auto_ptg: + gbp_details['app_profile_name'] = ( + self.name_mapper.application_policy_group( + None, pt_info.apg_id) if pt_info.apg_id + else self.aim_mech_driver.ap_name) + gbp_details['endpoint_group_name'] = pt_info.ptg_id + gbp_details['ptg_tenant'] = ( + self.name_mapper.project(None, pt_info.l3p_project_id)) + + # Update subnet gateway_ip and default_routes if needed. + if not pt_info.inject_default_route: + for subnet in gbp_details['subnets']: + del subnet['gateway_ip'] + subnet['host_routes'] = [ + r for r in subnet['host_routes'] + if r['destination'] not in + [md_const.IPV4_ANY_CIDR, md_const.IPV4_METADATA_CIDR]] + + # Add segmentation labels. + gbp_details['segmentation_labels'] = ( + info.get('gbp_segmentation_labels')) + + # REVISIT: If/when support for the proxy_group extension is + # added to the aim_mapping PD, update promiscuous_mode to True + # if this PT has a cluster_id that identifies a different PT + # whose group_default_gateway set. diff --git a/gbpservice/neutron/services/grouppolicy/drivers/cisco/apic/config.py b/gbpservice/neutron/services/grouppolicy/drivers/cisco/apic/config.py new file mode 100644 index 000000000..6028ebb36 --- /dev/null +++ b/gbpservice/neutron/services/grouppolicy/drivers/cisco/apic/config.py @@ -0,0 +1,59 @@ +# Copyright (c) 2019 Cisco Systems Inc. +# All Rights Reserved. +# +# 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_config import cfg + + +# REVISIT: Auto-PTG is currently config driven to align with the +# config driven behavior of the older driver but is slated for +# removal. +opts = [ + cfg.BoolOpt('create_auto_ptg', + default=True, + help=_("Automatically create a PTG when a L2 Policy " + "gets created. This is currently an aim_mapping " + "policy driver specific feature.")), + cfg.BoolOpt('create_per_l3p_implicit_contracts', + default=True, + help=_("This configuration is set to True to migrate a " + "deployment that has l3_policies without implicit " + "AIM contracts (these are deployments which have " + "AIM implicit contracts per tenant). A Neutron server " + "restart is required for this configuration to take " + "effect. The creation of the implicit contracts " + "happens at the time of the AIM policy driver " + "initialization. The configuration can be set to " + "False to avoid recreating the implicit contracts " + "on subsequent Neutron server restarts. This " + "option will be removed in the O release")), + cfg.BoolOpt('advertise_mtu', # REVISIT: Move to apic_aim MD. + default=True, + help=_('If True, advertise network MTU values if core plugin ' + 'calculates them. MTU is advertised to running ' + 'instances via DHCP and RA MTU options.')), + cfg.IntOpt('nested_host_vlan', # REVISIT: Move to apic_aim MD. + default=4094, + help=_("This is a locally siginificant VLAN used to provide " + "connectivity to the OpenStack VM when configured " + "to host the nested domain (Kubernetes/OpenShift). " + "Any traffic originating from the VM and intended " + "to go on the Neutron network, is tagged with this " + "VLAN. The VLAN is stripped by the Opflex installed " + "flows on the integration bridge and the traffic is " + "forwarded on the Neutron network.")), +] + + +cfg.CONF.register_opts(opts, "aim_mapping") diff --git a/gbpservice/neutron/tests/unit/plugins/ml2plus/test_apic_aim.py b/gbpservice/neutron/tests/unit/plugins/ml2plus/test_apic_aim.py index 4769f20f2..8a54b0384 100644 --- a/gbpservice/neutron/tests/unit/plugins/ml2plus/test_apic_aim.py +++ b/gbpservice/neutron/tests/unit/plugins/ml2plus/test_apic_aim.py @@ -38,6 +38,7 @@ from neutron.db import api as db_api from neutron.db import provisioning_blocks from neutron.db import segments_db from neutron.plugins.ml2 import driver_context +from neutron.plugins.ml2 import models as ml2_models from neutron.tests.unit.api import test_extensions from neutron.tests.unit.db import test_db_base_plugin_v2 as test_plugin from neutron.tests.unit.extensions import test_address_scope @@ -45,6 +46,7 @@ from neutron.tests.unit.extensions import test_l3 from neutron.tests.unit.extensions import test_securitygroup from neutron.tests.unit.plugins.ml2 import test_tracked_resources as tr_res from neutron.tests.unit import testlib_api +from neutron_lib.api.definitions import portbindings from neutron_lib.callbacks import registry from neutron_lib import constants as n_constants from neutron_lib import context as n_context @@ -8280,3 +8282,285 @@ class TestPortOnPhysicalNodeSingleDriver(TestPortOnPhysicalNode): mechanism_drivers=['logger', 'apic_aim']) self.expected_binding_info = [('apic_aim', 'opflex'), ('apic_aim', 'vlan')] + + +class TestOpflexRpc(ApicAimTestCase): + def setUp(self, *args, **kwargs): + super(TestOpflexRpc, self).setUp(*args, **kwargs) + + def _check_response(self, request, response, port, net, subnets, + network_type='opflex', vm_name='someid'): + epg = aim_resource.EndpointGroup.from_dn( + net['apic:distinguished_names']['EndpointGroup']) + + vrf = aim_resource.VRF.from_dn( + net['apic:distinguished_names']['VRF']) + + self.assertEqual(request['device'], response['device']) + self.assertEqual(request['request_id'], response['request_id']) + self.assertEqual(request['timestamp'], response['timestamp']) + + neutron_details = response['neutron_details'] + self.assertEqual( + port['admin_state_up'], neutron_details['admin_state_up']) + self.assertEqual( + port['device_owner'], neutron_details['device_owner']) + self.assertEqual( + sorted(port['fixed_ips'], key=lambda x: x['ip_address']), + sorted(neutron_details['fixed_ips'], + key=lambda x: x['ip_address'])) + self.assertEqual(net['id'], neutron_details['network_id']) + self.assertEqual(network_type, neutron_details['network_type']) + self.assertEqual('physnet1', neutron_details['physical_network']) + self.assertEqual(port['id'], neutron_details['port_id']) + + gbp_details = response['gbp_details'] + self.assertEqual(epg.app_profile_name, gbp_details['app_profile_name']) + self.assertEqual(request['device'], gbp_details['device']) + if self.driver.apic_optimized_dhcp_lease_time > 0: + self.assertEqual(self.driver.apic_optimized_dhcp_lease_time, + gbp_details['dhcp_lease_time']) + else: + self.assertNotIn('dhcp_lease_time', gbp_details) + self.assertEqual(net['dns_domain'], gbp_details['dns_domain']) + self.assertEqual(self.driver.enable_dhcp_opt, + gbp_details['enable_dhcp_optimization']) + self.assertEqual(self.driver.enable_metadata_opt, + gbp_details['enable_metadata_optimization']) + self.assertEqual(epg.name, gbp_details['endpoint_group_name']) + # floating_ip tested in TestGbpDetailsForML2 + self.assertEqual(port['binding:host_id'], gbp_details['host']) + self.assertEqual(vrf.tenant_name + ' ' + vrf.name, + gbp_details['l3_policy_id']) + # host_snat_ips tested in TestGbpDetailsForML2 + self.assertEqual(net['mtu'], gbp_details['interface_mtu']) + # ip_mapping tested in TestGbpDetailsForML2 + self.assertEqual(port['mac_address'], gbp_details['mac_address']) + # nested_domain_* and nested_host_vlan tested in TestNestedDomain + self.assertEqual(port['id'], gbp_details['port_id']) + self.assertEqual(not port['port_security_enabled'], + gbp_details['promiscuous_mode']) + self.assertEqual(epg.tenant_name, gbp_details['ptg_tenant']) + # security_group tested in TestGbpDetailsForML2 + # segmentation_labels tested in TestPolicyTarget + self._check_response_subnets(gbp_details['subnets'], subnets) + self.assertEqual(vm_name, gbp_details['vm-name']) + self.assertEqual(vrf.name, gbp_details['vrf_name']) + self.assertEqual(sorted([sn['cidr'] for sn in subnets]), + sorted(gbp_details['vrf_subnets'])) + self.assertEqual(vrf.tenant_name, gbp_details['vrf_tenant']) + + # trunk_details tests in TestVlanAwareVM + + def _check_response_subnets(self, subnet_details, subnets): + self.assertEqual(len(subnets), len(subnet_details)) + for subnet, details in zip( + sorted(subnets, key=lambda x: x['cidr']), + sorted(subnet_details, key=lambda x: x['cidr'])): + dhcp_ports = subnet.get('_dhcp_ports', []) + dhcp_server_ips = [ip['ip_address'] for port in dhcp_ports + for ip in port['fixed_ips'] + if ip['subnet_id'] == subnet['id']] + dhcp_server_ports = {port['mac_address']: + [ip['ip_address'] + for ip in port['fixed_ips'] + if ip['subnet_id'] == subnet['id']] + for port in dhcp_ports} + self.assertEqual(len(dhcp_ports), len(dhcp_server_ports)) + dns_nameservers = subnet['dns_nameservers'] or dhcp_server_ips + host_routes = subnet['host_routes'] + gateway_ip = subnet['gateway_ip'] + default_routes = [] + metadata_routes = [] + for route in host_routes: + if route['destination'] == '0.0.0.0/0': + default_routes.append(route) + elif route['destination'] == '169.254.169.254/16': + metadata_routes.append(route) + if not default_routes and gateway_ip: + host_routes.append( + {'destination': '0.0.0.0/0', 'nexthop': gateway_ip}) + if (not metadata_routes + and dhcp_server_ports and not default_routes): + # This test may not work if there are multiple DHCP + # ports for the subnet, since which DHCP port's IPs + # will be used for the metadata routes is not + # deterministic. Therefore, be sure to specify + # metadata routes or default routes for subnets with + # multiple DHCP ports. + for ip in dhcp_server_ports.values()[0]: + host_routes.append( + {'destination': '169.254.169.254/16', + 'nexthop': ip}) + + self.assertEqual(subnet['cidr'], details['cidr']) + self.assertEqual(sorted(dhcp_server_ips), + sorted(details['dhcp_server_ips'])) + self.assertEqual(dhcp_server_ports, details['dhcp_server_ports']) + self.assertEqual(sorted(dns_nameservers), + sorted(details['dns_nameservers'])) + self.assertEqual(gateway_ip, details['gateway_ip']) + self.assertEqual(subnet['enable_dhcp'], details['enable_dhcp']) + self.assertEqual(sorted(host_routes), + sorted(details['host_routes'])) + self.assertEqual(subnet['id'], details['id']) + self.assertEqual(subnet['ip_version'], details['ip_version']) + + def _check_fail_response(self, request, response): + self.assertEqual(request['device'], response['device']) + self.assertEqual(request['request_id'], response['request_id']) + self.assertEqual(request['timestamp'], response['timestamp']) + self.assertNotIn('neutron_details', response) + self.assertNotIn('gbp_details', response) + self.assertNotIn('trunk_details', response) + + def test_endpoint_details_bound(self): + self.driver.apic_optimized_dhcp_lease_time = 100 + host = 'host1' + self._register_agent('host1', AGENT_CONF_OPFLEX) + net = self._make_network( + self.fmt, 'net1', True, + arg_list=('dns_domain',), dns_domain='example.com.') + net_id = net['network']['id'] + + dns_nameservers1 = ['192.168.1.201', '172.16.1.200'] + host_routes1 = [ + {'destination': '172.16.0.0/24', 'nexthop': '10.0.1.2'}, + {'destination': '192.168.0.0/24', 'nexthop': '10.0.1.3'}, + ] + subnet1 = self._make_subnet( + self.fmt, net, '10.0.1.1', '10.0.1.0/24', + dns_nameservers=dns_nameservers1, + host_routes=host_routes1)['subnet'] + subnet1_id = subnet1['id'] + + host_routes2 = [ + {'destination': '169.254.169.254/16', 'nexthop': '10.0.1.2'}, + ] + subnet2 = self._make_subnet( + self.fmt, net, '10.0.2.1', '10.0.2.0/24', + host_routes=host_routes2)['subnet'] + subnet2_id = subnet2['id'] + + subnet3 = self._make_subnet( + self.fmt, net, '10.0.3.1', '10.0.3.0/24')['subnet'] + subnet3_id = subnet3['id'] + + # Create multiple DHCP ports and multiple subnets to exercise + # various combinations building dhcp_server_ids and + # dhcp_server_ports in subnet details. One subnet has two DHCP + # IPs on different DHCP ports. Another has two DHCP IPs on the + # same DHCP port, which does not seem very useful, but is + # allowed by Neutron. + + dhcp1 = self._make_port( + self.fmt, net_id, fixed_ips=[ + {'subnet_id': subnet1_id}, + {'subnet_id': subnet2_id}], + device_owner='network:dhcp')['port'] + + dhcp2 = self._make_port( + self.fmt, net_id, fixed_ips=[ + {'subnet_id': subnet2_id}], + device_owner='network:dhcp')['port'] + + dhcp3 = self._make_port( + self.fmt, net_id, fixed_ips=[ + {'subnet_id': subnet3_id}, + {'subnet_id': subnet3_id}], + device_owner='network:dhcp')['port'] + + subnet1['_dhcp_ports'] = [dhcp1] + subnet2['_dhcp_ports'] = [dhcp1, dhcp2] + subnet3['_dhcp_ports'] = [dhcp3] + subnets = [subnet1, subnet2, subnet3] + + fixed_ips = [{'subnet_id': subnet1_id, 'ip_address': '10.0.1.10'}, + {'subnet_id': subnet2_id, 'ip_address': '10.0.2.20'}, + {'subnet_id': subnet3_id, 'ip_address': '10.0.3.30'}] + port = self._make_port(self.fmt, net_id, fixed_ips=fixed_ips)['port'] + port_id = port['id'] + + self.driver._set_vm_name(self.db_session, 'someid', 'a name') + port = self._bind_port_to_host(port_id, host)['port'] + self.assertEqual('ovs', port['binding:vif_type']) + + # Call the request_endpoint_details RPC handler. + request = { + 'device': 'tap' + port_id, + 'timestamp': 12345, + 'request_id': 'a_request' + } + response = self.driver.request_endpoint_details( + n_context.get_admin_context(), request=request, host=host) + + self._check_response( + request, response, port, net['network'], subnets, vm_name='a name') + + # Call the get_vrf_details RPC handler and check its response. + vrf = aim_resource.VRF.from_dn( + net['network']['apic:distinguished_names']['VRF']) + vrf_id = vrf.tenant_name + ' ' + vrf.name + response = self.driver.get_vrf_details( + n_context.get_admin_context(), vrf_id=vrf_id) + self.assertEqual(vrf_id, response['l3_policy_id']) + self.assertEqual(vrf.tenant_name, response['vrf_tenant']) + self.assertEqual(vrf.name, response['vrf_name']) + self.assertEqual(sorted([sn['cidr'] for sn in subnets]), + sorted(response['vrf_subnets'])) + + def test_endpoint_details_unbound(self): + host = 'host1' + net = self._make_network(self.fmt, 'net1', True) + net_id = net['network']['id'] + + subnet = self._make_subnet( + self.fmt, net, '10.0.1.1', '10.0.1.0/24')['subnet'] + + subnets = [subnet] + + port = self._make_port(self.fmt, net_id)['port'] + port_id = port['id'] + + # Not calling self._register_agent('host1', AGENT_CONF_OPFLEX) + # in order to force a hierarchical binding to ensure the + # bottom level segment info is returned from the RPC. Also, + # not calling self.driver._set_vm_name() to test use of + # device_id when name not in cache. + port = self._bind_port_to_host(port_id, host)['port'] + self.assertEqual('ovs', port['binding:vif_type']) + + # Unbind the port, as if binding failed, leaving it bindable. + self.db_session.query(ml2_models.PortBinding).filter_by( + port_id=port['id']).update( + {'vif_type': portbindings.VIF_TYPE_BINDING_FAILED}) + + # Call the RPC handler. + request = { + 'device': 'tap' + port_id, + 'timestamp': 12345, + 'request_id': 'a_request' + } + response = self.driver.request_endpoint_details( + n_context.get_admin_context(), request=request, host=host) + + self._check_response( + request, response, port, net['network'], subnets, + network_type='vlan') + + def test_endpoint_details_nonexistent_port(self): + host = 'host1' + + # Call the RPC handler. + request = { + 'device': 'tapa9d98938-7bbe-4eae-ba2e-375f9bc3ab45', + 'timestamp': 12345, + 'request_id': 'a_request' + } + response = self.driver.request_endpoint_details( + n_context.get_admin_context(), request=request, host=host) + + self._check_fail_response(request, response) + + # REVISIT: Test with missing request, missing device, invalid + # device prefix, unbindable port, port bound to wrong host. diff --git a/gbpservice/neutron/tests/unit/services/grouppolicy/test_aim_mapping_driver.py b/gbpservice/neutron/tests/unit/services/grouppolicy/test_aim_mapping_driver.py index 88e2d918d..f5b49705c 100644 --- a/gbpservice/neutron/tests/unit/services/grouppolicy/test_aim_mapping_driver.py +++ b/gbpservice/neutron/tests/unit/services/grouppolicy/test_aim_mapping_driver.py @@ -2629,6 +2629,11 @@ class TestPolicyTargetGroupRollback(AIMBaseTestCase): class TestGbpDetailsForML2(AIMBaseTestCase, test_securitygroup.SecurityGroupsTestCase): + # REVISIT: Once the new RPC handler implementation in the apic_aim + # mechanism driver is complete and tested, move this unit test + # class to test_apic_aim (or a new module) and remove the + # enable_raw_sql and enable_new_rpc flags. + def setUp(self, *args, **kwargs): super(TestGbpDetailsForML2, self).setUp(*args, **kwargs) cfg.CONF.set_override('path_mtu', 1000, group='ml2') @@ -2642,6 +2647,8 @@ class TestGbpDetailsForML2(AIMBaseTestCase, self.assertEqual(mapping, req_mapping['gbp_details']) self.assertEqual(port_id, mapping['port_id']) self.assertEqual(expected_epg_name, mapping['endpoint_group_name']) + expected_epg_ap_name = self.driver.aim_mech_driver.ap_name + self.assertEqual(expected_epg_ap_name, mapping['app_profile_name']) exp_tenant = (self.name_mapper.project(None, expected_epg_tenant) if map_tenant_name else expected_epg_tenant) self.assertEqual(exp_tenant, mapping['ptg_tenant']) @@ -2658,6 +2665,8 @@ class TestGbpDetailsForML2(AIMBaseTestCase, dhcp_server_port = dhcp_server_ports[dhcp_port['mac_address']] self.assertEqual(dhcp_server_port[0], dhcp_port['fixed_ips'][0]['ip_address']) + self.assertEqual([dhcp_port['fixed_ips'][0]['ip_address']], + mapping['subnets'][0]['dhcp_server_ips']) if default_route: self.assertTrue( {'destination': '0.0.0.0/0', 'nexthop': default_route} in @@ -2679,21 +2688,24 @@ class TestGbpDetailsForML2(AIMBaseTestCase, def _verify_fip_details(self, mapping, fip, ext_epg_tenant, ext_epg_name, ext_epg_app_profile='OpenStack'): self.assertEqual(1, len(mapping['floating_ip'])) - fip = copy.deepcopy(fip) - fip['nat_epg_name'] = ext_epg_name - fip['nat_epg_tenant'] = ext_epg_tenant - fip['nat_epg_app_profile'] = ext_epg_app_profile - fip_mapping = mapping['floating_ip'][0] + # REVISIT: The port_id, project_id, and floating_network_id + # are not used by the agent, and the new RPC implementation + # doesn't provide them, so these assertions are commented out + # until the RPC implementations are cleaned up. self.assertEqual(fip['id'], fip_mapping['id']) - self.assertEqual(fip['port_id'], fip_mapping['port_id']) - self.assertEqual(fip['project_id'], fip_mapping['project_id']) + # self.assertEqual(fip['port_id'], fip_mapping['port_id']) + # self.assertEqual(fip['project_id'], fip_mapping['project_id']) self.assertEqual(fip['fixed_ip_address'], fip_mapping['fixed_ip_address']) self.assertEqual(fip['floating_ip_address'], fip_mapping['floating_ip_address']) - self.assertEqual(fip['floating_network_id'], - fip_mapping['floating_network_id']) + # self.assertEqual(fip['floating_network_id'], + # fip_mapping['floating_network_id']) + self.assertEqual(ext_epg_name, fip_mapping['nat_epg_name']) + self.assertEqual(ext_epg_tenant, fip_mapping['nat_epg_tenant']) + self.assertEqual(ext_epg_app_profile, + fip_mapping['nat_epg_app_profile']) def _verify_ip_mapping_details(self, mapping, ext_segment_name, ext_epg_tenant, ext_epg_name, @@ -2715,9 +2727,11 @@ class TestGbpDetailsForML2(AIMBaseTestCase, mapping['host_snat_ips'][0]) def _do_test_get_gbp_details(self, pre_vrf=None, - enable_raw_sql=False): + enable_raw_sql=False, + enable_new_rpc=False): self.driver.aim_mech_driver.enable_raw_sql_for_device_rpc = ( enable_raw_sql) + self.driver.aim_mech_driver.enable_new_rpc = enable_new_rpc self.driver.aim_mech_driver.apic_optimized_dhcp_lease_time = 100 ext_net1, rtr1, ext_net1_sub = self._setup_external_network( 'es1', dn='uni/tn-t1/out-l1/instP-n1') @@ -2858,6 +2872,9 @@ class TestGbpDetailsForML2(AIMBaseTestCase, 'uni:tn-t1:out-l2:instP-n2', 't1', 'EXT-l2') self._verify_host_snat_ip_details(mapping, 'uni:tn-t1:out-l2:instP-n2', '200.200.0.3', '200.200.0.1/16') + # Make sure 2nd RPC returned SNAT IP allocated in 1st RPC. + self._verify_host_snat_ip_details(req_mapping['gbp_details'], + 'uni:tn-t1:out-l2:instP-n2', '200.200.0.3', '200.200.0.1/16') self.assertEqual(1000, mapping['interface_mtu']) self.assertEqual(100, mapping['dhcp_lease_time']) @@ -2892,6 +2909,9 @@ class TestGbpDetailsForML2(AIMBaseTestCase, def test_get_gbp_details_with_raw_sql(self): self._do_test_get_gbp_details(enable_raw_sql=True) + def test_get_gbp_details_with_new_rpc(self): + self._do_test_get_gbp_details(enable_new_rpc=True) + def test_get_gbp_details_pre_existing_vrf(self): aim_ctx = aim_context.AimContext(self.db_session) vrf = self.aim_mgr.create( @@ -2907,6 +2927,14 @@ class TestGbpDetailsForML2(AIMBaseTestCase, self._do_test_get_gbp_details(pre_vrf=vrf, enable_raw_sql=True) + def test_get_gbp_details_pre_existing_vrf_with_new_rpc(self): + aim_ctx = aim_context.AimContext(self.db_session) + vrf = self.aim_mgr.create( + aim_ctx, aim_resource.VRF(tenant_name='common', name='ctx1', + monitored=True)) + self._do_test_get_gbp_details(pre_vrf=vrf, + enable_new_rpc=True) + class TestPolicyTarget(AIMBaseTestCase, test_securitygroup.SecurityGroupsTestCase): @@ -3248,10 +3276,18 @@ class TestPolicyTarget(AIMBaseTestCase, def _verify_gbp_details_assertions(self, mapping, req_mapping, port_id, expected_epg_name, expected_epg_tenant, subnet, default_route=None, - map_tenant_name=True): + map_tenant_name=True, + prefix_ap_name=False): self.assertEqual(mapping, req_mapping['gbp_details']) self.assertEqual(port_id, mapping['port_id']) self.assertEqual(expected_epg_name, mapping['endpoint_group_name']) + # This method is not used with APGs, but it is used with + # external network in common tenant. + expected_epg_ap_name = ( + self.driver.aim_mech_driver.ap_name if not prefix_ap_name else + self.driver.aim_mech_driver.apic_system_id + '_' + + self.driver.aim_mech_driver.ap_name) + self.assertEqual(expected_epg_ap_name, mapping['app_profile_name']) exp_tenant = (self.name_mapper.project(None, expected_epg_tenant) if map_tenant_name else expected_epg_tenant) self.assertEqual(exp_tenant, mapping['ptg_tenant']) @@ -3298,21 +3334,24 @@ class TestPolicyTarget(AIMBaseTestCase, def _verify_fip_details(self, mapping, fip, ext_epg_tenant, ext_epg_name, ext_epg_app_profile='OpenStack'): self.assertEqual(1, len(mapping['floating_ip'])) - fip = copy.deepcopy(fip) - fip['nat_epg_name'] = ext_epg_name - fip['nat_epg_tenant'] = ext_epg_tenant - fip['nat_epg_app_profile'] = ext_epg_app_profile - fip_mapping = mapping['floating_ip'][0] + # REVISIT: The port_id, project_id, and floating_network_id + # are not used by the agent, and the new RPC implementation + # doesn't provide them, so these assertions are commented out + # until the RPC implementations are cleaned up. self.assertEqual(fip['id'], fip_mapping['id']) - self.assertEqual(fip['port_id'], fip_mapping['port_id']) - self.assertEqual(fip['project_id'], fip_mapping['project_id']) + # self.assertEqual(fip['port_id'], fip_mapping['port_id']) + # self.assertEqual(fip['project_id'], fip_mapping['project_id']) self.assertEqual(fip['fixed_ip_address'], fip_mapping['fixed_ip_address']) self.assertEqual(fip['floating_ip_address'], fip_mapping['floating_ip_address']) - self.assertEqual(fip['floating_network_id'], - fip_mapping['floating_network_id']) + # self.assertEqual(fip['floating_network_id'], + # fip_mapping['floating_network_id']) + self.assertEqual(ext_epg_name, fip_mapping['nat_epg_name']) + self.assertEqual(ext_epg_tenant, fip_mapping['nat_epg_tenant']) + self.assertEqual(ext_epg_app_profile, + fip_mapping['nat_epg_app_profile']) def _verify_ip_mapping_details(self, mapping, ext_segment_name, ext_epg_tenant, ext_epg_name, @@ -3333,9 +3372,11 @@ class TestPolicyTarget(AIMBaseTestCase, 'prefixlen': int(prefix)}, mapping['host_snat_ips'][0]) - def _do_test_get_gbp_details(self, pre_vrf=None, enable_raw_sql=False): + def _do_test_get_gbp_details(self, pre_vrf=None, enable_raw_sql=False, + enable_new_rpc=False): self.driver.aim_mech_driver.enable_raw_sql_for_device_rpc = ( enable_raw_sql) + self.driver.aim_mech_driver.enable_new_rpc = enable_new_rpc self.driver.aim_mech_driver.apic_optimized_dhcp_lease_time = 100 es1, es1_sub = self._setup_external_segment( 'es1', dn='uni/tn-t1/out-l1/instP-n1') @@ -3428,7 +3469,7 @@ class TestPolicyTarget(AIMBaseTestCase, port = self._update('ports', port['id'], data)['port'] mapping = self.driver.get_gbp_details( self._neutron_admin_context, device='tap%s' % pt2['port_id'], - host='h2') + host='h1') self.assertEqual(pt2['port_id'], mapping['port_id']) self._verify_ip_mapping_details(mapping, 'uni:tn-t1:out-l1:instP-n1', 't1', 'EXT-l1') @@ -3460,13 +3501,15 @@ class TestPolicyTarget(AIMBaseTestCase, port = self._update('ports', port['id'], data)['port'] mapping = self.driver.get_gbp_details( self._neutron_admin_context, device='tap%s' % pt2['port_id'], - host='h2') + host='h1') self.assertEqual(2000, mapping['interface_mtu']) def _do_test_gbp_details_no_pt(self, use_as=True, routed=True, - pre_vrf=None, enable_raw_sql=False): + pre_vrf=None, enable_raw_sql=False, + enable_new_rpc=False): self.driver.aim_mech_driver.enable_raw_sql_for_device_rpc = ( enable_raw_sql) + self.driver.aim_mech_driver.enable_new_rpc = enable_new_rpc # Create port and bind it address_scope = self._make_address_scope_for_vrf( pre_vrf.dn if pre_vrf else None, @@ -3587,6 +3630,9 @@ class TestPolicyTarget(AIMBaseTestCase, def test_get_gbp_details_with_raw_sql(self): self._do_test_get_gbp_details(enable_raw_sql=True) + def test_get_gbp_details_with_new_rpc(self): + self._do_test_get_gbp_details(enable_new_rpc=True) + def test_get_gbp_details_pre_existing_vrf(self): aim_ctx = aim_context.AimContext(self.db_session) vrf = self.aim_mgr.create( @@ -3601,6 +3647,13 @@ class TestPolicyTarget(AIMBaseTestCase, monitored=True)) self._do_test_get_gbp_details(pre_vrf=vrf, enable_raw_sql=True) + def test_get_gbp_details_pre_existing_vrf_with_new_rpc(self): + aim_ctx = aim_context.AimContext(self.db_session) + vrf = self.aim_mgr.create( + aim_ctx, aim_resource.VRF(tenant_name='common', name='ctx1', + monitored=True)) + self._do_test_get_gbp_details(pre_vrf=vrf, enable_new_rpc=True) + def test_get_gbp_details_no_pt(self): # Test that traditional Neutron ports behave correctly from the # RPC perspective @@ -3611,6 +3664,11 @@ class TestPolicyTarget(AIMBaseTestCase, # RPC perspective self._do_test_gbp_details_no_pt(enable_raw_sql=True) + def test_get_gbp_details_no_pt_with_new_rpc(self): + # Test that traditional Neutron ports behave correctly from the + # RPC perspective + self._do_test_gbp_details_no_pt(enable_new_rpc=True) + def test_get_gbp_details_no_pt_pre_existing_vrf(self): aim_ctx = aim_context.AimContext(self.db_session) vrf = self.aim_mgr.create( @@ -3625,12 +3683,22 @@ class TestPolicyTarget(AIMBaseTestCase, monitored=True)) self._do_test_gbp_details_no_pt(pre_vrf=vrf, enable_raw_sql=True) + def test_get_gbp_details_no_pt_pre_existing_vrf_with_new_rpc(self): + aim_ctx = aim_context.AimContext(self.db_session) + vrf = self.aim_mgr.create( + aim_ctx, aim_resource.VRF(tenant_name='common', name='ctx1', + monitored=True)) + self._do_test_gbp_details_no_pt(pre_vrf=vrf, enable_new_rpc=True) + def test_get_gbp_details_no_pt_no_as(self): self._do_test_gbp_details_no_pt(use_as=False) def test_get_gbp_details_no_pt_no_as_with_raw_sql(self): self._do_test_gbp_details_no_pt(use_as=False, enable_raw_sql=True) + def test_get_gbp_details_no_pt_no_as_with_new_rpc(self): + self._do_test_gbp_details_no_pt(use_as=False, enable_new_rpc=True) + def test_get_gbp_details_no_pt_no_as_unrouted(self): self._do_test_gbp_details_no_pt(use_as=False, routed=False) @@ -3638,9 +3706,15 @@ class TestPolicyTarget(AIMBaseTestCase, self._do_test_gbp_details_no_pt(use_as=False, routed=False, enable_raw_sql=True) - def _test_gbp_details_ext_net_no_pt(self, enable_raw_sql=False): + def test_get_gbp_details_no_pt_no_as_unrouted_with_new_rpc(self): + self._do_test_gbp_details_no_pt(use_as=False, routed=False, + enable_new_rpc=True) + + def _test_gbp_details_ext_net_no_pt(self, enable_raw_sql=False, + enable_new_rpc=False): self.driver.aim_mech_driver.enable_raw_sql_for_device_rpc = ( enable_raw_sql) + self.driver.aim_mech_driver.enable_new_rpc = enable_new_rpc # Test ports created on Neutron external networks ext_net1, _, sn1 = self._setup_external_network( 'l1', dn='uni/tn-common/out-l1/instP-n1') @@ -3679,7 +3753,7 @@ class TestPolicyTarget(AIMBaseTestCase, host='h1') self._verify_gbp_details_assertions( mapping, req_mapping, port_id, "EXT-l1", "common", sn1, - map_tenant_name=False) + map_tenant_name=False, prefix_ap_name=True) vrf_id = '%s %s' % ("common", "openstack_EXT-l1") vrf_mapping = self.driver.get_vrf_details( @@ -3725,6 +3799,9 @@ class TestPolicyTarget(AIMBaseTestCase, def test_gbp_details_ext_net_no_pt_with_raw_sql(self): self._test_gbp_details_ext_net_no_pt(enable_raw_sql=True) + def test_gbp_details_ext_net_no_pt_with_new_rpc(self): + self._test_gbp_details_ext_net_no_pt(enable_new_rpc=True) + def test_ip_address_owner_update(self): l3p = self.create_l3_policy(name='myl3')['l3_policy'] l2p = self.create_l2_policy(name='myl2', @@ -5633,7 +5710,7 @@ class TestNestedDomain(AIMBaseTestCase): p1 = self._bind_port_to_host(p1['id'], 'host1')['port'] details = self.driver.get_gbp_details( self._neutron_admin_context, device='tap%s' % p1['id'], - host='h1') + host='host1') self.assertEqual('myk8s', details['nested_domain_name']) self.assertEqual('k8s', details['nested_domain_type']) self.assertEqual(4093, details['nested_domain_infra_vlan']) @@ -5665,7 +5742,7 @@ class TestNestedDomain(AIMBaseTestCase): p1 = self._bind_port_to_host(p1['id'], 'host1')['port'] details = self.driver.get_gbp_details( self._neutron_admin_context, device='tap%s' % p1['id'], - host='h1') + host='host1') self.assertEqual('', details['nested_domain_name']) self.assertEqual('', details['nested_domain_type']) self.assertIsNone(details['nested_domain_infra_vlan']) @@ -5682,6 +5759,13 @@ class TestNestedDomainWithRawSql(TestNestedDomain): self.driver.aim_mech_driver.enable_raw_sql_for_device_rpc = True +class TestNestedDomainWithNewRpc(TestNestedDomain): + + def setUp(self, **kwargs): + super(TestNestedDomainWithNewRpc, self).setUp(**kwargs) + self.driver.aim_mech_driver.enable_new_rpc = True + + class TestNeutronPortOperation(AIMBaseTestCase): def setUp(self, **kwargs): @@ -5705,7 +5789,7 @@ class TestNeutronPortOperation(AIMBaseTestCase): p1 = self._bind_port_to_host(p1['id'], 'host1')['port'] details = self.driver.get_gbp_details( self._neutron_admin_context, device='tap%s' % p1['id'], - host='h1') + host='host1') self.assertFalse(details['promiscuous_mode']) p2 = self._make_port(self.fmt, net['network']['id'], @@ -5715,7 +5799,7 @@ class TestNeutronPortOperation(AIMBaseTestCase): p2 = self._bind_port_to_host(p2['id'], 'host1')['port'] details = self.driver.get_gbp_details( self._neutron_admin_context, device='tap%s' % p2['id'], - host='h1') + host='host1') self.assertFalse(details['promiscuous_mode']) p3 = self._make_port(self.fmt, net['network']['id'], @@ -5725,16 +5809,19 @@ class TestNeutronPortOperation(AIMBaseTestCase): p3 = self._bind_port_to_host(p3['id'], 'host1')['port'] details = self.driver.get_gbp_details( self._neutron_admin_context, device='tap%s' % p3['id'], - host='h1') + host='host1') self.assertTrue(details['promiscuous_mode']) + # REVISIT: Test port name ending with PROMISCUOUS_SUFFIX, or + # is that deprecated? + # test DHCP port p1_dhcp = self._make_port(self.fmt, net['network']['id'], device_owner=n_constants.DEVICE_OWNER_DHCP)['port'] p1_dhcp = self._bind_port_to_host(p1_dhcp['id'], 'host1')['port'] details = self.driver.get_gbp_details( self._neutron_admin_context, device='tap%s' % p1_dhcp['id'], - host='h1') + host='host1') self.assertTrue(details['promiscuous_mode']) p2_dhcp = self._make_port(self.fmt, net['network']['id'], @@ -5743,7 +5830,7 @@ class TestNeutronPortOperation(AIMBaseTestCase): p2_dhcp = self._bind_port_to_host(p2_dhcp['id'], 'host1')['port'] details = self.driver.get_gbp_details( self._neutron_admin_context, device='tap%s' % p2_dhcp['id'], - host='h1') + host='host1') self.assertTrue(details['promiscuous_mode']) p3_dhcp = self._make_port(self.fmt, net['network']['id'], @@ -5752,9 +5839,14 @@ class TestNeutronPortOperation(AIMBaseTestCase): p3_dhcp = self._bind_port_to_host(p3_dhcp['id'], 'host1')['port'] details = self.driver.get_gbp_details( self._neutron_admin_context, device='tap%s' % p3_dhcp['id'], - host='h1') + host='host1') self.assertTrue(details['promiscuous_mode']) + # REVISIT: If we support proxy groups, we also need to test + # that promiscuous_mode is True when the port belongs to a PT + # that has a cluster_id, is not the master, and the master has + # a group_default_gateway. + def _aap_is_cidr(self, aap): cidr = netaddr.IPNetwork(aap['ip_address']) if cidr.prefixlen != 32: @@ -5791,11 +5883,13 @@ class TestNeutronPortOperation(AIMBaseTestCase): details = self.driver.get_gbp_details( self._neutron_admin_context, device='tap%s' % p1['id'], host='h1') - self.assertEqual(allow_addr, details['allowed_address_pairs']) + self.assertEqual(sorted(allow_addr), + sorted(details['allowed_address_pairs'])) details = self.driver.get_gbp_details( self._neutron_admin_context, device='tap%s' % p2['id'], host='h2') - self.assertEqual(allow_addr, details['allowed_address_pairs']) + self.assertEqual(sorted(allow_addr), + sorted(details['allowed_address_pairs'])) # Call agent => plugin RPC, requesting ownership of a /32 IP ip_owner_info = {'port': p1['id'], @@ -5827,7 +5921,8 @@ class TestNeutronPortOperation(AIMBaseTestCase): return expected_aaps expected_aaps1 = _get_expected_aaps(allow_addr, owned_addr[0]) - self.assertEqual(expected_aaps1, details['allowed_address_pairs']) + self.assertEqual(sorted(expected_aaps1), + sorted(details['allowed_address_pairs'])) # Call RPC sent by the agent, requesting ownership of a /32 IP ip_owner_info = {'port': p2['id'], @@ -5840,7 +5935,8 @@ class TestNeutronPortOperation(AIMBaseTestCase): host='h2') expected_aaps2 = _get_expected_aaps(allow_addr, owned_addr[1]) - self.assertEqual(expected_aaps2, details['allowed_address_pairs']) + self.assertEqual(sorted(expected_aaps2), + sorted(details['allowed_address_pairs'])) # set allowed-address as fixed-IP of ports p3 and p4, which also have # floating-IPs. Verify that FIP is "stolen" by p1 and p2 @@ -5914,7 +6010,8 @@ class TestNeutronPortOperation(AIMBaseTestCase): self._neutron_admin_context, device='tap%s' % p1['id'], host='h1') expected_aaps3 = _get_expected_aaps(update_addr, update_owned_addr[0]) - self.assertEqual(expected_aaps3, details['allowed_address_pairs']) + self.assertEqual(sorted(expected_aaps3), + sorted(details['allowed_address_pairs'])) p2 = self._update('ports', p2['id'], {'port': {'allowed_address_pairs': update_addr}}, @@ -5930,7 +6027,8 @@ class TestNeutronPortOperation(AIMBaseTestCase): self._neutron_admin_context, device='tap%s' % p2['id'], host='h2') expected_aaps4 = _get_expected_aaps(update_addr, update_owned_addr[1]) - self.assertEqual(expected_aaps4, details['allowed_address_pairs']) + self.assertEqual(sorted(expected_aaps4), + sorted(details['allowed_address_pairs'])) def test_gbp_details_for_allowed_address_pair(self): # 'aap' is configured, 'owned' is IP requested from agent @@ -5959,6 +6057,15 @@ class TestNeutronPortOperation(AIMBaseTestCase): owned_addr, update_addr, update_owned_addr) def test_port_bound_other_agent(self): + # REVISIT: This test should call request_endpoint_details + # rather than get_gbp_details, since the Opflex agent no + # longer calls get_gbp_details. The new + # request_endpoint_details implementation returns a response + # without a gbp_details key to indicate that the port either + # isn't bound or is bound to a different host. For now, we + # accept either RPC implemention's response from + # get_gbp_details. + self._register_agent('h1', test_aim_md.AGENT_CONF_OPFLEX) self._register_agent('h2', test_aim_md.AGENT_CONF_OPFLEX) net = self._make_network(self.fmt, 'net1', True) @@ -5970,24 +6077,24 @@ class TestNeutronPortOperation(AIMBaseTestCase): details = self.driver.get_gbp_details( self._neutron_admin_context, device='tap%s' % p1['id'], host='h1') - self.assertEqual('', details['host']) + self.assertEqual('', details.get('host', '')) details = self.driver.get_gbp_details( self._neutron_admin_context, device='tap%s' % p1['id'], host='h2') - self.assertEqual('', details['host']) + self.assertEqual('', details.get('host', '')) - # Test port bound to h1, queries from h1 and h2 + # Test port bound to h2, queries from h1 and h2 p1 = self._bind_port_to_host(p1['id'], 'h2')['port'] details = self.driver.get_gbp_details( self._neutron_admin_context, device='tap%s' % p1['id'], host='h1') - self.assertEqual('h2', details['host']) + self.assertEqual('h2', details.get('host', 'h2')) details = self.driver.get_gbp_details( self._neutron_admin_context, device='tap%s' % p1['id'], host='h2') self.assertEqual('h2', details['host']) - # Test rebind of port to h2, queries from h1 and h2 + # Test rebind of port to h1, queries from h1 and h2 p1 = self._bind_port_to_host(p1['id'], 'h1')['port'] details = self.driver.get_gbp_details( self._neutron_admin_context, device='tap%s' % p1['id'], @@ -5996,7 +6103,32 @@ class TestNeutronPortOperation(AIMBaseTestCase): details = self.driver.get_gbp_details( self._neutron_admin_context, device='tap%s' % p1['id'], host='h2') - self.assertEqual('h1', details['host']) + self.assertEqual('h1', details.get('host', 'h1')) + + +# REVISIT: This test class is disabled because two of its tests fail +# with the following SQL error: +# +# OperationalError: (sqlite3.OperationalError) near "'1.2.3.250'": +# syntax error [SQL: u"SELECT DISTINCT id FROM ports JOIN +# ipallocations AS ipallocations_1 ON ipallocations_1.port_id = +# ports.id WHERE ports.network_id = +# 'e7b26ed0-9b92-47b5-a5ca-fd9b19dd4bc2' AND +# ipallocations_1.ip_address in (u'1.2.3.250')"] (Background on this +# error at: http://sqlalche.me/e/e3q8) +# +# class TestNeutronPortOperationWithRawSql(TestNeutronPortOperation): +# +# def setUp(self, **kwargs): +# super(TestNeutronPortOperationWithRawSql, self).setUp(**kwargs) +# self.driver.aim_mech_driver.enable_raw_sql_for_device_rpc = True + + +class TestNeutronPortOperationWithNewRpc(TestNeutronPortOperation): + + def setUp(self, **kwargs): + super(TestNeutronPortOperationWithNewRpc, self).setUp(**kwargs) + self.driver.aim_mech_driver.enable_new_rpc = True class TestPerL3PImplicitContractsConfig(TestL2PolicyWithAutoPTG): @@ -6073,3 +6205,88 @@ class TestVlanAwareVM(AIMBaseTestCase): def test_trunk_master_port(self): self._do_test_gbp_details_no_pt() + + +class TestVlanAwareVMWithRawSql(TestVlanAwareVM): + + def setUp(self, **kwargs): + super(TestVlanAwareVMWithRawSql, self).setUp(**kwargs) + self.driver.aim_mech_driver.enable_raw_sql_for_device_rpc = True + + +class TestVlanAwareVMWithNewRpc(TestVlanAwareVM): + + def setUp(self, **kwargs): + super(TestVlanAwareVMWithNewRpc, self).setUp(**kwargs) + self.driver.aim_mech_driver.enable_new_rpc = True + + +class TestL2PolicyRouteInjection(AIMBaseTestCase): + + def _verify_rpc_response(self, port_id, inject, metadata): + # Invoke request_endpoint_details RPC handler. + request = {'device': 'tap%s' % port_id, 'timestamp': 0, + 'request_id': 'a_request_id'} + response = self.driver.request_endpoint_details( + nctx.get_admin_context(), request=request, host='host1') + + # Check subnet details. + subnet_details = response['gbp_details']['subnets'][0] + expected_host_routes = [] + if inject: + self.assertIn('gateway_ip', subnet_details) + expected_host_routes.append( + {'destination': '0.0.0.0/0', + 'nexthop': subnet_details['gateway_ip']}) + if metadata: + expected_host_routes.append( + {'destination': '169.254.169.254/16', + 'nexthop': subnet_details['dns_nameservers'][0]}) + else: + self.assertNotIn('gateway_ip', subnet_details) + self.assertEqual(sorted(expected_host_routes), + sorted(subnet_details['host_routes'])) + + def _test_route_injection(self, inject): + # Create GBP resources and bind port. + l2p = self.create_l2_policy( + inject_default_route=inject)['l2_policy'] + ptg = self.create_policy_target_group( + l2_policy_id=l2p['id'])['policy_target_group'] + pt = self.create_policy_target( + policy_target_group_id=ptg['id'])['policy_target'] + port_id = pt['port_id'] + self._bind_port_to_host(port_id, 'host1') + + # Test without metadata route. + self._verify_rpc_response(port_id, inject, False) + + # Create a DHCP port on the PTG's subnet to enable metadata + # route injection. + fixed_ips = [{'subnet_id': ptg['subnets'][0]}] + self._make_port( + self.fmt, l2p['network_id'], fixed_ips=fixed_ips, + device_owner='network:dhcp') + + # Test with metadata route. + self._verify_rpc_response(port_id, inject, True) + + def test_route_injection_on(self): + self._test_route_injection(True) + + def test_route_injection_off(self): + self._test_route_injection(False) + + +class TestL2PolicyRouteInjectionWithRawSql(TestL2PolicyRouteInjection): + + def setUp(self, **kwargs): + super(TestL2PolicyRouteInjectionWithRawSql, self).setUp(**kwargs) + self.driver.aim_mech_driver.enable_raw_sql_for_device_rpc = True + + +class TestL2PolicyRouteInjectionWithNewRpc(TestL2PolicyRouteInjection): + + def setUp(self, **kwargs): + super(TestL2PolicyRouteInjectionWithNewRpc, self).setUp(**kwargs) + self.driver.aim_mech_driver.enable_new_rpc = True