Skip to content

Commit de91c65

Browse files
authored
[Test Gap] A VLAN interface should stay up when all of its member ports are operationally down (sonic-net#15244)
Fixes sonic-net#14790 When all member ports of a VLAN are down, the VLAN interface should still remain operationally up. The added test file (test_vlan_ports_down.py) performs the following steps: Setup: Brings down all member ports of a VLAN interface. Asserts that the VLAN interface is operationally Up. Asserts that the VLAN interface's IPv4 and IPv6 subnets are advertised to the T1 neighbors. Asserts that IPv4 decapsulation feature works for the VLAN interface. Tear-down: Starts up all member ports of the VLAN interface. What is the motivation for this PR? When all member ports of a VLAN are down, the VLAN interface should still remain operationally up. How did you do it? We select a VLAN and then bring all of its member ports down. Then we assert all 3 conditions mentioned in the summary. How did you verify/test it? Ran the test on a virtual T0 switch (with Broadcom ASIC).
1 parent 976fc8c commit de91c65

File tree

6 files changed

+241
-13
lines changed

6 files changed

+241
-13
lines changed

.azure-pipelines/pr_test_scripts.yaml

+2
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,7 @@ t0:
181181
- vlan/test_host_vlan.py
182182
- vlan/test_vlan.py
183183
- vlan/test_vlan_ping.py
184+
- vlan/test_vlan_ports_down.py
184185
- vxlan/test_vnet_route_leak.py
185186
- vxlan/test_vnet_vxlan.py
186187
- vxlan/test_vxlan_decap.py
@@ -245,6 +246,7 @@ t0-2vlans:
245246
- dhcp_relay/test_dhcpv6_relay.py
246247
- vlan/test_host_vlan.py
247248
- vlan/test_vlan_ping.py
249+
- vlan/test_vlan_ports_down.py
248250

249251
t0-sonic:
250252
- bgp/test_bgp_fact.py

docs/api_wiki/README.md

+2
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,8 @@ def test_fun(duthosts, rand_one_dut_hostname, ptfhost):
171171

172172
- [get_interfaces_status](sonichost_methods/get_interfaces_status.md) - Get interfaces status on the DUT and parse the result into a dict.
173173

174+
- [show_ipv6_interfaces](sonichost_methods/show_ipv6_interfaces.md) - Retrieve information about IPv6 interfaces and parse the result into a dict.
175+
174176
- [get_intf_link_local_ipv6_addr](sonichost_methods/get_intf_link_local_ipv6_addr.md) - Get the link local ipv6 address of the interface
175177

176178
- [get_ip_route_info](sonichost_methods/get_ip_route_info.md) - Returns route information for a destionation. The destination could an ip address or ip prefix.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# show_ipv6_interfaces
2+
3+
- [Overview](#overview)
4+
- [Examples](#examples)
5+
- [Arguments](#arguments)
6+
- [Expected Output](#expected-output)
7+
8+
## Overview
9+
Retrieve information about IPv6 interfaces and parse the result into a dict.
10+
11+
## Examples
12+
```python
13+
def test_fun(duthosts, rand_one_dut_hostname):
14+
duthost = duthosts[rand_one_dut_hostname]
15+
16+
ipv6_ifs = duthost.show_ipv6_interfaces()
17+
```
18+
19+
## Arguments
20+
This function takes no arguments.
21+
22+
## Expected Output
23+
Returns a dictionary containing information about the DUT's IPv6 interfaces.
24+
Note: The result does NOT contain link-local IPv6 addresses.
25+
26+
Example output:
27+
28+
```json
29+
{
30+
"Ethernet16": {
31+
"master": "Bridge",
32+
"ipv6 address/mask": "fe80::2048:23ff:fe27:33d8%Ethernet16/64",
33+
"admin": "up",
34+
"oper": "up",
35+
"bgp neighbor": "N/A",
36+
"neighbor ip": "N/A"
37+
},
38+
"PortChannel101": {
39+
"master": "",
40+
"ipv6 address/mask": "fc00::71/126",
41+
"admin": "up",
42+
"oper": "up",
43+
"bgp neighbor": "ARISTA01T1",
44+
"neighbor ip": "fc00::72"
45+
},
46+
"eth5": {
47+
"master": "",
48+
"ipv6 address/mask": "fe80::5054:ff:fee6:bea6%eth5/64",
49+
"admin": "up",
50+
"oper": "up",
51+
"bgp neighbor": "N/A",
52+
"neighbor ip": "N/A"
53+
}
54+
}
55+
```

tests/common/devices/sonic.py

+48
Original file line numberDiff line numberDiff line change
@@ -1896,6 +1896,54 @@ def get_interfaces_status(self):
18961896
'''
18971897
return {x.get('interface'): x for x in self.show_and_parse('show interfaces status')}
18981898

1899+
def show_ipv6_interfaces(self):
1900+
'''
1901+
Retrieves information about IPv6 interfaces by running "show ipv6 interfaces" on the DUT
1902+
and then parses the result into a dict.
1903+
1904+
Example output:
1905+
{
1906+
"Ethernet16": {
1907+
'master': 'Bridge',
1908+
'ipv6 address/mask': 'fe80::2048:23ff:fe27:33d8%Ethernet16/64',
1909+
'admin': 'up',
1910+
'oper': 'up',
1911+
'bgp neighbor': 'N/A',
1912+
'neighbor ip': 'N/A'
1913+
},
1914+
"PortChannel101": {
1915+
'master': '',
1916+
'ipv6 address/mask': 'fc00::71/126',
1917+
'admin': 'up',
1918+
'oper': 'up',
1919+
'bgp neighbor': 'ARISTA01T1',
1920+
'neighbor ip': 'fc00::72'
1921+
},
1922+
"eth5": {
1923+
'master': '',
1924+
'ipv6 address/mask': 'fe80::5054:ff:fee6:bea6%eth5/64',
1925+
'admin': 'up',
1926+
'oper': 'up',
1927+
'bgp neighbor': 'N/A',
1928+
'neighbor ip': 'N/A'
1929+
}
1930+
}
1931+
'''
1932+
result = {iface_info["interface"]: iface_info for iface_info in self.show_and_parse("show ipv6 interfaces")}
1933+
# Some interfaces have two IPv6 addresses: One public and one link-local address.
1934+
# Since show_and_parse parses each line separately, it cannot handle this case properly.
1935+
# So for interfaces that have two IPv6 addresses, we ignore the second line (which corresponds
1936+
# to the link-local address).
1937+
if "" in result:
1938+
del result[""]
1939+
for iface in result.keys():
1940+
del result[iface]["interface"] # redundant, because it is equal to iface
1941+
admin_oper = result[iface]["admin/oper"].split('/')
1942+
del result[iface]["admin/oper"]
1943+
result[iface]["admin"] = admin_oper[0]
1944+
result[iface]["oper"] = admin_oper[1]
1945+
return result
1946+
18991947
def get_crm_facts(self):
19001948
"""Run various 'crm show' commands and parse their output to gather CRM facts
19011949

tests/vlan/test_autostate_disabled.py

+6-13
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@ class TestAutostateDisabled:
3737
up/up status when at least one Layer 2 port becomes active in that VLAN.
3838
3939
In SONiC, all vlans are bound to a single bridge interface, so the vlan interface will go down only if the bridge
40-
is down. Since bridge goes down when all the associated interfaces are down, if all the vlan members across all
41-
the vlans go down, the bridge will go down and the vlan interface will go down.
40+
is down. If all the vlan members across all the vlans go down, the bridge should still remain up so as to prevent
41+
the vlan interface from going down.
4242
4343
For more information about autostate, see:
4444
* https://www.cisco.com/c/en/us/support/docs/switches/catalyst-6500-series-switches/41141-188.html
@@ -48,7 +48,6 @@ def test_autostate_disabled(self, duthosts, enum_frontend_dut_hostname):
4848
"""
4949
Verify vlan interface autostate is disabled on SONiC.
5050
"""
51-
pytest.skip("Temporarily skipped to let the sonic-swss submodule be updated.")
5251

5352
duthost = duthosts[enum_frontend_dut_hostname]
5453
dut_hostname = duthost.hostname
@@ -84,16 +83,10 @@ def test_autostate_disabled(self, duthosts, enum_frontend_dut_hostname):
8483

8584
# Check whether the oper_state of vlan interface is changed as expected.
8685
ip_ifs = duthost.show_ip_interface()['ansible_facts']['ip_interfaces']
87-
if len(vlan_available) > 1:
88-
# If more than one vlan comply with the above test requirements, then there are members in other vlans
89-
# that are still up. Therefore, the bridge is still up, and vlan interface should be up.
90-
pytest_assert(ip_ifs.get(vlan, {}).get('oper_state') == "up",
91-
'vlan interface of {vlan} is not up as expected'.format(vlan=vlan))
92-
else:
93-
# If only one vlan comply with the above test requirements, then all the vlan members across all the
94-
# vlans are down. Therefore, the bridge is down, and vlan interface should be down.
95-
pytest_assert(ip_ifs.get(vlan, {}).get('oper_state') == "down",
96-
'vlan interface of {vlan} is not down as expected'.format(vlan=vlan))
86+
# Even if all member ports of all vlans are down, the dummy interface is still expected to be
87+
# up. Therefore, the bridge should still be up, which means that vlan interfaces should be up.
88+
pytest_assert(ip_ifs.get(vlan, {}).get('oper_state') == "up",
89+
'vlan interface of {vlan} is not up as expected'.format(vlan=vlan))
9790
finally:
9891
# Restore all interfaces to their original admin_state.
9992
self.restore_interface_admin_state(duthost, ifs_status)

tests/vlan/test_vlan_ports_down.py

+128
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import pytest
2+
import logging
3+
import ptf.testutils as testutils
4+
import ptf.mask as mask
5+
import time
6+
7+
from netaddr import IPNetwork, NOHOST
8+
from tests.common.helpers.assertions import pytest_assert
9+
from scapy.all import IP, Ether
10+
11+
logger = logging.getLogger(__name__)
12+
13+
pytestmark = [
14+
pytest.mark.topology('t0')
15+
]
16+
17+
18+
@pytest.fixture(scope='module')
19+
def vlan_ports_setup(duthosts, rand_one_dut_hostname):
20+
"""
21+
Setup: Brings down all member ports of a VLAN.
22+
Teardown: Restores the admin state of all member ports of the VLAN selected in the Setup phase.
23+
"""
24+
duthost = duthosts[rand_one_dut_hostname]
25+
vlan_brief = duthost.get_vlan_brief()
26+
if not vlan_brief:
27+
pytest.skip("The testbed does not have any VLANs.")
28+
# Selecting the first VLAN in 'vlan_brief'
29+
vlan_name = next(iter(vlan_brief))
30+
vlan_members = vlan_brief[vlan_name]["members"]
31+
ifs_status = duthost.get_interfaces_status()
32+
vlan_up_members = [port for port in vlan_members if ifs_status[port]["admin"] == "up"]
33+
logger.info(f"Bringing down all member ports of {vlan_name}...")
34+
for vlan_up_port in vlan_up_members:
35+
duthost.shell(f"sudo config interface shutdown {vlan_up_port}")
36+
time.sleep(5) # Sleep for 5 seconds to ensure T1 switches update their routing table
37+
yield vlan_name
38+
logger.info(f"Restoring the previous admin state of all member ports of {vlan_name}...")
39+
for vlan_port in vlan_up_members:
40+
duthost.shell(f"sudo config interface startup {vlan_port}")
41+
42+
43+
def test_vlan_ports_down(vlan_ports_setup, duthosts, rand_one_dut_hostname, nbrhosts, tbinfo, ptfadapter):
44+
"""
45+
Asserts the following conditions when all member ports of a VLAN interface are down:
46+
1. The VLAN interface's oper status remains Up.
47+
2. The VLAN's subnet IP is advertised to the T1 neighbors.
48+
3. The IP decapsulation feature works for packets that are sent to the VLAN interfaces's IP address.
49+
"""
50+
duthost = duthosts[rand_one_dut_hostname]
51+
vlan_name = vlan_ports_setup
52+
ip_interfaces = duthost.show_ip_interface()["ansible_facts"]["ip_interfaces"]
53+
vlan_info = ip_interfaces[vlan_name]
54+
logger.info(f"Checking if {vlan_name} is oper UP...")
55+
# check if the VLAN interface is operationally Up (IPv4)
56+
pytest_assert(vlan_info["oper_state"] == "up", f"{vlan_name} is operationally down.")
57+
58+
ipv6_interfaces = duthost.show_ipv6_interfaces()
59+
vlan_info_ipv6 = ipv6_interfaces[vlan_name]
60+
# check if the VLAN interface is operationally Up (IPv6)
61+
pytest_assert(vlan_info_ipv6["oper"] == "up", f"{vlan_name} is operationally down.")
62+
63+
logger.info("Checking BGP routes on T1 neighbors...")
64+
vlan_subnet = str(IPNetwork(f"{vlan_info['ipv4']}/{vlan_info['prefix_len']}", flags=NOHOST))
65+
vlan_subnet_ipv6 = str(IPNetwork(vlan_info_ipv6["ipv6 address/mask"], flags=NOHOST))
66+
nbrcount = 0
67+
for nbrname, nbrhost in nbrhosts.items():
68+
nbrhost = nbrhost["host"]
69+
# check IPv4 routes on nbrhost
70+
logger.info(f"Checking IPv4 routes on {nbrname}...")
71+
try:
72+
vlan_route = nbrhost.get_route(vlan_subnet)["vrfs"]["default"]
73+
except Exception:
74+
# nbrhost might be unreachable. Skip it.
75+
logger.info(f"{nbrname} might be unreachable.")
76+
continue
77+
pytest_assert(vlan_route["bgpRouteEntries"],
78+
f"{vlan_name}'s IPv4 subnet is not advertised to the T1 neighbor {nbrname}.")
79+
# check IPv6 routes on nbrhost
80+
logger.info(f"Checking IPv6 routes on {nbrname}...")
81+
try:
82+
vlan_route_ipv6 = nbrhost.get_route(vlan_subnet_ipv6)["vrfs"]["default"]
83+
except Exception:
84+
# nbrhost might be unreachable. Skip it.
85+
logger.info(f"{nbrname} might be unreachable.")
86+
continue
87+
pytest_assert(vlan_route_ipv6["bgpRouteEntries"],
88+
f"{vlan_name}'s IPv6 subnet is not advertised to the T1 neighbor {nbrname}.")
89+
nbrcount += 1
90+
if nbrcount == 0:
91+
pytest.skip("Could not get routing info from any T1 neighbors.")
92+
if duthost.facts["asic_type"].lower() == "vs":
93+
logger.info("Skipping IP-in-IP decapsulation test for the 'vs' ASIC type.")
94+
return
95+
logger.info("Starting the IP-in-IP decapsulation test...")
96+
mg_facts = duthost.get_extended_minigraph_facts(tbinfo)
97+
# Use the first Ethernet port associated with the first portchannel to send test packets to the DUT
98+
portchannel_info = next(iter(mg_facts["minigraph_portchannels"].values()))
99+
ptf_src_port = portchannel_info["members"][0]
100+
ptf_src_port_index = mg_facts["minigraph_ptf_indices"][ptf_src_port]
101+
ptf_dst_port_indices = list(mg_facts["minigraph_ptf_indices"].values())
102+
# Test IPv4 in IPv4 decapsulation.
103+
# Outer IP packet:
104+
# src: 1.1.1.1
105+
# dst: VLAN interface's IPv4 address
106+
# Inner IP packet:
107+
# src: 2.2.2.2
108+
# dst: 3.3.3.3
109+
# Expectation: The T0 switch (DUT) decapsulates the outer IP packet and sends
110+
# the inner IP packet to the default gateway (one of the connected T1 switches).
111+
inner_pkt = testutils.simple_udp_packet(ip_src="2.2.2.2",
112+
ip_dst="3.3.3.3")
113+
outer_pkt = testutils.simple_ipv4ip_packet(eth_src=ptfadapter.dataplane.get_mac(0, ptf_src_port_index),
114+
eth_dst=duthost.facts["router_mac"],
115+
ip_src="1.1.1.1",
116+
ip_dst=vlan_info["ipv4"],
117+
inner_frame=inner_pkt["IP"])
118+
exp_pkt = inner_pkt.copy()
119+
exp_pkt = mask.Mask(exp_pkt)
120+
exp_pkt.set_do_not_care_packet(Ether, "src")
121+
exp_pkt.set_do_not_care_packet(Ether, "dst")
122+
exp_pkt.set_do_not_care_packet(IP, "ttl")
123+
exp_pkt.set_do_not_care_packet(IP, "chksum")
124+
exp_pkt.set_do_not_care_packet(IP, "tos")
125+
logger.info("Sending the IP-in-IP packet...")
126+
testutils.send(ptfadapter, ptf_src_port_index, outer_pkt)
127+
logger.info("IP-in-IP packet sent.")
128+
testutils.verify_packet_any_port(ptfadapter, exp_pkt, ports=ptf_dst_port_indices)

0 commit comments

Comments
 (0)