Blanton_Script/blanton_traffic_linespeed.sh V0.3.2 drives the whole
loopback traffic run from the DUT shell via bcmcmd, for both switch
boards (unit 0 and 1):
- init : vlan remove 1 pbm=cd, then create/add the 108 loopback
pairs (VLAN 30..137, cdN <-> cdN+32)
- clear : clear c
- show : show c (raw)
- start : tx <count> length=<len> VLantag=<vid> per VLAN
- stop : remove the pairs, optional --destroy
- ps : port status of the cabled ports
- report: parse the show c MIB_TPOK/MIB_RPOK counters and lay them
out one pair per block with a cross-check verdict
(cdA.TX == cdB.RX and cdA.RX == cdB.TX), PASS green /
FAIL red / NA yellow, --tolerance for snapshot skew,
--tsv, --raw and -f <log> for offline analysis; the raw
capture is kept under /tmp
- run : ps -> init -> clear -> start -> report
Options -u/-c/-l/-p/-T/-d/-n and a sourceable function API, in the
style of the other blanton_*.sh tools. Command output matches the
former Tera Term macros byte for byte; report output matches
tools/bcm_mibpair_report_V1.1.0.py, added here as the off-box
equivalent, together with the raw bcmcmd command lists under tools/.
ScriptA sources the tool, ScriptB runs ps/init/clear/show/start and
drops stress_hhmd and stress_pcie, ScriptC stops the traffic and
prints the pair counter report.
326 lines
11 KiB
Python
326 lines
11 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
bcm_mibpair_report_V1.1.0.py
|
|
|
|
Author : ETWen
|
|
Date : 20260730
|
|
|
|
Purpose : Extract MIB_TPOK / MIB_RPOK counters from a Broadcom drivshell log
|
|
and lay them out horizontally, one loopback pair per block:
|
|
|
|
Port TX RX PASS/FAIL
|
|
cd1 723629081133 581693309458 PASS
|
|
cd33 581693309458 723629081133 PASS
|
|
|
|
Pair membership and pair order come from the
|
|
"vlan remove <vid> pbm=cdA,cdB" lines in the same log, so the report
|
|
always follows the order the test actually ran in.
|
|
|
|
Verdict per pair comes from the loopback cross-check: for a pair,
|
|
cdA.TPOK must equal cdB.RPOK and cdA.RPOK must equal cdB.TPOK.
|
|
PASS - both directions match (within --tolerance)
|
|
FAIL - at least one direction is off; delta is listed in the
|
|
trailing summary
|
|
NA - the pair has no counters in the log
|
|
|
|
Usage : ./bcm_mibpair_report_V1.1.0.py <drivshell.log> [options]
|
|
|
|
-a, --all also emit pairs that have no counters in the log
|
|
(values shown as "-")
|
|
-b, --block repeat the header and blank-separate each pair
|
|
-g, --grouped keep the SDK's comma grouping (1,234,567)
|
|
-p, --with-port render "cd1(2)" instead of "cd1"
|
|
-t, --tsv tab-separated output (for Excel / pandas)
|
|
-s, --no-status drop the PASS/FAIL column
|
|
-T, --tolerance N allowed |delta| in packets before a pair is FAIL
|
|
(default 0). A +/-1 skew is normal: the SDK reads
|
|
counters port-by-port while traffic still runs, so
|
|
-T 1 is a reasonable production setting.
|
|
-q, --quiet suppress the trailing summary comments
|
|
-o, --output write to a file instead of stdout
|
|
|
|
Notes : - The trailing "+delta" column printed by the SDK is stripped.
|
|
- A cd port missing from the dump usually means the console
|
|
scrolled or auto-logged out mid-"show c", NOT that the port was
|
|
down. Cross-check against "ps" before drawing conclusions.
|
|
- A +/-1 packet delta between a pair is normal snapshot skew: the
|
|
SDK reads counters port-by-port while traffic is still running.
|
|
|
|
Exit : 0 = every pair had counters and cross-checked clean
|
|
1 = usage / input error
|
|
2 = at least one pair is FAIL or NA (see summary)
|
|
|
|
Version History
|
|
V1.0.0 20260730 Initial Version (parity with bcm_mibpair_report_V1.2.0.sh)
|
|
V1.0.1 20260730 Handle BrokenPipeError quietly when piped into head
|
|
V1.1.0 20260730 Add PASS/FAIL/NA verdict column and --tolerance
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import re
|
|
import sys
|
|
from dataclasses import dataclass, field
|
|
from pathlib import Path
|
|
from typing import Iterable, TextIO
|
|
|
|
EXIT_OK = 0
|
|
EXIT_USAGE = 1
|
|
EXIT_WARN = 2
|
|
|
|
# "vlan remove 31 pbm=cd1,cd33"
|
|
RE_PBM = re.compile(r"pbm=(cd\d+),(cd\d+)")
|
|
|
|
# "MIB_TPOK.cd1(2) : 723,629,081,133 +723,629,081,133"
|
|
RE_MIB = re.compile(
|
|
r"^MIB_(?P<kind>TPOK|RPOK)\.(?P<cd>cd\d+)\((?P<lport>\d+)\)\s*:\s*(?P<val>[\d,]+)"
|
|
)
|
|
|
|
MISSING = "-"
|
|
|
|
|
|
@dataclass
|
|
class PortCounters:
|
|
"""TPOK / RPOK for a single cd port."""
|
|
|
|
lport: int | None = None
|
|
tx: int | None = None
|
|
rx: int | None = None
|
|
|
|
@property
|
|
def complete(self) -> bool:
|
|
return self.tx is not None and self.rx is not None
|
|
|
|
|
|
@dataclass
|
|
class LogData:
|
|
"""Everything the report needs, extracted from one drivshell log."""
|
|
|
|
pairs: list[tuple[str, str]] = field(default_factory=list)
|
|
ports: dict[str, PortCounters] = field(default_factory=dict)
|
|
|
|
def counters(self, cd: str) -> PortCounters:
|
|
return self.ports.setdefault(cd, PortCounters())
|
|
|
|
|
|
def parse_log(lines: Iterable[str]) -> LogData:
|
|
"""Pull pbm pair definitions and TPOK/RPOK counters out of a drivshell log."""
|
|
data = LogData()
|
|
seen_pairs: set[tuple[str, str]] = set()
|
|
|
|
for raw in lines:
|
|
line = raw.rstrip("\r\n")
|
|
|
|
for a, b in RE_PBM.findall(line):
|
|
if (a, b) not in seen_pairs:
|
|
seen_pairs.add((a, b))
|
|
data.pairs.append((a, b))
|
|
|
|
# drivshell echoes commands, so a MIB line may be prefixed with
|
|
# "drivshell>" noise; anchor on the MIB_ token instead of the line start.
|
|
idx = line.find("MIB_")
|
|
if idx < 0:
|
|
continue
|
|
m = RE_MIB.match(line[idx:])
|
|
if not m:
|
|
continue
|
|
|
|
pc = data.counters(m.group("cd"))
|
|
pc.lport = int(m.group("lport"))
|
|
value = int(m.group("val").replace(",", ""))
|
|
if m.group("kind") == "TPOK":
|
|
pc.tx = value
|
|
else:
|
|
pc.rx = value
|
|
|
|
return data
|
|
|
|
|
|
def fmt_value(value: int | None, grouped: bool) -> str:
|
|
if value is None:
|
|
return MISSING
|
|
return f"{value:,}" if grouped else str(value)
|
|
|
|
|
|
def fmt_port(cd: str, pc: PortCounters, with_port: bool) -> str:
|
|
if with_port and pc.lport is not None:
|
|
return f"{cd}({pc.lport})"
|
|
return cd
|
|
|
|
|
|
PASS, FAIL, NA = "PASS", "FAIL", "NA"
|
|
|
|
|
|
def pair_deltas(data: LogData, a: str, b: str) -> tuple[int, int] | None:
|
|
"""(cdA.TX - cdB.RX, cdA.RX - cdB.TX), or None if counters are missing."""
|
|
pa, pb = data.counters(a), data.counters(b)
|
|
if not (pa.complete and pb.complete):
|
|
return None
|
|
return pa.tx - pb.rx, pa.rx - pb.tx
|
|
|
|
|
|
def verdict(data: LogData, a: str, b: str, tolerance: int) -> str:
|
|
d = pair_deltas(data, a, b)
|
|
if d is None:
|
|
return NA
|
|
return PASS if max(abs(d[0]), abs(d[1])) <= tolerance else FAIL
|
|
|
|
|
|
def render(data: LogData, args: argparse.Namespace, out: TextIO) -> int:
|
|
status = not args.no_status
|
|
rows: list[tuple[str, ...]] = []
|
|
blocks: list[list[tuple[str, ...]]] = []
|
|
missing: list[tuple[str, str]] = []
|
|
failed: list[tuple[str, str, int, int]] = []
|
|
tally = {PASS: 0, FAIL: 0, NA: 0}
|
|
|
|
for a, b in data.pairs:
|
|
pa, pb = data.counters(a), data.counters(b)
|
|
v = verdict(data, a, b, args.tolerance)
|
|
tally[v] += 1
|
|
if v == NA:
|
|
missing.append((a, b))
|
|
if not args.all:
|
|
continue
|
|
elif v == FAIL:
|
|
d_tx, d_rx = pair_deltas(data, a, b)
|
|
failed.append((a, b, d_tx, d_rx))
|
|
|
|
block = []
|
|
for cd, pc in ((a, pa), (b, pb)):
|
|
row = (
|
|
fmt_port(cd, pc, args.with_port),
|
|
fmt_value(pc.tx, args.grouped),
|
|
fmt_value(pc.rx, args.grouped),
|
|
)
|
|
block.append(row + (v,) if status else row)
|
|
blocks.append(block)
|
|
rows.extend(block)
|
|
|
|
header = ("Port", "TX", "RX", "PASS/FAIL") if status else ("Port", "TX", "RX")
|
|
|
|
if args.tsv:
|
|
if not args.block:
|
|
print("\t".join(header), file=out)
|
|
for block in blocks:
|
|
if args.block:
|
|
print("\t".join(header), file=out)
|
|
for row in block:
|
|
print("\t".join(row), file=out)
|
|
if args.block:
|
|
print(file=out)
|
|
else:
|
|
# size columns to the widest cell actually emitted
|
|
ncol = len(header)
|
|
width = [
|
|
max([len(r[i]) for r in rows] + [len(header[i])]) + 2
|
|
for i in range(ncol)
|
|
]
|
|
|
|
def line(row: tuple[str, ...]) -> str:
|
|
cells = [f"{row[0]:<{width[0]}}"]
|
|
cells += [f"{row[i]:>{width[i]}}" for i in range(1, ncol)]
|
|
return "".join(cells).rstrip()
|
|
|
|
if not args.block:
|
|
print(line(header), file=out)
|
|
for block in blocks:
|
|
if args.block:
|
|
print(line(header), file=out)
|
|
for row in block:
|
|
print(line(row), file=out)
|
|
if args.block:
|
|
print(file=out)
|
|
|
|
if not args.quiet:
|
|
if not args.block:
|
|
print(file=out)
|
|
print(f"# pairs in pbm list : {len(data.pairs)}", file=out)
|
|
print(f"# pairs reported : {len(blocks)}", file=out)
|
|
print(
|
|
f"# PASS {tally[PASS]} FAIL {tally[FAIL]} NA {tally[NA]}"
|
|
f" (tolerance {args.tolerance})",
|
|
file=out,
|
|
)
|
|
if missing:
|
|
joined = " ".join(f"{a}/{b}" for a, b in missing)
|
|
print(f"# NA (no counters): {joined}", file=out)
|
|
for a, b, d_tx, d_rx in failed:
|
|
print(
|
|
f"# FAIL {a}/{b}: {a}.TX-{b}.RX={d_tx:+d} {a}.RX-{b}.TX={d_rx:+d}",
|
|
file=out,
|
|
)
|
|
|
|
return EXIT_WARN if (tally[FAIL] or tally[NA]) else EXIT_OK
|
|
|
|
|
|
def build_parser() -> argparse.ArgumentParser:
|
|
p = argparse.ArgumentParser(
|
|
description="Render Broadcom drivshell MIB_TPOK/RPOK counters as a "
|
|
"loopback-pair table.",
|
|
epilog="Exit 2 means missing counters and/or a cross-check mismatch.",
|
|
)
|
|
p.add_argument("log", type=Path, help="drivshell log file ('-' for stdin)")
|
|
p.add_argument("-a", "--all", action="store_true",
|
|
help="also emit pairs with no counters")
|
|
p.add_argument("-b", "--block", action="store_true",
|
|
help="repeat header and blank-separate each pair")
|
|
p.add_argument("-g", "--grouped", action="store_true",
|
|
help="keep comma grouping in values")
|
|
p.add_argument("-p", "--with-port", action="store_true",
|
|
help="render cd1(2) instead of cd1")
|
|
p.add_argument("-t", "--tsv", action="store_true",
|
|
help="tab-separated output")
|
|
p.add_argument("-s", "--no-status", action="store_true",
|
|
help="drop the PASS/FAIL column")
|
|
p.add_argument("-T", "--tolerance", type=int, default=0, metavar="N",
|
|
help="allowed |delta| in packets before a pair is FAIL "
|
|
"(default 0; -T 1 absorbs normal snapshot skew)")
|
|
p.add_argument("-q", "--quiet", action="store_true",
|
|
help="suppress trailing summary comments")
|
|
p.add_argument("-o", "--output", type=Path, default=None,
|
|
help="write to a file instead of stdout")
|
|
return p
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
args = build_parser().parse_args(argv)
|
|
|
|
try:
|
|
if str(args.log) == "-":
|
|
data = parse_log(sys.stdin)
|
|
else:
|
|
# console captures are frequently not clean UTF-8
|
|
with args.log.open("r", encoding="utf-8", errors="replace") as fh:
|
|
data = parse_log(fh)
|
|
except OSError as exc:
|
|
print(f"error: cannot read log: {exc}", file=sys.stderr)
|
|
return EXIT_USAGE
|
|
|
|
if not data.pairs:
|
|
print("error: no 'pbm=cdA,cdB' pair definition found in log",
|
|
file=sys.stderr)
|
|
return EXIT_USAGE
|
|
|
|
try:
|
|
if args.output:
|
|
with args.output.open("w", encoding="utf-8", newline="\n") as fh:
|
|
return render(data, args, fh)
|
|
return render(data, args, sys.stdout)
|
|
except BrokenPipeError:
|
|
# downstream closed the pipe (e.g. "| head") - not an error
|
|
try:
|
|
sys.stdout.close()
|
|
except BrokenPipeError:
|
|
pass
|
|
return EXIT_OK
|
|
except OSError as exc:
|
|
print(f"error: cannot write output: {exc}", file=sys.stderr)
|
|
return EXIT_USAGE
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|