Skip to content

Commit bf26ec9

Browse files
[dualtor]: Add utilities for dual ToR mocking (#2945)
* [dualtor]: Add utilities for dual ToR mocking * Apply config DB tables to mock dual ToR setup * Apply kernel configurations (neighbor entries and route) * Apply orchagent config to mock dual ToR setup Signed-off-by: Lawrence Lee <lawlee@microsoft.com>
1 parent 50f5130 commit bf26ec9

1 file changed

Lines changed: 327 additions & 0 deletions

File tree

Lines changed: 327 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,327 @@
1+
import json
2+
import logging
3+
import os
4+
import pytest
5+
6+
from ipaddress import ip_interface, IPv4Interface, IPv6Interface, \
7+
ip_address, IPv4Address
8+
9+
__all__ = ['apply_active_state_to_orchagent', 'apply_dual_tor_neigh_entries', 'apply_dual_tor_peer_switch_route', 'apply_mock_dual_tor_kernel_configs',
10+
'apply_mock_dual_tor_tables', 'apply_mux_cable_table_to_dut', 'apply_peer_switch_table_to_dut', 'apply_standby_state_to_orchagent', 'apply_tunnel_table_to_dut',
11+
'mock_peer_switch_loopback_ip', 'mock_server_base_ip_addr']
12+
13+
logger = logging.getLogger(__name__)
14+
15+
'''
16+
Fixtures and helper methods to configure a single ToR testbed to mock the standby or active ToR in a dual ToR testbed
17+
18+
Test functions wishing to apply the full mock config must use the following fixtures:
19+
- apply_mock_dual_tor_tables
20+
- apply_mock_dual_tor_kernel_configs
21+
- apply_active_state_to_orchagent OR apply_standby_state_to_orchagent
22+
'''
23+
24+
def _apply_config_to_swss(dut, swss_config_str, swss_filename='swss_config_file'):
25+
'''
26+
Applies a given configuration string to the SWSS container
27+
28+
Args:
29+
dut: DUT object
30+
swss_config_str: String containing the configuration to be applied
31+
swss_filename: The filename to use for copying the config file around (default='swss_config_file')
32+
'''
33+
34+
dut_filename = os.path.join('/tmp',swss_filename)
35+
36+
dut.shell('echo "{}" > {}'.format(swss_config_str, dut_filename))
37+
dut.shell('docker cp {} swss:{}'.format(dut_filename, swss_filename))
38+
dut.shell('docker exec swss sh -c "swssconfig {}"'.format(swss_filename))
39+
40+
41+
def _apply_dual_tor_state_to_orchagent(dut, state):
42+
'''
43+
Helper function to configure active/standby state in orchagent
44+
45+
Args:
46+
dut: DUT object
47+
state: either 'active' or 'standby'
48+
'''
49+
50+
logger.info("Applying {} state to orchagent".format(state))
51+
52+
vlan_intfs = sorted(dut.get_vlan_intfs(), key=lambda intf: int(intf.replace('Ethernet', '')))
53+
54+
intf_configs = []
55+
56+
for intf in vlan_intfs:
57+
'''
58+
For each VLAN interface, create one configuration to be applied to orchagent
59+
Each interface configuration has the following structure:
60+
61+
{
62+
"MUX_CABLE_TABLE:<intf name>": {
63+
"state": <active/standby>
64+
}
65+
"OP": "SET"
66+
}
67+
'''
68+
intf_config_dict = {}
69+
state_dict = {}
70+
71+
state_key = '"MUX_CABLE_TABLE:{}"'.format(intf)
72+
state_dict = {'"state"': '"{}"'.format(state)}
73+
intf_config_dict[state_key] = state_dict
74+
intf_config_dict['"OP"'] = '"SET"'
75+
76+
intf_configs.append(intf_config_dict)
77+
78+
swss_config_str = json.dumps(intf_configs, indent=4)
79+
logger.debug('SWSS config string is {}'.format(swss_config_str))
80+
swss_filename = '/mux{}.json'.format(state)
81+
_apply_config_to_swss(dut, swss_config_str, swss_filename)
82+
83+
yield
84+
logger.info("Removing {} state from orchagent".format(state))
85+
86+
for i in range(len(intf_configs)):
87+
intf_configs[i]['"OP"'] = '"DEL"'
88+
89+
swss_config_str = json.dumps(intf_configs, indent=4)
90+
swss_filename = '/mux{}.json'.format(state)
91+
_apply_config_to_swss(dut, swss_config_str, swss_filename)
92+
93+
94+
@pytest.fixture(scope='module')
95+
def apply_active_state_to_orchagent(duthosts, rand_one_dut_hostname):
96+
dut = duthosts[rand_one_dut_hostname]
97+
98+
for func in _apply_dual_tor_state_to_orchagent(dut, 'active'):
99+
yield func
100+
101+
102+
@pytest.fixture(scope='module')
103+
def apply_standby_state_to_orchagent(duthosts, rand_one_dut_hostname):
104+
dut = duthosts[rand_one_dut_hostname]
105+
106+
for func in _apply_dual_tor_state_to_orchagent(dut, 'standby'):
107+
yield func
108+
109+
110+
@pytest.fixture(scope='module')
111+
def mock_peer_switch_loopback_ip(duthosts, rand_one_dut_hostname):
112+
'''
113+
Returns the mocked peer switch loopback IP
114+
115+
The peer switch loopback is always the next IP address after the DUT loopback
116+
117+
Returns:
118+
IPv4Interface object
119+
'''
120+
121+
dut = duthosts[rand_one_dut_hostname]
122+
lo_facts = dut.get_running_config_facts()['LOOPBACK_INTERFACE']
123+
loopback_intf = list(lo_facts.keys())[0]
124+
125+
peer_ipv4_loopback = None
126+
127+
for ip_addr_str in lo_facts[loopback_intf]:
128+
ip_addr = ip_interface(ip_addr_str)
129+
130+
if type(ip_addr) is IPv4Interface:
131+
peer_ipv4_loopback = ip_addr + 1
132+
133+
logger.debug("Mocked peer switch loopback is {}".format(peer_ipv4_loopback))
134+
return peer_ipv4_loopback
135+
136+
137+
@pytest.fixture(scope='module')
138+
def mock_server_base_ip_addr(duthosts, rand_one_dut_hostname):
139+
'''
140+
Calculates the IP address of the first server
141+
142+
These base addresses are always the next IPs after the VLAN address
143+
144+
Returns:
145+
IPv4Interface and IPv6 interface objects reperesenting the first server addresses
146+
'''
147+
dut = duthosts[rand_one_dut_hostname]
148+
vlan_interface = dut.get_running_config_facts()['VLAN_INTERFACE']
149+
150+
vlan = list(vlan_interface.keys())[0]
151+
152+
server_ipv4_base_addr = None
153+
server_ipv6_base_addr = None
154+
155+
for ip_addr_str in vlan_interface[vlan].keys():
156+
ip_addr = ip_interface(ip_addr_str)
157+
158+
if type(ip_addr) is IPv4Interface:
159+
server_ipv4_base_addr = ip_addr + 1
160+
elif type(ip_addr) is IPv6Interface:
161+
server_ipv6_base_addr = ip_addr + 1
162+
163+
logger.debug("Mocked server IP base addresses are: {} and {}".format(server_ipv4_base_addr, server_ipv6_base_addr))
164+
return server_ipv4_base_addr, server_ipv6_base_addr
165+
166+
167+
@pytest.fixture(scope='module')
168+
def apply_dual_tor_neigh_entries(duthosts, rand_one_dut_hostname, ptfadapter, tbinfo, mock_server_base_ip_addr):
169+
'''
170+
Apply neighber table entries for servers
171+
'''
172+
logger.info("Applying dual ToR neighbor entries")
173+
174+
dut = duthosts[rand_one_dut_hostname]
175+
176+
server_ipv4_base_addr, _ = mock_server_base_ip_addr
177+
178+
server_ip_to_mac_map = {}
179+
180+
vlan_intfs = sorted(dut.get_vlan_intfs(), key=lambda intf: int(intf.replace('Ethernet', '')))
181+
dut_ptf_intf_map = dut.get_extended_minigraph_facts(tbinfo)['minigraph_ptf_indices']
182+
183+
for i, intf in enumerate(vlan_intfs):
184+
# For each VLAN interface, get the corresponding PTF interface MAC
185+
ptf_port_index = dut_ptf_intf_map[intf]
186+
ptf_mac = ptfadapter.dataplane.ports[(0, ptf_port_index)].mac()
187+
server_ip_to_mac_map[server_ipv4_base_addr.ip + i] = ptf_mac
188+
189+
vlan_interface = dut.get_running_config_facts()['VLAN_INTERFACE']
190+
vlan = list(vlan_interface.keys())[0]
191+
192+
for ip, mac in server_ip_to_mac_map.items():
193+
# Use `ip neigh replace` in case entries already exist for the target IP
194+
# If there are no pre-existing entries, equivalent to `ip neigh add`
195+
dut.shell('ip -4 neigh replace {} lladdr {} dev {}'.format(ip, mac, vlan))
196+
197+
yield
198+
199+
logger.info("Removing dual ToR neighbor entries")
200+
201+
for ip in server_ip_to_mac_map.keys():
202+
dut.shell('ip -4 neigh del {} dev {}'.format(ip, vlan))
203+
204+
205+
@pytest.fixture(scope='module')
206+
def apply_dual_tor_peer_switch_route(duthosts, rand_one_dut_hostname, mock_peer_switch_loopback_ip):
207+
'''
208+
Apply the tunnel route to reach the peer switch via the T1 switches
209+
'''
210+
logger.info("Applying dual ToR peer switch loopback route")
211+
dut = duthosts[rand_one_dut_hostname]
212+
bgp_neighbors = dut.bgp_facts()['ansible_facts']['bgp_neighbors'].keys()
213+
214+
ipv4_neighbors = []
215+
216+
for neighbor in bgp_neighbors:
217+
neighbor_ip = ip_address(neighbor)
218+
219+
if type(neighbor_ip) is IPv4Address:
220+
ipv4_neighbors.append(neighbor)
221+
222+
nexthop_str = ''
223+
for neighbor in ipv4_neighbors:
224+
nexthop_str += 'nexthop via {} '.format(neighbor)
225+
226+
# Use `ip route replace` in case a rule already exists for this IP
227+
# If there are no pre-existing routes, equivalent to `ip route add`
228+
dut.shell('ip route replace {} {}'.format(mock_peer_switch_loopback_ip, nexthop_str))
229+
230+
yield
231+
232+
logger.info("Removing dual ToR peer switch loopback route")
233+
234+
dut.shell('ip route del {}'.format(mock_peer_switch_loopback_ip))
235+
236+
237+
@pytest.fixture(scope='module')
238+
def apply_peer_switch_table_to_dut(duthosts, rand_one_dut_hostname, mock_peer_switch_loopback_ip):
239+
'''
240+
Adds the PEER_SWITCH table to config DB
241+
'''
242+
logger.info("Applying PEER_SWITCH table")
243+
dut = duthosts[rand_one_dut_hostname]
244+
peer_switch_key = 'PEER_SWITCH|switch_hostname'
245+
246+
dut.shell('redis-cli -n 4 HSET "{}" "address_ipv4" "{}"'.format(peer_switch_key, mock_peer_switch_loopback_ip))
247+
248+
yield
249+
logger.info("Removing peer switch table")
250+
251+
dut.shell('redis-cli -n 4 DEL "{}"'.format(peer_switch_key))
252+
253+
254+
@pytest.fixture(scope='module')
255+
def apply_tunnel_table_to_dut(duthosts, rand_one_dut_hostname, mock_peer_switch_loopback_ip):
256+
'''
257+
Adds the TUNNEL table to config DB
258+
'''
259+
logger.info("Applying TUNNEL table")
260+
dut = duthosts[rand_one_dut_hostname]
261+
262+
dut_loopback = (mock_peer_switch_loopback_ip - 1).ip
263+
264+
tunnel_key = 'TUNNEL|MuxTunnel0'
265+
tunnel_params = {
266+
'dscp_mode': 'uniform',
267+
'dst_ip': dut_loopback,
268+
'ecn_mode': 'copy_from_outer',
269+
'encap_ecn_mode': 'standard',
270+
'ttl_mode': 'pipe',
271+
'tunnel_type': 'IPINIP'
272+
}
273+
274+
for param, value in tunnel_params.items():
275+
dut.shell('redis-cli -n 4 HSET "{}" "{}" "{}"'.format(tunnel_key, param, value))
276+
277+
yield
278+
logger.info("Removing tunnel table")
279+
280+
dut.shell('redis-cli -n 4 DEL "{}"'.format(tunnel_key))
281+
282+
283+
@pytest.fixture(scope='module')
284+
def apply_mux_cable_table_to_dut(duthosts, rand_one_dut_hostname, mock_server_base_ip_addr):
285+
'''
286+
Adds the MUX_CABLE table to config DB
287+
'''
288+
logger.info("Applying MUX_CABLE table")
289+
dut = duthosts[rand_one_dut_hostname]
290+
291+
server_ipv4_base_addr, server_ipv6_base_addr = mock_server_base_ip_addr
292+
293+
vlan_intfs = sorted(dut.get_vlan_intfs(), key=lambda intf: int(intf.replace('Ethernet', '')))
294+
295+
keys_inserted = []
296+
297+
for i, intf in enumerate(vlan_intfs):
298+
server_ipv4 = str(server_ipv4_base_addr + i)
299+
server_ipv6 = str(server_ipv6_base_addr + i)
300+
key = 'MUX_CABLE|{}'.format(intf)
301+
keys_inserted.append(key)
302+
303+
dut.shell('redis-cli -n 4 HSET "{}" "server_ipv4" "{}"'.format(key, server_ipv4))
304+
dut.shell('redis-cli -n 4 HSET "{}" "server_ipv6" "{}"'.format(key, server_ipv6))
305+
dut.shell('redis-cli -n 4 HSET "{}" "state" "auto"'.format(key))
306+
307+
yield
308+
logger.info("Removing mux cable table")
309+
310+
for key in keys_inserted:
311+
dut.shell('redis-cli -n 4 DEL "{}"'.format(key))
312+
313+
314+
@pytest.fixture(scope='module')
315+
def apply_mock_dual_tor_tables(apply_mux_cable_table_to_dut, apply_tunnel_table_to_dut, apply_peer_switch_table_to_dut):
316+
'''
317+
Wraps all table fixtures for convenience
318+
'''
319+
logger.info("Done applying database tables for dual ToR mock")
320+
321+
322+
@pytest.fixture(scope='module')
323+
def apply_mock_dual_tor_kernel_configs(apply_dual_tor_peer_switch_route, apply_dual_tor_neigh_entries):
324+
'''
325+
Wraps all kernel related (routes and neighbor entries) fixtures for convenience
326+
'''
327+
logger.info("Done applying kernel configs for dual ToR mock")

0 commit comments

Comments
 (0)