From cb81ec8dd2ea5501dc99b01cf95fa5e664c1d210 Mon Sep 17 00:00:00 2001 From: Lawrence Lee Date: Thu, 18 Feb 2021 22:59:10 -0800 Subject: [PATCH] [garp_service]: Create garp_service to run on PTF (#2992) Create a new supervisor service to run on the PTF which sends GARP messages for each configured interface. This service takes two option CLI arguments, --conf and --interval. --conf specfiies the location of the configuration file (default to /tmp/garp_conf.json). --interval specifies the interval to wait between re-sending GARP messages (default to None, which causes the messages to only be sent once). Create a fixture to automatically configure/run this fixture in ptfhost_utils.py --- tests/common/dualtor/dual_tor_utils.py | 29 ---------- tests/common/fixtures/ptfhost_utils.py | 39 +++++++++++++ tests/scripts/garp_service.py | 79 ++++++++++++++++++++++++++ tests/templates/garp_service.conf.j2 | 10 ++++ 4 files changed, 128 insertions(+), 29 deletions(-) create mode 100644 tests/scripts/garp_service.py create mode 100644 tests/templates/garp_service.conf.j2 diff --git a/tests/common/dualtor/dual_tor_utils.py b/tests/common/dualtor/dual_tor_utils.py index c7caa0f20f..68282813eb 100644 --- a/tests/common/dualtor/dual_tor_utils.py +++ b/tests/common/dualtor/dual_tor_utils.py @@ -3,9 +3,7 @@ import json from datetime import datetime from tests.ptf_runner import ptf_runner -import ptf.testutils as testutils -from ipaddress import ip_interface from natsort import natsorted from tests.common.config_reload import config_reload from tests.common.helpers.assertions import pytest_assert @@ -550,30 +548,3 @@ def check_tunnel_balance(ptfhost, active_tor_mac, standby_tor_mac, active_tor_ip log_file=log_file, qlen=2000, socket_recv_size=16384) - -@pytest.fixture(scope='function', autouse=True) -def start_linkmgrd_heartbeat(ptfadapter, duthost, tbinfo): - ''' - Send a GARP from from PTF->ToR from each PTF port connected to a mux cable - - This is needed since linkmgrd will not start sending heartbeats until the PTF MAC is learned in the DUT neighbor table - ''' - garp_pkts = {} - - ptf_indices = duthost.get_extended_minigraph_facts(tbinfo)["minigraph_ptf_indices"] - mux_cable_table = duthost.get_running_config_facts()['MUX_CABLE'] - - for vlan_intf, config in mux_cable_table.items(): - ptf_port_index = ptf_indices[vlan_intf] - server_ip = ip_interface(config['server_ipv4']) - ptf_mac = ptfadapter.dataplane.ports[(0, ptf_port_index)].mac() - - garp_pkt = testutils.simple_arp_packet(eth_src=ptf_mac, - hw_snd=ptf_mac, - ip_snd=str(server_ip.ip), - ip_tgt=str(server_ip.ip), # Re-use server IP as target IP, since it is within the subnet of the VLAN IP - arp_op=2) - garp_pkts[ptf_port_index] = garp_pkt - - for port, pkt in garp_pkts.items(): - testutils.send_packet(ptfadapter, port, pkt) diff --git a/tests/common/fixtures/ptfhost_utils.py b/tests/common/fixtures/ptfhost_utils.py index 068a904c5d..ca3b8b7812 100644 --- a/tests/common/fixtures/ptfhost_utils.py +++ b/tests/common/fixtures/ptfhost_utils.py @@ -1,7 +1,9 @@ +import json import os import pytest import logging +from ipaddress import ip_interface from jinja2 import Template from natsort import natsorted @@ -10,6 +12,7 @@ ROOT_DIR = "/root" OPT_DIR = "/opt" +TMP_DIR = '/tmp' SUPERVISOR_CONFIG_DIR = "/etc/supervisor/conf.d/" SCRIPTS_SRC_DIR = "scripts/" TEMPLATES_DIR = "templates/" @@ -21,6 +24,8 @@ ICMP_RESPONDER_CONF_TEMPL = "icmp_responder.conf.j2" CHANGE_MAC_ADDRESS_SCRIPT = "scripts/change_mac.sh" REMOVE_IP_ADDRESS_SCRIPT = "scripts/remove_ip.sh" +GARP_SERVICE_PY = 'garp_service.py' +GARP_SERVICE_CONF_TEMPL = 'garp_service.conf.j2' @pytest.fixture(scope="session", autouse=True) @@ -193,3 +198,37 @@ def run_icmp_responder(duthost, ptfhost, tbinfo): logging.debug("Stop running icmp_responder") ptfhost.shell("supervisorctl stop icmp_responder") + + +@pytest.fixture(scope='session', autouse=True) +def run_garp_service(duthost, ptfhost, tbinfo, change_mac_addresses): + + garp_config = {} + + ptf_indices = duthost.get_extended_minigraph_facts(tbinfo)["minigraph_ptf_indices"] + mux_cable_table = duthost.get_running_config_facts()['MUX_CABLE'] + + logger.info("Generating GARP service config file") + + for vlan_intf, config in mux_cable_table.items(): + ptf_port_index = ptf_indices[vlan_intf] + server_ip = ip_interface(config['server_ipv4']).ip + + garp_config[ptf_port_index] = { + 'target_ip': '{}'.format(server_ip) + } + + ptfhost.copy(src=os.path.join(SCRIPTS_SRC_DIR, GARP_SERVICE_PY), dest=OPT_DIR) + + with open(os.path.join(TEMPLATES_DIR, GARP_SERVICE_CONF_TEMPL)) as f: + template = Template(f.read()) + + ptfhost.copy(content=json.dumps(garp_config, indent=4, sort_keys=True), dest=os.path.join(TMP_DIR, 'garp_conf.json')) + ptfhost.copy(content=template.render(garp_service_args = '--interval 1'), dest=os.path.join(SUPERVISOR_CONFIG_DIR, 'garp_service.conf')) + logger.info("Starting GARP Service on PTF host") + ptfhost.shell('supervisorctl update') + ptfhost.shell('supervisorctl start garp_service') + + yield + + ptfhost.shell('supervisorctl stop garp_service') diff --git a/tests/scripts/garp_service.py b/tests/scripts/garp_service.py new file mode 100644 index 0000000000..3a52fd6730 --- /dev/null +++ b/tests/scripts/garp_service.py @@ -0,0 +1,79 @@ +import argparse +import json +import ptf +import ptf.testutils as testutils +import time + +from ipaddress import ip_interface +from scapy.all import conf +from scapy.arch import get_if_hwaddr + +class GarpService: + + def __init__(self, garp_config_file, interval): + self.garp_config_file = garp_config_file + self.interval = interval + self.packets = {} + self.dataplane = ptf.dataplane_instance + + def gen_garp_packets(self): + ''' + Read the config file and generate a GARP packet for each configured interface + ''' + + with open(self.garp_config_file) as f: + garp_config = json.load(f) + + for port, config in garp_config.items(): + intf_name = 'eth{}'.format(port) + source_mac = get_if_hwaddr(intf_name) + source_ip_str = config['target_ip'] + source_ip = str(ip_interface(source_ip_str).ip) + + # PTF uses Scapy to create packets, so this is ok to create + # packets through PTF even though we are using Scapy to send the packets + garp_pkt = testutils.simple_arp_packet(eth_src=source_mac, + hw_snd=source_mac, + ip_snd=source_ip, + ip_tgt=source_ip, # Re-use server IP as target IP, since it is within the subnet of the VLAN IP + arp_op=2) + self.packets[intf_name] = garp_pkt + + def send_garp_packets(self): + ''' + For each created GARP packet/interface pair, create an L2 socket. + Then send every packet through its associated socket according to the self.interval + ''' + self.gen_garp_packets() + + sockets = {} + + for intf, packet in self.packets.items(): + socket = conf.L2socket(iface=intf) + sockets[socket] = packet + + try: + while True: + for socket, packet in sockets.items(): + socket.send(packet) + + if self.interval is None: + break + + time.sleep(self.interval) + + finally: + for socket in sockets.keys(): + socket.close() + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description='GARP Service') + parser.add_argument('--conf', '-c', dest='conf_file', required=False, default='/tmp/garp_conf.json', action='store', help='The configuration file for GARP Service (default "/tmp/garp_conf.json")') + parser.add_argument('--interval', '-i', dest='interval', required=False, type=int, default=None, action='store', help='The interval at which to re-send GARP messages. If None or not specified, messages will only be set once at service startup') + args = parser.parse_args() + conf_file = args.conf_file + interval = args.interval + + garp_service = GarpService(conf_file, interval) + garp_service.send_garp_packets() diff --git a/tests/templates/garp_service.conf.j2 b/tests/templates/garp_service.conf.j2 new file mode 100644 index 0000000000..c15d7967cc --- /dev/null +++ b/tests/templates/garp_service.conf.j2 @@ -0,0 +1,10 @@ +[program:garp_service] +command=/usr/bin/python /opt/garp_service.py {{ garp_service_args }} +process_name=garp_service +stdout_logfile=/tmp/garp_service.out.log +stderr_logfile=/tmp/garp_service.err.log +redirect_stderr=false +autostart=false +autorestart=false +startsecs=1 +numprocs=1