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"