diff --git a/etc/neutron/plugins/ml2/ml2_conf_cisco.ini b/etc/neutron/plugins/ml2/ml2_conf_cisco.ini index fced7bb6d2..c2582e0835 100644 --- a/etc/neutron/plugins/ml2/ml2_conf_cisco.ini +++ b/etc/neutron/plugins/ml2/ml2_conf_cisco.ini @@ -75,6 +75,8 @@ # apic_app_profile_name = openstack_app # Agent timers for State reporting and topology discovery # apic_sync_interval = 30 +# apic_agent_report_interval = 30 +# apic_agent_poll_interval = 2 # Specify your network topology. # This section indicates how your compute nodes are connected to the fabric's diff --git a/etc/neutron/rootwrap.d/cisco-apic.filters b/etc/neutron/rootwrap.d/cisco-apic.filters new file mode 100644 index 0000000000..69e4afcc89 --- /dev/null +++ b/etc/neutron/rootwrap.d/cisco-apic.filters @@ -0,0 +1,16 @@ +# neutron-rootwrap command filters for nodes on which neutron is +# expected to control network +# +# This file should be owned by (and only-writeable by) the root user + +# format seems to be +# cmd-name: filter-name, raw-command, user, args + +[Filters] + +# cisco-apic filters +lldpctl: CommandFilter, lldpctl, root + +# ip_lib filters +ip: IpFilter, ip, root +ip_exec: IpNetnsExecFilter, ip, root diff --git a/neutron/plugins/ml2/drivers/cisco/apic/apic_topology.py b/neutron/plugins/ml2/drivers/cisco/apic/apic_topology.py new file mode 100644 index 0000000000..8f08175cb5 --- /dev/null +++ b/neutron/plugins/ml2/drivers/cisco/apic/apic_topology.py @@ -0,0 +1,355 @@ +# Copyright (c) 2014 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. +# +# @author: Mandeep Dhami (dhami@noironetworks.com), Cisco Systems Inc. + +import re +import sys + +import eventlet + +eventlet.monkey_patch() + +from oslo.config import cfg + +from neutron.agent.common import config +from neutron.agent.linux import ip_lib +from neutron.agent.linux import utils +from neutron.common import config as common_cfg +from neutron.common import rpc +from neutron.common import utils as neutron_utils +from neutron.db import agents_db +from neutron import manager +from neutron.openstack.common.gettextutils import _LE, _LI +from neutron.openstack.common import lockutils +from neutron.openstack.common import log as logging +from neutron.openstack.common import periodic_task +from neutron.openstack.common import service as svc +from neutron.plugins.ml2.drivers.cisco.apic import mechanism_apic as ma +from neutron.plugins.ml2.drivers import type_vlan # noqa + +from neutron import service + +ACI_PORT_DESCR_FORMATS = [ + 'topology/pod-1/node-(\d+)/sys/conng/path-\[eth(\d+)/(\d+)\]', + 'topology/pod-1/paths-(\d+)/pathep-\[eth(\d+)/(\d+)\]', +] +AGENT_FORCE_UPDATE_COUNT = 100 +BINARY_APIC_SERVICE_AGENT = 'neutron-cisco-apic-service-agent' +BINARY_APIC_HOST_AGENT = 'neutron-cisco-apic-host-agent' +TOPIC_APIC_SERVICE = 'apic-service' +TYPE_APIC_SERVICE_AGENT = 'cisco-apic-service-agent' +TYPE_APIC_HOST_AGENT = 'cisco-apic-host-agent' + + +LOG = logging.getLogger(__name__) + + +class ApicTopologyService(manager.Manager): + + RPC_API_VERSION = '1.1' + + def __init__(self, host=None): + if host is None: + host = neutron_utils.get_hostname() + super(ApicTopologyService, self).__init__(host=host) + + self.conf = cfg.CONF.ml2_cisco_apic + self.conn = None + self.peers = {} + self.invalid_peers = [] + self.dispatcher = None + self.state = None + self.state_agent = None + self.topic = TOPIC_APIC_SERVICE + self.apic_manager = ma.APICMechanismDriver.get_apic_manager(False) + + def init_host(self): + LOG.info(_LI("APIC service agent starting ...")) + self.state = { + 'binary': BINARY_APIC_SERVICE_AGENT, + 'host': self.host, + 'topic': self.topic, + 'configurations': {}, + 'start_flag': True, + 'agent_type': TYPE_APIC_SERVICE_AGENT, + } + + self.conn = rpc.create_connection(new=True) + self.dispatcher = [self, agents_db.AgentExtRpcCallback()] + self.conn.create_consumer( + self.topic, self.dispatcher, fanout=True) + self.conn.consume_in_threads() + + def after_start(self): + LOG.info(_LI("APIC service agent started")) + + def report_send(self, context): + if not self.state_agent: + return + LOG.debug("APIC service agent: sending report state") + + try: + self.state_agent.report_state(context, self.state) + self.state.pop('start_flag', None) + except AttributeError: + # This means the server does not support report_state + # ignore it + return + except Exception: + LOG.exception(_LE("APIC service agent: failed in reporting state")) + + @lockutils.synchronized('apic_service') + def update_link(self, context, + host, interface, mac, + switch, module, port): + LOG.debug("APIC service agent: received update_link: %s", + ", ".join(map(str, + [host, interface, mac, switch, module, port]))) + + nlink = (host, interface, mac, switch, module, port) + clink = self.peers.get((host, interface), None) + + if switch == 0: + # this is a link delete, remove it + if clink is not None: + self.apic_manager.remove_hostlink(*clink) + self.peers.pop((host, interface)) + else: + if clink is None: + # add new link to database + self.apic_manager.add_hostlink(*nlink) + self.peers[(host, interface)] = nlink + elif clink != nlink: + # delete old link and add new one (don't update in place) + self.apic_manager.remove_hostlink(*clink) + self.peers.pop((host, interface)) + self.apic_manager.add_hostlink(*nlink) + self.peers[(host, interface)] = nlink + + +class ApicTopologyServiceNotifierApi(rpc.RpcProxy): + + RPC_API_VERSION = '1.1' + + def __init__(self): + super(ApicTopologyServiceNotifierApi, self).__init__( + topic=TOPIC_APIC_SERVICE, + default_version=self.RPC_API_VERSION) + + def update_link(self, context, host, interface, mac, switch, module, port): + self.fanout_cast( + context, self.make_msg( + 'update_link', + host=host, interface=interface, mac=mac, + switch=switch, module=module, port=port), + topic=TOPIC_APIC_SERVICE) + + def delete_link(self, context, host, interface): + self.fanout_cast( + context, self.make_msg( + 'delete_link', + host=host, interface=interface, mac=None, + switch=0, module=0, port=0), + topic=TOPIC_APIC_SERVICE) + + +class ApicTopologyAgent(manager.Manager): + def __init__(self, host=None): + if host is None: + host = neutron_utils.get_hostname() + super(ApicTopologyAgent, self).__init__(host=host) + + self.conf = cfg.CONF.ml2_cisco_apic + self.count_current = 0 + self.count_force_send = AGENT_FORCE_UPDATE_COUNT + self.interfaces = {} + self.lldpcmd = None + self.peers = {} + self.port_desc_re = map(re.compile, ACI_PORT_DESCR_FORMATS) + self.root_helper = self.conf.root_helper + self.service_agent = ApicTopologyServiceNotifierApi() + self.state = None + self.state_agent = None + self.topic = TOPIC_APIC_SERVICE + self.uplink_ports = [] + self.invalid_peers = [] + + def init_host(self): + LOG.info(_LI("APIC host agent: agent starting on %s"), self.host) + self.state = { + 'binary': BINARY_APIC_HOST_AGENT, + 'host': self.host, + 'topic': self.topic, + 'configurations': {}, + 'start_flag': True, + 'agent_type': TYPE_APIC_HOST_AGENT, + } + + self.uplink_ports = [] + for inf in self.conf.apic_host_uplink_ports: + if ip_lib.device_exists(inf): + self.uplink_ports.append(inf) + else: + # ignore unknown interfaces + LOG.error(_LE("No such interface (ignored): %s"), inf) + self.lldpcmd = ['lldpctl', '-f', 'keyvalue'] + self.uplink_ports + + def after_start(self): + LOG.info(_LI("APIC host agent: started on %s"), self.host) + + @periodic_task.periodic_task + def _check_for_new_peers(self, context): + LOG.debug("APIC host agent: _check_for_new_peers") + + if not self.lldpcmd: + return + try: + # Check if we must send update even if there is no change + force_send = False + self.count_current += 1 + if self.count_current >= self.count_force_send: + force_send = True + self.count_current = 0 + + # Check for new peers + new_peers = self._get_peers() + new_peers = self._valid_peers(new_peers) + + # Make a copy of current interfaces + curr_peers = {} + for interface in self.peers: + curr_peers[interface] = self.peers[interface] + # Based curr -> new updates, add the new interfaces + self.peers = {} + for interface in new_peers: + peer = new_peers[interface] + self.peers[interface] = peer + if (interface in curr_peers and + curr_peers[interface] != peer): + self.service_agent.update_link( + context, peer[0], peer[1], None, 0, 0, 0) + if (interface not in curr_peers or + curr_peers[interface] != peer or + force_send): + self.service_agent.update_link(context, *peer) + if interface in curr_peers: + curr_peers.pop(interface) + + # Any interface still in curr_peers need to be deleted + for peer in curr_peers.values(): + self.service_agent.update_link( + context, peer[0], peer[1], None, 0, 0, 0) + + except Exception: + LOG.exception(_LE("APIC service agent: exception in LLDP parsing")) + + def _get_peers(self): + peers = {} + lldpkeys = utils.execute(self.lldpcmd, self.root_helper) + for line in lldpkeys.splitlines(): + if '=' not in line: + continue + fqkey, value = line.split('=', 1) + lldp, interface, key = fqkey.split('.', 2) + if key == 'port.descr': + for regexp in self.port_desc_re: + match = regexp.match(value) + if match: + mac = self._get_mac(interface) + switch, module, port = match.group(1, 2, 3) + peer = (self.host, interface, mac, + switch, module, port) + if interface not in peers: + peers[interface] = [] + peers[interface].append(peer) + return peers + + def _valid_peers(self, peers): + # Reduce the peers array to one valid peer per interface + # NOTE: + # There is a bug in lldpd daemon that it keeps reporting + # old peers even after their updates have stopped + # we keep track of that report remove them from peers + + valid_peers = {} + invalid_peers = [] + for interface in peers: + curr_peer = None + for peer in peers[interface]: + if peer in self.invalid_peers or curr_peer: + invalid_peers.append(peer) + else: + curr_peer = peer + if curr_peer is not None: + valid_peers[interface] = curr_peer + + self.invalid_peers = invalid_peers + return valid_peers + + def _get_mac(self, interface): + if interface in self.interfaces: + return self.interfaces[interface] + try: + mac = ip_lib.IPDevice(interface).link.address + self.interfaces[interface] = mac + return mac + except Exception: + # we can safely ignore it, it is only needed for debugging + LOG.exception( + _LE("APIC service agent: can not get MACaddr for %s"), + interface) + + def report_send(self, context): + if not self.state_agent: + return + LOG.debug("APIC host agent: sending report state") + + try: + self.state_agent.report_state(context, self.state) + self.state.pop('start_flag', None) + except AttributeError: + # This means the server does not support report_state + # ignore it + return + except Exception: + LOG.exception(_LE("APIC host agent: failed in reporting state")) + + +def launch(binary, manager, topic=None): + cfg.CONF(project='neutron') + common_cfg.init(sys.argv[1:]) + config.setup_logging(cfg.CONF) + report_period = cfg.CONF.ml2_cisco_apic.apic_agent_report_interval + poll_period = cfg.CONF.ml2_cisco_apic.apic_agent_poll_interval + server = service.Service.create( + binary=binary, manager=manager, topic=topic, + report_interval=report_period, periodic_interval=poll_period) + svc.launch(server).wait() + + +def service_main(): + launch( + BINARY_APIC_SERVICE_AGENT, + 'neutron.plugins.ml2.drivers.' + + 'cisco.apic.apic_topology.ApicTopologyService', + TOPIC_APIC_SERVICE) + + +def agent_main(): + launch( + BINARY_APIC_HOST_AGENT, + 'neutron.plugins.ml2.drivers.' + + 'cisco.apic.apic_topology.ApicTopologyAgent') diff --git a/neutron/plugins/ml2/drivers/cisco/apic/config.py b/neutron/plugins/ml2/drivers/cisco/apic/config.py index 178a1e9a8f..c1e1bc2d08 100644 --- a/neutron/plugins/ml2/drivers/cisco/apic/config.py +++ b/neutron/plugins/ml2/drivers/cisco/apic/config.py @@ -84,6 +84,12 @@ apic_opts = [ cfg.IntOpt('apic_sync_interval', default=0, help=_("Synchronization interval in seconds")), + cfg.FloatOpt('apic_agent_report_interval', + default=30, + help=_('Interval between agent status updates (in sec)')), + cfg.FloatOpt('apic_agent_poll_interval', + default=2, + help=_('Interval between agent poll for topology (in sec)')), ] 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 7e2e9c4a4e..ac7e008f65 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 @@ -79,6 +79,18 @@ APIC_KEY = 'key' KEYSTONE_TOKEN = '123Token123' +APIC_UPLINK_PORTS = ['uplink_port'] + +SERVICE_HOST = 'host1' +SERVICE_HOST_IFACE = 'eth0' +SERVICE_HOST_MAC = 'aa:ee:ii:oo:uu:yy' + +SERVICE_PEER_CHASSIS_NAME = 'leaf4' +SERVICE_PEER_CHASSIS = 'topology/pod-1/node-' + APIC_EXT_SWITCH +SERVICE_PEER_PORT_LOCAL = 'Eth%s/%s' % (APIC_EXT_MODULE, APIC_EXT_PORT) +SERVICE_PEER_PORT_DESC = ('topology/pod-1/paths-%s/pathep-[%s]' % + (APIC_EXT_SWITCH, SERVICE_PEER_PORT_LOCAL.lower())) + class ControllerMixin(object): @@ -175,6 +187,7 @@ class ConfigMixin(object): 'apic_node_profile': APIC_NODE_PROF, 'apic_entity_profile': APIC_ATT_ENT_PROF, 'apic_function_profile': APIC_FUNC_PROF, + 'apic_host_uplink_ports': APIC_UPLINK_PORTS } for opt, val in apic_test_config.items(): cfg.CONF.set_override(opt, val, 'ml2_cisco_apic') diff --git a/neutron/tests/unit/ml2/drivers/cisco/apic/test_cisco_apic_topology_agent.py b/neutron/tests/unit/ml2/drivers/cisco/apic/test_cisco_apic_topology_agent.py new file mode 100644 index 0000000000..5c1a10a9c1 --- /dev/null +++ b/neutron/tests/unit/ml2/drivers/cisco/apic/test_cisco_apic_topology_agent.py @@ -0,0 +1,208 @@ +# Copyright (c) 2014 Cisco Systems +# 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. +# +# @author: Ivar Lazzaro (ivarlazzaro@gmail.com), Cisco Systems, Inc. + +import sys + +import mock + +sys.modules["apicapi"] = mock.Mock() + +from neutron.plugins.ml2.drivers.cisco.apic import apic_topology +from neutron.tests import base +from neutron.tests.unit.ml2.drivers.cisco.apic import ( + test_cisco_apic_common as mocked) + +NOTIFIER = ('neutron.plugins.ml2.drivers.cisco.apic.' + 'apic_topology.ApicTopologyServiceNotifierApi') +RPC_CONNECTION = 'neutron.common.rpc.Connection' +AGENTS_DB = 'neutron.db.agents_db' +PERIODIC_TASK = 'neutron.openstack.common.periodic_task' +DEV_EXISTS = 'neutron.agent.linux.ip_lib.device_exists' +IP_DEVICE = 'neutron.agent.linux.ip_lib.IPDevice' +EXECUTE = 'neutron.agent.linux.utils.execute' + +LLDP_CMD = ['lldpctl', '-f', 'keyvalue'] +ETH0 = mocked.SERVICE_HOST_IFACE + +LLDPCTL_RES = ( + 'lldp.' + ETH0 + '.via=LLDP\n' + 'lldp.' + ETH0 + '.rid=1\n' + 'lldp.' + ETH0 + '.age=0 day, 20:55:54\n' + 'lldp.' + ETH0 + '.chassis.mac=' + mocked.SERVICE_HOST_MAC + '\n' + 'lldp.' + ETH0 + '.chassis.name=' + mocked.SERVICE_PEER_CHASSIS_NAME + '\n' + 'lldp.' + ETH0 + '.chassis.descr=' + mocked.SERVICE_PEER_CHASSIS + '\n' + 'lldp.' + ETH0 + '.chassis.Bridge.enabled=on\n' + 'lldp.' + ETH0 + '.chassis.Router.enabled=on\n' + 'lldp.' + ETH0 + '.port.local=' + mocked.SERVICE_PEER_PORT_LOCAL + '\n' + 'lldp.' + ETH0 + '.port.descr=' + mocked.SERVICE_PEER_PORT_DESC) + + +class TestCiscoApicTopologyService(base.BaseTestCase, + mocked.ControllerMixin, + mocked.ConfigMixin): + + def setUp(self): + super(TestCiscoApicTopologyService, self).setUp() + mocked.ControllerMixin.set_up_mocks(self) + mocked.ConfigMixin.set_up_mocks(self) + # Patch notifier + notifier_c = mock.patch(NOTIFIER).start() + self.notifier = mock.Mock() + notifier_c.return_value = self.notifier + # Patch Connection + connection_c = mock.patch(RPC_CONNECTION).start() + self.connection = mock.Mock() + connection_c.return_value = self.connection + # Patch agents db + self.agents_db = mock.patch(AGENTS_DB).start() + self.service = apic_topology.ApicTopologyService() + self.service.apic_manager = mock.Mock() + + def test_init_host(self): + self.service.init_host() + self.connection.create_consumer.ensure_called_once() + self.connection.consume_in_threads.ensure_called_once() + + def test_update_link_add_nopeers(self): + self.service.peers = {} + args = (mocked.SERVICE_HOST, mocked.SERVICE_HOST_IFACE, + mocked.SERVICE_HOST_MAC, mocked.APIC_EXT_SWITCH, + mocked.APIC_EXT_MODULE, mocked.APIC_EXT_PORT) + self.service.update_link(None, *args) + self.service.apic_manager.add_hostlink.assert_called_once_with(*args) + self.assertEqual(args, + self.service.peers[(mocked.SERVICE_HOST, + mocked.SERVICE_HOST_IFACE)]) + + def test_update_link_add_with_peers_diff(self): + args = (mocked.SERVICE_HOST, mocked.SERVICE_HOST_IFACE, + mocked.SERVICE_HOST_MAC, mocked.APIC_EXT_SWITCH, + mocked.APIC_EXT_MODULE, mocked.APIC_EXT_PORT) + args_prime = args[:2] + tuple(x + '1' for x in args[2:]) + self.service.peers = {args_prime[:2]: args_prime} + self.service.update_link(None, *args) + self.service.apic_manager.remove_hostlink.assert_called_once_with( + *args_prime) + self.service.apic_manager.add_hostlink.assert_called_once_with(*args) + self.assertEqual( + args, self.service.peers[ + (mocked.SERVICE_HOST, mocked.SERVICE_HOST_IFACE)]) + + def test_update_link_add_with_peers_eq(self): + args = (mocked.SERVICE_HOST, mocked.SERVICE_HOST_IFACE, + mocked.SERVICE_HOST_MAC, + mocked.APIC_EXT_SWITCH, + mocked.APIC_EXT_MODULE, mocked.APIC_EXT_PORT) + self.service.peers = {args[:2]: args} + self.service.update_link(None, *args) + + def test_update_link_rem_with_peers(self): + args = (mocked.SERVICE_HOST, mocked.SERVICE_HOST_IFACE, + mocked.SERVICE_HOST_MAC, 0, + mocked.APIC_EXT_MODULE, mocked.APIC_EXT_PORT) + self.service.peers = {args[:2]: args} + self.service.update_link(None, *args) + self.service.apic_manager.remove_hostlink.assert_called_once_with( + *args) + self.assertFalse(bool(self.service.peers)) + + def test_update_link_rem_no_peers(self): + args = (mocked.SERVICE_HOST, mocked.SERVICE_HOST_IFACE, + mocked.SERVICE_HOST_MAC, 0, + mocked.APIC_EXT_MODULE, mocked.APIC_EXT_PORT) + self.service.update_link(None, *args) + + +class TestCiscoApicTopologyAgent(base.BaseTestCase, + mocked.ControllerMixin, + mocked.ConfigMixin): + + def setUp(self): + super(TestCiscoApicTopologyAgent, self).setUp() + mocked.ControllerMixin.set_up_mocks(self) + mocked.ConfigMixin.set_up_mocks(self) + # Patch notifier + notifier_c = mock.patch(NOTIFIER).start() + self.notifier = mock.Mock() + notifier_c.return_value = self.notifier + # Patch device_exists + self.dev_exists = mock.patch(DEV_EXISTS).start() + # Patch IPDevice + ipdev_c = mock.patch(IP_DEVICE).start() + self.ipdev = mock.Mock() + ipdev_c.return_value = self.ipdev + self.ipdev.link.address = mocked.SERVICE_HOST_MAC + # Patch execute + self.execute = mock.patch(EXECUTE).start() + self.execute.return_value = LLDPCTL_RES + # Patch tasks + self.periodic_task = mock.patch(PERIODIC_TASK).start() + self.agent = apic_topology.ApicTopologyAgent() + self.agent.host = mocked.SERVICE_HOST + self.agent.service_agent = mock.Mock() + self.agent.lldpcmd = LLDP_CMD + + def test_init_host_device_exists(self): + self.agent.lldpcmd = None + self.dev_exists.return_value = True + self.agent.init_host() + self.assertEqual(LLDP_CMD + mocked.APIC_UPLINK_PORTS, + self.agent.lldpcmd) + + def test_init_host_device_not_exist(self): + self.agent.lldpcmd = None + self.dev_exists.return_value = False + self.agent.init_host() + self.assertEqual(LLDP_CMD, self.agent.lldpcmd) + + def test_get_peers(self): + self.agent.peers = {} + peers = self.agent._get_peers() + expected = [(mocked.SERVICE_HOST, mocked.SERVICE_HOST_IFACE, + mocked.SERVICE_HOST_MAC, mocked.APIC_EXT_SWITCH, + mocked.APIC_EXT_MODULE, mocked.APIC_EXT_PORT)] + self.assertEqual(expected, + peers[mocked.SERVICE_HOST_IFACE]) + + def test_check_for_new_peers_no_peers(self): + self.agent.peers = {} + expected = (mocked.SERVICE_HOST, mocked.SERVICE_HOST_IFACE, + mocked.SERVICE_HOST_MAC, mocked.APIC_EXT_SWITCH, + mocked.APIC_EXT_MODULE, mocked.APIC_EXT_PORT) + peers = {mocked.SERVICE_HOST_IFACE: [expected]} + context = mock.Mock() + with mock.patch.object(self.agent, '_get_peers', + return_value=peers): + self.agent._check_for_new_peers(context) + self.assertEqual(expected, + self.agent.peers[mocked.SERVICE_HOST_IFACE]) + self.agent.service_agent.update_link.assert_called_once_with( + context, *expected) + + def test_check_for_new_peers_with_peers(self): + expected = (mocked.SERVICE_HOST, mocked.SERVICE_HOST_IFACE, + mocked.SERVICE_HOST_MAC, mocked.APIC_EXT_SWITCH, + mocked.APIC_EXT_MODULE, mocked.APIC_EXT_PORT) + peers = {mocked.SERVICE_HOST_IFACE: [expected]} + self.agent.peers = {mocked.SERVICE_HOST_IFACE: + [tuple(x + '1' for x in expected)]} + context = mock.Mock() + with mock.patch.object(self.agent, '_get_peers', + return_value=peers): + self.agent._check_for_new_peers(context) + self.agent.service_agent.update_link.assert_called_with( + context, *expected) \ No newline at end of file