Skip to content

Commit ca6248b

Browse files
authored
[action] [PR:18033] Generate PFC storm using PFC backpressure on additional Arista devices (#222)
<!-- Please make sure you've read and understood our contributing guidelines; https://github.com/sonic-net/SONiC/blob/gh-pages/CONTRIBUTING.md Please provide following information to help code review process a bit easier: --> ### Description of PR NOTE: This is a duplicate of sonic-net/sonic-mgmt#17971, as @veronica-arista is away. Add a pfc_gen script for Arista fanout devices to generate pfc pause frames by setting PFC backpressure status in hardware. Enable the storm generation to use this script on interfaces that are connected to Arista fanout devices running EOS or Sonic. Currently supports the following Arista devices: Arista-7060X6 Arista-7060DX5 Arista-7060PX5 Arista-7060CX Arista-7260CX3 Arista-7260QX3 This is implemented on top of PR sonic-net/sonic-mgmt#15594 Summary: Fixes # (issue) ### Type of change <!-- - Fill x for your type of change. - e.g. - [x] Bug fix --> - [ ] Bug fix - [ ] Testbed and Framework(new/improvement) - [ ] New Test case - [ ] Skipped for non-supported platforms - [x] Test case improvement ### Back port request - [x] 202012 - [ ] 202205 - [ ] 202305 - [ ] 202311 - [ ] 202405 - [x] 202411 ### Approach #### What is the motivation for this PR? Adds more reliable PFC storm generation for interfaces connected to Arista fanout devices #### How did you do it? #### How did you verify/test it? Ran sonic-mgmt pfcwd tests on testbed with Arista fanout switches of the listed SKUs running EOS. Ran the pfc_gen script manually on Arista switches running Sonic. #### Any platform specific information? #### Supported testbed topology if it's a new test case? ### Documentation <!-- (If it's a new feature, new test case) Did you update documentation/Wiki relevant to your implementation? Link to the wiki page? -->
1 parent d9fa13b commit ca6248b

6 files changed

+343
-6
lines changed
+246
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
#!/usr/bin/env python
2+
3+
"""
4+
Script to generate PFC storm.
5+
6+
"""
7+
import sys
8+
import optparse
9+
import logging
10+
import logging.handlers
11+
import re
12+
import signal
13+
import subprocess
14+
import time
15+
16+
17+
logger = logging.getLogger('MyLogger')
18+
logger.setLevel(logging.DEBUG)
19+
20+
21+
class SignalCleanup():
22+
def __init__(self, fanoutPfcStorm, endMsg):
23+
self.fanoutPfcStorm = fanoutPfcStorm
24+
self.endMsg = endMsg
25+
signal.signal(signal.SIGTERM, self.sigHandler)
26+
27+
def sigHandler(self, *args):
28+
self.fanoutPfcStorm.endAllPfcStorm()
29+
30+
logger.debug(self.endMsg)
31+
sys.exit(0)
32+
33+
34+
class FanoutPfcStorm():
35+
'''
36+
For eos this class expects all interfaces to be in the front panel interface format
37+
ex. Ethernet1/1 and not et1_1
38+
For sonic, the interfaces are the default format
39+
'''
40+
def __init__(self, priority, chipName, os):
41+
self.intfsEnabled = []
42+
self.priority = priority
43+
self.switchChip = chipName
44+
self.os = os
45+
if os == 'sonic':
46+
self.intfToMmuPort, self.intfToPort = self._parseInterfaceMapFullSonic()
47+
else:
48+
self.intfToMmuPort, self.intfToPort = self._parseInterfaceMapFull()
49+
50+
def _shellCmd(self, cmd):
51+
output = ""
52+
result = subprocess.run([f"{cmd}"], capture_output=True, text=True, shell=True)
53+
if result.returncode == 0:
54+
output = result.stdout
55+
return output
56+
57+
def _cliCmd(self, cmd):
58+
output = ""
59+
if self.os == 'sonic':
60+
result = subprocess.run([f"bcmcmd '{cmd}'"], capture_output=True, text=True, shell=True)
61+
else:
62+
result = subprocess.run(
63+
["Cli", "-c", f"{cmd}"], capture_output=True, text=True)
64+
if result.returncode == 0:
65+
output = result.stdout
66+
return output
67+
68+
def _bcmltshellCmd(self, cmd):
69+
if self.os == 'sonic':
70+
return self._cliCmd(f"bsh -c \"{cmd}\"")
71+
else:
72+
return self._cliCmd(f"en\nplatform trident shell\nbcmltshell\n{cmd}")
73+
74+
def _bcmshellCmd(self, cmd):
75+
return self._cliCmd(f"en\nplatform trident shell\n{cmd}")
76+
77+
def _parseInterfaceMapFull(self):
78+
intfToMmuPort = {}
79+
intfToPort = {}
80+
81+
output = self._cliCmd("en\nshow platform trident interface map full")
82+
83+
for line in output.splitlines():
84+
mo = re.search(
85+
r'Intf: (?P<intf>Ethernet\S+).{1,50}Port: {1,3}(?P<port>\S+)'
86+
r'.{1,100}P2M\[ {0,3}\d+\]: {1,3}(?P<mmu>\S+)',
87+
line
88+
)
89+
if mo is None:
90+
continue
91+
intfToMmuPort[mo.group('intf')] = mo.group('mmu')
92+
intfToPort[mo.group('intf')] = mo.group('port')
93+
94+
return intfToMmuPort, intfToPort
95+
96+
def _parseInterfaceMapFullSonic(self):
97+
intfToMmuPort = {}
98+
intfTolPort = {}
99+
lPortToIntf = {}
100+
101+
if self.switchChip.startswith(("Tomahawk5", "Tomahawk4")):
102+
output = self._bcmltshellCmd('knet netif info')
103+
for info in output.split("Network interface Info:"):
104+
mo = re.search(r"Name: (?P<intf>Ethernet\d+)[\s\S]{1,100}Port: (?P<lport>\d+)", info)
105+
if mo is None:
106+
continue
107+
lPortToIntf[mo.group('lport')] = mo.group('intf')
108+
intfTolPort[mo.group('intf')] = mo.group('lport')
109+
output = self._cliCmd("show portmap")
110+
for line in output.splitlines():
111+
entries = line.split()
112+
if len(entries) == 7:
113+
lport = entries[2]
114+
mmuPort = entries[4]
115+
if lport in lPortToIntf:
116+
intfToMmuPort[lPortToIntf[lport]] = mmuPort
117+
intfTolPort[mo.group('intf')] = mo.group('lport')
118+
else:
119+
output = self._cliCmd('knet netif show')
120+
for info in output.split("Interface ID"):
121+
mo = re.search(r"name=(?P<intf>Ethernet\d+)[\s\S]{1,100}port=(?P<lport>\S+)", info)
122+
if mo is None:
123+
continue
124+
lPortToIntf[mo.group('lport')] = mo.group('intf')
125+
intfTolPort[mo.group('intf')] = mo.group('lport')
126+
output = self._cliCmd("show portmap")
127+
for line in output.splitlines():
128+
entries = line.split()
129+
if len(entries) == 9:
130+
lport = entries[0]
131+
mmuPort = entries[4]
132+
if lport in lPortToIntf:
133+
intfToMmuPort[lPortToIntf[lport]] = mmuPort
134+
intfTolPort[mo.group('intf')] = mo.group('lport')
135+
136+
return intfToMmuPort, intfTolPort
137+
138+
def _endPfcStorm(self, intf):
139+
'''
140+
Intf format is Ethernet1/1
141+
142+
The users of this class are only expected to call
143+
startPfcStorm and endAllPfcStorm
144+
'''
145+
mmuPort = self.intfToMmuPort[intf]
146+
port = self.intfToPort[intf]
147+
if self.switchChip.startswith(("Tomahawk5", "Tomahawk4")):
148+
self._bcmltshellCmd(f"pt MMU_INTFO_XPORT_BKP_HW_UPDATE_DISr set BCMLT_PT_PORT={mmuPort} PAUSE_PFC_BKP=0")
149+
self._bcmltshellCmd(f"pt MMU_INTFO_TO_XPORT_BKPr set BCMLT_PT_PORT={mmuPort} PAUSE_PFC_BKP=0")
150+
else:
151+
self._bcmshellCmd(f"setreg CHFC2PFC_STATE.{port} PRI_BKP=0")
152+
self._cliCmd(f"en\nconf\n\nint {intf}\nno priority-flow-control on")
153+
if self.os == 'sonic':
154+
for prio in range(8):
155+
self._cliCmd(f"config interface pfc priority {intf} {prio} off")
156+
else:
157+
for prio in range(8):
158+
self._cliCmd(f"en\nconf\n\nint {intf}\nno priority-flow-control priority {prio} no-drop")
159+
160+
def startPfcStorm(self, intf):
161+
if intf in self.intfsEnabled:
162+
return
163+
self.intfsEnabled.append(intf)
164+
165+
mmuPort = self.intfToMmuPort[intf]
166+
port = self.intfToPort[intf]
167+
if self.os == 'sonic':
168+
for prio in range(8):
169+
if (1 << prio) & self.priority:
170+
self._shellCmd(f"config interface pfc priority {intf} {prio} on")
171+
else:
172+
self._cliCmd(f"en\nconf\n\nint {intf}\npriority-flow-control on")
173+
for prio in range(8):
174+
if (1 << prio) & self.priority:
175+
self._cliCmd(f"en\nconf\n\nint {intf}\npriority-flow-control priority {prio} no-drop")
176+
177+
if self.switchChip.startswith(("Tomahawk5", "Tomahawk4")):
178+
self._bcmltshellCmd(f"pt MMU_INTFO_XPORT_BKP_HW_UPDATE_DISr set BCMLT_PT_PORT={mmuPort} PAUSE_PFC_BKP=1")
179+
self._bcmltshellCmd(f"pt MMU_INTFO_TO_XPORT_BKPr set BCMLT_PT_PORT={mmuPort} PAUSE_PFC_BKP={self.priority}")
180+
else:
181+
self._bcmshellCmd(f"setreg CHFC2PFC_STATE.{port} PRI_BKP={self.priority}")
182+
183+
def endAllPfcStorm(self):
184+
for intf in self.intfsEnabled:
185+
self._endPfcStorm(intf)
186+
187+
188+
def main():
189+
usage = "usage: %prog [options] arg1 arg2"
190+
parser = optparse.OptionParser(usage=usage)
191+
parser.add_option("-i", "--interface", type="string", dest="interface",
192+
help="Interface list to send packets, separated by ','", metavar="Interface")
193+
parser.add_option('-p', "--priority", type="int", dest="priority",
194+
help="PFC class enable bitmap.", metavar="Priority", default=-1)
195+
parser.add_option("-r", "--rsyslog-server", type="string", dest="rsyslog_server",
196+
default="127.0.0.1", help="Rsyslog server IPv4 address", metavar="IPAddress")
197+
parser.add_option("-c", "--chipName", type="string", dest="chipName", metavar="ChipName",
198+
help="Name of chip in the switch, i.e. Tomahawk5")
199+
parser.add_option("-o", "--os", type="string", dest="os",
200+
help="Operating system (eos or sonic)", default="eos")
201+
202+
(options, args) = parser.parse_args()
203+
204+
if options.interface is None:
205+
print("Need to specify the interface to send PFC pause frame packets.")
206+
parser.print_help()
207+
sys.exit(1)
208+
209+
if options.chipName is None:
210+
print("Need to specify the ChipName to determine what cli to generate PFC pause frame packets.")
211+
parser.print_help()
212+
sys.exit(1)
213+
214+
if options.priority > 255 or options.priority < 0:
215+
print("Enable class bitmap is not valid. Need to be in range 0-255.")
216+
parser.print_help()
217+
sys.exit(1)
218+
219+
# Configure logging
220+
handler = logging.handlers.SysLogHandler(address=(options.rsyslog_server, 514))
221+
logger.addHandler(handler)
222+
223+
# List of front panel kernel intfs
224+
interfaces = options.interface.split(',')
225+
226+
fs = FanoutPfcStorm(options.priority, options.chipName, options.os)
227+
SignalCleanup(fs, 'PFC_STORM_END')
228+
229+
logger.debug('PFC_STORM_DEBUG')
230+
for intf in interfaces:
231+
if options.os == 'eos':
232+
intf = frontPanelIntfFromKernelIntfName(intf)
233+
fs.startPfcStorm(intf)
234+
logger.debug('PFC_STORM_START')
235+
236+
# wait forever until stop
237+
while True:
238+
time.sleep(100)
239+
240+
241+
def frontPanelIntfFromKernelIntfName(intf):
242+
return intf.replace("et", "Ethernet").replace("_", "/")
243+
244+
245+
if __name__ == "__main__":
246+
main()

tests/common/helpers/pfc_storm.py

+67-6
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,26 @@
1313
logger = logging.getLogger(__name__)
1414

1515

16+
def get_chip_name_if_asic_pfc_storm_supported(fanout):
17+
hwSkuInfo = {
18+
"Arista DCS-7060DX5": "Tomahawk4",
19+
"Arista DCS-7060PX5": "Tomahawk4",
20+
"Arista DCS-7060X6": "Tomahawk5",
21+
"Arista-7060X6": "Tomahawk5",
22+
"Arista DCS-7060CX": "Tomahawk",
23+
"Arista-7060CX": "Tomahawk",
24+
"Arista DCS-7260CX3": "Tomahawk2",
25+
"Arista-7260CX3": "Tomahawk2",
26+
"Arista-7260QX3": "Tomahawk2",
27+
}
28+
29+
for sku, chip in hwSkuInfo.items():
30+
if fanout.startswith(sku):
31+
return chip
32+
33+
return None
34+
35+
1636
class PFCStorm(object):
1737
""" PFC storm/start on different interfaces on a fanout connected to the DUT"""
1838

@@ -29,6 +49,7 @@ def __init__(self, duthost, fanout_graph_facts, fanouthosts, **kwargs):
2949
fanouthosts(AnsibleHost) : fanout instance
3050
kwargs(dict):
3151
peer_info(dict): keys are 'peerdevice', 'pfc_fanout_interface'. Optional: 'hwsku'
52+
pfc_gen_chip_name(string) : chip name of switch where PFC frames are generated. default: None
3253
pfc_queue_index(int) : queue on which the PFC storm should be generated. default: 3
3354
pfc_frames_number(int) : Number of PFC frames to generate. default: 100000
3455
pfc_gen_file(string): Script which generates the PFC traffic. default: 'pfc_gen.py'
@@ -43,6 +64,7 @@ def __init__(self, duthost, fanout_graph_facts, fanouthosts, **kwargs):
4364
self.fanout_hosts = fanouthosts
4465
self.pfc_gen_file = kwargs.pop('pfc_gen_file', "pfc_gen.py")
4566
self.pfc_gen_multiprocess = kwargs.pop('pfc_gen_multiprocess', False)
67+
self.pfc_gen_chip_name = None
4668
self.pfc_queue_idx = kwargs.pop('pfc_queue_index', 3)
4769
self.pfc_frames_number = kwargs.pop('pfc_frames_number', 100000)
4870
self.send_pfc_frame_interval = kwargs.pop('send_pfc_frame_interval', 0)
@@ -125,13 +147,39 @@ def _create_pfc_gen(self):
125147
if not out['stat']['exists'] or not out['stat']['isdir']:
126148
self.peer_device.file(path=pfc_gen_fpath, state="touch")
127149

150+
def _get_eos_fanout_version(self):
151+
"""
152+
Get version info for eos fanout device
153+
"""
154+
cmd = 'Cli -c "show version"'
155+
return self.peer_device.shell(cmd)['stdout_lines']
156+
157+
def _get_sonic_fanout_hwsku(self):
158+
"""
159+
Get hwsku for sonic fanout device
160+
"""
161+
cmd = 'show version'
162+
out_lines = self.peer_device.shell(cmd)['stdout_lines']
163+
for line in out_lines:
164+
if line.startswith('HwSKU:'):
165+
return line.split()[1]
166+
128167
def deploy_pfc_gen(self):
129168
"""
130169
Deploy the pfc generation file on the fanout
131170
"""
132171
if self.asic_type == 'vs':
133172
return
134173
if self.peer_device.os in ('eos', 'sonic'):
174+
chip_name = None
175+
if self.peer_device.os == 'eos':
176+
chip_name = get_chip_name_if_asic_pfc_storm_supported(self._get_eos_fanout_version()[0])
177+
elif self.peer_device.os == 'sonic':
178+
chip_name = get_chip_name_if_asic_pfc_storm_supported(self._get_sonic_fanout_hwsku())
179+
if self.peer_device.os == 'eos' and chip_name:
180+
self.pfc_gen_file = "pfc_gen_brcm_xgs.py"
181+
self.pfc_gen_file_test_name = "pfc_gen_brcm_xgs.py"
182+
self.pfc_gen_chip_name = chip_name
135183
src_pfc_gen_file = "common/helpers/{}".format(self.pfc_gen_file)
136184
self._create_pfc_gen()
137185
if self.fanout_asic_type == 'mellanox':
@@ -185,6 +233,7 @@ def _update_template_args(self):
185233
"pfc_queue_index": self.pfc_queue_idx,
186234
"pfc_frames_number": self.pfc_frames_number,
187235
"pfc_fanout_interface": self.peer_info['pfc_fanout_interface'] if self.asic_type != 'vs' else "",
236+
"pfc_gen_chip_name": self.pfc_gen_chip_name,
188237
"ansible_eth0_ipv4_addr": self.ip_addr,
189238
"peer_hwsku": self.peer_info['hwsku'] if self.asic_type != 'vs' else "",
190239
"send_pfc_frame_interval": self.send_pfc_frame_interval,
@@ -210,15 +259,21 @@ def _prepare_start_template(self):
210259
Populates the pfc storm start template
211260
"""
212261
self._update_template_args()
213-
if self.dut.topo_type == 't2' and self.peer_device.os == 'sonic':
262+
if self.asic_type == 'vs':
263+
self.pfc_start_template = os.path.join(
264+
TEMPLATES_DIR, "pfc_storm_eos.j2")
265+
elif self.dut.topo_type == 't2' and self.peer_device.os == 'sonic':
214266
self.pfc_start_template = os.path.join(
215267
TEMPLATES_DIR, "pfc_storm_{}_t2.j2".format(self.peer_device.os))
216268
elif self.fanout_asic_type == 'mellanox' and self.peer_device.os == 'sonic':
217269
self.pfc_start_template = os.path.join(
218270
TEMPLATES_DIR, "pfc_storm_mlnx_{}.j2".format(self.peer_device.os))
219-
elif self.asic_type == 'vs':
271+
elif ((self.peer_device.os == 'eos' and
272+
get_chip_name_if_asic_pfc_storm_supported(self._get_eos_fanout_version()[0])) or
273+
(self.peer_device.os == 'sonic' and
274+
get_chip_name_if_asic_pfc_storm_supported(self._get_sonic_fanout_hwsku()))):
220275
self.pfc_start_template = os.path.join(
221-
TEMPLATES_DIR, "pfc_storm_eos.j2")
276+
TEMPLATES_DIR, "pfc_storm_arista_{}.j2".format(self.peer_device.os))
222277
else:
223278
self.pfc_start_template = os.path.join(
224279
TEMPLATES_DIR, "pfc_storm_{}.j2".format(self.peer_device.os))
@@ -229,15 +284,21 @@ def _prepare_stop_template(self):
229284
Populates the pfc storm stop template
230285
"""
231286
self._update_template_args()
232-
if self.dut.topo_type == 't2' and self.peer_device.os == 'sonic':
287+
if self.asic_type == 'vs':
288+
self.pfc_stop_template = os.path.join(
289+
TEMPLATES_DIR, "pfc_storm_stop_eos.j2")
290+
elif self.dut.topo_type == 't2' and self.peer_device.os == 'sonic':
233291
self.pfc_stop_template = os.path.join(
234292
TEMPLATES_DIR, "pfc_storm_stop_{}_t2.j2".format(self.peer_device.os))
235293
elif self.fanout_asic_type == 'mellanox' and self.peer_device.os == 'sonic':
236294
self.pfc_stop_template = os.path.join(
237295
TEMPLATES_DIR, "pfc_storm_stop_mlnx_{}.j2".format(self.peer_device.os))
238-
elif self.asic_type == 'vs':
296+
elif ((self.peer_device.os == 'eos' and
297+
get_chip_name_if_asic_pfc_storm_supported(self._get_eos_fanout_version()[0])) or
298+
(self.peer_device.os == 'sonic' and
299+
get_chip_name_if_asic_pfc_storm_supported(self._get_sonic_fanout_hwsku()))):
239300
self.pfc_stop_template = os.path.join(
240-
TEMPLATES_DIR, "pfc_storm_stop_eos.j2")
301+
TEMPLATES_DIR, "pfc_storm_stop_arista_{}.j2".format(self.peer_device.os))
241302
else:
242303
self.pfc_stop_template = os.path.join(
243304
TEMPLATES_DIR, "pfc_storm_stop_{}.j2".format(self.peer_device.os))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
bash
2+
cd {{pfc_gen_dir}}
3+
{% if (pfc_asym is defined) and (pfc_asym == True) %}
4+
{% if pfc_storm_defer_time is defined %}sleep {{pfc_storm_defer_time}} &&{% endif %} sudo python3 {{pfc_gen_file}} -c {{pfc_gen_chip_name}} -p {{pfc_queue_index}} -i {{pfc_fanout_interface | replace("Ethernet", "et") | replace("/", "_")}} > /dev/null 2>&1 &
5+
{% else %}
6+
{% if pfc_storm_defer_time is defined %}sleep {{pfc_storm_defer_time}} &&{% endif %} sudo python3 {{pfc_gen_file}} -c {{pfc_gen_chip_name}} -p {{(1).__lshift__(pfc_queue_index)}} -i {{pfc_fanout_interface | replace("Ethernet", "et") | replace("/", "_")}} -r {{ansible_eth0_ipv4_addr}} > /dev/null 2>&1 &
7+
{% endif %}
8+
exit
9+
exit

0 commit comments

Comments
 (0)