diff --git a/ansible/group_vars/sonic_latest/package_versions.yml b/ansible/group_vars/sonic_latest/package_versions.yml index 89e66e7562e..208da3a36d7 100644 --- a/ansible/group_vars/sonic_latest/package_versions.yml +++ b/ansible/group_vars/sonic_latest/package_versions.yml @@ -24,3 +24,4 @@ image_id_fpm: docker-fpm:{{ dockers_tag }} image_id_snmp_sv2: docker-snmp-sv2:{{ dockers_tag }} image_id_lldp_sv2: docker-lldp-sv2:{{ dockers_tag }} image_id_teamd: docker-teamd:{{ dockers_tag }} +image_id_dhcp_relay: docker-dhcp-relay:{{ dockers_tag }} diff --git a/ansible/library/minigraph_facts.py b/ansible/library/minigraph_facts.py index 56adcddd776..6ad9a988cc1 100644 --- a/ansible/library/minigraph_facts.py +++ b/ansible/library/minigraph_facts.py @@ -8,6 +8,7 @@ import copy import ipaddr as ipaddress from collections import defaultdict +from natsort import natsort from lxml import etree as ET @@ -327,7 +328,6 @@ def reconcile_mini_graph_locations(filename, hostname): return mini_graph_path, root - def parse_xml(filename, hostname): mini_graph_path, root = reconcile_mini_graph_locations(filename, hostname) @@ -356,7 +356,7 @@ def parse_xml(filename, hostname): elif hwsku == "Force10-S6100": for i in range(0, 4): for j in range(0, 16): - port_alias_map["fortyGigE1/%d/%d" % (i+1, j+1)] = "Ethernet%d" % (i * 16 + j + 1) + port_alias_map["fortyGigE1/%d/%d" % (i+1, j+1)] = "Ethernet%d" % (i * 16 + j) elif hwsku == "Arista-7050-QX32": for i in range(1, 25): port_alias_map["Ethernet%d/1" % i] = "Ethernet%d" % ((i - 1) * 4) @@ -390,6 +390,34 @@ def parse_xml(filename, hostname): for i,member in enumerate(pc['members']): pc['members'][i] = port_alias_map[member] + + # Create port index map. Since we currently output a mix of NGS names + # and SONiC mapped names, we include both in this map. + # SONiC aliases, when sorted in natural sort order, match the phyical port + # index order, so we sort by SONiC port alias, and map + # back to NGS names after sorting using this inverted map + # + # TODO: Move all alias-related code out of minigraph_facts.py and into + # its own module to be used as another layer after parsing the minigraph. + inverted_port_alias_map = {v: k for k, v in port_alias_map.iteritems()} + + # Start by creating a list of all port aliases + port_alias_list = [] + for k, v in port_alias_map.iteritems(): + port_alias_list.append(v) + + # Sort the list in natural order + port_alias_list_sorted = natsort(port_alias_list) + + # Create map from SONiC alias to physical index and NGS name to physical index + port_index_map = {} + for idx, val in enumerate(port_alias_list_sorted): + port_index_map[val] = idx + port_index_map[inverted_port_alias_map[val]] = idx + + + + # Generate results Tree = lambda: defaultdict(Tree) results = Tree() @@ -414,6 +442,7 @@ def parse_xml(filename, hostname): results['minigraph_as_xml'] = mini_graph_path results['minigraph_console'] = get_console_info(devices, console_dev, console_port) results['minigraph_mgmt'] = get_mgmt_info(devices, mgmt_dev, mgmt_port) + results['minigraph_port_indices'] = port_index_map return results @@ -468,5 +497,4 @@ def print_parse_xml(hostname): if __name__ == "__main__": main() - #debug_main() diff --git a/ansible/minigraph/OCPSCH01040AALF.xml b/ansible/minigraph/OCPSCH01040AALF.xml index 2506ad7944a..86d8ca1a6e6 100644 --- a/ansible/minigraph/OCPSCH01040AALF.xml +++ b/ansible/minigraph/OCPSCH01040AALF.xml @@ -117,6 +117,12 @@ Ethernet4 + + + OCPSCH01040AALF + ACS-S6000 + + diff --git a/ansible/minigraph/OCPSCH01040BBLF.xml b/ansible/minigraph/OCPSCH01040BBLF.xml index 465d39744d9..59be72e31bf 100644 --- a/ansible/minigraph/OCPSCH01040BBLF.xml +++ b/ansible/minigraph/OCPSCH01040BBLF.xml @@ -98,6 +98,32 @@ + + + + 40000 + DeviceInterfaceLink + OCPSCH0104001MS + Ethernet0 + OCPSCH01040BBLF + Ethernet0 + + + 40000 + DeviceInterfaceLink + OCPSCH0104002MS + Ethernet0 + OCPSCH01040BBLF + Ethernet4 + + + + + OCPSCH01040BBLF + ACS-MSN2700 + + + OCPSCH01040BBLF ACS-MSN2700 diff --git a/ansible/minigraph/OCPSCH01040CCLF.xml b/ansible/minigraph/OCPSCH01040CCLF.xml index 654b90ee2e9..ce5cb71f25f 100644 --- a/ansible/minigraph/OCPSCH01040CCLF.xml +++ b/ansible/minigraph/OCPSCH01040CCLF.xml @@ -98,6 +98,32 @@ + + + + 40000 + DeviceInterfaceLink + OCPSCH0104001MS + Ethernet0 + OCPSCH01040CCLF + Ethernet0 + + + 40000 + DeviceInterfaceLink + OCPSCH0104002MS + Ethernet0 + OCPSCH01040CCLF + Ethernet4 + + + + + OCPSCH01040CCLF + ACS-CAVM + + + OCPSCH01040CCLF ACS-CAVM diff --git a/ansible/minigraph/OCPSCH01040EELF.xml b/ansible/minigraph/OCPSCH01040EELF.xml index cbf6e4c1dbf..59c60fccdc5 100644 --- a/ansible/minigraph/OCPSCH01040EELF.xml +++ b/ansible/minigraph/OCPSCH01040EELF.xml @@ -98,6 +98,32 @@ + + + + 40000 + DeviceInterfaceLink + OCPSCH0104001MS + Ethernet0 + OCPSCH01040EELF + Ethernet0 + + + 40000 + DeviceInterfaceLink + OCPSCH0104002MS + Ethernet0 + OCPSCH01040EELF + Ethernet4 + + + + + OCPSCH01040EELF + ACS-BF + + + OCPSCH01040EELF ACS-BF diff --git a/ansible/minigraph/OCPSCH01040FFLF.xml b/ansible/minigraph/OCPSCH01040FFLF.xml index 1be36e538e9..e7b9afa01b0 100644 --- a/ansible/minigraph/OCPSCH01040FFLF.xml +++ b/ansible/minigraph/OCPSCH01040FFLF.xml @@ -98,6 +98,32 @@ + + + + 40000 + DeviceInterfaceLink + OCPSCH0104001MS + Ethernet0 + OCPSCH01040FFLF + Ethernet0 + + + 40000 + DeviceInterfaceLink + OCPSCH0104002MS + Ethernet0 + OCPSCH01040FFLF + Ethernet4 + + + + + OCPSCH01040FFLF + ACS-A7050-QX32 + + + OCPSCH01040FFLF ACS-A7050-QX32 diff --git a/ansible/minigraph/OCPSCH01040GGLF.xml b/ansible/minigraph/OCPSCH01040GGLF.xml index ad56373bdac..978577e5443 100644 --- a/ansible/minigraph/OCPSCH01040GGLF.xml +++ b/ansible/minigraph/OCPSCH01040GGLF.xml @@ -98,6 +98,32 @@ + + + + 40000 + DeviceInterfaceLink + OCPSCH0104001MS + Ethernet0 + OCPSCH01040GGLF + Ethernet0 + + + 40000 + DeviceInterfaceLink + OCPSCH0104002MS + Ethernet0 + OCPSCH01040GGLF + Ethernet4 + + + + + OCPSCH01040GGLF + ACS-BF + + + OCPSCH01040GGLF ACS-BF diff --git a/ansible/minigraph/OCPSCH01040HHLF.xml b/ansible/minigraph/OCPSCH01040HHLF.xml index f643cb3955e..ba223330b9e 100644 --- a/ansible/minigraph/OCPSCH01040HHLF.xml +++ b/ansible/minigraph/OCPSCH01040HHLF.xml @@ -98,6 +98,32 @@ + + + + 40000 + DeviceInterfaceLink + OCPSCH0104001MS + Ethernet0 + OCPSCH01040HHLF + Ethernet0 + + + 40000 + DeviceInterfaceLink + OCPSCH0104002MS + Ethernet0 + OCPSCH01040HHLF + Ethernet4 + + + + + OCPSCH01040HHLF + ACS-BF + + + OCPSCH01040HHLF ACS-BF diff --git a/ansible/minigraph/switch-t0.xml b/ansible/minigraph/switch-t0.xml new file mode 100644 index 00000000000..413aedf7e23 --- /dev/null +++ b/ansible/minigraph/switch-t0.xml @@ -0,0 +1,315 @@ + + + + + + false + switch-t0 + 10.0.0.56 + ARISTA01T1 + 10.0.0.57 + 1 + 180 + 60 + + + switch-t0 + FC00::71 + ARISTA01T1 + FC00::72 + 1 + 180 + 60 + + + false + switch-t0 + 10.0.0.58 + ARISTA02T1 + 10.0.0.59 + 1 + 180 + 60 + + + switch-t0 + FC00::75 + ARISTA02T1 + FC00::76 + 1 + 180 + 60 + + + false + switch-t0 + 10.0.0.60 + ARISTA03T1 + 10.0.0.61 + 1 + 180 + 60 + + + switch-t0 + FC00::79 + ARISTA03T1 + FC00::7A + 1 + 180 + 60 + + + false + switch-t0 + 10.0.0.62 + ARISTA04T1 + 10.0.0.63 + 1 + 180 + 60 + + + switch-t0 + FC00::7D + ARISTA04T1 + FC00::7E + 1 + 180 + 60 + + + + + 65100 + switch-t0 + + +
10.0.0.57
+ + + +
+ +
10.0.0.59
+ + + +
+ +
10.0.0.61
+ + + +
+ +
10.0.0.63
+ + + +
+
+ +
+ + 64600 + ARISTA01T1 + + + + 64600 + ARISTA02T1 + + + + 64600 + ARISTA03T1 + + + + 64600 + ARISTA04T1 + + +
+
+ + + + + + HostIP + Loopback0 + + 10.1.0.32/32 + + 10.1.0.32/32 + + + HostIP1 + Loopback0 + + FC00:1::32/128 + + FC00:1::32/128 + + + + + HostIP + eth0 + + 10.0.0.100/24 + + 10.0.0.100/24 + + + + + + + switch-t0 + + + PortChannel01 + fortyGigE0/112 + + + + PortChannel02 + fortyGigE0/116 + + + + PortChannel03 + fortyGigE0/120 + + + + PortChannel04 + fortyGigE0/124 + + + + + + Vlan1000 + fortyGigE0/4;fortyGigE0/8;fortyGigE0/12;fortyGigE0/16;fortyGigE0/20;fortyGigE0/24;fortyGigE0/28;fortyGigE0/32;fortyGigE0/36;fortyGigE0/40;fortyGigE0/44;fortyGigE0/48;fortyGigE0/52;fortyGigE0/56;fortyGigE0/60;fortyGigE0/64;fortyGigE0/68;fortyGigE0/72;fortyGigE0/76;fortyGigE0/80;fortyGigE0/84;fortyGigE0/88;fortyGigE0/92;fortyGigE0/96 + False + 0.0.0.0/0 + + 1000 + 1000 + 192.168.0.0/27 + + + + + + PortChannel01 + 10.0.0.56/31 + + + + PortChannel01 + FC00::71/126 + + + + PortChannel02 + 10.0.0.58/31 + + + + PortChannel02 + FC00::75/126 + + + + PortChannel03 + 10.0.0.60/31 + + + + PortChannel03 + FC00::79/126 + + + + PortChannel04 + 10.0.0.62/31 + + + + PortChannel04 + FC00::7D/126 + + + + Vlan1000 + 192.168.0.1/27 + + + + + + + + + + + + DeviceInterfaceLink + ARISTA01T1 + Ethernet1/1 + switch-t0 + fortyGigE0/112 + + + DeviceInterfaceLink + ARISTA02T1 + Ethernet1/1 + switch-t0 + fortyGigE0/116 + + + DeviceInterfaceLink + ARISTA03T1 + Ethernet1/1 + switch-t0 + fortyGigE0/120 + + + DeviceInterfaceLink + ARISTA04T1 + Ethernet1/1 + switch-t0 + fortyGigE0/124 + + + + + switch-t0 + Force10-S6000 + + + ARISTA01T1 + Arista + + + ARISTA02T1 + Arista + + + ARISTA03T1 + Arista + + + ARISTA04T1 + Arista + + + + switch-t0 + Force10-S6000 +
diff --git a/ansible/minigraph/switch1.xml b/ansible/minigraph/switch1.xml index f585f142730..4771c9a0427 100644 --- a/ansible/minigraph/switch1.xml +++ b/ansible/minigraph/switch1.xml @@ -1045,6 +1045,12 @@ Ethernet1 + + + switch1 + Force10-S6000 + + switch1 Force10-S6000 diff --git a/ansible/minigraph/switch2.xml b/ansible/minigraph/switch2.xml index 61d383cc10e..1e42e638f63 100644 --- a/ansible/minigraph/switch2.xml +++ b/ansible/minigraph/switch2.xml @@ -1045,6 +1045,12 @@ Ethernet1 + + + switch2 + ACS-MSN2700 + + switch2 ACS-MSN2700 diff --git a/ansible/minigraph/switch3.xml b/ansible/minigraph/switch3.xml index e1025643513..acd1416fdfb 100644 --- a/ansible/minigraph/switch3.xml +++ b/ansible/minigraph/switch3.xml @@ -910,6 +910,12 @@ Ethernet1 + + + switch3 + Force10-S6000 + + switch3 Force10-S6000 diff --git a/ansible/minigraph/switch5.xml b/ansible/minigraph/switch5.xml index 14ac242f9fe..e3754462b89 100644 --- a/ansible/minigraph/switch5.xml +++ b/ansible/minigraph/switch5.xml @@ -1262,6 +1262,12 @@ Ethernet1 + + + switch5 + ACS-MSN2700 + + switch5 ACS-MSN2700 diff --git a/ansible/roles/sonic-common/tasks/dhcp_relay.yml b/ansible/roles/sonic-common/tasks/dhcp_relay.yml new file mode 100644 index 00000000000..583a53b734f --- /dev/null +++ b/ansible/roles/sonic-common/tasks/dhcp_relay.yml @@ -0,0 +1,16 @@ +- name: Copy DHCP relay docker config file to device + become: true + template: src=dhcp_relay.yml.j2 + dest=/etc/sonic/dhcp_relay.yml + mode=0644 + +- name: Ensure DHCP Relay container started + include: sonicdocker.yml + vars: + docker_container: dhcp_relay + docker_image: "{{ image_id_dhcp_relay }}" + docker_privileged: yes + docker_state: reloaded + docker_volumes: + - /etc/sonic/:/etc/sonic/:ro + diff --git a/ansible/roles/sonic-common/tasks/main.yml b/ansible/roles/sonic-common/tasks/main.yml index a6649610af9..1ea562d3c52 100644 --- a/ansible/roles/sonic-common/tasks/main.yml +++ b/ansible/roles/sonic-common/tasks/main.yml @@ -1,3 +1,4 @@ +# Gather minigraph facts - name: Gathering minigraph facts about the device minigraph_facts: host={{ inventory_hostname }} become: no @@ -8,7 +9,7 @@ sonic_asic_type: broadcom when: sonic_hwsku in broadcom_hwskus tags: always - + - name: Set sonic_asic_type fact set_fact: sonic_asic_type: mellanox @@ -38,7 +39,7 @@ dest=/etc/sonic/minigraph.xml mode=0644 tags: always - + # Syslog - name: Install Syslog daemon become: true @@ -103,6 +104,7 @@ # Assign hostname - name: Assign hostname + become: true hostname: name={{ inventory_hostname }} tags: system @@ -138,6 +140,7 @@ register: if_copy tags: network,unsafe +# Do the same change in /etc/network/interfaces here - name: Add MGMT Default Route to table default become: true command: ip route add default via {{ minigraph_mgmt_interface["gwaddr"] }} dev eth0 table default @@ -153,6 +156,7 @@ changed_when: False - name: Add mgmt ip rule if it does not exist + become: true shell: /bin/ip rule add from {{ minigraph_mgmt_interface['addr'] }}/32 table default tags: network,unsafe when: ip_rules.stdout.find("from {{ minigraph_mgmt_interface['addr'] }} lookup default") == -1 @@ -169,7 +173,7 @@ ## Redis database - include: database.yml - tags: + tags: - swss - database - unsafe @@ -217,6 +221,11 @@ - include: lldp.yml tags: lldp +# DHCP Relay +- include: dhcp_relay.yml + tags: dhcp_relay + when: minigraph_devices[inventory_hostname]['type'] == "ToRRouter" + - command: /bin/true notify: Clean up apt diff --git a/ansible/roles/sonic-common/templates/dhcp_relay.yml.j2 b/ansible/roles/sonic-common/templates/dhcp_relay.yml.j2 new file mode 100644 index 00000000000..e5348b9c749 --- /dev/null +++ b/ansible/roles/sonic-common/templates/dhcp_relay.yml.j2 @@ -0,0 +1 @@ +dhcp_servers: "{{ dhcp_servers | join(' ') }}" diff --git a/ansible/roles/sonic-common/templates/etc/systemd/system/dhcp_relay.j2 b/ansible/roles/sonic-common/templates/etc/systemd/system/dhcp_relay.j2 new file mode 100644 index 00000000000..71f4b4c1d51 --- /dev/null +++ b/ansible/roles/sonic-common/templates/etc/systemd/system/dhcp_relay.j2 @@ -0,0 +1,12 @@ +[Unit] +Description=DHCP relay container +Requires=docker.service +After=docker.service + +[Service] +User={{ sonicadmin_user }} +ExecStart=/usr/bin/docker start -a dhcp_relay +ExecStop=/usr/bin/docker stop dhcp_relay + +[Install] +WantedBy=multi-user.target diff --git a/ansible/roles/test/files/ptftests/dhcp_relay_test.py b/ansible/roles/test/files/ptftests/dhcp_relay_test.py new file mode 100644 index 00000000000..f5e17ec8da7 --- /dev/null +++ b/ansible/roles/test/files/ptftests/dhcp_relay_test.py @@ -0,0 +1,393 @@ +import ast +import struct +import ipaddress + +# Packet Test Framework imports +import ptf +import ptf.packet as scapy +import ptf.testutils as testutils +from ptf import config +from ptf.base_tests import BaseTest +from ptf.mask import Mask + + +# Helper function to increment an IP address +# ip_addr should be passed as a dot-decimal string +# Return value is also a dot-decimal string +def incrementIpAddress(ip_addr, by=1): + new_addr = ipaddress.ip_address(unicode(ip_addr)) + new_addr = new_addr + by + return str(new_addr) + + + +class DataplaneBaseTest(BaseTest): + def __init__(self): + BaseTest.__init__(self) + + def setUp(self): + self.dataplane = ptf.dataplane_instance + self.dataplane.flush() + if config["log_dir"] is not None: + filename = os.path.join(config["log_dir"], str(self)) + ".pcap" + self.dataplane.start_pcap(filename) + + def tearDown(self): + if config["log_dir"] is not None: + self.dataplane.stop_pcap() + +""" + This test simulates a new host booting up of the Vlan network of a ToR and + requesting an IP address via DHCP. Setup is as follows: + - DHCP client is simulated by crafting and sending packets on a port + connected to Vlan of ToR. + - PTF listens/sends on injected interfaces which link ToR to leaves. With this, + we can listen for traffic sent from DHCP relay out to would-be DHCP servers + + This test performs the following functionality: + 1.) Simulated client broadcasts a DHCPDISCOVER message + 2.) Verify DHCP relay running on ToR receives the DHCPDISCOVER message + and relays it to all of its known DHCP servers + 3.) Simulate DHCPOFFER message broadcast from a DHCP server to the ToR + 4.) Verify DHCP relay receives the DHCPOFFER message and forwards it to our + simulated client. + 5.) Simulated client broadcasts a DHCPREQUEST message + 6.) Verify DHCP relay running on ToR receives the DHCPREQUEST message + and relays it to all of its known DHCP servers + 7.) Simulate DHCPACK message sent from a DHCP server to the ToR + 8.) Verify DHCP relay receives the DHCPACK message and forwards it to our + simulated client. + + + To run: place the following in a shell script (this will test against str-s6000-acs-12 (ec:f4:bb:fe:88:0a)): + ptf --test-dir test dhcp_relay_test.DHCPTest --platform remote -t "verbose=True; client_port_index=\"4\"; leaf_port_indices=\"[28, 29, 30, 31]\"; server_ip=\"2.2.2.2\"; relay_iface_name=\"Vlan1000\"; relay_iface_ip=\"192.168.0.1\"; relay_iface_mac=\"ec:f4:bb:fe:88:0a\"; relay_iface_netmask=\"255.255.255.224\"" --disable-ipv6 --disable-vxlan --disable-geneve --disable-erspan --disable-mpls --disable-nvgre + + The above command is configured to test with the following configuration: + - Vlan IP of DuT is 192.168.0.1, MAC address is ec:f4:bb:fe:88:0a (this is configured to test against str-s6000-acs-12) + - Simulated client will live on PTF interface eth4 (interface number 4) + - Assumes leaf switches are connected to injected PTF interfaces 28, 29, 30, 31 + - Test will simulate replies from server with IP '2.2.2.2' + - Simulated server will offer simulated client IP '192.168.0.2' with a subnet of '255.255.255.0' (this should be in the Vlan of DuT) + + + DHCP Relay currently installed with SONiC is isc-dhcp-relay + + TODO???: + 1) DHCP Renew Test + 2) DHCP NACK Test + 3) DHCP Option 82 - remote ID test when available + 4) Test with multiple DHCP Servers + +""" + +class DHCPTest(DataplaneBaseTest): + + BROADCAST_MAC = 'ff:ff:ff:ff:ff:ff' + BROADCAST_IP = '255.255.255.255' + DEFAULT_ROUTE_IP = '0.0.0.0' + DHCP_CLIENT_PORT = 68 + DHCP_SERVER_PORT = 67 + DHCP_LEASE_TIME_OFFSET = 292 + DHCP_LEASE_TIME_LEN = 6 + LEASE_TIME = 86400 + + # Number of DHCP servers relay is forwarding to + # Currently this is the number of DHCP servers specified in str.yml + # TODO: Calculate this number dynamically + NUM_DHCP_SERVERS = 48 + + def __init__(self): + DataplaneBaseTest.__init__(self) + + + def setUp(self): + DataplaneBaseTest.setUp(self) + + self.test_params = testutils.test_params_get() + + # These are the interfaces we are injected into that link to out leaf switches + self.server_port_indices = ast.literal_eval(self.test_params['leaf_port_indices']) + self.server_ip = self.test_params['server_ip'] + + self.relay_iface_name = self.test_params['relay_iface_name'] + self.relay_iface_ip = self.test_params['relay_iface_ip'] + self.relay_iface_mac = self.test_params['relay_iface_mac'] + + self.client_port_index = int(self.test_params['client_port_index']) + self.client_iface_mac = self.dataplane.get_mac(0, self.client_port_index) + + # relay_agent_info is a byte string created by the relay agent to specify which + # interface it received the message on. It is stored as suboption 1 of option 82. + # It consists of the following: + # Byte 0: Suboption number, always set to 1 + # Byte 1: Length of suboption data in bytes (i.e., length of interface name) + # Bytes 2+: Suboption data (interface name) + self.relay_agent_info = struct.pack('BB', 1, len(self.relay_iface_name)) + self.relay_agent_info += self.relay_iface_name + + # We'll assign our client the IP address 1 greater than our relay interface (i.e., gateway) IP + self.client_ip = incrementIpAddress(self.relay_iface_ip, 1) + self.client_subnet = self.test_params['relay_iface_netmask'] + + + def tearDown(self): + DataplaneBaseTest.tearDown(self) + + + """ + Packet generation functions/wrappers + + """ + + def create_dhcp_discover_packet(self): + return testutils.dhcp_discover_packet(eth_client=self.client_iface_mac) + + def create_dhcp_discover_relayed_packet(self): + my_chaddr = ''.join([chr(int(octet, 16)) for octet in self.client_iface_mac.split(':')]) + + # Relay modifies the DHCPDISCOVER message in the following ways: + # 1.) Increments the hops count in the DHCP header + # 2.) Updates the gateway IP address in hte BOOTP header (if it is 0.0.0.0) + # 3.) Replaces the source IP with the IP of the interface which the relay + # received the broadcast DHCPDISCOVER message on + # 4.) Replaces the destination IP with the IP address of the DHCP server + # each message is being forwarded to + # Here, the actual destination MAC should be the MAC of the leaf the relay + # forwards through and the destination IP should be the IP of the DHCP server + # the relay is forwarding to. We don't need to confirm these, so we'll + # just mask them off later + # + # TODO: Relay also replaces source IP with IP of interface on which it received the + # broadcast DHCPDISCOVER from client. This appears to be loopback. + # We could pull from minigraph and check here. + pkt = scapy.Ether(dst=self.BROADCAST_MAC, src=self.relay_iface_mac, type=0x0800) + pkt /= scapy.IP(src=self.DEFAULT_ROUTE_IP, dst=self.BROADCAST_IP, len=328, ttl=64) + pkt /= scapy.UDP(sport=self.DHCP_SERVER_PORT, dport=self.DHCP_SERVER_PORT, len=308) + pkt /= scapy.BOOTP(op=1, + htype=1, + hlen=6, + hops=1, + xid=0, + secs=0, + flags=0x8000, + ciaddr=self.DEFAULT_ROUTE_IP, + yiaddr=self.DEFAULT_ROUTE_IP, + siaddr=self.DEFAULT_ROUTE_IP, + giaddr=self.relay_iface_ip, + chaddr=my_chaddr) + pkt /= scapy.DHCP(options=[('message-type', 'discover'), + ('relay_agent_Information', self.relay_agent_info), + ('end')]) + + # The isc-dhcp-relay adds 44 bytes of padding to our discover packet + pkt /= scapy.PADDING('\x00' * 44) + return pkt + + def create_dhcp_offer_packet(self): + return testutils.dhcp_offer_packet(eth_client=self.client_iface_mac, + eth_server=self.relay_iface_mac, + ip_server=self.relay_iface_ip, + ip_offered=self.client_ip, + ip_gateway=self.relay_iface_ip, + netmask_client=self.client_subnet, + dhcp_lease=self.LEASE_TIME, + padding_bytes=0) + + def create_dhcp_request_packet(self): + return testutils.dhcp_request_packet(eth_client=self.client_iface_mac, + ip_server=self.server_ip, + ip_requested=self.client_ip) + + def create_dhcp_request_relayed_packet(self): + my_chaddr = ''.join([chr(int(octet, 16)) for octet in self.client_iface_mac.split(':')]) + + # Here, the actual destination MAC should be the MAC of the leaf the relay + # forwards through and the destination IP should be the IP of the DHCP server + # the relay is forwarding to. We don't need to confirm these, so we'll + # just mask them off later + # + # TODO: Relay also replaces source IP with IP of interface on which it received the + # broadcast DHCPDISCOVER from client. This appears to be loopback. + # We could pull from minigraph and check here. + pkt = scapy.Ether(dst=self.BROADCAST_MAC, src=self.relay_iface_mac, type=0x0800) + pkt /= scapy.IP(src=self.DEFAULT_ROUTE_IP, dst=self.BROADCAST_IP, len=328, ttl=64) + pkt /= scapy.UDP(sport=self.DHCP_SERVER_PORT, dport=self.DHCP_SERVER_PORT, len=308) + pkt /= scapy.BOOTP(op=1, + htype=1, + hlen=6, + hops=1, + xid=0, + secs=0, + flags=0x8000, + ciaddr=self.DEFAULT_ROUTE_IP, + yiaddr=self.DEFAULT_ROUTE_IP, + siaddr=self.DEFAULT_ROUTE_IP, + giaddr=self.relay_iface_ip, + chaddr=my_chaddr) + pkt /= scapy.DHCP(options=[('message-type', 'request'), + ('requested_addr', self.client_ip), + ('server_id', self.server_ip), + ('relay_agent_Information', self.relay_agent_info), + ('end')]) + + # The isc-dhcp-relay adds 32 bytes of padding to our request + pkt /= scapy.PADDING('\x00' * 32) + return pkt + + def create_dhcp_ack_packet(self): + return testutils.dhcp_ack_packet(eth_client=self.client_iface_mac, + eth_server=self.relay_iface_mac, + ip_server=self.relay_iface_ip, + ip_offered=self.client_ip, + netmask_client=self.client_subnet, + ip_gateway=self.relay_iface_ip, + dhcp_lease=self.LEASE_TIME, + padding_bytes=0) + + + """ + Send/receive functions + + """ + + # Simulate client coming on vlan and broadcasting a DHCPDISCOVER message + def client_send_discover(self): + # Form and send DHCPDISCOVER packet + dhcp_discover = self.create_dhcp_discover_packet() + testutils.send_packet(self, self.client_port_index, dhcp_discover) + + # Verify that the DHCP relay actually received and relayed the DHCPDISCOVER message to all of + # its known DHCP servers. We also verify that the relay inserted Option 82 information in the + # packet. + def verify_relayed_discover(self): + # Create a packet resembling a relayed DCHPDISCOVER packet + dhcp_discover_relayed = self.create_dhcp_discover_relayed_packet() + + # Mask off fields we don't care about matching + masked_discover = Mask(dhcp_discover_relayed) + masked_discover.set_do_not_care_scapy(scapy.Ether, "dst") + + masked_discover.set_do_not_care_scapy(scapy.IP, "version") + masked_discover.set_do_not_care_scapy(scapy.IP, "ihl") + masked_discover.set_do_not_care_scapy(scapy.IP, "tos") + masked_discover.set_do_not_care_scapy(scapy.IP, "len") + masked_discover.set_do_not_care_scapy(scapy.IP, "id") + masked_discover.set_do_not_care_scapy(scapy.IP, "flags") + masked_discover.set_do_not_care_scapy(scapy.IP, "frag") + masked_discover.set_do_not_care_scapy(scapy.IP, "ttl") + masked_discover.set_do_not_care_scapy(scapy.IP, "proto") + masked_discover.set_do_not_care_scapy(scapy.IP, "chksum") + masked_discover.set_do_not_care_scapy(scapy.IP, "src") + masked_discover.set_do_not_care_scapy(scapy.IP, "dst") + masked_discover.set_do_not_care_scapy(scapy.IP, "options") + + masked_discover.set_do_not_care_scapy(scapy.UDP, "chksum") + + masked_discover.set_do_not_care_scapy(scapy.BOOTP, "sname") + masked_discover.set_do_not_care_scapy(scapy.BOOTP, "file") + + masked_discover.set_do_not_care_scapy(scapy.PADDING, "load") + + # Count the number of these packets received on the ports connected to our leaves + discover_count = testutils.count_matched_packets_all_ports(self, masked_discover, self.server_port_indices) + self.assertTrue(discover_count == self.NUM_DHCP_SERVERS, + "Failed: Discover count of %d != %d (NUM_DHCP_SERVERS)" % (discover_count, self.NUM_DHCP_SERVERS)) + + # Simulate a DHCP server sending a DHCPOFFER message to client. + # We do this by injecting a DHCPOFFER message on the link connected to one + # of our leaf switches. + def server_send_offer(self): + dhcp_offer = self.create_dhcp_offer_packet() + testutils.send_packet(self, self.client_port_index, dhcp_offer) + + # Verify that the DHCPOFFER would be received by our simulated client + def verify_offer_received(self): + dhcp_offer = self.create_dhcp_offer_packet() + + masked_offer = Mask(dhcp_offer) + masked_offer.set_do_not_care_scapy(scapy.Ether, "dst") + masked_offer.set_do_not_care_scapy(scapy.UDP, "chksum") + masked_offer.set_do_not_care_scapy(scapy.IP, "chksum") + + # Mask out lease time since it changes depending on when the server recieves the request + # Lease time in ack can be slightly different than in offer, since lease time varies slightly + # We also want to ignore the checksums since they will vary a bit depending on the timestamp + # Offset is byte 292, 6 byte field, set_do_not_care() expects values in bits + masked_offer.set_do_not_care((self.DHCP_LEASE_TIME_OFFSET * 8), (self.DHCP_LEASE_TIME_LEN * 8)) + + # NOTE: verify_packet() will fail for us via an assert, so no nedd to check a return value here + testutils.verify_packet(self, masked_offer, self.client_port_index) + + # Simulate our client sending a DHCPREQUEST message + def client_send_request(self): + dhcp_request = self.create_dhcp_request_packet() + testutils.send_packet(self, self.client_port_index, dhcp_request) + + # Verify that the DHCP relay actually received and relayed the DHCPREQUEST message to all of + # its known DHCP servers. We also verify that the relay inserted Option 82 information in the + # packet. + def verify_relayed_request(self): + # Create a packet resembling a relayed DCHPREQUEST packet + dhcp_request_relayed = self.create_dhcp_request_relayed_packet() + + # Mask off fields we don't care about matching + masked_request = Mask(dhcp_request_relayed) + masked_request.set_do_not_care_scapy(scapy.Ether, "dst") + + masked_request.set_do_not_care_scapy(scapy.IP, "version") + masked_request.set_do_not_care_scapy(scapy.IP, "ihl") + masked_request.set_do_not_care_scapy(scapy.IP, "tos") + masked_request.set_do_not_care_scapy(scapy.IP, "len") + masked_request.set_do_not_care_scapy(scapy.IP, "id") + masked_request.set_do_not_care_scapy(scapy.IP, "flags") + masked_request.set_do_not_care_scapy(scapy.IP, "frag") + masked_request.set_do_not_care_scapy(scapy.IP, "ttl") + masked_request.set_do_not_care_scapy(scapy.IP, "proto") + masked_request.set_do_not_care_scapy(scapy.IP, "chksum") + masked_request.set_do_not_care_scapy(scapy.IP, "src") + masked_request.set_do_not_care_scapy(scapy.IP, "dst") + masked_request.set_do_not_care_scapy(scapy.IP, "options") + + masked_request.set_do_not_care_scapy(scapy.UDP, "chksum") + + masked_request.set_do_not_care_scapy(scapy.BOOTP, "sname") + masked_request.set_do_not_care_scapy(scapy.BOOTP, "file") + + masked_request.set_do_not_care_scapy(scapy.PADDING, "load") + + # Count the number of these packets received on the ports connected to our leaves + request_count = testutils.count_matched_packets_all_ports(self, masked_request, self.server_port_indices) + self.assertTrue(request_count == self.NUM_DHCP_SERVERS, + "Failed: Request count of %d != %d (NUM_DHCP_SERVERS)" % (request_count, self.NUM_DHCP_SERVERS)) + + # Simulate a DHCP server sending a DHCPOFFER message to client from one of our leaves + def server_send_ack(self): + dhcp_ack = self.create_dhcp_ack_packet() + testutils.send_packet(self, self.client_port_index, dhcp_ack) + + # Verify that the DHCPACK would be received by our simulated client + def verify_ack_received(self): + dhcp_ack = self.create_dhcp_ack_packet() + + # Mask out lease time, ip checksum, udp checksum (explanation above) + masked_ack = Mask(dhcp_ack) + masked_ack.set_do_not_care_scapy(scapy.Ether, "dst") + masked_ack.set_do_not_care_scapy(scapy.UDP, "chksum") + masked_ack.set_do_not_care_scapy(scapy.IP, "chksum") + + # Also mask out lease time (see comment in verify_offer_received() above) + masked_ack.set_do_not_care((self.DHCP_LEASE_TIME_OFFSET * 8), (self.DHCP_LEASE_TIME_LEN * 8)) + + # NOTE: verify_packet() will fail for us via an assert, so no nedd to check a return value here + testutils.verify_packet(self, masked_ack, self.client_port_index) + + def runTest(self): + self.client_send_discover() + self.verify_relayed_discover() + self.server_send_offer() + self.verify_offer_received() + self.client_send_request() + self.verify_relayed_request() + self.server_send_ack() + self.verify_ack_received() + diff --git a/ansible/roles/test/tasks/dhcp_relay.yml b/ansible/roles/test/tasks/dhcp_relay.yml new file mode 100644 index 00000000000..5aa59af1afc --- /dev/null +++ b/ansible/roles/test/tasks/dhcp_relay.yml @@ -0,0 +1,50 @@ +# We choose client port index to be index of first port on Vlan +- name: Obtain client port index + set_fact: + client_port_name: "{{ minigraph_vlan_interfaces[0]['members'].split(' ')[0] }}" + +- set_fact: + client_port_index: "{{ minigraph_port_indices[client_port_name] }}" + + +- name: Obtain leaf port indices + set_fact: + leaf_port_indices: [] + +- set_fact: + leaf_port_indices: "{{ leaf_port_indices }} + [ {{ minigraph_port_indices[item.key] }} ]" + with_dict: "{{ minigraph_neighbors }}" + when: minigraph_devices[item.value.name] is defined and minigraph_devices[item.value.name]['type'] == "LeafRouter" + + +- name: Obtain MAC address of {{ minigraph_vlan_interfaces[0]['name'] }} interface + become: true + shell: "cat /sys/class/net/{{ minigraph_vlan_interfaces[0]['name'] }}/address" + register: result + +- set_fact: + relay_iface_mac: "{{ result.stdout | from_yaml }}" + + +- name: Copy the DHCP relay test to PTF container + become: true + copy: src=roles/test/files/ptftests/dhcp_relay_test.py dest=/root/test/ + delegate_to: "{{ ptf_host }}" + + +# Run the DHCP relay PTF test +- include: ptf_runner.yml + vars: + ptf_test_name: DHCP Relay Test + ptf_test_dir: test + ptf_test_path: dhcp_relay_test.DHCPTest + ptf_platform: remote + ptf_test_params: + - client_port_index=\"{{ client_port_index }}\" + - leaf_port_indices=\"{{ leaf_port_indices }}\" + - server_ip=\"{{ dhcp_servers[0] }}\" + - relay_iface_name=\"{{ minigraph_vlan_interfaces[0]['name'] }}\" + - relay_iface_ip=\"{{ minigraph_vlan_interfaces[0]['addr'] }}\" + - relay_iface_mac=\"{{ relay_iface_mac }}\" + - relay_iface_netmask=\"{{ minigraph_vlan_interfaces[0]['mask'] }}\" + diff --git a/ansible/roles/test/tasks/sonic.yml b/ansible/roles/test/tasks/sonic.yml index 5dc71725ab7..d0160a7f237 100644 --- a/ansible/roles/test/tasks/sonic.yml +++ b/ansible/roles/test/tasks/sonic.yml @@ -28,6 +28,11 @@ include: snmp.yml tags: snmp +- name: Test DHCP Relay + include: dhcp_relay.yml + tags: dhcp_relay + when: minigraph_devices[inventory_hostname]['type'] == "ToRRouter" + - name: Test SNMP CPU include: snmp/cpu.yml tags: snmp_cpu @@ -35,11 +40,11 @@ - name: Test SNMP Interfaces include: snmp/interfaces.yml tags: snmp_interfaces - + - name: BGP facts test include: bgp_fact.yml tags: bgp_fact - + ### when callng BGP flaps test, please add command line of which VMs host to test against ### -e "vmhost_num='01'" - fail: msg="Please set vmhost_num variable"