From: Ivar Lazzaro Date: Fri, 22 Aug 2014 01:17:03 +0000 (-0700) Subject: Apic drivers enhancements (second approach): L2 refactor X-Git-Url: https://review.fuel-infra.org/gitweb?a=commitdiff_plain;h=9660177d485b8d28f2839358320eb2b407ca3b48;p=openstack-build%2Fneutron-build.git Apic drivers enhancements (second approach): L2 refactor - refactor to leverage Client's transactional capabilities - General refactor to improve the driver's reliability Implements blueprint: apic-driver-enhancements Change-Id: I4deb171381e62e70818218957d82b5e27954aeb9 --- diff --git a/etc/neutron/plugins/ml2/ml2_conf_cisco.ini b/etc/neutron/plugins/ml2/ml2_conf_cisco.ini index bb2dadbed..ea361d046 100644 --- a/etc/neutron/plugins/ml2/ml2_conf_cisco.ini +++ b/etc/neutron/plugins/ml2/ml2_conf_cisco.ini @@ -58,6 +58,12 @@ # Password for the APIC controller # apic_password=password +# Whether use SSl for connecting to the APIC controller or not +# apic_use_ssl=True + +# How to map names to APIC: use_uuid or use_name +# apic_name_mapping=use_name + # Names for APIC objects used by Neutron # Note: When deploying multiple clouds against one APIC, # these names must be unique between the clouds. @@ -66,26 +72,43 @@ # apic_node_profile=openstack_profile # apic_entity_profile=openstack_entity # apic_function_profile=openstack_function - -# The following flag will cause all the node profiles on the APIC to -# be cleared when neutron-server starts. This is typically used only -# for test environments that require clean-slate startup conditions. -# apic_clear_node_profiles=False +# apic_app_profile_name=openstack_app # Specify your network topology. # This section indicates how your compute nodes are connected to the fabric's # switches and ports. The format is as follows: # -# [switch:] +# [apic_switch:] # ,= # # You can have multiple sections, one for each switch in your fabric that is # participating in Openstack. e.g. # -# [switch:17] +# [apic_switch:17] # ubuntu,ubuntu1=1/10 # ubuntu2,ubuntu3=1/11 # -# [switch:18] +# [apic_switch:18] # ubuntu5,ubuntu6=1/1 # ubuntu7,ubuntu8=1/2 + +# Describe external connectivity. +# In this section you can specify the external network configuration in order +# for the plugin to be able to teach the fabric how to route the internal +# traffic to the outside world. The external connectivity configuration +# format is as follows: +# +# [apic_external_network:] +# switch= +# port= +# encap= +# cidr_exposed= +# gateway_ip= +# +# An example follows: +# [apic_external_network:network_ext] +# switch=203 +# port=1/34 +# encap=vlan-100 +# cidr_exposed=10.10.40.2/16 +# gateway_ip=10.10.40.1 diff --git a/neutron/db/migration/alembic_migrations/versions/32f3915891fd_cisco_apic_driver_update.py b/neutron/db/migration/alembic_migrations/versions/32f3915891fd_cisco_apic_driver_update.py new file mode 100644 index 000000000..09d914c24 --- /dev/null +++ b/neutron/db/migration/alembic_migrations/versions/32f3915891fd_cisco_apic_driver_update.py @@ -0,0 +1,66 @@ +# Copyright 2014 OpenStack Foundation +# +# 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. +# + +""" cisco_apic_driver_update + +Revision ID: 32f3915891fd + +""" + +# revision identifiers, used by Alembic. +revision = '32f3915891fd' +down_revision = 'aae5706a396' + + +from alembic import op +import sqlalchemy as sa + + +def upgrade(active_plugins=None, options=None): + + op.drop_table('cisco_ml2_apic_port_profiles') + + op.create_table( + 'cisco_ml2_apic_host_links', + sa.Column('host', sa.String(length=255), nullable=False), + sa.Column('ifname', sa.String(length=64), nullable=False), + sa.Column('ifmac', sa.String(length=32), nullable=True), + sa.Column('swid', sa.String(length=32), nullable=False), + sa.Column('module', sa.String(length=32), nullable=False), + sa.Column('port', sa.String(length=32), nullable=False), + sa.PrimaryKeyConstraint('host', 'ifname')) + + op.create_table( + 'cisco_ml2_apic_names', + sa.Column('neutron_id', sa.String(length=36), nullable=False), + sa.Column('neutron_type', sa.String(length=32), nullable=False), + sa.Column('apic_name', sa.String(length=255), nullable=False), + sa.PrimaryKeyConstraint('neutron_id', 'neutron_type')) + + +def downgrade(active_plugins=None, options=None): + + op.drop_table('cisco_ml2_apic_names') + op.drop_table('cisco_ml2_apic_host_links') + + op.create_table( + 'cisco_ml2_apic_port_profiles', + sa.Column('node_id', sa.String(length=255), nullable=False), + sa.Column('profile_id', sa.String(length=64), nullable=False), + sa.Column('hpselc_id', sa.String(length=64), nullable=False), + sa.Column('module', sa.String(length=10), nullable=False), + sa.Column('from_port', sa.Integer(), nullable=False), + sa.Column('to_port', sa.Integer(), nullable=False), + sa.PrimaryKeyConstraint('node_id')) diff --git a/neutron/db/migration/alembic_migrations/versions/HEAD b/neutron/db/migration/alembic_migrations/versions/HEAD index b340f2714..b3519602a 100644 --- a/neutron/db/migration/alembic_migrations/versions/HEAD +++ b/neutron/db/migration/alembic_migrations/versions/HEAD @@ -1 +1 @@ -aae5706a396 +32f3915891fd diff --git a/neutron/plugins/ml2/drivers/cisco/__init__.py b/neutron/plugins/ml2/drivers/cisco/__init__.py index 788cea1f7..e69de29bb 100644 --- a/neutron/plugins/ml2/drivers/cisco/__init__.py +++ b/neutron/plugins/ml2/drivers/cisco/__init__.py @@ -1,14 +0,0 @@ -# Copyright (c) 2013 OpenStack Foundation -# 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. diff --git a/neutron/plugins/ml2/drivers/cisco/apic/apic_model.py b/neutron/plugins/ml2/drivers/cisco/apic/apic_model.py index 19774fd46..9111b1546 100644 --- a/neutron/plugins/ml2/drivers/cisco/apic/apic_model.py +++ b/neutron/plugins/ml2/drivers/cisco/apic/apic_model.py @@ -16,11 +16,15 @@ # @author: Arvind Somya (asomya@cisco.com), Cisco Systems Inc. import sqlalchemy as sa +from sqlalchemy import orm from sqlalchemy import sql from neutron.db import api as db_api from neutron.db import model_base +from neutron.db import models_v2 +from neutron.plugins.ml2 import models as models_ml2 + class NetworkEPG(model_base.BASEV2): @@ -35,20 +39,6 @@ class NetworkEPG(model_base.BASEV2): nullable=False) -class PortProfile(model_base.BASEV2): - - """Port profiles created on the APIC.""" - - __tablename__ = 'cisco_ml2_apic_port_profiles' - - node_id = sa.Column(sa.String(255), nullable=False, primary_key=True) - profile_id = sa.Column(sa.String(64), nullable=False) - hpselc_id = sa.Column(sa.String(64), nullable=False) - module = sa.Column(sa.String(10), nullable=False) - from_port = sa.Column(sa.Integer(), nullable=False) - to_port = sa.Column(sa.Integer(), nullable=False) - - class TenantContract(model_base.BASEV2): """Contracts (and Filters) created on the APIC.""" @@ -61,6 +51,30 @@ class TenantContract(model_base.BASEV2): filter_id = sa.Column(sa.String(64), nullable=False) +class HostLink(model_base.BASEV2): + + """Connectivity of host links.""" + + __tablename__ = 'cisco_ml2_apic_host_links' + + host = sa.Column(sa.String(255), nullable=False, primary_key=True) + ifname = sa.Column(sa.String(64), nullable=False, primary_key=True) + ifmac = sa.Column(sa.String(32), nullable=True) + swid = sa.Column(sa.String(32), nullable=False) + module = sa.Column(sa.String(32), nullable=False) + port = sa.Column(sa.String(32), nullable=False) + + +class ApicName(model_base.BASEV2): + """Mapping of names created on the APIC.""" + + __tablename__ = 'cisco_ml2_apic_names' + + neutron_id = sa.Column(sa.String(36), nullable=False, primary_key=True) + neutron_type = sa.Column(sa.String(32), nullable=False, primary_key=True) + apic_name = sa.Column(sa.String(255), nullable=False) + + class ApicDbModel(object): """DB Model to manage all APIC DB interactions.""" @@ -68,42 +82,6 @@ class ApicDbModel(object): def __init__(self): self.session = db_api.get_session() - def get_port_profile_for_node(self, node_id): - """Returns a port profile for a switch if found in the DB.""" - return self.session.query(PortProfile).filter_by( - node_id=node_id).first() - - def get_profile_for_module_and_ports(self, node_id, profile_id, - module, from_port, to_port): - """Returns profile for module and ports. - - Grabs the profile row from the DB for the specified switch, - module (linecard) and from/to port combination. - """ - return self.session.query(PortProfile).filter_by( - node_id=node_id, - module=module, - profile_id=profile_id, - from_port=from_port, - to_port=to_port).first() - - def get_profile_for_module(self, node_id, profile_id, module): - """Returns the first profile for a switch module from the DB.""" - return self.session.query(PortProfile).filter_by( - node_id=node_id, - profile_id=profile_id, - module=module).first() - - def add_profile_for_module_and_ports(self, node_id, profile_id, - hpselc_id, module, - from_port, to_port): - """Adds a profile for switch, module and port range.""" - row = PortProfile(node_id=node_id, profile_id=profile_id, - hpselc_id=hpselc_id, module=module, - from_port=from_port, to_port=to_port) - self.session.add(row) - self.session.flush() - def get_provider_contract(self): """Returns provider EPG from the DB if found.""" return self.session.query(NetworkEPG).filter_by( @@ -170,10 +148,92 @@ class ApicDbModel(object): return contract - def delete_profile_for_node(self, node_id): - """Deletes the port profile for a node.""" - profile = self.session.query(PortProfile).filter_by( - node_id=node_id).first() - if profile: - self.session.delete(profile) - self.session.flush() + def add_hostlink(self, host, ifname, ifmac, swid, module, port): + link = HostLink(host=host, ifname=ifname, ifmac=ifmac, + swid=swid, module=module, port=port) + with self.session.begin(subtransactions=True): + self.session.merge(link) + + def get_hostlinks(self): + return self.session.query(HostLink).all() + + def get_hostlink(self, host, ifname): + return self.session.query(HostLink).filter_by( + host=host, ifname=ifname).first() + + def get_hostlinks_for_host(self, host): + return self.session.query(HostLink).filter_by( + host=host).all() + + def get_hostlinks_for_host_switchport(self, host, swid, module, port): + return self.session.query(HostLink).filter_by( + host=host, swid=swid, module=module, port=port).all() + + def get_hostlinks_for_switchport(self, swid, module, port): + return self.session.query(HostLink).filter_by( + swid=swid, module=module, port=port).all() + + def delete_hostlink(self, host, ifname): + with self.session.begin(subtransactions=True): + try: + self.session.query(HostLink).filter_by(host=host, + ifname=ifname).delete() + except orm.exc.NoResultFound: + return + + def get_switches(self): + return self.session.query(HostLink.swid).distinct() + + def get_modules_for_switch(self, swid): + return self.session.query( + HostLink.module).filter_by(swid=swid).distinct() + + def get_ports_for_switch_module(self, swid, module): + return self.session.query( + HostLink.port).filter_by(swid=swid, module=module).distinct() + + def get_switch_and_port_for_host(self, host): + return self.session.query( + HostLink.swid, HostLink.module, HostLink.port).filter_by( + host=host).distinct() + + def get_tenant_network_vlan_for_host(self, host): + pb = models_ml2.PortBinding + po = models_v2.Port + ns = models_ml2.NetworkSegment + return self.session.query( + po.tenant_id, ns.network_id, ns.segmentation_id).filter( + po.id == pb.port_id).filter(pb.host == host).filter( + po.network_id == ns.network_id).distinct() + + def add_apic_name(self, neutron_id, neutron_type, apic_name): + name = ApicName(neutron_id=neutron_id, + neutron_type=neutron_type, + apic_name=apic_name) + with self.session.begin(subtransactions=True): + self.session.add(name) + + def update_apic_name(self, neutron_id, neutron_type, apic_name): + with self.session.begin(subtransactions=True): + name = self.session.query(ApicName).filter_by( + neutron_id=neutron_id, neutron_type=neutron_type).first() + if name: + name.apic_name = apic_name + self.session.merge(name) + else: + self.add_apic_name(neutron_id, neutron_type, apic_name) + + def get_apic_names(self): + return self.session.query(ApicName).all() + + def get_apic_name(self, neutron_id, neutron_type): + return self.session.query(ApicName.apic_name).filter_by( + neutron_id=neutron_id, neutron_type=neutron_type).first() + + def delete_apic_name(self, neutron_id): + with self.session.begin(subtransactions=True): + try: + self.session.query(ApicName).filter_by( + neutron_id=neutron_id).delete() + except orm.exc.NoResultFound: + return diff --git a/neutron/plugins/ml2/drivers/cisco/apic/config.py b/neutron/plugins/ml2/drivers/cisco/apic/config.py index 2b922c1bc..0309cd896 100644 --- a/neutron/plugins/ml2/drivers/cisco/apic/config.py +++ b/neutron/plugins/ml2/drivers/cisco/apic/config.py @@ -18,6 +18,22 @@ from oslo.config import cfg +DEFAULT_ROOT_HELPER = ('sudo /usr/local/bin/neutron-rootwrap ' + '/etc/neutron/rootwrap.conf') + + +# oslo.config limits ${var} expansion to global variables +# That is why apic_system_id as a global variable +global_opts = [ + cfg.StrOpt('apic_system_id', + default='openstack', + help=_("Prefix for APIC domain/names/profiles created")), +] + + +cfg.CONF.register_opts(global_opts) + + apic_opts = [ cfg.ListOpt('apic_hosts', default=[], @@ -27,58 +43,96 @@ apic_opts = [ help=_("Username for the APIC controller")), cfg.StrOpt('apic_password', help=_("Password for the APIC controller"), secret=True), + cfg.StrOpt('apic_name_mapping', + default='use_name', + help=_("Name mapping strategy to use: use_uuid | use_name")), cfg.BoolOpt('apic_use_ssl', default=True, help=_("Use SSL to connect to the APIC controller")), - cfg.StrOpt('apic_vmm_provider', default='VMware', - help=_("Name for the VMM domain provider")), - cfg.StrOpt('apic_vmm_domain', default='openstack', - help=_("Name for the VMM domain to be created for Openstack")), - cfg.StrOpt('apic_vlan_ns_name', default='openstack_ns', - help=_("Name for the vlan namespace to be used for openstack")), - cfg.StrOpt('apic_vlan_range', default='2:4093', - help=_("Range of VLAN's to be used for Openstack")), - cfg.StrOpt('apic_node_profile', default='openstack_profile', + cfg.StrOpt('apic_domain_name', + default='${apic_system_id}', + help=_("Name for the domain created on APIC")), + cfg.StrOpt('apic_app_profile_name', + default='${apic_system_id}_app', + help=_("Name for the app profile used for Openstack")), + cfg.StrOpt('apic_vlan_ns_name', + default='${apic_system_id}_vlan_ns', + help=_("Name for the vlan namespace to be used for Openstack")), + cfg.StrOpt('apic_node_profile', + default='${apic_system_id}_node_profile', help=_("Name of the node profile to be created")), - cfg.StrOpt('apic_entity_profile', default='openstack_entity', + cfg.StrOpt('apic_entity_profile', + default='${apic_system_id}_entity_profile', help=_("Name of the entity profile to be created")), - cfg.StrOpt('apic_function_profile', default='openstack_function', + cfg.StrOpt('apic_function_profile', + default='${apic_system_id}_function_profile', help=_("Name of the function profile to be created")), - cfg.BoolOpt('apic_clear_node_profiles', default=False, - help=_("Clear the node profiles on the APIC at startup " - "(mainly used for testing)")), + cfg.StrOpt('apic_lacp_profile', + default='${apic_system_id}_lacp_profile', + help=_("Name of the LACP profile to be created")), + cfg.ListOpt('apic_host_uplink_ports', + default=[], + help=_('The uplink ports to check for ACI connectivity')), + cfg.ListOpt('apic_vpc_pairs', + default=[], + help=_('The switch pairs for VPC connectivity')), + cfg.StrOpt('apic_vlan_range', + default='2:4093', + help=_("Range of VLAN's to be used for Openstack")), + cfg.StrOpt('root_helper', + default=DEFAULT_ROOT_HELPER, + help=_("Setup root helper as rootwrap or sudo")), ] cfg.CONF.register_opts(apic_opts, "ml2_cisco_apic") -def get_switch_and_port_for_host(host_id): - for switch, connected in _switch_dict.items(): - for port, hosts in connected.items(): - if host_id in hosts: - return switch, port +def _get_specific_config(prefix): + """retrieve config in the format [:].""" + conf_dict = {} + multi_parser = cfg.MultiConfigParser() + multi_parser.read(cfg.CONF.config_file) + for parsed_file in multi_parser.parsed: + for parsed_item in parsed_file.keys(): + if parsed_item.startswith(prefix): + switch, switch_id = parsed_item.split(':') + if switch.lower() == prefix: + conf_dict[switch_id] = parsed_file[parsed_item].items() + return conf_dict -_switch_dict = {} +def create_switch_dictionary(): + switch_dict = {} + conf = _get_specific_config('apic_switch') + for switch_id in conf: + switch_dict[switch_id] = switch_dict.get(switch_id, {}) + for host_list, port in conf[switch_id]: + hosts = host_list.split(',') + port = port[0] + switch_dict[switch_id][port] = ( + switch_dict[switch_id].get(port, []) + hosts) + return switch_dict -def create_switch_dictionary(): - multi_parser = cfg.MultiConfigParser() - read_ok = multi_parser.read(cfg.CONF.config_file) +def create_vpc_dictionary(): + vpc_dict = {} + for pair in cfg.CONF.ml2_cisco_apic.apic_vpc_pairs: + pair_tuple = pair.split(':') + if (len(pair_tuple) != 2 or + any(map(lambda x: not x.isdigit(), pair_tuple))): + # Validation error, ignore this item + continue + vpc_dict[pair_tuple[0]] = pair_tuple[1] + vpc_dict[pair_tuple[1]] = pair_tuple[0] + return vpc_dict - if len(read_ok) != len(cfg.CONF.config_file): - raise cfg.Error(_("Some config files were not parsed properly")) - for parsed_file in multi_parser.parsed: - for parsed_item in parsed_file.keys(): - if parsed_item.startswith('apic_switch'): - switch, switch_id = parsed_item.split(':') - if switch.lower() == 'apic_switch': - _switch_dict[switch_id] = {} - port_cfg = parsed_file[parsed_item].items() - for host_list, port in port_cfg: - hosts = host_list.split(',') - port = port[0] - _switch_dict[switch_id][port] = hosts - - return _switch_dict +def create_external_network_dictionary(): + router_dict = {} + conf = _get_specific_config('apic_external_network') + for net_id in conf: + router_dict[net_id] = router_dict.get(net_id, {}) + for key, value in conf[net_id]: + router_dict[net_id][key] = value[0] if value else None + + return router_dict diff --git a/neutron/plugins/ml2/drivers/cisco/apic/mechanism_apic.py b/neutron/plugins/ml2/drivers/cisco/apic/mechanism_apic.py index 58bbfb9ab..3d063e4fe 100644 --- a/neutron/plugins/ml2/drivers/cisco/apic/mechanism_apic.py +++ b/neutron/plugins/ml2/drivers/cisco/apic/mechanism_apic.py @@ -16,16 +16,19 @@ # @author: Arvind Somya (asomya@cisco.com), Cisco Systems Inc. from apicapi import apic_manager +from apicapi import exceptions as exc +from keystoneclient.v2_0 import client as keyclient import netaddr - from oslo.config import cfg -from neutron.extensions import portbindings +from neutron.common import constants as n_constants +from neutron.openstack.common import lockutils from neutron.openstack.common import log from neutron.plugins.common import constants from neutron.plugins.ml2 import driver_api as api from neutron.plugins.ml2.drivers.cisco.apic import apic_model from neutron.plugins.ml2.drivers.cisco.apic import config +from neutron.plugins.ml2 import models LOG = log.getLogger(__name__) @@ -34,94 +37,232 @@ LOG = log.getLogger(__name__) class APICMechanismDriver(api.MechanismDriver): @staticmethod - def get_apic_manager(): + def get_apic_manager(client=True): apic_config = cfg.CONF.ml2_cisco_apic network_config = { 'vlan_ranges': cfg.CONF.ml2_type_vlan.network_vlan_ranges, 'switch_dict': config.create_switch_dictionary(), + 'vpc_dict': config.create_vpc_dictionary(), + 'external_network_dict': + config.create_external_network_dictionary(), } + apic_system_id = cfg.CONF.apic_system_id + keyclient_param = keyclient if client else None + keystone_authtoken = cfg.CONF.keystone_authtoken if client else None return apic_manager.APICManager(apic_model.ApicDbModel(), log, - network_config, apic_config) + network_config, apic_config, + keyclient_param, keystone_authtoken, + apic_system_id) def initialize(self): + # initialize apic self.apic_manager = APICMechanismDriver.get_apic_manager() + self.name_mapper = self.apic_manager.apic_mapper self.apic_manager.ensure_infra_created_on_apic() + self.apic_manager.ensure_bgp_pod_policy_created_on_apic() - def _perform_port_operations(self, context): + @lockutils.synchronized('apic-portlock') + def _perform_path_port_operations(self, context, port): + # Get network + network_id = context.network.current['id'] + anetwork_id = self.name_mapper.network(context, network_id) # Get tenant details from port context tenant_id = context.current['tenant_id'] - - # Get network - network = context.network.current['id'] - - # Get port - port = context.current + tenant_id = self.name_mapper.tenant(context, tenant_id) # Get segmentation id if not context.bound_segment: - LOG.debug(_("Port %s is not bound to a segment"), port) + LOG.debug("Port %s is not bound to a segment", port) return seg = None - if (context.bound_segment.get(api.NETWORK_TYPE) in - [constants.TYPE_VLAN]): + if (context.bound_segment.get(api.NETWORK_TYPE) + in [constants.TYPE_VLAN]): seg = context.bound_segment.get(api.SEGMENTATION_ID) + # hosts on which this vlan is provisioned + host = context.host + # Create a static path attachment for the host/epg/switchport combo + with self.apic_manager.apic.transaction() as trs: + self.apic_manager.ensure_path_created_for_port( + tenant_id, anetwork_id, host, seg, transaction=trs) + def _perform_gw_port_operations(self, context, port): + router_id = port.get('device_id') + network = context.network.current + anetwork_id = self.name_mapper.network(context, network['id']) + router_info = self.apic_manager.ext_net_dict.get(network['name']) + + if router_id and router_info: + address = router_info['cidr_exposed'] + next_hop = router_info['gateway_ip'] + encap = router_info.get('encap') # No encap if None + switch = router_info['switch'] + module, sport = router_info['port'].split('/') + with self.apic_manager.apic.transaction() as trs: + # Get/Create contract + arouter_id = self.name_mapper.router(context, router_id) + cid = self.apic_manager.get_router_contract(arouter_id) + # Ensure that the external ctx exists + self.apic_manager.ensure_context_enforced() + # Create External Routed Network and configure it + self.apic_manager.ensure_external_routed_network_created( + anetwork_id, transaction=trs) + self.apic_manager.ensure_logical_node_profile_created( + anetwork_id, switch, module, sport, encap, + address, transaction=trs) + self.apic_manager.ensure_static_route_created( + anetwork_id, switch, next_hop, transaction=trs) + self.apic_manager.ensure_external_epg_created( + anetwork_id, transaction=trs) + self.apic_manager.ensure_external_epg_consumed_contract( + anetwork_id, cid, transaction=trs) + self.apic_manager.ensure_external_epg_provided_contract( + anetwork_id, cid, transaction=trs) + + def _perform_port_operations(self, context): + # Get port + port = context.current # Check if a compute port - if not port['device_owner'].startswith('compute'): - # Not a compute port, return - return + if port.get('device_owner', '').startswith('compute'): + self._perform_path_port_operations(context, port) + elif port.get('device_owner') == n_constants.DEVICE_OWNER_ROUTER_GW: + self._perform_gw_port_operations(context, port) + elif port.get('device_owner') == n_constants.DEVICE_OWNER_DHCP: + self._perform_path_port_operations(context, port) - host = context.host - # Check host that the dhcp agent is running on - filters = {'device_owner': 'network:dhcp', - 'network_id': network} - dhcp_ports = context._plugin.get_ports(context._plugin_context, - filters=filters) - dhcp_hosts = [] - for dhcp_port in dhcp_ports: - dhcp_hosts.append(dhcp_port.get(portbindings.HOST_ID)) - - # Create a static path attachment for this host/epg/switchport combo - self.apic_manager.ensure_tenant_created_on_apic(tenant_id) - if dhcp_hosts: - for dhcp_host in dhcp_hosts: - self.apic_manager.ensure_path_created_for_port(tenant_id, - network, - dhcp_host, seg) - if host not in dhcp_hosts: - self.apic_manager.ensure_path_created_for_port(tenant_id, network, - host, seg) + def _delete_contract(self, context): + port = context.current + network_id = self.name_mapper.network( + context, context.network.current['id']) + arouter_id = self.name_mapper.router(context, + port.get('device_id')) + self.apic_manager.delete_external_epg_contract(arouter_id, + network_id) + + def _get_active_path_count(self, context): + return context._plugin_context.session.query( + models.PortBinding).filter_by( + host=context.host, segment=context._binding.segment).count() + + @lockutils.synchronized('apic-portlock') + def _delete_port_path(self, context, atenant_id, anetwork_id): + if not self._get_active_path_count(context): + self.apic_manager.ensure_path_deleted_for_port( + atenant_id, anetwork_id, + context.host) + + def _delete_path_if_last(self, context): + if not self._get_active_path_count(context): + tenant_id = context.current['tenant_id'] + atenant_id = self.name_mapper.tenant(context, tenant_id) + network_id = context.network.current['id'] + anetwork_id = self.name_mapper.network(context, network_id) + self._delete_port_path(context, atenant_id, anetwork_id) + + def _get_subnet_info(self, context, subnet): + tenant_id = subnet['tenant_id'] + network_id = subnet['network_id'] + network = context._plugin.get_network(context._plugin_context, + network_id) + if not network.get('router:external'): + cidr = netaddr.IPNetwork(subnet['cidr']) + gateway_ip = '%s/%s' % (subnet['gateway_ip'], str(cidr.prefixlen)) + + # Convert to APIC IDs + tenant_id = self.name_mapper.tenant(context, tenant_id) + network_id = self.name_mapper.network(context, network_id) + return tenant_id, network_id, gateway_ip def create_port_postcommit(self, context): self._perform_port_operations(context) + def update_port_precommit(self, context): + orig = context.original + curr = context.current + if (orig['device_owner'] != curr['device_owner'] + or orig['device_id'] != curr['device_id']): + raise exc.ApicOperationNotSupported( + resource='Port', msg='Port device owner and id cannot be ' + 'changed.') + def update_port_postcommit(self, context): self._perform_port_operations(context) + def delete_port_postcommit(self, context): + port = context.current + # Check if a compute port + if port.get('device_owner', '').startswith('compute'): + self._delete_path_if_last(context) + elif port.get('device_owner') == n_constants.DEVICE_OWNER_ROUTER_GW: + self._delete_contract(context) + elif port.get('device_owner') == n_constants.DEVICE_OWNER_DHCP: + self._delete_path_if_last(context) + def create_network_postcommit(self, context): - net_id = context.current['id'] - tenant_id = context.current['tenant_id'] - net_name = context.current['name'] + if not context.current.get('router:external'): + tenant_id = context.current['tenant_id'] + network_id = context.current['id'] - self.apic_manager.ensure_bd_created_on_apic(tenant_id, net_id) - # Create EPG for this network - self.apic_manager.ensure_epg_created_for_network(tenant_id, net_id, - net_name) + # Convert to APIC IDs + tenant_id = self.name_mapper.tenant(context, tenant_id) + network_id = self.name_mapper.network(context, network_id) + + # Create BD and EPG for this network + with self.apic_manager.apic.transaction() as trs: + self.apic_manager.ensure_bd_created_on_apic(tenant_id, + network_id, + transaction=trs) + self.apic_manager.ensure_epg_created( + tenant_id, network_id, transaction=trs) def delete_network_postcommit(self, context): - net_id = context.current['id'] - tenant_id = context.current['tenant_id'] + if not context.current.get('router:external'): + tenant_id = context.current['tenant_id'] + network_id = context.current['id'] - self.apic_manager.delete_bd_on_apic(tenant_id, net_id) - self.apic_manager.delete_epg_for_network(tenant_id, net_id) + # Convert to APIC IDs + tenant_id = self.name_mapper.tenant(context, tenant_id) + network_id = self.name_mapper.network(context, network_id) + + # Delete BD and EPG for this network + with self.apic_manager.apic.transaction() as trs: + self.apic_manager.delete_epg_for_network(tenant_id, network_id, + transaction=trs) + self.apic_manager.delete_bd_on_apic(tenant_id, network_id, + transaction=trs) + else: + network_name = context.current['name'] + if self.apic_manager.ext_net_dict.get(network_name): + network_id = self.name_mapper.network(context, + context.current['id']) + self.apic_manager.delete_external_routed_network(network_id) def create_subnet_postcommit(self, context): - tenant_id = context.current['tenant_id'] - network_id = context.current['network_id'] - gateway_ip = context.current['gateway_ip'] - cidr = netaddr.IPNetwork(context.current['cidr']) - netmask = str(cidr.prefixlen) - gateway_ip = gateway_ip + '/' + netmask - - self.apic_manager.ensure_subnet_created_on_apic(tenant_id, network_id, - gateway_ip) + info = self._get_subnet_info(context, context.current) + if info: + tenant_id, network_id, gateway_ip = info + # Create subnet on BD + self.apic_manager.ensure_subnet_created_on_apic( + tenant_id, network_id, gateway_ip) + + def update_subnet_postcommit(self, context): + if context.current['gateway_ip'] != context.original['gateway_ip']: + with self.apic_manager.apic.transaction() as trs: + info = self._get_subnet_info(context, context.original) + if info: + tenant_id, network_id, gateway_ip = info + # Delete subnet + self.apic_manager.ensure_subnet_deleted_on_apic( + tenant_id, network_id, gateway_ip, transaction=trs) + info = self._get_subnet_info(context, context.current) + if info: + tenant_id, network_id, gateway_ip = info + # Create subnet + self.apic_manager.ensure_subnet_created_on_apic( + tenant_id, network_id, gateway_ip, transaction=trs) + + def delete_subnet_postcommit(self, context): + info = self._get_subnet_info(context, context.current) + if info: + tenant_id, network_id, gateway_ip = info + self.apic_manager.ensure_subnet_deleted_on_apic( + tenant_id, network_id, gateway_ip) diff --git a/neutron/tests/unit/ml2/drivers/cisco/apic/test_cisco_apic_common.py b/neutron/tests/unit/ml2/drivers/cisco/apic/test_cisco_apic_common.py index 12318a2d7..7e2e9c4a4 100644 --- a/neutron/tests/unit/ml2/drivers/cisco/apic/test_cisco_apic_common.py +++ b/neutron/tests/unit/ml2/drivers/cisco/apic/test_cisco_apic_common.py @@ -15,13 +15,12 @@ # # @author: Henry Gessau, Cisco Systems -import mock +import contextlib import requests +import mock from oslo.config import cfg -from neutron.common import config as neutron_config -from neutron.plugins.ml2 import config as ml2_config from neutron.tests import base @@ -45,9 +44,8 @@ APIC_SUBJECT = 'testSubject' APIC_FILTER = 'carbonFilter' APIC_ENTRY = 'forcedEntry' -APIC_VMMP = 'OpenStack' +APIC_SYSTEM_ID = 'sysid' APIC_DOMAIN = 'cumuloNimbus' -APIC_PDOM = 'rainStorm' APIC_NODE_PROF = 'red' APIC_LEAF = 'green' @@ -68,6 +66,19 @@ APIC_VLANID_TO = 2999 APIC_VLAN_FROM = 'vlan-%d' % APIC_VLANID_FROM APIC_VLAN_TO = 'vlan-%d' % APIC_VLANID_TO +APIC_ROUTER = 'router_id' + +APIC_EXT_SWITCH = '203' +APIC_EXT_MODULE = '1' +APIC_EXT_PORT = '34' +APIC_EXT_ENCAP = 'vlan-100' +APIC_EXT_CIDR_EXPOSED = '10.10.40.2/16' +APIC_EXT_GATEWAY_IP = '10.10.40.1' + +APIC_KEY = 'key' + +KEYSTONE_TOKEN = '123Token123' + class ControllerMixin(object): @@ -114,6 +125,10 @@ class ControllerMixin(object): self.mock_response_for_post('aaaLogin', userName=APIC_USR, token='ok', refreshTimeoutSeconds=timeout) + @contextlib.contextmanager + def fake_transaction(self, *args, **kwargs): + yield 'transaction' + class ConfigMixin(object): @@ -124,8 +139,13 @@ class ConfigMixin(object): def set_up_mocks(self): # Mock the configuration file - args = ['--config-file', base.etcdir('neutron.conf.test')] - neutron_config.init(args=args) + base.BaseTestCase.config_parse() + + # Configure global option apic_system_id + cfg.CONF.set_override('apic_system_id', APIC_SYSTEM_ID) + + # Configure option keystone_authtoken + cfg.CONF.keystone_authtoken = KEYSTONE_TOKEN # Configure the ML2 mechanism drivers and network types ml2_opts = { @@ -133,14 +153,23 @@ class ConfigMixin(object): 'tenant_network_types': ['vlan'], } for opt, val in ml2_opts.items(): - ml2_config.cfg.CONF.set_override(opt, val, 'ml2') + cfg.CONF.set_override(opt, val, 'ml2') + + # Configure the ML2 type_vlan opts + ml2_type_vlan_opts = { + 'vlan_ranges': ['physnet1:100:199'], + } + cfg.CONF.set_override('network_vlan_ranges', + ml2_type_vlan_opts['vlan_ranges'], + 'ml2_type_vlan') + self.vlan_ranges = ml2_type_vlan_opts['vlan_ranges'] # Configure the Cisco APIC mechanism driver apic_test_config = { 'apic_hosts': APIC_HOSTS, 'apic_username': APIC_USR, 'apic_password': APIC_PWD, - 'apic_vmm_domain': APIC_DOMAIN, + 'apic_domain_name': APIC_SYSTEM_ID, 'apic_vlan_ns_name': APIC_VLAN_NAME, 'apic_vlan_range': '%d:%d' % (APIC_VLANID_FROM, APIC_VLANID_TO), 'apic_node_profile': APIC_NODE_PROF, @@ -149,13 +178,43 @@ class ConfigMixin(object): } for opt, val in apic_test_config.items(): cfg.CONF.set_override(opt, val, 'ml2_cisco_apic') + self.apic_config = cfg.CONF.ml2_cisco_apic + # Configure switch topology apic_switch_cfg = { - 'apic_switch:east01': {'ubuntu1,ubuntu2': ['3/11']}, - 'apic_switch:east02': {'rhel01,rhel02': ['4/21'], - 'rhel03': ['4/22']}, + 'apic_switch:101': {'ubuntu1,ubuntu2': ['3/11']}, + 'apic_switch:102': {'rhel01,rhel02': ['4/21'], + 'rhel03': ['4/22']}, + } + self.switch_dict = { + '101': { + '3/11': ['ubuntu1', 'ubuntu2'], + }, + '102': { + '4/21': ['rhel01', 'rhel02'], + '4/22': ['rhel03'], + }, + } + self.vpc_dict = { + '201': '202', + '202': '201', } - self.mocked_parser = mock.patch.object(cfg, - 'MultiConfigParser').start() + self.external_network_dict = { + APIC_NETWORK + '-name': { + 'switch': APIC_EXT_SWITCH, + 'port': APIC_EXT_MODULE + '/' + APIC_EXT_PORT, + 'encap': APIC_EXT_ENCAP, + 'cidr_exposed': APIC_EXT_CIDR_EXPOSED, + 'gateway_ip': APIC_EXT_GATEWAY_IP, + }, + } + self.mocked_parser = mock.patch.object( + cfg, 'MultiConfigParser').start() self.mocked_parser.return_value.read.return_value = [apic_switch_cfg] self.mocked_parser.return_value.parsed = [apic_switch_cfg] + + +class FakeDbContract(object): + + def __init__(self, contract_id): + self.contract_id = contract_id diff --git a/neutron/tests/unit/ml2/drivers/cisco/apic/test_cisco_apic_mechanism_driver.py b/neutron/tests/unit/ml2/drivers/cisco/apic/test_cisco_apic_mechanism_driver.py index 8526132e0..33cc3ce22 100644 --- a/neutron/tests/unit/ml2/drivers/cisco/apic/test_cisco_apic_mechanism_driver.py +++ b/neutron/tests/unit/ml2/drivers/cisco/apic/test_cisco_apic_mechanism_driver.py @@ -15,13 +15,16 @@ # # @author: Henry Gessau, Cisco Systems -import mock import sys +import mock + sys.modules["apicapi"] = mock.Mock() +from neutron.common import constants as n_constants from neutron.extensions import portbindings from neutron.plugins.ml2.drivers.cisco.apic import mechanism_apic as md +from neutron.plugins.ml2.drivers import type_vlan # noqa from neutron.tests import base from neutron.tests.unit.ml2.drivers.cisco.apic import ( test_cisco_apic_common as mocked) @@ -50,13 +53,27 @@ class TestCiscoApicMechDriver(base.BaseTestCase, self.mock_apic_manager_login_responses() self.driver = md.APICMechanismDriver() + self.driver.synchronizer = None + md.APICMechanismDriver.get_base_synchronizer = mock.Mock() self.driver.vif_type = 'test-vif_type' self.driver.cap_port_filter = 'test-cap_port_filter' + self.driver.name_mapper = mock.Mock() + self.driver.name_mapper.tenant.return_value = mocked.APIC_TENANT + self.driver.name_mapper.network.return_value = mocked.APIC_NETWORK + self.driver.name_mapper.subnet.return_value = mocked.APIC_SUBNET + self.driver.name_mapper.port.return_value = mocked.APIC_PORT + self.driver.name_mapper.router.return_value = mocked.APIC_ROUTER + self.driver.name_mapper.app_profile.return_value = mocked.APIC_AP + self.driver.apic_manager = mock.Mock( + name_mapper=mock.Mock(), ext_net_dict=self.external_network_dict) + + self.driver.apic_manager.apic.transaction = self.fake_transaction def test_initialize(self): - mgr = self.driver.apic_manager = mock.Mock() + mgr = self.driver.apic_manager self.driver.initialize() mgr.ensure_infra_created_on_apic.assert_called_once() + mgr.ensure_bgp_pod_policy_created_on_apic.assert_called_once() def test_update_port_postcommit(self): net_ctx = self._get_network_context(mocked.APIC_TENANT, @@ -65,36 +82,95 @@ class TestCiscoApicMechDriver(base.BaseTestCase, port_ctx = self._get_port_context(mocked.APIC_TENANT, mocked.APIC_NETWORK, 'vm1', net_ctx, HOST_ID1) - mgr = self.driver.apic_manager = mock.Mock() + mgr = self.driver.apic_manager self.driver.update_port_postcommit(port_ctx) - mgr.ensure_tenant_created_on_apic.assert_called_once_with( - mocked.APIC_TENANT) mgr.ensure_path_created_for_port.assert_called_once_with( mocked.APIC_TENANT, mocked.APIC_NETWORK, HOST_ID1, - ENCAP) + ENCAP, transaction='transaction') + + def test_update_gw_port_postcommit(self): + net_ctx = self._get_network_context(mocked.APIC_TENANT, + mocked.APIC_NETWORK, + TEST_SEGMENT1, external=True) + port_ctx = self._get_port_context(mocked.APIC_TENANT, + mocked.APIC_NETWORK, + 'vm1', net_ctx, HOST_ID1, gw=True) + mgr = self.driver.apic_manager + mgr.get_router_contract.return_value = mocked.FakeDbContract( + mocked.APIC_CONTRACT) + self.driver.update_port_postcommit(port_ctx) + mgr.get_router_contract.assert_called_once_with( + port_ctx.current['device_id']) + mgr.ensure_context_enforced.assert_called_once() + mgr.ensure_external_routed_network_created.assert_called_once_with( + mocked.APIC_NETWORK, transaction='transaction') + mgr.ensure_logical_node_profile_created.assert_called_once_with( + mocked.APIC_NETWORK, mocked.APIC_EXT_SWITCH, + mocked.APIC_EXT_MODULE, mocked.APIC_EXT_PORT, + mocked.APIC_EXT_ENCAP, mocked.APIC_EXT_CIDR_EXPOSED, + transaction='transaction') + mgr.ensure_static_route_created.assert_called_once_with( + mocked.APIC_NETWORK, mocked.APIC_EXT_SWITCH, + mocked.APIC_EXT_GATEWAY_IP, transaction='transaction') + mgr.ensure_external_epg_created.assert_called_once_with( + mocked.APIC_NETWORK, transaction='transaction') + mgr.ensure_external_epg_consumed_contract.assert_called_once_with( + mocked.APIC_NETWORK, mgr.get_router_contract.return_value, + transaction='transaction') + mgr.ensure_external_epg_provided_contract.assert_called_once_with( + mocked.APIC_NETWORK, mgr.get_router_contract.return_value, + transaction='transaction') + + def test_update_gw_port_postcommit_fail_contract_create(self): + net_ctx = self._get_network_context(mocked.APIC_TENANT, + mocked.APIC_NETWORK, + TEST_SEGMENT1, external=True) + port_ctx = self._get_port_context(mocked.APIC_TENANT, + mocked.APIC_NETWORK, + 'vm1', net_ctx, HOST_ID1, gw=True) + mgr = self.driver.apic_manager + self.driver.update_port_postcommit(port_ctx) + mgr.ensure_external_routed_network_deleted.assert_called_once() def test_create_network_postcommit(self): ctx = self._get_network_context(mocked.APIC_TENANT, mocked.APIC_NETWORK, TEST_SEGMENT1) - mgr = self.driver.apic_manager = mock.Mock() + mgr = self.driver.apic_manager self.driver.create_network_postcommit(ctx) mgr.ensure_bd_created_on_apic.assert_called_once_with( - mocked.APIC_TENANT, mocked.APIC_NETWORK) - mgr.ensure_epg_created_for_network.assert_called_once_with( - mocked.APIC_TENANT, mocked.APIC_NETWORK, - mocked.APIC_NETWORK + '-name') + mocked.APIC_TENANT, mocked.APIC_NETWORK, transaction='transaction') + mgr.ensure_epg_created.assert_called_once_with( + mocked.APIC_TENANT, mocked.APIC_NETWORK, transaction='transaction') + + def test_create_external_network_postcommit(self): + ctx = self._get_network_context(mocked.APIC_TENANT, + mocked.APIC_NETWORK, + TEST_SEGMENT1, external=True) + mgr = self.driver.apic_manager + self.driver.create_network_postcommit(ctx) + self.assertFalse(mgr.ensure_bd_created_on_apic.called) + self.assertFalse(mgr.ensure_epg_created.called) def test_delete_network_postcommit(self): ctx = self._get_network_context(mocked.APIC_TENANT, mocked.APIC_NETWORK, TEST_SEGMENT1) - mgr = self.driver.apic_manager = mock.Mock() + mgr = self.driver.apic_manager self.driver.delete_network_postcommit(ctx) mgr.delete_bd_on_apic.assert_called_once_with( - mocked.APIC_TENANT, mocked.APIC_NETWORK) + mocked.APIC_TENANT, mocked.APIC_NETWORK, transaction='transaction') mgr.delete_epg_for_network.assert_called_once_with( - mocked.APIC_TENANT, mocked.APIC_NETWORK) + mocked.APIC_TENANT, mocked.APIC_NETWORK, transaction='transaction') + + def test_delete_external_network_postcommit(self): + ctx = self._get_network_context(mocked.APIC_TENANT, + mocked.APIC_NETWORK, + TEST_SEGMENT1, external=True) + mgr = self.driver.apic_manager + self.driver.delete_network_postcommit(ctx) + mgr.delete_external_routed_network.assert_called_once_with( + mocked.APIC_NETWORK) def test_create_subnet_postcommit(self): net_ctx = self._get_network_context(mocked.APIC_TENANT, @@ -103,18 +179,20 @@ class TestCiscoApicMechDriver(base.BaseTestCase, subnet_ctx = self._get_subnet_context(SUBNET_GATEWAY, SUBNET_CIDR, net_ctx) - mgr = self.driver.apic_manager = mock.Mock() + mgr = self.driver.apic_manager self.driver.create_subnet_postcommit(subnet_ctx) mgr.ensure_subnet_created_on_apic.assert_called_once_with( mocked.APIC_TENANT, mocked.APIC_NETWORK, '%s/%s' % (SUBNET_GATEWAY, SUBNET_NETMASK)) def _get_network_context(self, tenant_id, net_id, seg_id=None, - seg_type='vlan'): + seg_type='vlan', external=False): network = {'id': net_id, 'name': net_id + '-name', 'tenant_id': tenant_id, 'provider:segmentation_id': seg_id} + if external: + network['router:external'] = True if seg_id: network_segments = [{'id': seg_id, 'segmentation_id': ENCAP, @@ -132,7 +210,8 @@ class TestCiscoApicMechDriver(base.BaseTestCase, 'cidr': cidr} return FakeSubnetContext(subnet, network) - def _get_port_context(self, tenant_id, net_id, vm_id, network, host): + def _get_port_context(self, tenant_id, net_id, vm_id, network, host, + gw=False): port = {'device_id': vm_id, 'device_owner': 'compute', 'binding:host_id': host, @@ -140,6 +219,9 @@ class TestCiscoApicMechDriver(base.BaseTestCase, 'id': mocked.APIC_PORT, 'name': mocked.APIC_PORT, 'network_id': net_id} + if gw: + port['device_owner'] = n_constants.DEVICE_OWNER_ROUTER_GW + port['device_id'] = mocked.APIC_ROUTER return FakePortContext(port, network) @@ -165,6 +247,9 @@ class FakeSubnetContext(object): def __init__(self, subnet, network): self._subnet = subnet self._network = network + self._plugin = mock.Mock() + self._plugin_context = mock.Mock() + self._plugin.get_network.return_value = {} @property def current(self): @@ -179,11 +264,11 @@ class FakePortContext(object): """To generate port context for testing purposes only.""" def __init__(self, port, network): - self._fake_plugin = mock.Mock() - self._fake_plugin.get_ports.return_value = [] - self._fake_plugin_context = None self._port = port self._network = network + self._plugin = mock.Mock() + self._plugin_context = mock.Mock() + self._plugin.get_ports.return_value = [] if network.network_segments: self._bound_segment = network.network_segments[0] else: @@ -193,14 +278,6 @@ class FakePortContext(object): def current(self): return self._port - @property - def _plugin(self): - return self._fake_plugin - - @property - def _plugin_context(self): - return self._fake_plugin_context - @property def network(self): return self._network diff --git a/neutron/tests/unit/services/l3_router/test_l3_apic_plugin.py b/neutron/tests/unit/services/l3_router/test_l3_apic_plugin.py index f05b7cdac..8d829a5ea 100644 --- a/neutron/tests/unit/services/l3_router/test_l3_apic_plugin.py +++ b/neutron/tests/unit/services/l3_router/test_l3_apic_plugin.py @@ -15,12 +15,15 @@ # # @author: Arvind Somya (asomya@cisco.com), Cisco Systems -import mock import sys +import mock + sys.modules["apicapi"] = mock.Mock() from neutron.services.l3_router import l3_apic +from neutron.tests.unit.ml2.drivers.cisco.apic import ( + test_cisco_apic_common as mocked) from neutron.tests.unit import testlib_api TENANT = 'tenant1' @@ -58,10 +61,14 @@ class FakePort(object): self.subnet_id = SUBNET -class TestCiscoApicL3Plugin(testlib_api.SqlTestCase): +class TestCiscoApicL3Plugin(testlib_api.SqlTestCase, + mocked.ControllerMixin, + mocked.ConfigMixin): def setUp(self): super(TestCiscoApicL3Plugin, self).setUp() + mocked.ControllerMixin.set_up_mocks(self) + mocked.ConfigMixin.set_up_mocks(self) self.plugin = l3_apic.ApicL3ServicePlugin() self.context = FakeContext() self.context.tenant_id = TENANT