From bdf51685d79e65de7fb39b66e4c48c9c6e23bd3e Mon Sep 17 00:00:00 2001 From: Amit Bose Date: Wed, 7 Dec 2016 15:02:46 -0800 Subject: [PATCH] [AIM] Add support for physical-domain nodes Openstack hosts (compute node/network node) that do not have the OpFlex agent running are considered physical-domain nodes in the ACI fabric. Such hosts can now be supported assuming the node is capable of handling VLAN-encapsulated traffic (for instance by running an agent like the openvswitch agent). The two scenarios supported are: * Tenant network is of type VLAN * Tenant network is of type OpFlex with some ports on physical nodes. Hierarchical binding is used here with a dynamic VLAN segment. Change-Id: I25fdd31b8ca98119e7f94d40808c001b112140e5 Signed-off-by: Amit Bose --- .../drivers/apic_aim/mechanism_driver.py | 168 +++++++++- .../unit/plugins/ml2plus/test_apic_aim.py | 317 +++++++++++++++++- 2 files changed, 468 insertions(+), 17 deletions(-) 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 1146285c5..5813a881e 100644 --- a/gbpservice/neutron/plugins/ml2plus/drivers/apic_aim/mechanism_driver.py +++ b/gbpservice/neutron/plugins/ml2plus/drivers/apic_aim/mechanism_driver.py @@ -17,6 +17,7 @@ import sqlalchemy as sa from aim.aim_lib import nat_strategy from aim import aim_manager +from aim.api import infra as aim_infra from aim.api import resource as aim_resource from aim.common import utils from aim import config as aim_cfg @@ -36,6 +37,7 @@ from neutron.extensions import portbindings from neutron import manager from neutron.plugins.common import constants as pconst from neutron.plugins.ml2 import driver_api as api +from neutron.plugins.ml2 import models from opflexagent import constants as ofcst from opflexagent import rpc as ofrpc from oslo_log import log @@ -887,9 +889,6 @@ class ApicMechanismDriver(api_plus.MechanismDriver): {'port': current['id'], 'net': context.network.current['id']}) - # TODO(rkukura): Add support for baremetal hosts, SR-IOV and - # other situations requiring dynamic segments. - # Check the VNIC type. vnic_type = current.get(portbindings.VNIC_TYPE, portbindings.VNIC_NORMAL) @@ -905,8 +904,36 @@ class ApicMechanismDriver(api_plus.MechanismDriver): return # Try to bind OpFlex agent. - self._agent_bind_port(context, ofcst.AGENT_TYPE_OPFLEX_OVS, - self._opflex_bind_port) + if self._agent_bind_port(context, ofcst.AGENT_TYPE_OPFLEX_OVS, + self._opflex_bind_port): + return + + # If we reached here, it means that either there is no active opflex + # agent running on the host, or the agent on the host is not + # configured for this physical network. Treat the host as a physical + # node (i.e. has no OpFlex agent running) and try binding + # hierarchically if the network-type is OpFlex. + self._bind_physical_node(context) + + def update_port_precommit(self, context): + port = context.current + if (self._use_static_path(context.original_bottom_bound_segment) and + context.original_host != context.host): + # remove static binding for old host + self._update_static_path(context, host=context.original_host, + segment=context.original_bottom_bound_segment, remove=True) + self._release_dynamic_segment(context, use_original=True) + + if (self._is_port_bound(port) and + self._use_static_path(context.bottom_bound_segment)): + self._update_static_path(context) + + def delete_port_precommit(self, context): + port = context.current + if (self._is_port_bound(port) and + self._use_static_path(context.bottom_bound_segment)): + self._update_static_path(context, remove=True) + self._release_dynamic_segment(context) def create_floatingip(self, context, current): if current['port_id']: @@ -937,6 +964,7 @@ class ApicMechanismDriver(api_plus.MechanismDriver): for segment in context.segments_to_bind: if bind_strategy(context, segment, agent): LOG.debug("Bound using segment: %s", segment) + return True else: LOG.warning(_LW("Refusing to bind port %(port)s to dead " "agent: %(agent)s"), @@ -944,7 +972,7 @@ class ApicMechanismDriver(api_plus.MechanismDriver): def _opflex_bind_port(self, context, segment, agent): network_type = segment[api.NETWORK_TYPE] - if network_type == ofcst.TYPE_OPFLEX: + if self._is_opflex_type(network_type): opflex_mappings = agent['configurations'].get('opflex_networks') LOG.debug("Checking segment: %(segment)s " "for physical network: %(mappings)s ", @@ -955,15 +983,42 @@ class ApicMechanismDriver(api_plus.MechanismDriver): elif network_type != 'local': return False - context.set_binding(segment[api.ID], - portbindings.VIF_TYPE_OVS, - {portbindings.CAP_PORT_FILTER: False, - portbindings.OVS_HYBRID_PLUG: False}) + self._complete_binding(context, segment) + return True def _dvs_bind_port(self, context, segment, agent): # TODO(rkukura): Implement DVS port binding return False + def _bind_physical_node(self, context): + # Bind physical nodes hierarchically by creating a dynamic segment. + for segment in context.segments_to_bind: + net_type = segment[api.NETWORK_TYPE] + # TODO(amitbose) For ports on baremetal (Ironic) hosts, use + # binding:profile to decide if dynamic segment should be created. + if self._is_opflex_type(net_type): + # TODO(amitbose) Consider providing configuration options + # for picking network-type and physical-network name + # for the dynamic segment + dyn_seg = context.allocate_dynamic_segment( + {api.NETWORK_TYPE: pconst.TYPE_VLAN}) + LOG.info(_LI('Allocated dynamic-segment %(s)s for port %(p)s'), + {'s': dyn_seg, 'p': context.current['id']}) + dyn_seg['aim_ml2_created'] = True + context.continue_binding(segment[api.ID], [dyn_seg]) + return True + elif segment.get('aim_ml2_created'): + # Complete binding if another driver did not bind the + # dynamic segment that we created. + self._complete_binding(context, segment) + return True + + def _complete_binding(self, context, segment): + context.set_binding(segment[api.ID], + portbindings.VIF_TYPE_OVS, + {portbindings.CAP_PORT_FILTER: False, + portbindings.OVS_HYBRID_PLUG: False}) + @property def plugin(self): if not self._core_plugin: @@ -1042,6 +1097,19 @@ class ApicMechanismDriver(api_plus.MechanismDriver): name=aname) return bd, epg + def _map_external_network(self, session, network): + l3out, ext_net, ns = self._get_aim_nat_strategy(network) + if ext_net: + aim_ctx = aim_context.AimContext(db_session=session) + for o in (ns.get_l3outside_resources(aim_ctx, l3out) or []): + if isinstance(o, aim_resource.EndpointGroup): + return o + + def _map_network_to_epg(self, session, network): + if self._is_external(network): + return self._map_external_network(session, network) + return self._map_network(session, network)[1] + def _map_subnet(self, subnet, gw_ip, bd): prefix_len = subnet['cidr'].split('/')[1] gw_ip_mask = gw_ip + '/' + prefix_len @@ -1480,3 +1548,83 @@ class ApicMechanismDriver(api_plus.MechanismDriver): extn_db_sn.snat_host_pool.is_(None))) .all()) return [s[0] for s in other_sn] + + def _is_opflex_type(self, net_type): + return net_type == ofcst.TYPE_OPFLEX + + def _is_supported_non_opflex_type(self, net_type): + return net_type in [pconst.TYPE_VLAN] + + def _use_static_path(self, bound_segment): + return (bound_segment and + self._is_supported_non_opflex_type( + bound_segment[api.NETWORK_TYPE])) + + def _update_static_path(self, port_context, host=None, segment=None, + remove=False): + host = host or port_context.host + segment = segment or port_context.bottom_bound_segment + session = port_context._plugin_context.session + + if not segment: + LOG.debug('Port %s is not bound to any segment', + port_context.current['id']) + return + if remove: + # check if there are any other ports from this network on the host + exist = (session.query(models.PortBindingLevel) + .filter_by(host=host, segment_id=segment['id']) + .filter(models.PortBindingLevel.port_id != + port_context.current['id']) + .first()) + if exist: + return + else: + if (segment.get(api.NETWORK_TYPE) in [pconst.TYPE_VLAN]): + seg = segment[api.SEGMENTATION_ID] + else: + LOG.info(_LI('Unsupported segmentation type for static path ' + 'binding: %s'), + segment.get(api.NETWORK_TYPE)) + return + + aim_ctx = aim_context.AimContext(db_session=session) + host_link = self.aim.find(aim_ctx, aim_infra.HostLink, host_name=host) + if not host_link or not host_link[0].path: + LOG.warning(_LW('No host link information found for host %s'), + host) + return + host_link = host_link[0].path + + epg = self._map_network_to_epg(session, port_context.network.current) + if not epg: + LOG.info(_LI('Network %s does not map to any EPG'), + port_context.network.current['id']) + return + epg = self.aim.get(aim_ctx, epg) + static_paths = [p for p in epg.static_paths + if p.get('path') != host_link] + if not remove: + static_paths.append({'path': host_link, 'encap': 'vlan-%s' % seg}) + LOG.debug('Setting static paths for EPG %s to %s', epg, static_paths) + self.aim.update(aim_ctx, epg, static_paths=static_paths) + + def _release_dynamic_segment(self, port_context, use_original=False): + top = (port_context.original_top_bound_segment if use_original + else port_context.top_bound_segment) + btm = (port_context.original_bottom_bound_segment if use_original + else port_context.bottom_bound_segment) + if (top and btm and + self._is_opflex_type(top[api.NETWORK_TYPE]) and + self._is_supported_non_opflex_type(btm[api.NETWORK_TYPE])): + # if there are no other ports bound to segment, release the segment + ports = (port_context._plugin_context.session + .query(models.PortBindingLevel) + .filter_by(segment_id=btm[api.ID]) + .filter(models.PortBindingLevel.port_id != + port_context.current['id']) + .first()) + if not ports: + LOG.info(_LI('Releasing dynamic-segment %(s)s for port %(p)s'), + {'s': btm, 'p': port_context.current['id']}) + port_context.release_dynamic_segment(btm[api.ID]) 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 3f99742e2..f78d843df 100644 --- a/gbpservice/neutron/tests/unit/plugins/ml2plus/test_apic_aim.py +++ b/gbpservice/neutron/tests/unit/plugins/ml2plus/test_apic_aim.py @@ -18,6 +18,7 @@ import netaddr from aim.aim_lib import nat_strategy from aim import aim_manager +from aim.api import infra as aim_infra from aim.api import resource as aim_resource from aim.api import status as aim_status from aim import config as aim_cfg @@ -28,11 +29,13 @@ from gbpservice.neutron.plugins.ml2plus.drivers.apic_aim import ( extension_db as extn_db) from keystoneclient.v3 import client as ksc_client from neutron.api import extensions +from neutron.common import constants as n_constants from neutron import context from neutron.db import api as db_api from neutron import manager from neutron.plugins.common import constants as service_constants from neutron.plugins.ml2 import config +from neutron.plugins.ml2 import db as ml2_db 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 @@ -51,6 +54,12 @@ AGENT_CONF_OPFLEX = {'alive': True, 'binary': 'somebinary', 'opflex_networks': None, 'bridge_mappings': {'physnet1': 'br-eth1'}}} +AGENT_CONF_OVS = {'alive': True, 'binary': 'somebinary', + 'topic': 'sometopic', + 'agent_type': n_constants.AGENT_TYPE_OVS, + 'configurations': { + 'bridge_mappings': {'physnet1': 'br-eth1'}}} + DN = 'apic:distinguished_names' CIDR = 'apic:external_cidrs' PROV = 'apic:external_provided_contracts' @@ -111,22 +120,20 @@ class ApicAimTestMixin(object): class ApicAimTestCase(test_address_scope.AddressScopeTestCase, test_l3.L3NatTestCaseMixin, ApicAimTestMixin): - def setUp(self): + def setUp(self, mechanism_drivers=None, tenant_network_types=None): # Enable the test mechanism driver to ensure that # we can successfully call through to all mechanism # driver apis. - config.cfg.CONF.set_override('mechanism_drivers', - ['logger', 'apic_aim'], - 'ml2') + mech = mechanism_drivers or ['logger', 'apic_aim'] + config.cfg.CONF.set_override('mechanism_drivers', mech, 'ml2') config.cfg.CONF.set_override('extension_drivers', ['apic_aim'], 'ml2') config.cfg.CONF.set_override('type_drivers', ['opflex', 'local', 'vlan'], 'ml2') - config.cfg.CONF.set_override('tenant_network_types', - ['opflex'], - 'ml2') + net_type = tenant_network_types or ['opflex'] + config.cfg.CONF.set_override('tenant_network_types', net_type, 'ml2') config.cfg.CONF.set_override('network_vlan_ranges', ['physnet1:1000:1099'], group='ml2_type_vlan') @@ -2304,3 +2311,299 @@ class TestSnatIpAllocation(ApicAimTestCase): for x in range(0, 8): fip = self._make_floatingip(self.fmt, ext_net['id'])['floatingip'] self.assertTrue(fip['floating_ip_address'] in ips) + + +class TestPortVlanNetwork(ApicAimTestCase): + + def setUp(self, **kwargs): + if kwargs.get('mechanism_drivers') is None: + kwargs['mechanism_drivers'] = ['logger', 'openvswitch', 'apic_aim'] + if kwargs.get('tenant_network_types') is None: + kwargs['tenant_network_types'] = ['vlan'] + super(TestPortVlanNetwork, self).setUp(**kwargs) + + aim_ctx = aim_context.AimContext(self.db_session) + self.hlink1 = aim_infra.HostLink( + host_name='h1', + interface_name='eth0', + path='topology/pod-1/paths-102/pathep-[eth1/7]') + self._register_agent('h1', AGENT_CONF_OVS) + self.aim_mgr.create(aim_ctx, self.hlink1) + + self.expected_binding_info = [('openvswitch', 'vlan')] + + def _net_2_epg(self, network): + if network['router:external']: + epg = aim_resource.EndpointGroup.from_dn( + network['apic:distinguished_names']['EndpointGroup']) + else: + epg = aim_resource.EndpointGroup( + tenant_name=network['tenant_id'], + app_profile_name=self._app_profile_name, + name=network['id']) + return epg + + def _check_binding(self, port_id, expected_binding_info=None): + port_context = self.plugin.get_bound_port_context( + context.get_admin_context(), port_id) + self.assertIsNotNone(port_context) + binding_info = [(bl['bound_driver'], + bl['bound_segment']['network_type']) + for bl in port_context.binding_levels] + self.assertEqual(expected_binding_info or self.expected_binding_info, + binding_info) + return port_context.bottom_bound_segment['segmentation_id'] + + def _check_no_dynamic_segment(self, network_id): + dyn_segments = ml2_db.get_network_segments( + context.get_admin_context().session, network_id, + filter_dynamic=True) + self.assertEqual(0, len(dyn_segments)) + + def _do_test_port_lifecycle(self, external_net=False): + aim_ctx = aim_context.AimContext(self.db_session) + + if external_net: + net1 = self._make_ext_network('net1', + dn='uni/tn-t1/out-l1/instP-n1') + else: + net1 = self._make_network(self.fmt, 'net1', True)['network'] + + hlink2 = aim_infra.HostLink( + host_name='h2', + interface_name='eth0', + path='topology/pod-1/paths-201/pathep-[eth1/19]') + self.aim_mgr.create(aim_ctx, hlink2) + self._register_agent('h2', AGENT_CONF_OVS) + + epg = self._net_2_epg(net1) + with self.subnet(network={'network': net1}) as sub1: + with self.port(subnet=sub1) as p1: + # unbound port -> no static paths expected + epg = self.aim_mgr.get(aim_ctx, epg) + self.assertEqual([], epg.static_paths) + + # bind to host h1 + p1 = self._bind_port_to_host(p1['port']['id'], 'h1') + vlan_h1 = self._check_binding(p1['port']['id']) + epg = self.aim_mgr.get(aim_ctx, epg) + self.assertEqual( + [{'path': self.hlink1.path, 'encap': 'vlan-%s' % vlan_h1}], + epg.static_paths) + + # move port to host h2 + p1 = self._bind_port_to_host(p1['port']['id'], 'h2') + vlan_h2 = self._check_binding(p1['port']['id']) + epg = self.aim_mgr.get(aim_ctx, epg) + self.assertEqual( + [{'path': hlink2.path, 'encap': 'vlan-%s' % vlan_h2}], + epg.static_paths) + + # delete port + self._delete('ports', p1['port']['id']) + self._check_no_dynamic_segment(net1['id']) + epg = self.aim_mgr.get(aim_ctx, epg) + self.assertEqual([], epg.static_paths) + + def test_port_lifecycle_internal_network(self): + self._do_test_port_lifecycle() + + def test_port_lifecycle_external_network(self): + self._do_test_port_lifecycle(external_net=True) + + def test_multiple_ports_on_host(self): + aim_ctx = aim_context.AimContext(self.db_session) + + net1 = self._make_network(self.fmt, 'net1', True)['network'] + epg = self._net_2_epg(net1) + with self.subnet(network={'network': net1}) as sub1: + with self.port(subnet=sub1) as p1: + # bind p1 to host h1 + p1 = self._bind_port_to_host(p1['port']['id'], 'h1') + vlan_p1 = self._check_binding(p1['port']['id']) + epg = self.aim_mgr.get(aim_ctx, epg) + self.assertEqual( + [{'path': self.hlink1.path, 'encap': 'vlan-%s' % vlan_p1}], + epg.static_paths) + + with self.port(subnet=sub1) as p2: + # bind p2 to host h1 + p2 = self._bind_port_to_host(p2['port']['id'], 'h1') + vlan_p2 = self._check_binding(p2['port']['id']) + self.assertEqual(vlan_p1, vlan_p2) + epg = self.aim_mgr.get(aim_ctx, epg) + self.assertEqual( + [{'path': self.hlink1.path, + 'encap': 'vlan-%s' % vlan_p2}], + epg.static_paths) + + self._delete('ports', p2['port']['id']) + self._check_binding(p1['port']['id']) + epg = self.aim_mgr.get(aim_ctx, epg) + self.assertEqual( + [{'path': self.hlink1.path, + 'encap': 'vlan-%s' % vlan_p1}], + epg.static_paths) + + self._delete('ports', p1['port']['id']) + self._check_no_dynamic_segment(net1['id']) + epg = self.aim_mgr.get(aim_ctx, epg) + self.assertEqual([], epg.static_paths) + + def test_multiple_networks_on_host(self): + aim_ctx = aim_context.AimContext(self.db_session) + + net1 = self._make_network(self.fmt, 'net1', True)['network'] + epg1 = self._net_2_epg(net1) + + with self.subnet(network={'network': net1}) as sub1: + with self.port(subnet=sub1) as p1: + # bind p1 to host h1 + p1 = self._bind_port_to_host(p1['port']['id'], 'h1') + vlan_p1 = self._check_binding(p1['port']['id']) + + epg1 = self.aim_mgr.get(aim_ctx, epg1) + self.assertEqual( + [{'path': self.hlink1.path, 'encap': 'vlan-%s' % vlan_p1}], + epg1.static_paths) + + net2 = self._make_network(self.fmt, 'net2', True)['network'] + epg2 = self._net_2_epg(net2) + + with self.subnet(network={'network': net2}) as sub2: + with self.port(subnet=sub2) as p2: + # bind p2 to host h1 + p2 = self._bind_port_to_host(p2['port']['id'], 'h1') + vlan_p2 = self._check_binding(p2['port']['id']) + + self.assertNotEqual(vlan_p1, vlan_p2) + + epg2 = self.aim_mgr.get(aim_ctx, epg2) + self.assertEqual( + [{'path': self.hlink1.path, 'encap': 'vlan-%s' % vlan_p2}], + epg2.static_paths) + + self._delete('ports', p2['port']['id']) + epg2 = self.aim_mgr.get(aim_ctx, epg2) + self._check_no_dynamic_segment(net2['id']) + self.assertEqual([], epg2.static_paths) + + epg1 = self.aim_mgr.get(aim_ctx, epg1) + self.assertEqual( + [{'path': self.hlink1.path, 'encap': 'vlan-%s' % vlan_p1}], + epg1.static_paths) + + def test_network_on_multiple_hosts(self): + aim_ctx = aim_context.AimContext(self.db_session) + + net1 = self._make_network(self.fmt, 'net1', True)['network'] + epg1 = self._net_2_epg(net1) + + hlink2 = aim_infra.HostLink( + host_name='h2', + interface_name='eth0', + path='topology/pod-1/paths-201/pathep-[eth1/19]') + self.aim_mgr.create(aim_ctx, hlink2) + self._register_agent('h2', AGENT_CONF_OVS) + + with self.subnet(network={'network': net1}) as sub1: + with self.port(subnet=sub1) as p1: + p1 = self._bind_port_to_host(p1['port']['id'], 'h1') + vlan_p1 = self._check_binding(p1['port']['id']) + with self.port(subnet=sub1) as p2: + p2 = self._bind_port_to_host(p2['port']['id'], 'h2') + vlan_p2 = self._check_binding(p2['port']['id']) + + self.assertEqual(vlan_p1, vlan_p2) + + epg1 = self.aim_mgr.get(aim_ctx, epg1) + self.assertEqual( + [{'path': self.hlink1.path, 'encap': 'vlan-%s' % vlan_p1}, + {'path': hlink2.path, 'encap': 'vlan-%s' % vlan_p2}], + sorted(epg1.static_paths, key=lambda x: x['path'])) + + self._delete('ports', p2['port']['id']) + epg1 = self.aim_mgr.get(aim_ctx, epg1) + self.assertEqual( + [{'path': self.hlink1.path, 'encap': 'vlan-%s' % vlan_p1}], + epg1.static_paths) + + self._delete('ports', p1['port']['id']) + epg1 = self.aim_mgr.get(aim_ctx, epg1) + self._check_no_dynamic_segment(net1['id']) + self.assertEqual([], epg1.static_paths) + + def test_port_binding_missing_hostlink(self): + aim_ctx = aim_context.AimContext(self.db_session) + + net1 = self._make_network(self.fmt, 'net1', True)['network'] + epg1 = self._net_2_epg(net1) + + self._register_agent('h-42', AGENT_CONF_OVS) + + with self.subnet(network={'network': net1}) as sub1: + with self.port(subnet=sub1) as p1: + p1 = self._bind_port_to_host(p1['port']['id'], 'h-42') + self._check_binding(p1['port']['id']) + + epg1 = self.aim_mgr.get(aim_ctx, epg1) + self.assertEqual([], epg1.static_paths) + + hlink42 = aim_infra.HostLink(host_name='h42', + interface_name='eth0') + self.aim_mgr.create(aim_ctx, hlink42) + with self.port(subnet=sub1) as p2: + p2 = self._bind_port_to_host(p2['port']['id'], 'h-42') + self._check_binding(p2['port']['id']) + + epg1 = self.aim_mgr.get(aim_ctx, epg1) + self.assertEqual([], epg1.static_paths) + + +class TestPortOnPhysicalNode(TestPortVlanNetwork): + # Tests for binding port on physical node where another ML2 mechanism + # driver completes port binding. + + def setUp(self, mechanism_drivers=None): + super(TestPortOnPhysicalNode, self).setUp( + mechanism_drivers=mechanism_drivers, + tenant_network_types=['opflex']) + self.expected_binding_info = [('apic_aim', 'opflex'), + ('openvswitch', 'vlan')] + + def test_mixed_ports_on_network(self): + aim_ctx = aim_context.AimContext(self.db_session) + + self._register_agent('opflex-1', AGENT_CONF_OPFLEX) + + net1 = self._make_network(self.fmt, 'net1', True)['network'] + epg1 = self._net_2_epg(net1) + + with self.subnet(network={'network': net1}) as sub1: + # "normal" port on opflex host + with self.port(subnet=sub1) as p1: + p1 = self._bind_port_to_host(p1['port']['id'], 'opflex-1') + self._check_binding(p1['port']['id'], + expected_binding_info=[('apic_aim', 'opflex')]) + epg1 = self.aim_mgr.get(aim_ctx, epg1) + self.assertEqual([], epg1.static_paths) + + # port on non-opflex host + with self.port(subnet=sub1) as p2: + p2 = self._bind_port_to_host(p2['port']['id'], 'h1') + vlan_p2 = self._check_binding(p2['port']['id']) + epg1 = self.aim_mgr.get(aim_ctx, epg1) + self.assertEqual( + [{'path': self.hlink1.path, 'encap': 'vlan-%s' % vlan_p2}], + epg1.static_paths) + + +class TestPortOnPhysicalNodeSingleDriver(TestPortOnPhysicalNode): + # Tests for binding port on physical node where no other ML2 mechanism + # driver fulfills port binding. + + def setUp(self, service_plugins=None): + super(TestPortOnPhysicalNodeSingleDriver, self).setUp( + mechanism_drivers=['logger', 'apic_aim']) + self.expected_binding_info = [('apic_aim', 'opflex'), + ('apic_aim', 'vlan')]