diff --git a/ansible/README.test.md b/ansible/README.test.md index c3ac324a06c..34354c1edd8 100644 --- a/ansible/README.test.md +++ b/ansible/README.test.md @@ -129,3 +129,10 @@ ansible-playbook test_sonic.yml -i inventory --limit {DUT_NAME} --tags bgp_multi This test only works for T1 related topologies(t1, t1-lag, ...) You might need to redeploy your VMs before you run this test due to the change for ToR VM router configuration changes `./testbed-cli.sh config-vm your-topo-name(vms1-1) your-vm-name(VM0108)` will do this for you + +### VLAN test +``` +ansible-playbook test_sonic.yml -i inventory --limit {DUT_NAME} --become --tags vlan --extra-vars "testbed_type={TESTBED_TYPE} ptf_host={PTF_HOST}" +``` +- Requires switch connected to a t0 testbed +- Requires switch connected to fanout switch and fanout switch need support [QinQ](https://en.wikipedia.org/wiki/IEEE_802.1ad). diff --git a/ansible/roles/test/files/ptftests/vlan_test.py b/ansible/roles/test/files/ptftests/vlan_test.py new file mode 100644 index 00000000000..c7a01c9eb82 --- /dev/null +++ b/ansible/roles/test/files/ptftests/vlan_test.py @@ -0,0 +1,271 @@ +import ast +import json +import logging +import subprocess + +from collections import defaultdict +from ipaddress import ip_address, ip_network + +import ptf +import ptf.packet as scapy +import ptf.dataplane as dataplane + +from ptf import config +from ptf.base_tests import BaseTest +from ptf.testutils import * +from ptf.mask import Mask + +class VlanTest(BaseTest): + def __init__(self): + BaseTest.__init__(self) + self.test_params = test_params_get() + #-------------------------------------------------------------------------- + + def log(self, message): + logging.info(message) + #-------------------------------------------------------------------------- + + def shell(self, cmds): + sp = subprocess.Popen(cmds, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + stdout, stderr = sp.communicate() + rc = sp.returncode + + return stdout, stderr, rc + #-------------------------------------------------------------------------- + + def setUp(self): + self.vlan_ports_list = ast.literal_eval(self.test_params["vlan_ports_list"]) + self.vlan_intf_list = ast.literal_eval(self.test_params["vlan_intf_list"]) + self.router_mac = self.test_params["router_mac"] + + for vlan_port in self.vlan_ports_list: + vlan_port["pvid"] = int(vlan_port["pvid"]) + vlan_port["port_index"] = int(vlan_port["port_index"]) + + self.dataplane = ptf.dataplane_instance + self.test_params = test_params_get() + self.log("Create VLAN intf") + + for vlan_port in self.vlan_ports_list: + for permit_vlanid in vlan_port["permit_vlanid"].keys(): + if int(permit_vlanid) != vlan_port["pvid"]: + self.shell(["ip", "link", "add", "link", "eth%d"%vlan_port["port_index"], + "name", "eth%d.%s"%(vlan_port["port_index"], permit_vlanid), + "type", "vlan", "id", str(permit_vlanid)]) + self.shell(["ip", "link", "set", + "eth%d.%s"%(vlan_port["port_index"], permit_vlanid), "up"]) + + self.setUpArpResponder() + self.log("Start arp_responder") + self.shell(["supervisorctl", "start", "arp_responder"]) + + logging.info("VLAN test starting ...") + pass + #-------------------------------------------------------------------------- + + def setUpArpResponder(self): + vlan_ports_list = self.vlan_ports_list + d = defaultdict(list) + for vlan_port in vlan_ports_list: + for permit_vlanid in vlan_port["permit_vlanid"].keys(): + if int(permit_vlanid) == vlan_port["pvid"]: + iface = "eth%d" % vlan_port["port_index"] + else: + iface = "eth%d.%s" % (vlan_port["port_index"], permit_vlanid) + d[iface].append(vlan_port["permit_vlanid"][str(permit_vlanid)]["peer_ip"]) + with open('/tmp/from_t1.json', 'w') as file: + json.dump(d, file) + + #-------------------------------------------------------------------------- + def tearDown(self): + logging.info("VLAN test ending ...") + + self.log("Stop arp_responder") + self.shell(["supervisorctl", "stop", "arp_responder"]) + + self.log("Delete VLAN intf") + for vlan_port in self.vlan_ports_list: + for permit_vlanid in vlan_port["permit_vlanid"].keys(): + if int(permit_vlanid) != vlan_port["pvid"]: + self.shell(["ip", "link", "delete", "eth%d.%d"%(vlan_port["port_index"], int(permit_vlanid))]) + + pass + + #-------------------------------------------------------------------------- + def build_icmp_packet(self, vlan_id, + src_mac="00:22:00:00:00:02", dst_mac="ff:ff:ff:ff:ff:ff", + src_ip="192.168.0.1", dst_ip="192.168.0.2", ttl=64): + pkt = simple_icmp_packet(pktlen=100 if vlan_id == 0 else 104, + eth_dst=dst_mac, + eth_src=src_mac, + dl_vlan_enable=False if vlan_id == 0 else True, + vlan_vid=vlan_id, + vlan_pcp=0, + ip_src=src_ip, + ip_dst=dst_ip, + ip_ttl=ttl) + return pkt + + + #-------------------------------------------------------------------------- + def verify_icmp_packets(self, vlan_port, vlan_id): + untagged_dst_ports = [] + tagged_dst_ports = [] + untagged_pkts = [] + tagged_pkts = [] + untagged_pkt = self.build_icmp_packet(0) + tagged_pkt = self.build_icmp_packet(vlan_id) + + for port in self.vlan_ports_list: + if vlan_port["port_index"] == port["port_index"]: + # Skip src port + continue + if port["pvid"] == vlan_id: + untagged_dst_ports.append(port["port_index"]) + untagged_pkts.append(untagged_pkt) + elif vlan_id in map(int, port["permit_vlanid"].keys()): + tagged_dst_ports.append(port["port_index"]) + tagged_pkts.append(tagged_pkt) + self.log("Verify untagged packets from ports " + str(untagged_dst_ports) + " tagged packets from ports " + str(tagged_dst_ports)) + verify_each_packet_on_each_port(self, untagged_pkts+tagged_pkts, untagged_dst_ports+tagged_dst_ports) + + #-------------------------------------------------------------------------- + def verify_icmp_packets_from_specified_port(self, port_id, vlan_id, src_mac, dst_mac, src_ip, dst_ip, ttl): + self.log("Verify packet from port " + str(port_id)) + pkt = self.build_icmp_packet(vlan_id, src_mac, dst_mac, src_ip, dst_ip, ttl) + verify_packet(self, pkt, port_id) + + #-------------------------------------------------------------------------- + def runTest(self): + vlan_ports_list = self.vlan_ports_list + vlan_intf_list = self.vlan_intf_list + + + # Test case #1 + self.log("Test case #1 starting ...") + # Send untagged packets from each port. + # Verify packets egress without tag from ports whose PVID same with ingress port + # Verify packets egress with tag from ports who include VLAN ID but PVID different from ingress port. + for vlan_port in vlan_ports_list: + pkt = self.build_icmp_packet(0) + self.log("Send untagged packet from {} ...".format(str(vlan_port["port_index"]))) + self.log(pkt.sprintf("%Ether.src% %IP.src% -> %Ether.dst% %IP.dst%")) + send(self, vlan_port["port_index"], pkt) + self.verify_icmp_packets(vlan_port, vlan_port["pvid"]) + + # Test case #2 + self.log("Test case #2 starting ...") + # Send tagged packets from each port. + # Verify packets egress without tag from ports whose PVID same with ingress port + # Verify packets egress with tag from ports who include VLAN ID but PVID different from ingress port. + for vlan_port in vlan_ports_list: + for permit_vlanid in map(int, vlan_port["permit_vlanid"].keys()): + pkt = self.build_icmp_packet(permit_vlanid) + self.log("Send tagged({}) packet from {} ...".format(permit_vlanid, str(vlan_port["port_index"]))) + self.log(pkt.sprintf("%Ether.src% %IP.src% -> %Ether.dst% %IP.dst%")) + send(self, vlan_port["port_index"], pkt) + self.verify_icmp_packets(vlan_port, permit_vlanid) + + # Test case #3 + # Send packets with invalid VLAN ID + # Verify no port can receive these pacekts + self.log("Test case #3 starting ...") + invalid_tagged_pkt = self.build_icmp_packet(4095) + masked_invalid_tagged_pkt = Mask(invalid_tagged_pkt) + masked_invalid_tagged_pkt.set_do_not_care_scapy(scapy.Dot1Q, "vlan") + + for vlan_port in vlan_ports_list: + src_port = vlan_port["port_index"] + dst_ports = [port["port_index"] for port in vlan_ports_list + if port != vlan_port ] + self.log("Send invalid tagged packet " + " from " + str(src_port) + "...") + self.log(invalid_tagged_pkt.sprintf("%Ether.src% %IP.src% -> %Ether.dst% %IP.dst%")) + send(self, src_port, invalid_tagged_pkt) + self.log("Check on " + str(dst_ports) + "...") + verify_no_packet_any(self, masked_invalid_tagged_pkt, dst_ports) + + # Test case #4 + # Send packets over VLAN interfaces. + # Verify packets can be receive on the egress port. + self.log("Test case #4 starting ...") + + target_list = [] + for vlan_port in vlan_ports_list: + for vlan_id in vlan_port["permit_vlanid"].keys(): + item = {"vlan_id": int(vlan_id), "port_index": vlan_port["port_index"], + "peer_ip": vlan_port["permit_vlanid"][vlan_id]["peer_ip"], + "remote_ip": vlan_port["permit_vlanid"][vlan_id]["remote_ip"], + "pvid": vlan_port["pvid"]} + target_list.append(item) + + for vlan_port in vlan_ports_list: + src_port = vlan_port["port_index"] + src_mac = self.dataplane.get_mac(0, src_port) + dst_mac = self.router_mac + for vlan_id in map(int, vlan_port["permit_vlanid"].keys()): + # Test for for directly-connected routing + src_ip = vlan_port["permit_vlanid"][str(vlan_id)]["peer_ip"] + for target in target_list: + if vlan_id == target["vlan_id"]: + # Skip same VLAN forwarding + continue + pkt = self.build_icmp_packet(vlan_id if vlan_id != vlan_port["pvid"] else 0, + src_mac, dst_mac, src_ip, target["peer_ip"]) + send(self, src_port, pkt) + self.log("Send {} packet from {} ...".format("untagged" if vlan_id == 0 else "tagged(%d)"%vlan_id, src_port)) + self.log(pkt.sprintf("%Ether.src% %IP.src% -> %Ether.dst% %IP.dst%")) + self.verify_icmp_packets_from_specified_port(target["port_index"], + target["vlan_id"] if target["vlan_id"] != target["pvid"] else 0, + dst_mac, self.dataplane.get_mac(0, target["port_index"]), + src_ip, target["peer_ip"], 63) + # Test for for indirectly-connected routing + src_ip = vlan_port["permit_vlanid"][str(vlan_id)]["remote_ip"] + for target in target_list: + if vlan_id == target["vlan_id"]: + # Skip same VLAN forwarding + continue + pkt = self.build_icmp_packet(vlan_id if vlan_id != vlan_port["pvid"] else 0, + src_mac, dst_mac, src_ip, target["remote_ip"]) + self.log("Send {} packet from {} ...".format("untagged" if vlan_id == 0 else "tagged(%d)"%vlan_id, src_port)) + self.log(pkt.sprintf("%Ether.src% %IP.src% -> %Ether.dst% %IP.dst%")) + send(self, src_port, pkt) + self.verify_icmp_packets_from_specified_port(target["port_index"], + target["vlan_id"] if target["vlan_id"] != target["pvid"] else 0, + dst_mac, self.dataplane.get_mac(0, target["port_index"]), + src_ip, target["remote_ip"], 63) + + # Test case #5 + # Send ICMP packets to VLAN interfaces. + # Verify ICMP reply packets can be received from ingress port. + self.log("Test case #5 starting ...") + for vlan_port in vlan_ports_list: + src_port = vlan_port["port_index"] + src_mac = self.dataplane.get_mac(0, src_port) + dst_mac = self.router_mac + for vlan_id in map(int, vlan_port["permit_vlanid"].keys()): + src_ip = vlan_port["permit_vlanid"][str(vlan_id)]["peer_ip"] + for vlan_intf in vlan_intf_list: + if int(vlan_intf["vlan_id"]) != vlan_id: + continue + dst_ip = vlan_intf["ip"].split("/")[0] + pkt = self.build_icmp_packet(vlan_id if vlan_id != vlan_port["pvid"] else 0, + src_mac, dst_mac, src_ip, dst_ip) + self.log("Send {} packet from {} ...".format("untagged" if vlan_id == 0 else "tagged(%d)"%vlan_id, src_port)) + self.log(pkt.sprintf("%Ether.src% %IP.src% -> %Ether.dst% %IP.dst%")) + send(self, src_port, pkt) + exp_pkt = simple_icmp_packet(eth_src=self.router_mac, + eth_dst=src_mac, + dl_vlan_enable=True if vlan_id != vlan_port["pvid"] else False, + vlan_vid=vlan_id if vlan_id != vlan_port["pvid"] else 0, + vlan_pcp=0, + ip_dst=src_ip, + ip_src=dst_ip, + icmp_type=0, + icmp_code=0) + + masked_exp_pkt = Mask(exp_pkt) + masked_exp_pkt.set_do_not_care_scapy(scapy.IP, "id") + + verify_packets(self, masked_exp_pkt, list(str(src_port))) + self.log("Verify packet from port " + str(src_port)) + #-------------------------------------------------------------------------- diff --git a/ansible/roles/test/tasks/vlan_cleanup.yml b/ansible/roles/test/tasks/vlan_cleanup.yml new file mode 100644 index 00000000000..e71d0c1fcae --- /dev/null +++ b/ansible/roles/test/tasks/vlan_cleanup.yml @@ -0,0 +1,21 @@ +- name: Restore all IP addresses on the LAGs + shell: ip addr add {{ (item.addr ~ "/" ~ item.mask)|ipaddr() }} dev {{ item.attachto }} + with_items: + - "{{ minigraph_portchannel_interfaces }}" + become: true + +- name: Bring up LAGs + shell: ifconfig {{ item.attachto }} up + with_items: + - "{{ minigraph_portchannel_interfaces }}" + become: true + +- name: Remove configuration for test + file: + state: absent + path: /etc/sonic/vlan_configuration.json + become: true + +- name: Reload configuration + shell: config reload -y + become: true diff --git a/ansible/roles/test/tasks/vlan_configure.yml b/ansible/roles/test/tasks/vlan_configure.yml new file mode 100644 index 00000000000..d8957746223 --- /dev/null +++ b/ansible/roles/test/tasks/vlan_configure.yml @@ -0,0 +1,55 @@ +- fail: msg="Please set ptf_host variable" + when: ptf_host is not defined + +- fail: msg="Invalid testbed_type value '{{testbed_type}}'" + when: testbed_type not in [ 't0' ] + +- debug: var=minigraph_portchannels + +- debug: var=minigraph_port_indices + +- debug: var=minigraph_ports + +- name: Generate VLAN ports information + template: src=roles/test/templates/vlan_info.j2 + dest=/tmp/vlan_info.yml + connection: local + +- name: Load VLAN ports info from file + include_vars: '/tmp/vlan_info.yml' + +- debug: var=vlan_ports_list +- debug: var=vlan_intf_list + +- name: Flush all IP addresses on the LAGs + shell: ip addr flush {{ item.attachto }} + with_items: + - "{{ minigraph_portchannel_interfaces }}" + become: true + +- name: Delete all IP addresses on the LAGs in config DB + shell: docker exec -i database redis-cli -n 4 del "PORTCHANNEL_INTERFACE|{{ item.attachto }}|{{ (item.addr ~ '/' ~ item.mask)|ipaddr()|upper() }}" + with_items: + - "{{ minigraph_portchannel_interfaces }}" + become: true + +- name: Shutdown LAGs + shell: ifconfig {{ item.attachto }} down + with_items: + - "{{ minigraph_portchannel_interfaces }}" + become: true + +- name: sleep for some time + pause: seconds=10 + +- name: Generate nessesary configuration for test + template: src=roles/test/templates/vlan_configuration.j2 + dest=/etc/sonic/vlan_configuration.json + become: true + +- name: Load configuration + shell: config load -y /etc/sonic/vlan_configuration.json + become: true + +- name: sleep for some time + pause: seconds=10 diff --git a/ansible/roles/test/tasks/vlan_test.yml b/ansible/roles/test/tasks/vlan_test.yml new file mode 100644 index 00000000000..0f4ce39c431 --- /dev/null +++ b/ansible/roles/test/tasks/vlan_test.yml @@ -0,0 +1,47 @@ +- name: Configure route for remote IP + shell: ip route add {{ item[0].permit_vlanid[item[1]].remote_ip }} via {{ item[0].permit_vlanid[item[1]].peer_ip }} + with_nested: + - "{{ vlan_ports_list }}" + - "{{ vlan_ports_list[0].permit_vlanid.keys() }}" + become: true + +- name: Set unique MACs to PTF interfaces + script: roles/test/files/helpers/change_mac.sh + delegate_to: "{{ptf_host}}" + +- name: Copy ARP responder to PTF + copy: src=roles/test/files/helpers/arp_responder.py dest=/opt + delegate_to: "{{ptf_host}}" + +- name: Copy arp responder supervisor configuration to the PTF container + template: src=arp_responder.conf.j2 dest=/etc/supervisor/conf.d/arp_responder.conf + vars: + - arp_responder_args: '' + delegate_to: "{{ ptf_host }}" + +- name: Reread supervisor configuration + shell: /usr/local/bin/supervisorctl reread + delegate_to: "{{ptf_host}}" + +- name: Update supervisor configuration + shell: /usr/local/bin/supervisorctl update + delegate_to: "{{ ptf_host }}" + +- name: Copy tests to the PTF container + copy: src=roles/test/files/ptftests dest=/root + delegate_to: "{{ ptf_host }}" + +- block: + - include: ptf_runner.yml + vars: + ptf_test_name: VLAN test + ptf_test_dir: ptftests + ptf_test_path: vlan_test.VlanTest + ptf_platform: remote + ptf_test_params: + - vlan_ports_list = \"{{ vlan_ports_list }}\" + - vlan_intf_list = \"{{ vlan_intf_list }}\" + - router_mac = \"{{ ansible_Ethernet0['macaddress'] }}\" + ptf_extra_options: "--relax --debug info --log-file /tmp/vlan_test.log" + rescue: + - debug: msg="PTF test raise error" diff --git a/ansible/roles/test/tasks/vlantb.yml b/ansible/roles/test/tasks/vlantb.yml new file mode 100644 index 00000000000..e62d84f4581 --- /dev/null +++ b/ansible/roles/test/tasks/vlantb.yml @@ -0,0 +1,20 @@ +#----------------------------------------- +# Apply Vlan configuration +#----------------------------------------- +- name: Vlan test setup on testbed + include: vlan_configure.yml + tags: vlan_configure + +#----------------------------------------- +# Run Vlan test +#----------------------------------------- +- name: Vlan test run on testbed + include: vlan_test.yml + tags: vlan_test + +#----------------------------------------- +# Clean up Vlan configuration +#----------------------------------------- +- name: Clean up Vlan test configuration on the testbed + include: vlan_cleanup.yml + tags: vlan_cleanup diff --git a/ansible/roles/test/templates/vlan_configuration.j2 b/ansible/roles/test/templates/vlan_configuration.j2 new file mode 100644 index 00000000000..bbdb2026b5e --- /dev/null +++ b/ansible/roles/test/templates/vlan_configuration.j2 @@ -0,0 +1,29 @@ +{ + "PORTCHANNEL_INTERFACE": {}, + "VLAN": { +{% for vlan_intf in vlan_intf_list %} + "Vlan{{ vlan_intf.vlan_id }}": { + "vlanid": {{ vlan_intf.vlan_id }} + }{{ "," if not loop.last else "" }} +{% endfor %} + }, + "VLAN_INTERFACE": { +{% for vlan_intf in vlan_intf_list %} + "Vlan{{ vlan_intf.vlan_id }}|{{ vlan_intf.ip }}": {}{{ "," if not loop.last else "" }} +{% endfor %} + }, + "VLAN_MEMBER": { +{% for vlan_port in vlan_ports_list %} +{% set outer_loop = loop %} +{% for permit_vlanid in vlan_port['permit_vlanid'].keys() %} + "Vlan{{ permit_vlanid }}|{{ vlan_port['dev'] }}": { +{% if vlan_port['pvid'] == permit_vlanid %} + "tagging_mode": "untagged" +{% else %} + "tagging_mode": "tagged" +{% endif %} + }{{ "," if not outer_loop.last or not loop.last else "" }} +{% endfor %} +{% endfor %} + } +} diff --git a/ansible/roles/test/templates/vlan_info.j2 b/ansible/roles/test/templates/vlan_info.j2 new file mode 100644 index 00000000000..dec14186944 --- /dev/null +++ b/ansible/roles/test/templates/vlan_info.j2 @@ -0,0 +1,35 @@ +--- + +{% set vlan_id_list = [ 100, 200 ] %} +vlan_ports_list: +{% for portchannel in minigraph_portchannels.keys()[:2] %} +{% set members = minigraph_portchannels[portchannel].members %} + - dev: {{ portchannel }} + port_index: {{ minigraph_port_indices[members[0]] }} + pvid: '{{ vlan_id_list[loop.index0%2] }}' + permit_vlanid: +{% for vlan in vlan_id_list %} + '{{ vlan }}': + peer_ip: '192.168.{{ vlan }}.{{ 2 + minigraph_port_indices.keys().index(members[0]) }}' + remote_ip: '{{vlan}}.1.1.{{ 2 + minigraph_port_indices.keys().index(members[0]) }}' +{% endfor %} +{% endfor %} +{% for port in minigraph_ports.keys()[:2] %} + - dev: {{ port }} + port_index: '{{ minigraph_port_indices[port]}}' + pvid: '{{ vlan_id_list[loop.index0%2] }}' + permit_vlanid: +{% for vlan in vlan_id_list %} + '{{ vlan }}': + peer_ip: '192.168.{{ vlan }}.{{ 2 + minigraph_port_indices.keys().index(port) }}' + remote_ip: '{{vlan}}.1.1.{{ 2 + minigraph_port_indices.keys().index(port) }}' +{% endfor %} +{% endfor %} + +vlan_intf_list: +{% for vlan in vlan_id_list %} + - vlan_id: '{{ (vlan|int) }}' + ip: '192.168.{{ vlan }}.1/24' +{% endfor %} + +...