Skip to content

Commit

Permalink
[garp_service]: Create garp_service to run on PTF (#2992)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
theasianpianist committed Feb 19, 2021
1 parent 9b41a7a commit cb81ec8
Show file tree
Hide file tree
Showing 4 changed files with 128 additions and 29 deletions.
29 changes: 0 additions & 29 deletions tests/common/dualtor/dual_tor_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
39 changes: 39 additions & 0 deletions tests/common/fixtures/ptfhost_utils.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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/"
Expand All @@ -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)
Expand Down Expand Up @@ -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')
79 changes: 79 additions & 0 deletions tests/scripts/garp_service.py
Original file line number Diff line number Diff line change
@@ -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()
10 changes: 10 additions & 0 deletions tests/templates/garp_service.conf.j2
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit cb81ec8

Please sign in to comment.