diff --git a/ansible/roles/test/files/helpers/ferret.py b/ansible/roles/test/files/helpers/ferret.py deleted file mode 100644 index 954d558e27e..00000000000 --- a/ansible/roles/test/files/helpers/ferret.py +++ /dev/null @@ -1,335 +0,0 @@ -#/usr/bin/env python - -# python t.py -f /tmp/vxlan_decap.json -s 192.168.8.1 - -import SimpleHTTPServer -import SocketServer -import select -import shutil -import json -import BaseHTTPServer -import time -import socket -import ctypes -import ssl -import struct -import binascii -import itertools -import argparse -import os - -from pprint import pprint - -from cStringIO import StringIO -from functools import partial -from collections import namedtuple - - -Record = namedtuple('Record', ['hostname', 'family', 'expired', 'lo', 'mac', 'vxlan_id']) - -ASIC_TYPE=None - - -class Ferret(BaseHTTPServer.BaseHTTPRequestHandler): - server_version = "FerretHTTP/0.1" - - def do_POST(self): - if not self.path.startswith('/Ferret/NeighborAdvertiser/Slices/'): - self.send_error(404, "URL is not supported") - else: - info = self.extract_info() - self.update_db(info) - self.send_resp(info) - - def extract_info(self): - c_len = int(self.headers.getheader('content-length', 0)) - body = self.rfile.read(c_len) - j = json.loads(body) - return j - - def generate_entries(self, hostname, family, expire, lo, info, mapping_family): - for i in info['vlanInterfaces']: - vxlan_id = int(i['vxlanId']) - for j in i[mapping_family]: - mac = str(j['macAddr']).replace(':', '') - addr = str(j['ipAddr']) - r = Record(hostname=hostname, family=family, expired=expire, lo=lo, mac=mac, vxlan_id=vxlan_id) - self.db[addr] = r - - return - - def update_db(self, info): - hostname = str(info['switchInfo']['name']) - lo_ipv4 = str(info['switchInfo']['ipv4Addr']) - lo_ipv6 = str(info['switchInfo']['ipv6Addr']) - duration = int(info['respondingSchemes']['durationInSec']) - expired = time.time() + duration - - self.generate_entries(hostname, 'ipv4', expired, lo_ipv4, info, 'ipv4AddrMappings') - self.generate_entries(hostname, 'ipv6', expired, lo_ipv6, info, 'ipv6AddrMappings') - - return - - def send_resp(self, info): - result = { - 'ipv4Addr': self.src_ip - } - f, l = self.generate_response(result) - self.send_response(200) - self.send_header("Content-type", "application/json") - self.send_header("Content-Length", str(l)) - self.send_header("Last-Modified", self.date_time_string()) - self.end_headers() - shutil.copyfileobj(f, self.wfile) - f.close() - return - - def generate_response(self, response): - f = StringIO() - json.dump(response, f) - l = f.tell() - f.seek(0) - return f, l - - -class RestAPI(object): - PORT = 448 - - def __init__(self, obj, db, src_ip): - self.httpd = SocketServer.TCPServer(("", self.PORT), obj) - self.context = ssl.SSLContext(ssl.PROTOCOL_TLS) - self.context.verify_mode = ssl.CERT_NONE - self.context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) - self.context.load_cert_chain(certfile="/opt/test.pem", keyfile="/opt/test.key") - self.httpd.socket=self.context.wrap_socket(self.httpd.socket, server_side=True) - self.db = db - obj.db = db - obj.src_ip = src_ip - - def handler(self): - return self.httpd.fileno() - - def handle(self): - return self.httpd.handle_request() - - -class Interface(object): - ETH_P_ALL = 0x03 - RCV_TIMEOUT = 1000 - RCV_SIZE = 4096 - SO_ATTACH_FILTER = 26 - - def __init__(self, iface, bpf_src): - self.iface = iface - self.socket = socket.socket(socket.AF_PACKET, socket.SOCK_RAW, socket.htons(self.ETH_P_ALL)) - if bpf_src is not None: - blob = ctypes.create_string_buffer(''.join(struct.pack("HBBI", *e) for e in bpf_src)) - address = ctypes.addressof(blob) - bpf = struct.pack('HL', len(bpf_src), address) - self.socket.setsockopt(socket.SOL_SOCKET, self.SO_ATTACH_FILTER, bpf) - self.socket.bind((self.iface, 0)) - self.socket.settimeout(self.RCV_TIMEOUT) - - def __del__(self): - self.socket.close() - - def handler(self): - return self.socket.fileno() - - def recv(self): - return self.socket.recv(self.RCV_SIZE) - - def send(self, data): - self.socket.send(data) - - -class Poller(object): - def __init__(self, httpd, interfaces, responder): - self.responder = responder - self.mapping = {interface.handler(): interface for interface in interfaces} - self.httpd = httpd - - def poll(self): - handlers = self.mapping.keys() + [self.httpd.handler()] - while True: - (rdlist, _, _) = select.select(handlers, [], []) - for handler in rdlist: - if handler == self.httpd.handler(): - self.httpd.handle() - else: - self.responder.action(self.mapping[handler]) - - -class Responder(object): - ARP_PKT_LEN = 60 - ARP_OP_REQUEST = 1 - def __init__(self, db): - self.arp_chunk = binascii.unhexlify('08060001080006040002') # defines a part of the packet for ARP Reply - self.arp_pad = binascii.unhexlify('00' * 18) - self.db = db - - def hexdump(self, data): - print " ".join("%02x" % ord(d) for d in data) - - def action(self, interface): - data = interface.recv() - - ext_dst_mac = data[0x00:0x06] - ext_src_mac = data[0x06:0x0c] - ext_eth_type = data[0x0c:0x0e] - if ext_eth_type != binascii.unhexlify('0800'): - print "Not 0x800 eth type" - self.hexdump(data) - print - return - src_ip = data[0x001a:0x001e] - dst_ip = data[0x1e:0x22] - gre_flags = data[0x22:0x24] - gre_type = data[0x24:0x26] - - gre_type_r = struct.unpack('!H', gre_type)[0] - self.hexdump(data) - if gre_type_r == 0x88be: # Broadcom - arp_request = data[0x26:] - if ASIC_TYPE == "barefoot": - # ERSPAN type 2 - # Ethernet(14) + IP(20) + GRE(4) + ERSPAN(8) = 46 = 0x2e - # Note: Count GRE as 4 byte, only mandatory fields. - # References: https://tools.ietf.org/html/rfc1701 - # https://tools.ietf.org/html/draft-foschiano-erspan-00 - arp_request = data[0x2E:] - - elif gre_type_r == 0x8949: # Mellanox - arp_request = data[0x3c:] - else: - print "GRE type 0x%x is not supported" % gre_type_r - self.hexdump(data) - print - return - - if len(arp_request) > self.ARP_PKT_LEN: - print "Too long packet" - self.hexdump(data) - print - return - - remote_mac, remote_ip, request_ip, op_type = self.extract_arp_info(arp_request) - # Don't send ARP response if the ARP op code is not request - if op_type != self.ARP_OP_REQUEST: - return - - request_ip_str = socket.inet_ntoa(request_ip) - - if request_ip_str not in self.db: - print "Not in db" - return - - r = self.db[request_ip_str] - if r.expired < time.time(): - print "Expired row in db" - del self.db[request_ip_str] - return - - if r.family == 'ipv4': - new_pkt = ext_src_mac + ext_dst_mac + ext_eth_type # outer eth frame - ipv4 = binascii.unhexlify('45000060977e400040110000') + dst_ip + src_ip # ip - crc = self.calculate_header_crc(ipv4) - ipv4 = ipv4[0:10] + crc + ipv4[12:] - new_pkt += ipv4 - new_pkt += binascii.unhexlify('c00012b5004c1280') # udp - new_pkt += binascii.unhexlify('08000000%06x00' % r.vxlan_id) # vxlan - - arp_reply = self.generate_arp_reply(binascii.unhexlify(r.mac), remote_mac, request_ip, remote_ip) - new_pkt += arp_reply - else: - print 'Support of family %s is not implemented' % r.family - return - - interface.send(new_pkt) - - return - - def calculate_header_crc(self, ipv4): - s = 0 - for l,r in zip(ipv4[::2], ipv4[1::2]): - l_u = struct.unpack("B", l)[0] - r_u = struct.unpack("B", r)[0] - s += (l_u << 8) + r_u - - c = s >> 16 - s = s & 0xffff - - while c != 0: - s += c - c = s >> 16 - s = s & 0xffff - - s = 0xffff - s - - return binascii.unhexlify("%x" % s) - - def extract_arp_info(self, data): - # remote_mac, remote_ip, request_ip, op_type - return data[6:12], data[28:32], data[38:42], (ord(data[20]) * 256 + ord(data[21])) - - def generate_arp_reply(self, local_mac, remote_mac, local_ip, remote_ip): - eth_hdr = remote_mac + local_mac - return eth_hdr + self.arp_chunk + local_mac + local_ip + remote_mac + remote_ip + self.arp_pad - -def get_bpf_for_bgp(): - bpf_src = [ - (0x28, 0, 0, 0x0000000c), # (000) ldh [12] - (0x15, 0, 2, 0x00000800), # (001) jeq #0x800 jt 2 jf 4 - (0x30, 0, 0, 0x00000017), # (002) ldb [23] - (0x15, 6, 7, 0x0000002f), # (003) jeq #0x2f jt 10 jf 11 - (0x15, 0, 6, 0x000086dd), # (004) jeq #0x86dd jt 5 jf 11 - (0x30, 0, 0, 0x00000014), # (005) ldb [20] - (0x15, 3, 0, 0x0000002f), # (006) jeq #0x2f jt 10 jf 7 - (0x15, 0, 3, 0x0000002c), # (007) jeq #0x2c jt 8 jf 11 - (0x30, 0, 0, 0x00000036), # (008) ldb [54] - (0x15, 0, 1, 0x0000002f), # (009) jeq #0x2f jt 10 jf 11 - (0x6, 0, 0, 0x00040000), # (010) ret #262144 - (0x6, 0, 0, 0x00000000), # (011) ret #0 - ] - return bpf_src - - -def extract_iface_names(config_file): - with open(config_file) as fp: - graph = json.load(fp) - - net_ports = [] - for name, val in graph['minigraph_portchannels'].items(): - members = ['eth%d' % graph['minigraph_port_indices'][member] for member in val['members']] - net_ports.extend(members) - - return net_ports - -def parse_args(): - parser = argparse.ArgumentParser(description='Ferret VXLAN API') - parser.add_argument('-f', '--config-file', help='file with configuration', required=True) - parser.add_argument('-s', '--src-ip', help='Ferret endpoint ip', required=True) - parser.add_argument('-a', '--asic-type', help='ASIC vendor name', type=str, required=False) - args = parser.parse_args() - if not os.path.isfile(args.config_file): - print "Can't open config file '%s'" % args.config_file - exit(1) - - global ASIC_TYPE - ASIC_TYPE = args.asic_type - return args.config_file, args.src_ip - -def main(): - db = {} - - config_file, src_ip = parse_args() - iface_names = extract_iface_names(config_file) - rest = RestAPI(Ferret, db, src_ip) - bpf_src = get_bpf_for_bgp() - ifaces = [Interface(iface_name, bpf_src) for iface_name in iface_names] - responder = Responder(db) - p = Poller(rest, ifaces, responder) - p.poll() - -if __name__ == '__main__': - main() diff --git a/ansible/roles/test/files/helpers/ferret.py b/ansible/roles/test/files/helpers/ferret.py new file mode 120000 index 00000000000..4d68dcb9adf --- /dev/null +++ b/ansible/roles/test/files/helpers/ferret.py @@ -0,0 +1 @@ +../../../../../tests/arp/files/ferret.py \ No newline at end of file diff --git a/ansible/roles/test/templates/ferret.conf.j2 b/ansible/roles/test/templates/ferret.conf.j2 deleted file mode 100644 index 485153c819f..00000000000 --- a/ansible/roles/test/templates/ferret.conf.j2 +++ /dev/null @@ -1,10 +0,0 @@ -[program:ferret] -command=/usr/bin/python /opt/ferret.py {{ ferret_args }} -process_name=ferret -stdout_logfile=/tmp/ferret.out.log -stderr_logfile=/tmp/ferret.err.log -redirect_stderr=false -autostart=false -autorestart=true -startsecs=1 -numprocs=1 diff --git a/ansible/roles/test/templates/ferret.conf.j2 b/ansible/roles/test/templates/ferret.conf.j2 new file mode 120000 index 00000000000..369de1049e9 --- /dev/null +++ b/ansible/roles/test/templates/ferret.conf.j2 @@ -0,0 +1 @@ +../../../../tests/templates/ferret.conf.j2 \ No newline at end of file diff --git a/tests/arp/args/__init__.py b/tests/arp/args/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/arp/args/wr_arp_args.py b/tests/arp/args/wr_arp_args.py new file mode 100644 index 00000000000..4ea83b99948 --- /dev/null +++ b/tests/arp/args/wr_arp_args.py @@ -0,0 +1,19 @@ +# WR-ARP Args file + +def add_wr_arp_args(parser): + ''' + Adding arguments required for wr arp test cases + + Args: + parser: pytest parser object + + Returns: + None + ''' + parser.addoption( + "--test_duration", + action="store", + type=int, + default=370, + help="Test duration", + ) diff --git a/tests/arp/conftest.py b/tests/arp/conftest.py new file mode 100644 index 00000000000..734ddd75c80 --- /dev/null +++ b/tests/arp/conftest.py @@ -0,0 +1,14 @@ +from args.wr_arp_args import add_wr_arp_args + +# WR-ARP pytest arguments +def pytest_addoption(parser): + ''' + Adds option to FDB pytest + + Args: + parser: pytest parser object + + Returns: + None + ''' + add_wr_arp_args(parser) diff --git a/tests/arp/files/ferret.conf.j2 b/tests/arp/files/ferret.conf.j2 new file mode 100644 index 00000000000..485153c819f --- /dev/null +++ b/tests/arp/files/ferret.conf.j2 @@ -0,0 +1,10 @@ +[program:ferret] +command=/usr/bin/python /opt/ferret.py {{ ferret_args }} +process_name=ferret +stdout_logfile=/tmp/ferret.out.log +stderr_logfile=/tmp/ferret.err.log +redirect_stderr=false +autostart=false +autorestart=true +startsecs=1 +numprocs=1 diff --git a/tests/arp/files/ferret.py b/tests/arp/files/ferret.py new file mode 100644 index 00000000000..954d558e27e --- /dev/null +++ b/tests/arp/files/ferret.py @@ -0,0 +1,335 @@ +#/usr/bin/env python + +# python t.py -f /tmp/vxlan_decap.json -s 192.168.8.1 + +import SimpleHTTPServer +import SocketServer +import select +import shutil +import json +import BaseHTTPServer +import time +import socket +import ctypes +import ssl +import struct +import binascii +import itertools +import argparse +import os + +from pprint import pprint + +from cStringIO import StringIO +from functools import partial +from collections import namedtuple + + +Record = namedtuple('Record', ['hostname', 'family', 'expired', 'lo', 'mac', 'vxlan_id']) + +ASIC_TYPE=None + + +class Ferret(BaseHTTPServer.BaseHTTPRequestHandler): + server_version = "FerretHTTP/0.1" + + def do_POST(self): + if not self.path.startswith('/Ferret/NeighborAdvertiser/Slices/'): + self.send_error(404, "URL is not supported") + else: + info = self.extract_info() + self.update_db(info) + self.send_resp(info) + + def extract_info(self): + c_len = int(self.headers.getheader('content-length', 0)) + body = self.rfile.read(c_len) + j = json.loads(body) + return j + + def generate_entries(self, hostname, family, expire, lo, info, mapping_family): + for i in info['vlanInterfaces']: + vxlan_id = int(i['vxlanId']) + for j in i[mapping_family]: + mac = str(j['macAddr']).replace(':', '') + addr = str(j['ipAddr']) + r = Record(hostname=hostname, family=family, expired=expire, lo=lo, mac=mac, vxlan_id=vxlan_id) + self.db[addr] = r + + return + + def update_db(self, info): + hostname = str(info['switchInfo']['name']) + lo_ipv4 = str(info['switchInfo']['ipv4Addr']) + lo_ipv6 = str(info['switchInfo']['ipv6Addr']) + duration = int(info['respondingSchemes']['durationInSec']) + expired = time.time() + duration + + self.generate_entries(hostname, 'ipv4', expired, lo_ipv4, info, 'ipv4AddrMappings') + self.generate_entries(hostname, 'ipv6', expired, lo_ipv6, info, 'ipv6AddrMappings') + + return + + def send_resp(self, info): + result = { + 'ipv4Addr': self.src_ip + } + f, l = self.generate_response(result) + self.send_response(200) + self.send_header("Content-type", "application/json") + self.send_header("Content-Length", str(l)) + self.send_header("Last-Modified", self.date_time_string()) + self.end_headers() + shutil.copyfileobj(f, self.wfile) + f.close() + return + + def generate_response(self, response): + f = StringIO() + json.dump(response, f) + l = f.tell() + f.seek(0) + return f, l + + +class RestAPI(object): + PORT = 448 + + def __init__(self, obj, db, src_ip): + self.httpd = SocketServer.TCPServer(("", self.PORT), obj) + self.context = ssl.SSLContext(ssl.PROTOCOL_TLS) + self.context.verify_mode = ssl.CERT_NONE + self.context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) + self.context.load_cert_chain(certfile="/opt/test.pem", keyfile="/opt/test.key") + self.httpd.socket=self.context.wrap_socket(self.httpd.socket, server_side=True) + self.db = db + obj.db = db + obj.src_ip = src_ip + + def handler(self): + return self.httpd.fileno() + + def handle(self): + return self.httpd.handle_request() + + +class Interface(object): + ETH_P_ALL = 0x03 + RCV_TIMEOUT = 1000 + RCV_SIZE = 4096 + SO_ATTACH_FILTER = 26 + + def __init__(self, iface, bpf_src): + self.iface = iface + self.socket = socket.socket(socket.AF_PACKET, socket.SOCK_RAW, socket.htons(self.ETH_P_ALL)) + if bpf_src is not None: + blob = ctypes.create_string_buffer(''.join(struct.pack("HBBI", *e) for e in bpf_src)) + address = ctypes.addressof(blob) + bpf = struct.pack('HL', len(bpf_src), address) + self.socket.setsockopt(socket.SOL_SOCKET, self.SO_ATTACH_FILTER, bpf) + self.socket.bind((self.iface, 0)) + self.socket.settimeout(self.RCV_TIMEOUT) + + def __del__(self): + self.socket.close() + + def handler(self): + return self.socket.fileno() + + def recv(self): + return self.socket.recv(self.RCV_SIZE) + + def send(self, data): + self.socket.send(data) + + +class Poller(object): + def __init__(self, httpd, interfaces, responder): + self.responder = responder + self.mapping = {interface.handler(): interface for interface in interfaces} + self.httpd = httpd + + def poll(self): + handlers = self.mapping.keys() + [self.httpd.handler()] + while True: + (rdlist, _, _) = select.select(handlers, [], []) + for handler in rdlist: + if handler == self.httpd.handler(): + self.httpd.handle() + else: + self.responder.action(self.mapping[handler]) + + +class Responder(object): + ARP_PKT_LEN = 60 + ARP_OP_REQUEST = 1 + def __init__(self, db): + self.arp_chunk = binascii.unhexlify('08060001080006040002') # defines a part of the packet for ARP Reply + self.arp_pad = binascii.unhexlify('00' * 18) + self.db = db + + def hexdump(self, data): + print " ".join("%02x" % ord(d) for d in data) + + def action(self, interface): + data = interface.recv() + + ext_dst_mac = data[0x00:0x06] + ext_src_mac = data[0x06:0x0c] + ext_eth_type = data[0x0c:0x0e] + if ext_eth_type != binascii.unhexlify('0800'): + print "Not 0x800 eth type" + self.hexdump(data) + print + return + src_ip = data[0x001a:0x001e] + dst_ip = data[0x1e:0x22] + gre_flags = data[0x22:0x24] + gre_type = data[0x24:0x26] + + gre_type_r = struct.unpack('!H', gre_type)[0] + self.hexdump(data) + if gre_type_r == 0x88be: # Broadcom + arp_request = data[0x26:] + if ASIC_TYPE == "barefoot": + # ERSPAN type 2 + # Ethernet(14) + IP(20) + GRE(4) + ERSPAN(8) = 46 = 0x2e + # Note: Count GRE as 4 byte, only mandatory fields. + # References: https://tools.ietf.org/html/rfc1701 + # https://tools.ietf.org/html/draft-foschiano-erspan-00 + arp_request = data[0x2E:] + + elif gre_type_r == 0x8949: # Mellanox + arp_request = data[0x3c:] + else: + print "GRE type 0x%x is not supported" % gre_type_r + self.hexdump(data) + print + return + + if len(arp_request) > self.ARP_PKT_LEN: + print "Too long packet" + self.hexdump(data) + print + return + + remote_mac, remote_ip, request_ip, op_type = self.extract_arp_info(arp_request) + # Don't send ARP response if the ARP op code is not request + if op_type != self.ARP_OP_REQUEST: + return + + request_ip_str = socket.inet_ntoa(request_ip) + + if request_ip_str not in self.db: + print "Not in db" + return + + r = self.db[request_ip_str] + if r.expired < time.time(): + print "Expired row in db" + del self.db[request_ip_str] + return + + if r.family == 'ipv4': + new_pkt = ext_src_mac + ext_dst_mac + ext_eth_type # outer eth frame + ipv4 = binascii.unhexlify('45000060977e400040110000') + dst_ip + src_ip # ip + crc = self.calculate_header_crc(ipv4) + ipv4 = ipv4[0:10] + crc + ipv4[12:] + new_pkt += ipv4 + new_pkt += binascii.unhexlify('c00012b5004c1280') # udp + new_pkt += binascii.unhexlify('08000000%06x00' % r.vxlan_id) # vxlan + + arp_reply = self.generate_arp_reply(binascii.unhexlify(r.mac), remote_mac, request_ip, remote_ip) + new_pkt += arp_reply + else: + print 'Support of family %s is not implemented' % r.family + return + + interface.send(new_pkt) + + return + + def calculate_header_crc(self, ipv4): + s = 0 + for l,r in zip(ipv4[::2], ipv4[1::2]): + l_u = struct.unpack("B", l)[0] + r_u = struct.unpack("B", r)[0] + s += (l_u << 8) + r_u + + c = s >> 16 + s = s & 0xffff + + while c != 0: + s += c + c = s >> 16 + s = s & 0xffff + + s = 0xffff - s + + return binascii.unhexlify("%x" % s) + + def extract_arp_info(self, data): + # remote_mac, remote_ip, request_ip, op_type + return data[6:12], data[28:32], data[38:42], (ord(data[20]) * 256 + ord(data[21])) + + def generate_arp_reply(self, local_mac, remote_mac, local_ip, remote_ip): + eth_hdr = remote_mac + local_mac + return eth_hdr + self.arp_chunk + local_mac + local_ip + remote_mac + remote_ip + self.arp_pad + +def get_bpf_for_bgp(): + bpf_src = [ + (0x28, 0, 0, 0x0000000c), # (000) ldh [12] + (0x15, 0, 2, 0x00000800), # (001) jeq #0x800 jt 2 jf 4 + (0x30, 0, 0, 0x00000017), # (002) ldb [23] + (0x15, 6, 7, 0x0000002f), # (003) jeq #0x2f jt 10 jf 11 + (0x15, 0, 6, 0x000086dd), # (004) jeq #0x86dd jt 5 jf 11 + (0x30, 0, 0, 0x00000014), # (005) ldb [20] + (0x15, 3, 0, 0x0000002f), # (006) jeq #0x2f jt 10 jf 7 + (0x15, 0, 3, 0x0000002c), # (007) jeq #0x2c jt 8 jf 11 + (0x30, 0, 0, 0x00000036), # (008) ldb [54] + (0x15, 0, 1, 0x0000002f), # (009) jeq #0x2f jt 10 jf 11 + (0x6, 0, 0, 0x00040000), # (010) ret #262144 + (0x6, 0, 0, 0x00000000), # (011) ret #0 + ] + return bpf_src + + +def extract_iface_names(config_file): + with open(config_file) as fp: + graph = json.load(fp) + + net_ports = [] + for name, val in graph['minigraph_portchannels'].items(): + members = ['eth%d' % graph['minigraph_port_indices'][member] for member in val['members']] + net_ports.extend(members) + + return net_ports + +def parse_args(): + parser = argparse.ArgumentParser(description='Ferret VXLAN API') + parser.add_argument('-f', '--config-file', help='file with configuration', required=True) + parser.add_argument('-s', '--src-ip', help='Ferret endpoint ip', required=True) + parser.add_argument('-a', '--asic-type', help='ASIC vendor name', type=str, required=False) + args = parser.parse_args() + if not os.path.isfile(args.config_file): + print "Can't open config file '%s'" % args.config_file + exit(1) + + global ASIC_TYPE + ASIC_TYPE = args.asic_type + return args.config_file, args.src_ip + +def main(): + db = {} + + config_file, src_ip = parse_args() + iface_names = extract_iface_names(config_file) + rest = RestAPI(Ferret, db, src_ip) + bpf_src = get_bpf_for_bgp() + ifaces = [Interface(iface_name, bpf_src) for iface_name in iface_names] + responder = Responder(db) + p = Poller(rest, ifaces, responder) + p.poll() + +if __name__ == '__main__': + main() diff --git a/tests/arp/test_wr_arp.py b/tests/arp/test_wr_arp.py new file mode 100644 index 00000000000..26319a92d13 --- /dev/null +++ b/tests/arp/test_wr_arp.py @@ -0,0 +1,216 @@ +import json +import logging +import pytest + +from common.platform.ssh_utils import prepare_testbed_ssh_keys as prepareTestbedSshKeys +from ptf_runner import ptf_runner + +logger = logging.getLogger(__name__) + +# Globals +PTFRUNNER_QLEN = 1000 +VXLAN_CONFIG_FILE = '/tmp/vxlan_decap.json' + +class TestWrArp: + ''' + TestWrArp Performs control plane assisted warm-reboo + ''' + def __prepareVxlanConfigData(self, duthost, ptfhost): + ''' + Prepares Vxlan Configuration data for Ferret service running on PTF host + + Args: + duthost (AnsibleHost): Device Under Test (DUT) + ptfhost (AnsibleHost): Packet Test Framework (PTF) + + Returns: + None + ''' + mgFacts = duthost.minigraph_facts(host=duthost.hostname)['ansible_facts'] + vxlanConfigData = { + 'minigraph_port_indices': mgFacts['minigraph_port_indices'], + 'minigraph_portchannel_interfaces': mgFacts['minigraph_portchannel_interfaces'], + 'minigraph_portchannels': mgFacts['minigraph_portchannels'], + 'minigraph_lo_interfaces': mgFacts['minigraph_lo_interfaces'], + 'minigraph_vlans': mgFacts['minigraph_vlans'], + 'minigraph_vlan_interfaces': mgFacts['minigraph_vlan_interfaces'], + 'dut_mac': duthost.setup()['ansible_facts']['ansible_Ethernet0']['macaddress'] + } + with open(VXLAN_CONFIG_FILE, 'w') as file: + file.write(json.dumps(vxlanConfigData, indent=4)) + + logger.info('Copying ferret config file to {0}'.format(ptfhost.hostname)) + ptfhost.copy(src=VXLAN_CONFIG_FILE, dest='/tmp/') + + @pytest.fixture(scope='class', autouse=True) + def setupFerret(self, duthost, ptfhost): + ''' + Sets Ferret service on PTF host. This class-scope fixture runs once before test start + + Args: + duthost (AnsibleHost): Device Under Test (DUT) + ptfhost (AnsibleHost): Packet Test Framework (PTF) + + Returns: + None + ''' + ptfhost.copy(src="arp/files/ferret.py", dest="/opt") + + result = duthost.shell( + cmd='''ip route show proto zebra type unicast | + sed -e '/default/d' -ne '/0\//p' | + head -n 1 | + sed -ne 's/0\/.*$/1/p' + ''' + ) + assert len(result['stderr_lines']) == 0, 'Could not obtain DIP' + + dip = result['stdout'] + logger.info('VxLan Sender {0}'.format(dip)) + + ptfhost.host.options['variable_manager'].extra_vars.update({ + 'ferret_args': '-f /tmp/vxlan_decap.json -s {0}'.format(dip) + }) + + logger.info('Copying ferret config file to {0}'.format(ptfhost.hostname)) + ptfhost.template(src='arp/files/ferret.conf.j2', dest='/etc/supervisor/conf.d/ferret.conf') + + logger.info('Generate pem and key files for ssl') + ptfhost.command( + cmd='''openssl req -new -x509 -keyout test.key -out test.pem -days 365 -nodes + -subj "/C=10/ST=Test/L=Test/O=Test/OU=Test/CN=test.com"''', + chdir='/opt' + ) + + self.__prepareVxlanConfigData(duthost, ptfhost) + + logger.info('Refreshing supervisor control with ferret configuration') + ptfhost.shell('supervisorctl reread && supervisorctl update') + + @pytest.fixture(scope='class', autouse=True) + def copyPtfDirectory(self, ptfhost): + ''' + Copys PTF directory to PTF host. This class-scope fixture runs once before test start + + Args: + ptfhost (AnsibleHost): Packet Test Framework (PTF) + + Returns: + None + ''' + ptfhost.copy(src="ptftests", dest="/root") + + @pytest.fixture(scope='class', autouse=True) + def setupRouteToPtfhost(self, duthost, ptfhost): + ''' + Sets routes up on DUT to PTF host. This class-scope fixture runs once before test start + + Args: + duthost (AnsibleHost): Device Under Test (DUT) + ptfhost (AnsibleHost): Packet Test Framework (PTF) + + Returns: + None + ''' + result = duthost.shell(cmd="ip route show table default | sed -n 's/default //p'") + assert len(result['stderr_lines']) == 0, 'Could not find the gateway for management port' + + gwIp = result['stdout'] + ptfIp = ptfhost.host.options['inventory_manager'].get_host(ptfhost.hostname).vars['ansible_host'] + + route = duthost.shell(cmd='ip route get {0}'.format(ptfIp))['stdout'] + if 'PortChannel' in route: + logger.info( + "Add explicit route for PTF host ({0}) through eth0 (mgmt) interface ({1})".format(ptfIp, gwIp) + ) + duthost.shell(cmd='ip route add {0}/32 {1}'.format(ptfIp, gwIp)) + + yield + + if 'PortChannel' in route: + logger.info( + "Delete explicit route for PTF host ({0}) through eth0 (mgmt) interface ({1})".format(ptfIp, gwIp) + ) + duthost.shell(cmd='ip route delete {0}/32 {1}'.format(ptfIp, gwIp)) + + @pytest.fixture(scope='class', autouse=True) + def removePtfhostIp(self, ptfhost): + ''' + Removes IP assigned to eth inerface of PTF host. This class-scope fixture runs once before test start + + Args: + ptfhost (AnsibleHost): Packet Test Framework (PTF) + + Returns: + None + ''' + ptfhost.script(src='scripts/remove_ip.sh') + + @pytest.fixture(scope='class', autouse=True) + def changePtfhostMacAddresses(self, ptfhost): + ''' + Change MAC addresses (unique) on PTF host. This class-scope fixture runs once before test start + + Args: + ptfhost (AnsibleHost): Packet Test Framework (PTF) + + Returns: + None + ''' + ptfhost.script(src="scripts/change_mac.sh") + + @pytest.fixture(scope='class', autouse=True) + def prepareSshKeys(self, duthost, ptfhost): + ''' + Prepares testbed ssh keys by generating ssh key on ptf host and adding this key to known_hosts on duthost + This class-scope fixture runs once before test start + + Args: + duthost (AnsibleHost): Device Under Test (DUT) + ptfhost (AnsibleHost): Packet Test Framework (PTF) + + Returns: + None + ''' + invetory = duthost.host.options['inventory'].split('/')[-1] + secrets = duthost.host.options['variable_manager']._hostvars[duthost.hostname]['secret_group_vars'] + + prepareTestbedSshKeys(duthost, ptfhost, secrets[invetory]['sonicadmin_user']) + + def testWrArp(self, request, duthost, ptfhost): + ''' + Control Plane Assistent test for Warm-Reboot. + + The test first start Ferret server, implemented in Python. Then initiate Warm-Reboot procedure. While the + host in Warm-Reboot test continuously sending ARP request to the Vlan member ports and expect to receive ARP + replies. The test will fail as soon as there is no replies for more than 25 seconds for one of the Vlan + member ports. + + Args: + request: pytest request object + duthost (AnsibleHost): Device Under Test (DUT) + ptfhost (AnsibleHost): Packet Test Framework (PTF) + + Returns: + None + ''' + testDuration = request.config.getoption('--test_duration') + ptfIp = ptfhost.host.options['inventory_manager'].get_host(ptfhost.hostname).vars['ansible_host'] + dutIp = duthost.host.options['inventory_manager'].get_host(duthost.hostname).vars['ansible_host'] + + logger.info('Warm-Reboot Control-Plane assist feature') + ptf_runner( + ptfhost, + 'ptftests', + 'wr_arp.ArpTest', + qlen=PTFRUNNER_QLEN, + platform_dir='ptftests', + platform='remote', + params={ + 'ferret_ip' : ptfIp, + 'dut_ssh' : dutIp, + 'config_file' : VXLAN_CONFIG_FILE, + 'how_long' : testDuration, + }, + log_file='/tmp/wr_arp.ArpTest.log' + )