fix(console): fix udev symlink naming bug + add portable audit tooling
Console fix: generate-config.sh wrote SYMLINK+="console/$name" (singular)
but ser2net.yaml opens /dev/consoles/$name (plural). They never matched,
so after every reboot the console ports failed until setup.sh's manual
fallback re-created the symlinks. Fixed the udev rule to use "consoles/"
to match ser2net and the README.
New portable read-only audit tools (AGPLv3-friendly, config-driven):
- perf/scripts/probe-storage.sh: disk/mount/export/SMART/storage.cfg probe
- perf/scripts/probe-network.sh: NIC/bond/LLDP/NFS/nconnect probe
- perf/scripts/conman-console.py: PTY-based conman console driver (replaces
the old sw-capture.py that conflicted with ser2net)
- perf/scripts/snmp-switch-audit.py: SNMP-based switch inventory (interfaces,
LLDP, LAG, VLANs) via pysnmp or net-snmp
Removed stale pre-conman switch tooling (sw-capture-remote.sh, sw-capture.py,
sw-probe.sh, sw-conman-probe.sh) and old .cmds files. Added fresh .cmds
files for the two cross-rack trunk endpoint switches.
💘 Generated with Crush
Assisted-by: Crush:glm-5.2
This commit is contained in:
@@ -121,7 +121,7 @@ for entry in "${ENTRIES[@]}"; do
|
|||||||
{
|
{
|
||||||
echo ""
|
echo ""
|
||||||
echo "# $name (TCP $tcp_port): $comment"
|
echo "# $name (TCP $tcp_port): $comment"
|
||||||
echo "SUBSYSTEM==\"tty\", ENV{ID_PATH}==\"*$id_path*\", SYMLINK+=\"console/$name\""
|
echo "SUBSYSTEM==\"tty\", ENV{ID_PATH}==\"*$id_path*\", SYMLINK+=\"consoles/$name\""
|
||||||
} >> "$UDEV_RULES"
|
} >> "$UDEV_RULES"
|
||||||
done
|
done
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,162 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
conman-console.py — Drive a serial console via a conman server, read-only.
|
||||||
|
|
||||||
|
Connects to a conmand server (RFC 127-style multiplexer) over the network,
|
||||||
|
opens a named console, sends the commands from a .cmds file, captures all
|
||||||
|
output, and disconnects. Requires no expect/tcl — pure stdlib PTY.
|
||||||
|
|
||||||
|
This replaces the old sw-capture-remote.sh workflow that killed the serial
|
||||||
|
device holder (conflicting with conman/ser2net). Instead, it talks to conman
|
||||||
|
over TCP, which multiplexes safely with other sessions.
|
||||||
|
|
||||||
|
All endpoints are configurable via environment variables so this works on
|
||||||
|
any network with a conman server:
|
||||||
|
|
||||||
|
CONMAN_SERVER conman server host:port (default: via CONSOLE_HOST)
|
||||||
|
CONSOLE console name to open (required)
|
||||||
|
CMDS_FILE file of commands to send (required)
|
||||||
|
TIMEOUT overall timeout in seconds (default: 45)
|
||||||
|
CMD_DELAY seconds between commands (default: 3)
|
||||||
|
WAKE_DELAY seconds after connect (default: 2)
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
CONMAN_SERVER=console-host:7890 \\
|
||||||
|
python3 conman-console.py --console pfv-core-sw01 --cmds switches/pfv-core-sw01.cmds
|
||||||
|
|
||||||
|
Lines starting with '!' or '#' in the cmds file are comments (skipped).
|
||||||
|
Blank lines are skipped. The conman escape sequence (&.) is sent automatically
|
||||||
|
to disconnect. A password prompt aborts immediately (we never send creds).
|
||||||
|
|
||||||
|
Exit codes:
|
||||||
|
0 clean run
|
||||||
|
1 usage / setup error
|
||||||
|
2 could not connect to conman server
|
||||||
|
3 timeout (partial output still printed)
|
||||||
|
4 password prompt encountered (aborted)
|
||||||
|
"""
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import pty
|
||||||
|
import re
|
||||||
|
import select
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
PWD_RE = re.compile(rb"[Pp]assword:\s*$")
|
||||||
|
MORE_RE = re.compile(rb"--\s*[Mm]ore\s*--|[Mm]ore:\s*<space>")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ap = argparse.ArgumentParser(
|
||||||
|
description="Drive a conman console session read-only via PTY")
|
||||||
|
ap.add_argument("--console", required=True,
|
||||||
|
help="console name (e.g. pfv-core-sw01)")
|
||||||
|
ap.add_argument("--cmds", required=True,
|
||||||
|
help="command file (one command per line; !/# = comment)")
|
||||||
|
ap.add_argument("--server",
|
||||||
|
default=os.environ.get("CONMAN_SERVER", ""),
|
||||||
|
help="conman server host:port (env: CONMAN_SERVER)")
|
||||||
|
ap.add_argument("--timeout", type=int,
|
||||||
|
default=int(os.environ.get("TIMEOUT", "45")),
|
||||||
|
help="overall timeout seconds (env: TIMEOUT)")
|
||||||
|
ap.add_argument("--cmd-delay", type=float,
|
||||||
|
default=float(os.environ.get("CMD_DELAY", "3")),
|
||||||
|
help="seconds between commands (env: CMD_DELAY)")
|
||||||
|
ap.add_argument("--wake-delay", type=float,
|
||||||
|
default=float(os.environ.get("WAKE_DELAY", "2")),
|
||||||
|
help="seconds after connect before first command (env: WAKE_DELAY)")
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
if not args.server:
|
||||||
|
sys.stderr.write("ERROR: --server or CONMAN_SERVER env required\n")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
with open(args.cmds) as f:
|
||||||
|
cmds = [l.strip() for l in f
|
||||||
|
if l.strip() and not l.strip().startswith(("!", "#"))]
|
||||||
|
|
||||||
|
pid, fd = pty.fork()
|
||||||
|
if pid == 0:
|
||||||
|
try:
|
||||||
|
os.execvp("conman",
|
||||||
|
["conman", "-d", args.server, "-f", args.console])
|
||||||
|
except OSError as e:
|
||||||
|
sys.stderr.write(f"ERROR: cannot exec conman: {e}\n")
|
||||||
|
os._exit(2)
|
||||||
|
os._exit(2)
|
||||||
|
|
||||||
|
output = b""
|
||||||
|
cmd_queue = list(cmds)
|
||||||
|
sent_disconnect = False
|
||||||
|
start = time.time()
|
||||||
|
last_action = 0.0
|
||||||
|
phase = "connect"
|
||||||
|
|
||||||
|
while time.time() - start < args.timeout:
|
||||||
|
ready, _, _ = select.select([fd], [], [], 0.5)
|
||||||
|
if ready:
|
||||||
|
try:
|
||||||
|
data = os.read(fd, 8192)
|
||||||
|
except OSError:
|
||||||
|
break
|
||||||
|
if not data:
|
||||||
|
break
|
||||||
|
output += data
|
||||||
|
|
||||||
|
if PWD_RE.search(output.split(b"\n")[-1] if output else b""):
|
||||||
|
sys.stderr.write("[ABORT] password prompt detected — "
|
||||||
|
"never sending credentials\n")
|
||||||
|
os.write(fd, b"&.\n")
|
||||||
|
break
|
||||||
|
|
||||||
|
# Handle pagination: send space to continue
|
||||||
|
if MORE_RE.search(output[-200:] if output else b""):
|
||||||
|
os.write(fd, b" ")
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
elapsed = time.time() - start
|
||||||
|
gap = elapsed - last_action
|
||||||
|
|
||||||
|
if phase == "connect" and gap >= args.wake_delay:
|
||||||
|
os.write(fd, b"\n")
|
||||||
|
phase = "send"
|
||||||
|
last_action = elapsed
|
||||||
|
elif phase == "send" and gap >= args.cmd_delay:
|
||||||
|
if cmd_queue:
|
||||||
|
cmd = cmd_queue.pop(0)
|
||||||
|
os.write(fd, (cmd + "\n").encode())
|
||||||
|
last_action = elapsed
|
||||||
|
else:
|
||||||
|
phase = "drain"
|
||||||
|
last_action = elapsed
|
||||||
|
elif phase == "drain" and gap >= args.cmd_delay:
|
||||||
|
os.write(fd, b"&.\n")
|
||||||
|
sent_disconnect = True
|
||||||
|
phase = "done"
|
||||||
|
last_action = elapsed
|
||||||
|
elif phase == "done" and gap >= 2:
|
||||||
|
break
|
||||||
|
|
||||||
|
if not sent_disconnect:
|
||||||
|
try:
|
||||||
|
os.write(fd, b"&.\n")
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
os.close(fd)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
os.waitpid(pid, 0)
|
||||||
|
except ChildProcessError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
sys.stdout.buffer.write(output)
|
||||||
|
sys.stdout.flush()
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
###############################################################################
|
||||||
|
# probe-network.sh
|
||||||
|
#
|
||||||
|
# READ-ONLY network + NFS ground-truth probe. Writes only stdout.
|
||||||
|
# Run on any Proxmox host to inventory its NICs, bonds, LLDP neighbors,
|
||||||
|
# NFS client mounts (including nconnect), ethtool link state, and error
|
||||||
|
# counters. No hardcoded values — fully portable.
|
||||||
|
#
|
||||||
|
# Usage (via tests/remote.sh):
|
||||||
|
# PROX_HOST=pfv-tsys6 bash tests/remote.sh prox-file perf/scripts/probe-network.sh
|
||||||
|
###############################################################################
|
||||||
|
set -u
|
||||||
|
echo "===== HOST: $(hostname -s) $(date -u +%FT%TZ) ====="
|
||||||
|
echo
|
||||||
|
echo "##### ip -br link #####"
|
||||||
|
ip -br link 2>&1
|
||||||
|
echo
|
||||||
|
echo "##### ip -br addr #####"
|
||||||
|
ip -br addr 2>&1
|
||||||
|
echo
|
||||||
|
echo "##### /etc/network/interfaces #####"
|
||||||
|
cat /etc/network/interfaces 2>&1
|
||||||
|
echo
|
||||||
|
echo "##### bond0 state (if present) #####"
|
||||||
|
if [ -r /proc/net/bonding/bond0 ]; then
|
||||||
|
cat /proc/net/bonding/bond0 2>&1
|
||||||
|
else
|
||||||
|
echo "(no bond0)"
|
||||||
|
fi
|
||||||
|
echo
|
||||||
|
echo "##### ethtool per physical NIC #####"
|
||||||
|
for nic in /sys/class/net/*; do
|
||||||
|
nic=$(basename "$nic")
|
||||||
|
case "$nic" in lo|bond*|br*|venet*|veth*|docker*|tap*|vnet*|fw*) continue;; esac
|
||||||
|
echo "--- ethtool $nic ---"
|
||||||
|
ethtool "$nic" 2>&1 | grep -iE 'Speed|Duplex|Port|Link|Supported link modes|Advertising|Auto-neg|Settings' || echo "(ethtool failed for $nic)"
|
||||||
|
done
|
||||||
|
echo
|
||||||
|
echo "##### lldpcli (if installed) #####"
|
||||||
|
if command -v lldpcli >/dev/null 2>&1; then
|
||||||
|
echo "--- lldpcli show neighbors ---"
|
||||||
|
lldpcli show neighbors 2>&1
|
||||||
|
echo
|
||||||
|
echo "--- lldpcli show interfaces ---"
|
||||||
|
lldpcli show interfaces 2>&1
|
||||||
|
echo
|
||||||
|
echo "--- lldpcli show chassis ---"
|
||||||
|
lldpcli show chassis 2>&1
|
||||||
|
else
|
||||||
|
echo "(lldpcli not installed)"
|
||||||
|
fi
|
||||||
|
echo
|
||||||
|
echo "##### lldpd / lldpad service #####"
|
||||||
|
systemctl is-active lldpd 2>&1 || true
|
||||||
|
systemctl is-enabled lldpd 2>&1 || true
|
||||||
|
echo
|
||||||
|
echo "##### NFS mounts (mount | grep nfs) #####"
|
||||||
|
mount | grep -i nfs 2>&1 || echo "(no nfs mounts)"
|
||||||
|
echo
|
||||||
|
echo "##### mount nconnect detail (nfsstat -m) #####"
|
||||||
|
nfsstat -m 2>&1
|
||||||
|
echo
|
||||||
|
echo "##### storage.cfg NFS stanzas (options) #####"
|
||||||
|
grep -A3 '^nfs:' /etc/pve/storage.cfg 2>&1
|
||||||
|
echo
|
||||||
|
echo "##### ip route #####"
|
||||||
|
ip route 2>&1
|
||||||
|
echo
|
||||||
|
echo "##### ethtool -S bond slaves (key counters) #####"
|
||||||
|
if [ -r /proc/net/bonding/bond0 ]; then
|
||||||
|
# shellcheck disable=SC2013 # intentional: extract NIC names from bonding info
|
||||||
|
for nic in $(grep -oE 'eth[0-9]+|en[psx][a-z0-9]+' /proc/net/bonding/bond0 2>/dev/null | sort -u); do
|
||||||
|
echo "--- ethtool -S $nic (errors) ---"
|
||||||
|
ethtool -S "$nic" 2>/dev/null | grep -iE 'error|drop|discard|crc|pause|miss' || echo "(no error counters)"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
echo
|
||||||
|
echo "##### ip neigh (ARP table, reachable/stale) #####"
|
||||||
|
ip neigh show 2>&1 | grep -vE ' FAILED|INCOMPLETE' | sort -t. -k4 -n
|
||||||
|
echo
|
||||||
|
echo "===== END $(hostname -s) ====="
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
###############################################################################
|
||||||
|
# probe-storage.sh
|
||||||
|
#
|
||||||
|
# READ-ONLY storage + disk ground-truth probe. Writes only stdout.
|
||||||
|
# Run on any Proxmox host (or any Linux NFS server) to inventory its physical
|
||||||
|
# disks, mounts, exports, SMART health, and Proxmox storage config.
|
||||||
|
#
|
||||||
|
# Portable: no hardcoded values. Uses only standard CLI tools + smartmontools.
|
||||||
|
#
|
||||||
|
# Usage (via tests/remote.sh):
|
||||||
|
# PROX_HOST=pfv-tsys4 bash tests/remote.sh prox-file perf/scripts/probe-storage.sh
|
||||||
|
#
|
||||||
|
# Or directly on a host:
|
||||||
|
# bash probe-storage.sh > storage-audit.txt
|
||||||
|
###############################################################################
|
||||||
|
set -u
|
||||||
|
echo "===== HOST: $(hostname -s) $(date -u +%FT%TZ) ====="
|
||||||
|
echo
|
||||||
|
echo "##### lsblk (tree, with model/serial/size/type) #####"
|
||||||
|
lsblk -o NAME,MAJ:MIN,SIZE,TYPE,MOUNTPOINT,MODEL,SERIAL,STATE,ROTA,TRAN,REV 2>&1
|
||||||
|
echo
|
||||||
|
echo "##### block devices by-id #####"
|
||||||
|
for dev in /dev/disk/by-id/*; do
|
||||||
|
[ -L "$dev" ] || continue
|
||||||
|
case "$(basename "$dev")" in *part[0-9]*) continue;; esac
|
||||||
|
ls -l "$dev"
|
||||||
|
done 2>&1
|
||||||
|
echo
|
||||||
|
echo "##### nvme list (if any) #####"
|
||||||
|
command -v nvme >/dev/null 2>&1 && nvme list 2>&1 || echo "(no nvme-cli or no nvme devices)"
|
||||||
|
echo
|
||||||
|
echo "##### blkid #####"
|
||||||
|
blkid 2>&1
|
||||||
|
echo
|
||||||
|
echo "##### mounted filesystems #####"
|
||||||
|
findmnt -o TARGET,SOURCE,FSTYPE,OPTIONS 2>&1
|
||||||
|
echo
|
||||||
|
echo "##### /etc/fstab #####"
|
||||||
|
cat /etc/fstab 2>&1
|
||||||
|
echo
|
||||||
|
echo "##### /etc/exports (+ exports.d) #####"
|
||||||
|
cat /etc/exports 2>&1
|
||||||
|
for f in /etc/exports.d/*.exports; do [ -f "$f" ] && echo "--- $f ---" && cat "$f"; done 2>&1
|
||||||
|
echo
|
||||||
|
echo "##### df -h (all mounts) #####"
|
||||||
|
df -h 2>&1
|
||||||
|
echo
|
||||||
|
echo "##### smartctl -a per block device #####"
|
||||||
|
command -v smartctl >/dev/null 2>&1 || echo "(smartctl not installed)"
|
||||||
|
for d in /dev/sd? /dev/nvme?n1; do
|
||||||
|
[ -b "$d" ] || continue
|
||||||
|
echo "----- smartctl -a $d -----"
|
||||||
|
smartctl -a "$d" 2>&1 | grep -iE 'Device Model|Model Number|Serial|Firmware|User Capacity|Rotation Rate|Form Factor|SATA Version|NVMe|SMART overall|Reallocated|Pending|Uncorrect|Power On|Temperature|Media and Data Integrity' || true
|
||||||
|
done
|
||||||
|
echo
|
||||||
|
echo "##### /etc/pve/storage.cfg #####"
|
||||||
|
cat /etc/pve/storage.cfg 2>&1
|
||||||
|
echo
|
||||||
|
echo "##### pvesm status #####"
|
||||||
|
pvesm status 2>&1
|
||||||
|
echo
|
||||||
|
echo "##### pvesm list per store #####"
|
||||||
|
for s in $(pvesm status 2>/dev/null | awk 'NR>1 && $3>0 {print $1}'); do
|
||||||
|
echo "--- pvesm list $s ---"
|
||||||
|
pvesm list "$s" 2>&1 | head -40
|
||||||
|
done
|
||||||
|
echo
|
||||||
|
echo "##### zpool status (if any) #####"
|
||||||
|
command -v zpool >/dev/null 2>&1 && zpool status 2>&1 || echo "(no zfs)"
|
||||||
|
echo
|
||||||
|
echo "##### lvm: pvs/vgs/lvs #####"
|
||||||
|
command -v pvs >/dev/null 2>&1 && { pvs 2>&1; echo; vgs 2>&1; echo; lvs 2>&1; } || echo "(no lvm tools)"
|
||||||
|
echo
|
||||||
|
echo "===== END $(hostname -s) ====="
|
||||||
@@ -0,0 +1,253 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
snmp-switch-audit.py — READ-ONLY switch inventory via SNMP.
|
||||||
|
|
||||||
|
Gathers interface status/speed/errors, LLDP neighbor topology, port-channel
|
||||||
|
(LAG) membership, and VLAN membership from any SNMPv2c-capable switch.
|
||||||
|
Designed for Dell/Radlan (Neyland) and standard IF/LLDP/Q-BRIDGE MIB switches,
|
||||||
|
but works on any SNMP-manageable device.
|
||||||
|
|
||||||
|
All parameters configurable via env vars or CLI flags so this works on any
|
||||||
|
network:
|
||||||
|
|
||||||
|
SNMP_COMMUNITY SNMPv2c community string (env, default: public)
|
||||||
|
SWITCH_IPS space-separated switch IPs (env, or pass as args)
|
||||||
|
OUTPUT_DIR where to write per-switch (env, default: returned-logs/snmp)
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
SNMP_COMMUNITY=kn3lmgmt SWITCH_IPS="192.168.0.9 192.168.0.12" \\
|
||||||
|
python3 snmp-switch-audit.py
|
||||||
|
|
||||||
|
# or pass IPs as positional args:
|
||||||
|
SNMP_COMMUNITY=kn3lmgmt python3 snmp-switch-audit.py 192.168.0.9 192.168.0.12
|
||||||
|
|
||||||
|
Requires: pysnmp (pip install pysnmp) or net-snmp utils (snmpwalk) on PATH.
|
||||||
|
Outputs: per-switch JSON + human-readable text in OUTPUT_DIR.
|
||||||
|
|
||||||
|
Read-only: sends only SNMP GET/GETNEXT/GETBULK. Never SETs anything.
|
||||||
|
"""
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
COMMUNITY = os.environ.get("SNMP_COMMUNITY", "public")
|
||||||
|
OUTPUT_DIR = os.environ.get("OUTPUT_DIR",
|
||||||
|
os.path.join(os.path.dirname(__file__), "..",
|
||||||
|
"..", "returned-logs", "snmp"))
|
||||||
|
|
||||||
|
# OID constants
|
||||||
|
OID_SYSDESCR = "1.3.6.1.2.1.1.1.0"
|
||||||
|
OID_SYSNAME = "1.3.6.1.2.1.1.5.0"
|
||||||
|
OID_IF_NAME = "1.3.6.1.2.1.31.1.1.1.1"
|
||||||
|
OID_IF_SPEED = "1.3.6.1.2.1.2.2.1.5"
|
||||||
|
OID_IF_OPER = "1.3.6.1.2.1.2.2.1.8"
|
||||||
|
OID_IF_INERR = "1.3.6.1.2.1.2.2.1.14"
|
||||||
|
OID_IF_OUTERR = "1.3.6.1.2.1.2.2.1.20"
|
||||||
|
OID_IF_INOCT = "1.3.6.1.2.1.31.1.1.1.6"
|
||||||
|
OID_IF_OUTOCT = "1.3.6.1.2.1.31.1.1.1.10"
|
||||||
|
OID_LACP_LAG = "1.2.840.10006.300.43.1.1.1.1"
|
||||||
|
OID_LLDP_REM_PORT = "1.0.8802.1.1.2.1.4.1.1.7"
|
||||||
|
OID_LLDP_REM_SYSNAME = "1.0.8802.1.1.2.1.4.1.1.9"
|
||||||
|
OID_LLDP_REM_CHASSIS = "1.0.8802.1.1.2.1.4.1.1.6"
|
||||||
|
OID_LLDP_REM_LOCALPORT = "1.0.8802.1.1.2.1.4.1.1.3"
|
||||||
|
OID_QBRIDGE_VLAN = "1.3.6.1.2.1.17.7.1.4.3.1.1"
|
||||||
|
|
||||||
|
|
||||||
|
def snmpget(ip, oid):
|
||||||
|
"""Single SNMP GET, returns string value or None."""
|
||||||
|
try:
|
||||||
|
r = subprocess.run(
|
||||||
|
["snmpget", "-Oqv", "-v2c", "-c", COMMUNITY, ip, oid],
|
||||||
|
capture_output=True, text=True, timeout=10)
|
||||||
|
if r.returncode == 0 and r.stdout.strip():
|
||||||
|
return r.stdout.strip().strip('"')
|
||||||
|
except (subprocess.TimeoutExpired, FileNotFoundError):
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def snmpwalk(ip, oid):
|
||||||
|
"""SNMP BULKWALK, returns dict of ifIndex -> value."""
|
||||||
|
try:
|
||||||
|
r = subprocess.run(
|
||||||
|
["snmpbulkwalk", "-Oqv", "-v2c", "-c", COMMUNITY, ip, oid],
|
||||||
|
capture_output=True, text=True, timeout=30)
|
||||||
|
if r.returncode != 0:
|
||||||
|
r = subprocess.run(
|
||||||
|
["snmpwalk", "-Oqv", "-v2c", "-c", COMMUNITY, ip, oid],
|
||||||
|
capture_output=True, text=True, timeout=30)
|
||||||
|
except (subprocess.TimeoutExpired, FileNotFoundError):
|
||||||
|
return {}
|
||||||
|
result = {}
|
||||||
|
for i, line in enumerate(r.stdout.strip().split("\n"), 1):
|
||||||
|
line = line.strip().strip('"')
|
||||||
|
if line:
|
||||||
|
result[i] = line
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def walk_indexed(ip, oid):
|
||||||
|
"""SNMP walk preserving OID index. Returns dict: index_str -> value."""
|
||||||
|
try:
|
||||||
|
r = subprocess.run(
|
||||||
|
["snmpbulkwalk", "-v2c", "-c", COMMUNITY, ip, oid],
|
||||||
|
capture_output=True, text=True, timeout=30)
|
||||||
|
if r.returncode != 0:
|
||||||
|
r = subprocess.run(
|
||||||
|
["snmpwalk", "-v2c", "-c", COMMUNITY, ip, oid],
|
||||||
|
capture_output=True, text=True, timeout=30)
|
||||||
|
except (subprocess.TimeoutExpired, FileNotFoundError):
|
||||||
|
return {}
|
||||||
|
result = {}
|
||||||
|
for line in r.stdout.strip().split("\n"):
|
||||||
|
m = re.search(r'(\d+)\s*=\s*(.+)', line)
|
||||||
|
if not m:
|
||||||
|
m = re.search(r'\.(\d+)\s*=\s*(.+)', line)
|
||||||
|
if m:
|
||||||
|
idx = m.group(1).split(".")[-1]
|
||||||
|
val = m.group(2).strip()
|
||||||
|
val = re.sub(r'^(INTEGER: |STRING: |Hex-STRING: |Gauge32: |Counter32: |Counter64: )', '', val)
|
||||||
|
result[idx] = val
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def audit_switch(ip):
|
||||||
|
"""Gather all data for one switch."""
|
||||||
|
data = {"ip": ip}
|
||||||
|
data["sysDescr"] = snmpget(ip, OID_SYSDESCR)
|
||||||
|
data["sysName"] = snmpget(ip, OID_SYSNAME)
|
||||||
|
if not data["sysDescr"]:
|
||||||
|
return data
|
||||||
|
|
||||||
|
names = snmpwalk(ip, OID_IF_NAME)
|
||||||
|
speeds = snmpwalk(ip, OID_IF_SPEED)
|
||||||
|
oper = snmpwalk(ip, OID_IF_OPER)
|
||||||
|
inerr = snmpwalk(ip, OID_IF_INERR)
|
||||||
|
outerr = snmpwalk(ip, OID_IF_OUTERR)
|
||||||
|
|
||||||
|
interfaces = []
|
||||||
|
for idx in sorted(names.keys()):
|
||||||
|
if idx not in names:
|
||||||
|
continue
|
||||||
|
speed_raw = speeds.get(idx, "0")
|
||||||
|
try:
|
||||||
|
speed_mbps = int(re.sub(r'\D', '', str(speed_raw))) // 1000000
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
speed_mbps = 0
|
||||||
|
is_up = str(oper.get(idx, "0")).strip() == "1"
|
||||||
|
interfaces.append({
|
||||||
|
"ifIndex": idx,
|
||||||
|
"name": names[idx],
|
||||||
|
"speedMbps": speed_mbps,
|
||||||
|
"up": is_up,
|
||||||
|
"inErrors": inerr.get(idx, "0"),
|
||||||
|
"outErrors": outerr.get(idx, "0"),
|
||||||
|
})
|
||||||
|
data["interfaces"] = interfaces
|
||||||
|
|
||||||
|
# LLDP neighbors
|
||||||
|
rem_ports = walk_indexed(ip, OID_LLDP_REM_PORT)
|
||||||
|
rem_sysnames = walk_indexed(ip, OID_LLDP_REM_SYSNAME)
|
||||||
|
rem_chassis = walk_indexed(ip, OID_LLDP_REM_CHASSIS)
|
||||||
|
rem_local = walk_indexed(ip, OID_LLDP_REM_LOCALPORT)
|
||||||
|
lldp = []
|
||||||
|
for idx in rem_ports:
|
||||||
|
lldp.append({
|
||||||
|
"localPort": rem_local.get(idx, "?"),
|
||||||
|
"remotePort": rem_ports[idx],
|
||||||
|
"remoteSysName": rem_sysnames.get(idx, ""),
|
||||||
|
"remoteChassis": rem_chassis.get(idx, ""),
|
||||||
|
})
|
||||||
|
data["lldpNeighbors"] = lldp
|
||||||
|
|
||||||
|
# LACP LAG table
|
||||||
|
lag_data = walk_indexed(ip, OID_LACP_LAG)
|
||||||
|
data["lagTable"] = lag_data
|
||||||
|
|
||||||
|
# VLAN membership
|
||||||
|
vlan_data = walk_indexed(ip, OID_QBRIDGE_VLAN)
|
||||||
|
data["vlans"] = vlan_data
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def print_switch(data):
|
||||||
|
"""Human-readable summary."""
|
||||||
|
print(f"\n{'='*60}")
|
||||||
|
print(f" {data.get('sysName', data['ip'])} ({data['ip']})")
|
||||||
|
print(f" {data.get('sysDescr', '?')}")
|
||||||
|
print(f"{'='*60}")
|
||||||
|
|
||||||
|
print(f"\n Active ports (UP only):")
|
||||||
|
print(f" {'Port':<12} {'Speed':>10} {'InErrors':>10} {'OutErrors':>10}")
|
||||||
|
print(f" {'-'*12} {'-'*10} {'-'*10} {'-'*10}")
|
||||||
|
for iface in data.get("interfaces", []):
|
||||||
|
if iface["up"]:
|
||||||
|
print(f" {iface['name']:<12} {iface['speedMbps']:>8}Mb "
|
||||||
|
f"{iface['inErrors']:>10} {iface['outErrors']:>10}")
|
||||||
|
|
||||||
|
err_ports = [i for i in data.get("interfaces", [])
|
||||||
|
if i["up"] and (int(i["inErrors"] or 0) > 0
|
||||||
|
or int(i["outErrors"] or 0) > 0)]
|
||||||
|
if err_ports:
|
||||||
|
print(f"\n *** PORTS WITH ERRORS ***")
|
||||||
|
for p in err_ports:
|
||||||
|
print(f" {p['name']}: inErr={p['inErrors']} outErr={p['outErrors']}")
|
||||||
|
|
||||||
|
if data.get("lldpNeighbors"):
|
||||||
|
print(f"\n LLDP neighbors:")
|
||||||
|
for n in data["lldpNeighbors"]:
|
||||||
|
sysname = n.get("remoteSysName", "") or "(unknown)"
|
||||||
|
print(f" local={n['localPort']:<6} remote={n['remotePort']:<20} {sysname}")
|
||||||
|
|
||||||
|
if data.get("lagTable"):
|
||||||
|
print(f"\n LACP/LAG table entries: {len(data['lagTable'])}")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ap = argparse.ArgumentParser(
|
||||||
|
description="READ-ONLY SNMP switch audit (portable, config-driven)")
|
||||||
|
ap.add_argument("switches", nargs="*",
|
||||||
|
help="switch IPs (env: SWITCH_IPS)")
|
||||||
|
ap.add_argument("--community", default=COMMUNITY,
|
||||||
|
help=f"SNMPv2c community (env: SNMP_COMMUNITY, default: {COMMUNITY})")
|
||||||
|
ap.add_argument("--output", default=OUTPUT_DIR,
|
||||||
|
help=f"output dir (env: OUTPUT_DIR)")
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
global COMMUNITY
|
||||||
|
COMMUNITY = args.community
|
||||||
|
|
||||||
|
ips = args.switches
|
||||||
|
if not ips:
|
||||||
|
env_ips = os.environ.get("SWITCH_IPS", "")
|
||||||
|
ips = env_ips.split()
|
||||||
|
|
||||||
|
if not ips:
|
||||||
|
ap.error("no switch IPs provided (pass as args or set SWITCH_IPS)")
|
||||||
|
|
||||||
|
os.makedirs(args.output, exist_ok=True)
|
||||||
|
all_data = []
|
||||||
|
|
||||||
|
for ip in ips:
|
||||||
|
data = audit_switch(ip.strip())
|
||||||
|
all_data.append(data)
|
||||||
|
print_switch(data)
|
||||||
|
outpath = os.path.join(args.output, f"switch-{ip}.json")
|
||||||
|
with open(outpath, "w") as f:
|
||||||
|
json.dump(data, f, indent=2)
|
||||||
|
print(f"\n -> {outpath}")
|
||||||
|
|
||||||
|
combined = os.path.join(args.output, "switches-all.json")
|
||||||
|
with open(combined, "w") as f:
|
||||||
|
json.dump(all_data, f, indent=2)
|
||||||
|
print(f"\n Combined: {combined}")
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# sw-capture-remote.sh - orchestrate a serial capture from this workstation.
|
|
||||||
#
|
|
||||||
# Flow:
|
|
||||||
# 1. De-conflict: abort if any local ssh to pfv-tsys4 is in flight
|
|
||||||
# (other agent could be there).
|
|
||||||
# 2. Free the serial port: kill whatever holds /dev/ttyUSBx
|
|
||||||
# (typically a screen session). Targeted, not blanket.
|
|
||||||
# 3. scp driver + .cmds to pfv-tsys4.
|
|
||||||
# 4. Run driver over ssh, capture stderr to console.
|
|
||||||
# 5. scp the resulting log back to returned-logs/.
|
|
||||||
#
|
|
||||||
# Usage:
|
|
||||||
# sw-capture-remote.sh <switch-name> [device]
|
|
||||||
#
|
|
||||||
# <switch-name> e.g. pfv-core-sw01 (must have switches/<name>.cmds)
|
|
||||||
# [device] /dev/ttyUSBx on pfv-tsys4. Defaults per switch map below.
|
|
||||||
#
|
|
||||||
# Currently scoped to pfv-core-sw01 only (per user direction). The other
|
|
||||||
# two switches are deferred; their defaults are placeholders.
|
|
||||||
set -u
|
|
||||||
|
|
||||||
SWITCH=${1:-}
|
|
||||||
DEVICE=${2:-}
|
|
||||||
|
|
||||||
if [ -z "$SWITCH" ]; then
|
|
||||||
echo "Usage: $0 <switch-name> [device]" >&2
|
|
||||||
echo " e.g. $0 pfv-core-sw01 /dev/ttyUSB2" >&2
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Switch -> default device map (ttyUSB2 = core-sw01 confirmed by user).
|
|
||||||
case "$SWITCH" in
|
|
||||||
pfv-core-sw01)
|
|
||||||
[ -z "$DEVICE" ] && DEVICE=/dev/ttyUSB2 ;;
|
|
||||||
pfv-r3-tor-mgmt)
|
|
||||||
[ -z "$DEVICE" ] && DEVICE=/dev/ttyUSB0 # TENTATIVE - unconfirmed
|
|
||||||
if [ "${2:-}" = "" ]; then
|
|
||||||
echo "NOTE: pfv-r3-tor-mgmt device is tentative (/dev/ttyUSB0)." >&2
|
|
||||||
echo " Pass the device explicitly if different." >&2
|
|
||||||
fi ;;
|
|
||||||
pfv-r3-tor-stor)
|
|
||||||
[ -z "$DEVICE" ] && DEVICE=/dev/ttyUSB1 # TENTATIVE - unconfirmed
|
|
||||||
if [ "${2:-}" = "" ]; then
|
|
||||||
echo "NOTE: pfv-r3-tor-stor device is tentative (/dev/ttyUSB1)." >&2
|
|
||||||
echo " Pass the device explicitly if different." >&2
|
|
||||||
fi ;;
|
|
||||||
*)
|
|
||||||
echo "unknown switch: $SWITCH" >&2; exit 2 ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
BAUD=9600
|
|
||||||
HOST=root@pfv-tsys4
|
|
||||||
HERE=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)
|
|
||||||
LOCAL_DRIVER=$HERE/scripts/sw-capture.py
|
|
||||||
LOCAL_CMDS=$HERE/switches/$SWITCH.cmds
|
|
||||||
LOCAL_LOG=$HERE/returned-logs/$SWITCH.log
|
|
||||||
REMOTE_DRIVER=/root/sw-capture.py
|
|
||||||
REMOTE_CMDS=/root/$SWITCH.cmds
|
|
||||||
REMOTE_LOG=/root/$SWITCH.log
|
|
||||||
|
|
||||||
[ -f "$LOCAL_DRIVER" ] || { echo "missing $LOCAL_DRIVER" >&2; exit 2; }
|
|
||||||
[ -f "$LOCAL_CMDS" ] || { echo "missing $LOCAL_CMDS" >&2; exit 2; }
|
|
||||||
|
|
||||||
ts() { date +%H:%M:%S; }
|
|
||||||
|
|
||||||
echo "[$(ts)] switch=$SWITCH device=$DEVICE baud=$BAUD host=$HOST"
|
|
||||||
|
|
||||||
# 1. De-conflict: any local ssh to pfv-tsys4 in flight?
|
|
||||||
echo "[$(ts)] checking for in-flight ssh to pfv-tsys4..."
|
|
||||||
# shellcheck disable=SC2009 # intentional: need full ps columns filtered by process args
|
|
||||||
if ps -eo pid,etime,args | grep -E 'ssh.*pfv-tsys4|scp.*pfv-tsys4' | grep -v grep >/tmp/.swcap.ps 2>&1; then
|
|
||||||
cat /tmp/.swcap.ps
|
|
||||||
echo "[$(ts)] ABORT: another ssh/scp to pfv-tsys4 is running (other agent?)." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo "[$(ts)] clear."
|
|
||||||
rm -f /tmp/.swcap.ps
|
|
||||||
|
|
||||||
# 2. Free the serial port: kill whatever holds $DEVICE.
|
|
||||||
echo "[$(ts)] freeing $DEVICE on $HOST (targeted; other screen sessions untouched)..."
|
|
||||||
ssh -o BatchMode=yes -o ConnectTimeout=8 "$HOST" \
|
|
||||||
"fuser -v $DEVICE 2>&1 | tee /dev/stderr; \
|
|
||||||
fuser -k -TERM $DEVICE 2>/dev/null; sleep 1; \
|
|
||||||
if fuser $DEVICE 2>/dev/null; then \
|
|
||||||
echo 'still held after SIGTERM, escalating to SIGKILL'; \
|
|
||||||
fuser -k -KILL $DEVICE 2>/dev/null; sleep 1; \
|
|
||||||
fi; \
|
|
||||||
fuser $DEVICE 2>/dev/null && echo 'STILL HELD' || echo 'FREE'"
|
|
||||||
|
|
||||||
# Re-check; abort if still held.
|
|
||||||
HELD=$(ssh -o BatchMode=yes "$HOST" "fuser $DEVICE 2>/dev/null && echo HELD || echo FREE")
|
|
||||||
if [ "$HELD" = "HELD" ]; then
|
|
||||||
echo "[$(ts)] ABORT: $DEVICE still held on $HOST." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 3. Copy driver + cmds.
|
|
||||||
echo "[$(ts)] copying driver + cmds to $HOST..."
|
|
||||||
scp -q "$LOCAL_DRIVER" "$HOST:$REMOTE_DRIVER"
|
|
||||||
scp -q "$LOCAL_CMDS" "$HOST:$REMOTE_CMDS"
|
|
||||||
|
|
||||||
# 4. Run the capture on pfv-tsys4. Stream stderr (progress) to console.
|
|
||||||
echo "[$(ts)] running capture..."
|
|
||||||
ssh -o BatchMode=yes -o ServerAliveInterval=10 "$HOST" \
|
|
||||||
"python3 $REMOTE_DRIVER \
|
|
||||||
--device $DEVICE --baud $BAUD \
|
|
||||||
--cmds $REMOTE_CMDS --log $REMOTE_LOG"
|
|
||||||
RC=$?
|
|
||||||
echo "[$(ts)] capture exit code: $RC"
|
|
||||||
|
|
||||||
# 5. Pull log back.
|
|
||||||
echo "[$(ts)] pulling log back to $LOCAL_LOG..."
|
|
||||||
mkdir -p "$(dirname "$LOCAL_LOG")"
|
|
||||||
scp -q "$HOST:$REMOTE_LOG" "$LOCAL_LOG"
|
|
||||||
if [ -f "$LOCAL_LOG" ]; then
|
|
||||||
SZ=$(wc -c < "$LOCAL_LOG")
|
|
||||||
echo "[$(ts)] OK: $LOCAL_LOG ($SZ bytes)"
|
|
||||||
echo "----- head -----"
|
|
||||||
head -30 "$LOCAL_LOG"
|
|
||||||
echo "----- tail -----"
|
|
||||||
tail -10 "$LOCAL_LOG"
|
|
||||||
else
|
|
||||||
echo "[$(ts)] ERROR: log not pulled back." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
exit $RC
|
|
||||||
@@ -1,255 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
sw-capture.py - drive a Dell switch over a serial console and log all output.
|
|
||||||
|
|
||||||
Read-only. Sends ONLY the commands listed in the supplied .cmds file
|
|
||||||
(comment lines starting with '!' and blank lines are skipped). Handles
|
|
||||||
`--More--` pagination by sending a space. Aborts cleanly on any password
|
|
||||||
prompt (we never supply credentials).
|
|
||||||
|
|
||||||
Pure stdlib (termios + select). No pyserial/expect required.
|
|
||||||
|
|
||||||
Exit codes:
|
|
||||||
0 clean run, every command saw a prompt again
|
|
||||||
2 could not synchronize with a prompt during wake
|
|
||||||
3 one or more commands timed out (log still written)
|
|
||||||
4 password prompt encountered (aborted)
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
sw-capture.py --device /dev/ttyUSB2 --baud 9600 \\
|
|
||||||
--cmds pfv-core-sw01.cmds --log /root/pfv-core-sw01.log
|
|
||||||
"""
|
|
||||||
import argparse
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import select
|
|
||||||
import sys
|
|
||||||
import termios
|
|
||||||
import time
|
|
||||||
|
|
||||||
PROMPT_RE = re.compile(rb'[>#]\s*$') # ends in # or > + spaces
|
|
||||||
MORE_RE = re.compile(rb'--\s*More\s*--') # pagination prompt
|
|
||||||
PWD_RE = re.compile(rb'[Pp]assword:\s*$') # enable / login password
|
|
||||||
|
|
||||||
BAUDS = {
|
|
||||||
'9600': termios.B9600,
|
|
||||||
'19200': termios.B19200,
|
|
||||||
'38400': termios.B38400,
|
|
||||||
'57600': termios.B57600,
|
|
||||||
'115200': termios.B115200,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def log(msg, level='INFO'):
|
|
||||||
sys.stderr.write(f'[{level}] {msg}\n')
|
|
||||||
sys.stderr.flush()
|
|
||||||
|
|
||||||
|
|
||||||
def open_port(device, baud):
|
|
||||||
"""Open the serial device raw at the requested baud, 8N1, no flow ctrl."""
|
|
||||||
fd = os.open(device, os.O_RDWR | os.O_NOCTTY)
|
|
||||||
try:
|
|
||||||
attrs = termios.tcgetattr(fd)
|
|
||||||
except termios.error:
|
|
||||||
log(f'{device} is not a termios-capable device', 'WARN')
|
|
||||||
return fd
|
|
||||||
|
|
||||||
# raw input
|
|
||||||
attrs[0] &= ~(termios.IGNBRK | termios.BRKINT | termios.PARMRK |
|
|
||||||
termios.ISTRIP | termios.INLCR | termios.IGNCR |
|
|
||||||
termios.ICRNL | termios.IXON)
|
|
||||||
# raw output
|
|
||||||
attrs[1] &= ~termios.OPOST
|
|
||||||
# 8N1, enable receiver, ignore modem control lines
|
|
||||||
attrs[2] &= ~(termios.CSIZE | termios.PARENB | termios.CSTOPB)
|
|
||||||
attrs[2] |= termios.CS8 | termios.CREAD | termios.CLOCAL
|
|
||||||
# raw local
|
|
||||||
attrs[3] &= ~(termios.ECHO | termios.ECHONL | termios.ICANON |
|
|
||||||
termios.ISIG | termios.IEXTEN)
|
|
||||||
# non-blocking-ish reads (select is the primary gate)
|
|
||||||
attrs[6][termios.VMIN] = 0
|
|
||||||
attrs[6][termios.VTIME] = 1
|
|
||||||
|
|
||||||
b = BAUDS.get(str(baud))
|
|
||||||
if b is None:
|
|
||||||
raise SystemExit(f'unsupported baud: {baud}')
|
|
||||||
# Set ispeed/ospeed directly on the attribute list. (Equivalent to
|
|
||||||
# termios.cfsetispeed/cfsetospeed, which are missing on some Python
|
|
||||||
# builds — e.g. the one on pfv-tsys4.)
|
|
||||||
attrs[4] = b # ispeed
|
|
||||||
attrs[5] = b # ospeed
|
|
||||||
termios.tcsetattr(fd, termios.TCSANOW, attrs)
|
|
||||||
return fd
|
|
||||||
|
|
||||||
|
|
||||||
def read_chunk(fd, timeout):
|
|
||||||
"""Read whatever arrives within `timeout`. Extends briefly on activity."""
|
|
||||||
buf = b''
|
|
||||||
deadline = time.time() + timeout
|
|
||||||
while True:
|
|
||||||
remaining = deadline - time.time()
|
|
||||||
if remaining <= 0:
|
|
||||||
return buf
|
|
||||||
r, _, _ = select.select([fd], [], [], min(0.5, remaining))
|
|
||||||
if not r:
|
|
||||||
if buf:
|
|
||||||
return buf
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
chunk = os.read(fd, 4096)
|
|
||||||
except OSError:
|
|
||||||
return buf
|
|
||||||
if not chunk:
|
|
||||||
return buf
|
|
||||||
buf += chunk
|
|
||||||
# keep collecting as long as bytes are flowing
|
|
||||||
deadline = time.time() + 0.3
|
|
||||||
|
|
||||||
|
|
||||||
def drain(fd, timeout=1.0):
|
|
||||||
total = 0
|
|
||||||
while True:
|
|
||||||
b = read_chunk(fd, timeout=timeout)
|
|
||||||
if not b:
|
|
||||||
return total
|
|
||||||
total += len(b)
|
|
||||||
|
|
||||||
|
|
||||||
def send(fd, s):
|
|
||||||
if isinstance(s, str):
|
|
||||||
s = s.encode()
|
|
||||||
os.write(fd, s)
|
|
||||||
|
|
||||||
|
|
||||||
def wait_for(fd, regex, timeout, on_more=None, on_pwd=None):
|
|
||||||
"""Read until `regex` matches the tail of the buffer, or timeout."""
|
|
||||||
buf = b''
|
|
||||||
deadline = time.time() + timeout
|
|
||||||
while time.time() < deadline:
|
|
||||||
remaining = deadline - time.time()
|
|
||||||
chunk = read_chunk(fd, timeout=min(1.0, remaining))
|
|
||||||
if chunk:
|
|
||||||
buf += chunk
|
|
||||||
tail64 = buf[-64:]
|
|
||||||
tail32 = buf[-32:]
|
|
||||||
tail128 = buf[-128:]
|
|
||||||
if on_more and MORE_RE.search(tail64):
|
|
||||||
on_more(fd)
|
|
||||||
continue
|
|
||||||
if on_pwd and PWD_RE.search(tail32):
|
|
||||||
on_pwd(buf)
|
|
||||||
return buf, 'pwd'
|
|
||||||
if regex.search(tail128):
|
|
||||||
return buf, 'ok'
|
|
||||||
return buf, 'timeout'
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
ap = argparse.ArgumentParser()
|
|
||||||
ap.add_argument('--device', required=True)
|
|
||||||
ap.add_argument('--baud', type=int, default=9600)
|
|
||||||
ap.add_argument('--cmds', required=True)
|
|
||||||
ap.add_argument('--log', required=True)
|
|
||||||
ap.add_argument('--per-cmd-timeout', type=float, default=45.0)
|
|
||||||
ap.add_argument('--wake-timeout', type=float, default=15.0)
|
|
||||||
ap.add_argument('--session-max', type=float, default=600.0)
|
|
||||||
args = ap.parse_args()
|
|
||||||
|
|
||||||
cmds = []
|
|
||||||
with open(args.cmds) as f:
|
|
||||||
for raw in f:
|
|
||||||
s = raw.rstrip('\n').strip()
|
|
||||||
if not s or s.startswith('!'):
|
|
||||||
continue
|
|
||||||
cmds.append(s)
|
|
||||||
log(f'parsed {len(cmds)} commands from {args.cmds}')
|
|
||||||
|
|
||||||
logf = open(args.log, 'wb', buffering=0)
|
|
||||||
|
|
||||||
def w(b):
|
|
||||||
if isinstance(b, str):
|
|
||||||
b = b.encode()
|
|
||||||
logf.write(b)
|
|
||||||
|
|
||||||
w(f'==== sw-capture {time.strftime("%Y-%m-%d %H:%M:%S")} ====\n')
|
|
||||||
w(f'device={args.device} baud={args.baud} cmds={args.cmds} '
|
|
||||||
f'n={len(cmds)} per_cmd_timeout={args.per_cmd_timeout}\n\n')
|
|
||||||
|
|
||||||
fd = open_port(args.device, args.baud)
|
|
||||||
log(f'opened {args.device} @ {args.baud} 8N1 raw')
|
|
||||||
|
|
||||||
session_start = time.time()
|
|
||||||
abort = False
|
|
||||||
|
|
||||||
def on_more(fd_):
|
|
||||||
log('--More-- -> space')
|
|
||||||
send(fd_, b' ')
|
|
||||||
|
|
||||||
def on_pwd(buf):
|
|
||||||
nonlocal abort
|
|
||||||
abort = True
|
|
||||||
log('password prompt detected (enable or login) - aborting; '
|
|
||||||
'no credentials supplied', 'ERROR')
|
|
||||||
w(buf)
|
|
||||||
w(b'\n[PASSWORD PROMPT - ABORTED]\n')
|
|
||||||
|
|
||||||
# WAKE: nudge with Ctrl-C + Enter, look for any prompt
|
|
||||||
drain(fd, 0.5)
|
|
||||||
synced = False
|
|
||||||
wake_deadline = time.time() + args.wake_timeout
|
|
||||||
attempt = 0
|
|
||||||
while time.time() < wake_deadline:
|
|
||||||
attempt += 1
|
|
||||||
send(fd, b'\x03')
|
|
||||||
time.sleep(0.2)
|
|
||||||
send(fd, b'\r')
|
|
||||||
buf, status = wait_for(fd, PROMPT_RE, timeout=3.0,
|
|
||||||
on_more=on_more, on_pwd=on_pwd)
|
|
||||||
w(buf)
|
|
||||||
if status == 'pwd':
|
|
||||||
logf.close(); os.close(fd); sys.exit(4)
|
|
||||||
if status == 'ok':
|
|
||||||
synced = True
|
|
||||||
log(f'prompt synced after {attempt} attempt(s)')
|
|
||||||
break
|
|
||||||
if not synced:
|
|
||||||
w(b'\n[NO PROMPT - ABORT]\n')
|
|
||||||
log('no prompt detected during wake window', 'ERROR')
|
|
||||||
logf.close(); os.close(fd); sys.exit(2)
|
|
||||||
|
|
||||||
# RUN commands verbatim from the .cmds list
|
|
||||||
failures = 0
|
|
||||||
for idx, cmd in enumerate(cmds, 1):
|
|
||||||
if time.time() - session_start > args.session_max:
|
|
||||||
log('session_max exceeded - stopping early', 'ERROR')
|
|
||||||
w(b'\n[SESSION_MAX - STOP]\n')
|
|
||||||
break
|
|
||||||
if abort:
|
|
||||||
break
|
|
||||||
log(f'[{idx}/{len(cmds)}] {cmd}')
|
|
||||||
send(fd, cmd + '\r')
|
|
||||||
buf, status = wait_for(fd, PROMPT_RE,
|
|
||||||
timeout=args.per_cmd_timeout,
|
|
||||||
on_more=on_more, on_pwd=on_pwd)
|
|
||||||
w(buf)
|
|
||||||
if status == 'pwd':
|
|
||||||
failures += 1
|
|
||||||
break
|
|
||||||
if status == 'timeout':
|
|
||||||
log(f'timeout after: {cmd}', 'WARN')
|
|
||||||
failures += 1
|
|
||||||
# try to resync: Ctrl-C + drain
|
|
||||||
send(fd, b'\x03')
|
|
||||||
time.sleep(0.3)
|
|
||||||
drain(fd, 0.5)
|
|
||||||
|
|
||||||
w(f'\n==== end {time.strftime("%Y-%m-%d %H:%M:%S")} '
|
|
||||||
f'failures={failures} ====\n')
|
|
||||||
logf.close()
|
|
||||||
os.close(fd)
|
|
||||||
log(f'done -> {args.log} failures={failures}')
|
|
||||||
sys.exit(0 if failures == 0 else 3)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# Probe conman state + expect availability on pfv-tsys4.
|
|
||||||
# Read-only. Decides whether we drive via conman+expect or expect-only.
|
|
||||||
set -u
|
|
||||||
|
|
||||||
# De-conflict: any ssh to pfv-tsys4 right now?
|
|
||||||
echo "===== LOCAL ssh activity ====="
|
|
||||||
# shellcheck disable=SC2009 # intentional: need full ps columns filtered by process args
|
|
||||||
ps -eo pid,etime,args | grep -E 'ssh.*pfv-tsys' | grep -v grep || echo "(none to pfv-tsys4)"
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "===== pfv-tsys4: conman + expect state ====="
|
|
||||||
ssh -o BatchMode=yes -o ConnectTimeout=5 root@pfv-tsys4 'bash -s' <<'REMOTE'
|
|
||||||
echo "--- conmand service ---"
|
|
||||||
systemctl is-active conmand 2>&1 || true
|
|
||||||
systemctl is-enabled conmand 2>&1 || true
|
|
||||||
systemctl status conmand --no-pager 2>&1 | head -15 || true
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "--- conman binary ---"
|
|
||||||
command -v conman && conman --version 2>&1 | head -2 || echo "conman: MISSING"
|
|
||||||
command -v conmand && echo "conmand present" || echo "conmand: MISSING"
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "--- /etc/conman.conf: ttyUSB2 entries ---"
|
|
||||||
grep -nE "ttyUSB2|core-sw|CONSOLE|LOG|SERIAL|BAUD" /etc/conman.conf 2>/dev/null | head -40 || echo "(no matches / no file)"
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "--- conman log dir ---"
|
|
||||||
ls -la /var/log/conman/ 2>&1 | head -20 || echo "(no /var/log/conman)"
|
|
||||||
ls -la /var/consoles/ 2>&1 | head -20 || echo "(no /var/consoles)"
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "--- expect availability ---"
|
|
||||||
command -v expect && expect -v 2>&1 || echo "expect: NOT installed"
|
|
||||||
echo "apt-cache policy expect:"
|
|
||||||
apt-cache policy expect 2>/dev/null | head -10 || echo "(apt-cache failed)"
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "--- other useful drivers ---"
|
|
||||||
for t in tclsh socat cu tip; do
|
|
||||||
command -v "$t" 2>/dev/null && echo " $t: present" || true
|
|
||||||
done
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "--- apt network reachability (quick) ---"
|
|
||||||
timeout 5 bash -c 'echo > /dev/tcp/deb.debian.org/80' 2>&1 && echo "apt network: OK" || echo "apt network: UNREACHABLE"
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "--- disk space for log ---"
|
|
||||||
df -h /root 2>&1 | tail -2
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "--- screen sessions (still 3?) ---"
|
|
||||||
screen -ls 2>&1 || true
|
|
||||||
REMOTE
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# Probe pfv-tsys4 for what's available to drive the serial console.
|
|
||||||
# Also snapshots local ssh/scp activity so we can de-conflict with the
|
|
||||||
# other agent running in this directory.
|
|
||||||
set -u
|
|
||||||
|
|
||||||
echo "===== LOCAL ssh/scp activity (other-agent de-confliction) ====="
|
|
||||||
# shellcheck disable=SC2009 # intentional: need full ps columns (etime,args) filtered by process args
|
|
||||||
ps -eo pid,ppid,etime,user,args | grep -E 'ssh|scp' | grep -v grep || echo "(none)"
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "===== Ping pfv-tsys4 ====="
|
|
||||||
ping -c1 -W2 pfv-tsys4 >/dev/null 2>&1 && echo "ping OK" || echo "ping FAIL"
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "===== Probe pfv-tsys4 over ssh ====="
|
|
||||||
ssh -o BatchMode=yes -o ConnectTimeout=5 root@pfv-tsys4 'bash -s' <<'REMOTE'
|
|
||||||
echo "--- host ---"
|
|
||||||
hostname; uname -a
|
|
||||||
echo "--- tools ---"
|
|
||||||
for t in python3 python expect screen minicom picocom stty fuser lsof; do
|
|
||||||
p=$(command -v "$t" 2>/dev/null) && echo "$t -> $p" || echo "$t -> MISSING"
|
|
||||||
done
|
|
||||||
echo "--- pyserial ---"
|
|
||||||
python3 -c "import serial; print('pyserial', serial.__version__)" 2>&1
|
|
||||||
echo "--- device node ---"
|
|
||||||
ls -l /dev/ttyUSB2 2>&1
|
|
||||||
stat -c '%n owner=%U:%G mode=%a' /dev/ttyUSB2 2>&1 || true
|
|
||||||
echo "--- who holds /dev/ttyUSB2 ---"
|
|
||||||
fuser -v /dev/ttyUSB2 2>&1 || echo "(fuser: none or n/a)"
|
|
||||||
lsof /dev/ttyUSB2 2>&1 | head -20 || true
|
|
||||||
echo "--- screen sessions on this host ---"
|
|
||||||
screen -ls 2>&1 || echo "(no screen / not installed)"
|
|
||||||
echo "--- current tty settings (only readable if not held exclusively) ---"
|
|
||||||
stty -F /dev/ttyUSB2 2>&1 || echo "(held exclusively - expected if screen is up)"
|
|
||||||
echo "--- baud hints in config/history ---"
|
|
||||||
grep -riE "ttyUSB2|115200|9600|baud" /etc/ ~/.screenrc ~/.bash_history 2>/dev/null | head -20 || true
|
|
||||||
echo "--- recent console-related processes ---"
|
|
||||||
ps -eo pid,etime,user,args | grep -E 'screen|minicom|picocom|ttyUSB' | grep -v grep || echo "(none)"
|
|
||||||
REMOTE
|
|
||||||
@@ -1,93 +1,11 @@
|
|||||||
!###############################################################################
|
! pfv-core-sw01 — Dell PowerConnect 5448 (core switch, rack 5)
|
||||||
!# pfv-core-sw01
|
! Read-only show commands for LAG/port-channel/VLAN audit
|
||||||
!#
|
|
||||||
!# Dell PowerConnect 5448 (48-port 1GbE copper, 4× combo SFP / 10G via stacking)
|
|
||||||
!# OS: DNOS 4.x / PowerConnect CLI
|
|
||||||
!#
|
|
||||||
!# This file is a flat list of READ-ONLY show commands.
|
|
||||||
!# Paste them in order from enable mode. Capture the whole session to
|
|
||||||
!# returned-logs/pfv-core-sw01.log
|
|
||||||
!#
|
|
||||||
!# To capture from a terminal:
|
|
||||||
!# 1. SSH/telnet in
|
|
||||||
!# 2. enable
|
|
||||||
!# 3. terminal datadump (disables --More-- pause)
|
|
||||||
!# 4. paste everything below
|
|
||||||
!###############################################################################
|
|
||||||
|
|
||||||
enable
|
|
||||||
terminal datadump
|
terminal datadump
|
||||||
|
|
||||||
! --- Identify & versions ---------------------------------------------------
|
|
||||||
show version
|
show version
|
||||||
show system
|
|
||||||
show inventory
|
|
||||||
show bootvar
|
|
||||||
show running-config
|
|
||||||
show stack
|
|
||||||
show cpu utilization
|
|
||||||
show memory
|
|
||||||
show logging
|
|
||||||
|
|
||||||
! --- Interfaces ------------------------------------------------------------
|
|
||||||
show interfaces status
|
show interfaces status
|
||||||
show interfaces configuration
|
|
||||||
show interfaces description
|
|
||||||
show interfaces counters
|
|
||||||
show interfaces counters errors
|
|
||||||
show interfaces counters detail
|
|
||||||
show interfaces counters cable 1-52
|
|
||||||
|
|
||||||
! --- Port-channels / LAG (4-port LACP to pfv-r3-tor-stor) -----------------
|
|
||||||
show interfaces port-channel
|
show interfaces port-channel
|
||||||
show lacp 1
|
show port-channel summary
|
||||||
show lacp 2
|
show lacp
|
||||||
show lacp 3
|
|
||||||
show lacp 4
|
|
||||||
show lacp 5
|
|
||||||
show lacp 6
|
|
||||||
|
|
||||||
! --- VLANs -----------------------------------------------------------------
|
|
||||||
show vlan
|
show vlan
|
||||||
show vlan detail
|
show running-config interface port-channel 1
|
||||||
show interfaces vlan 1
|
show running-config interface port-channel 3
|
||||||
show interfaces vlan 1000
|
|
||||||
show vlan info
|
|
||||||
|
|
||||||
! --- Spanning tree ---------------------------------------------------------
|
|
||||||
show spanning-tree
|
|
||||||
show spanning-tree active
|
|
||||||
show spanning-tree summary
|
|
||||||
show spanning-tree detail
|
|
||||||
|
|
||||||
! --- IGMP / multicast (matters for storage/iSCSI-NFS sometimes) -----------
|
|
||||||
show ip igmp snooping
|
|
||||||
show ip igmp snooping vlan 1
|
|
||||||
show ip igmp snooping vlan 1000
|
|
||||||
|
|
||||||
! --- QoS / CoS -------------------------------------------------------------
|
|
||||||
show qos
|
|
||||||
show qos interface
|
|
||||||
show class-of-service
|
|
||||||
|
|
||||||
! --- Storm control / loop protection --------------------------------------
|
|
||||||
show storm-control
|
|
||||||
show ports jumbo-frame
|
|
||||||
|
|
||||||
! --- ARP / MAC / FDB -------------------------------------------------------
|
|
||||||
show mac address-table
|
|
||||||
show arp
|
|
||||||
show ip route
|
|
||||||
|
|
||||||
! --- Environmental ---------------------------------------------------------
|
|
||||||
show environment
|
|
||||||
show fan
|
|
||||||
show power
|
|
||||||
show hardware
|
|
||||||
|
|
||||||
! --- Flow control ----------------------------------------------------------
|
|
||||||
show flowcontrol
|
|
||||||
show interfaces flowcontrol
|
|
||||||
|
|
||||||
! --- Save the capture, then exit ------------------------------------------
|
|
||||||
exit
|
|
||||||
|
|||||||
@@ -1,90 +0,0 @@
|
|||||||
!###############################################################################
|
|
||||||
!# pfv-r3-tor-mgmt
|
|
||||||
!#
|
|
||||||
!# Rack 3 management TOR (VLAN1 only for this switch).
|
|
||||||
!# Dell switch - OS variant unknown. Commands below cover the common
|
|
||||||
!# Dell CLI flavors (DNOS 4/6/9, OS6/OS9, N-Series). Some may not be
|
|
||||||
!# recognized on every OS; that is fine - skip errors and keep going.
|
|
||||||
!#
|
|
||||||
!# Paste from enable mode after `terminal datadump` (or `terminal length 0`
|
|
||||||
!# on DNOS 6/9). Capture session to returned-logs/pfv-r3-tor-mgmt.log
|
|
||||||
!###############################################################################
|
|
||||||
|
|
||||||
enable
|
|
||||||
terminal datadump
|
|
||||||
terminal length 0
|
|
||||||
|
|
||||||
! --- Identify ---------------------------------------------------------------
|
|
||||||
show version
|
|
||||||
show system
|
|
||||||
show inventory
|
|
||||||
show running-config
|
|
||||||
show bootvar
|
|
||||||
show cpu
|
|
||||||
show cpu utilization
|
|
||||||
show memory
|
|
||||||
show processes
|
|
||||||
show logging
|
|
||||||
|
|
||||||
! --- Interfaces -------------------------------------------------------------
|
|
||||||
show interface status
|
|
||||||
show interface description
|
|
||||||
show interface configuration
|
|
||||||
show interface counters
|
|
||||||
show interface counters errors
|
|
||||||
show interface counters detail
|
|
||||||
show interfaces status
|
|
||||||
show interfaces description
|
|
||||||
show interfaces counters
|
|
||||||
show interfaces counters errors
|
|
||||||
|
|
||||||
! --- Port channels / LAG ----------------------------------------------------
|
|
||||||
show port-channel summary
|
|
||||||
show port-channel detail
|
|
||||||
show interfaces port-channel
|
|
||||||
show lacp
|
|
||||||
show lacp neighbor
|
|
||||||
show lag
|
|
||||||
|
|
||||||
! --- VLANs ------------------------------------------------------------------
|
|
||||||
show vlan
|
|
||||||
show vlan brief
|
|
||||||
show vlan detail
|
|
||||||
|
|
||||||
! --- Spanning tree ----------------------------------------------------------
|
|
||||||
show spanning-tree
|
|
||||||
show spanning-tree summary
|
|
||||||
show spanning-tree active
|
|
||||||
|
|
||||||
! --- Multicast / snooping ---------------------------------------------------
|
|
||||||
show ip igmp snooping
|
|
||||||
show ip igmp snooping vlan 1
|
|
||||||
|
|
||||||
! --- QoS --------------------------------------------------------------------
|
|
||||||
show qos
|
|
||||||
show qos interface
|
|
||||||
|
|
||||||
! --- Jumbo / storm control --------------------------------------------------
|
|
||||||
show ports jumbo-frame
|
|
||||||
show storm-control
|
|
||||||
|
|
||||||
! --- FDB / ARP / routes -----------------------------------------------------
|
|
||||||
show mac address-table
|
|
||||||
show mac-address-table
|
|
||||||
show arp
|
|
||||||
show ip route
|
|
||||||
|
|
||||||
! --- Environment / hardware -------------------------------------------------
|
|
||||||
show environment
|
|
||||||
show fan
|
|
||||||
show power supplies
|
|
||||||
show power
|
|
||||||
show hardware
|
|
||||||
show transceiver
|
|
||||||
show inventory
|
|
||||||
|
|
||||||
! --- Flow control -----------------------------------------------------------
|
|
||||||
show flowcontrol
|
|
||||||
show interfaces flowcontrol
|
|
||||||
|
|
||||||
exit
|
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
!###############################################################################
|
|
||||||
!# pfv-r3-tor-stor — TARGETED LACP hash verification
|
|
||||||
!#
|
|
||||||
!# Purpose: confirm whether `port-channel load-balance layer-2-3-4` actually
|
|
||||||
!# took effect, and gather the LAG state needed to decide whether the LAG
|
|
||||||
!# must be bounced.
|
|
||||||
!#
|
|
||||||
!# Context: tsys6<->tsys7 is capped at exactly single-link speed (~943 Mbps)
|
|
||||||
!# with 90K-136K retransmits on multi-stream TCP, but a clean 928 Mbps / 0
|
|
||||||
!# retransmits on single-stream. That = offered load above 1 Gbps is being
|
|
||||||
!# dropped at a single egress port. We need to know if the switch hash is
|
|
||||||
!# really layer-2-3-4, and whether tsys6 + tsys7 ports are correctly
|
|
||||||
!# bundled into LACP groups.
|
|
||||||
!#
|
|
||||||
!# Paste from enable mode. Capture to:
|
|
||||||
!# returned-logs/pfv-r3-tor-stor-lacp-verify.log
|
|
||||||
!###############################################################################
|
|
||||||
|
|
||||||
enable
|
|
||||||
terminal datadump
|
|
||||||
terminal length 0
|
|
||||||
|
|
||||||
! --- 1. THE KEY COMMAND: what hash is actually active? -----------------------
|
|
||||||
! Expected: layer-2-3-4. If it still shows source-mac / dst-mac /
|
|
||||||
! src-dst-mac (layer 2 only), the change did NOT take effect.
|
|
||||||
show port-channel load-balance
|
|
||||||
show port-channel protocol
|
|
||||||
|
|
||||||
! --- 2. Which LAGs exist and which physical ports are members ----------------
|
|
||||||
! Look for the port-channels carrying tsys6 and tsys7. Confirm each has
|
|
||||||
! TWO member ports both in "bundled" / "Forwarding" state.
|
|
||||||
show port-channel summary
|
|
||||||
show port-channel detail
|
|
||||||
|
|
||||||
! --- 3. LACP actor/partner state per member port ----------------------------
|
|
||||||
! Every member port should show: Actor State Flag = AcLg (Active, Aggr),
|
|
||||||
! Partner Operational, and a non-zero Partner System ID.
|
|
||||||
show lacp
|
|
||||||
show lacp neighbor
|
|
||||||
|
|
||||||
! --- 4. Per-port counters during/after an iperf run -------------------------
|
|
||||||
! THIS is the switch-side equivalent of the host RX-counter test.
|
|
||||||
! Run an 8-stream iperf tsys7->tsys6 first, then immediately capture
|
|
||||||
! these so the byte counters reflect the test. The TWO tsys6-facing
|
|
||||||
! ports should show SIMILAR "RX bytes" (switch ingress from tsys6's
|
|
||||||
! bond is not the test direction; look at TX-out bytes toward tsys6,
|
|
||||||
! i.e. the ports' output counters). Replace Te1/0/x with the real port
|
|
||||||
! IDs shown by "show port-channel summary" above:
|
|
||||||
show interfaces counters
|
|
||||||
show interfaces counters errors
|
|
||||||
|
|
||||||
! --- 5. Running config line that set the hash (proof it was committed) ------
|
|
||||||
show running-config | include port-channel
|
|
||||||
|
|
||||||
! --- 6. Switch identity (to look up correct hash syntax for this model) -----
|
|
||||||
show version
|
|
||||||
show system
|
|
||||||
|
|
||||||
exit
|
|
||||||
|
|
||||||
!###############################################################################
|
|
||||||
!# DECISION TREE after reading the output:
|
|
||||||
!#
|
|
||||||
!# A) show port-channel load-balance reports layer-2-3-4 AND per-port
|
|
||||||
!# counters show traffic split across BOTH tsys6-facing ports:
|
|
||||||
!# -> Hash is fine. Re-test iperf; if still ~943 Mbps, the cap is
|
|
||||||
!# host-side (receiver RX bridge / IRQ / CPU).
|
|
||||||
!#
|
|
||||||
!# B) load-balance reports layer-2-3-4 BUT per-port counters show all traffic
|
|
||||||
!# on ONE tsys6-facing port:
|
|
||||||
!# -> Hash is configured but the LAG needs a bounce to pick it up.
|
|
||||||
!# Bounce the relevant port-channel:
|
|
||||||
!# config
|
|
||||||
!# interface port-channel N
|
|
||||||
!# shutdown
|
|
||||||
!# no shutdown
|
|
||||||
!# end
|
|
||||||
!# WARNING: drops all traffic on that LAG for ~2-5 s. Coordinate.
|
|
||||||
!#
|
|
||||||
!# C) load-balance still shows a layer-2-only policy:
|
|
||||||
!# -> The earlier change didn't commit to this context. Re-apply:
|
|
||||||
!# config
|
|
||||||
!# port-channel load-balance layer-2-3-4
|
|
||||||
!# end
|
|
||||||
!# show port-channel load-balance
|
|
||||||
!# copy running-config startup-config
|
|
||||||
!# Then handle as case (B) if counters still aren't split.
|
|
||||||
!###############################################################################
|
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
!###############################################################################
|
|
||||||
!# pfv-r3-tor-stor
|
|
||||||
!#
|
|
||||||
!# Rack 3 storage TOR (VLAN1000 storage + VLAN1).
|
|
||||||
!# Carries the 4-port LACP to pfv-core-sw01 (Rack 5).
|
|
||||||
!# Most important switch for cross-rack storage throughput.
|
|
||||||
!#
|
|
||||||
!# Paste from enable mode after `terminal datadump` (DNOS 4/6) or
|
|
||||||
!# `terminal length 0` (DNOS 9 / OS9). Capture to returned-logs/pfv-r3-tor-stor.log
|
|
||||||
!###############################################################################
|
|
||||||
|
|
||||||
enable
|
|
||||||
terminal datadump
|
|
||||||
terminal length 0
|
|
||||||
|
|
||||||
! --- Identify ---------------------------------------------------------------
|
|
||||||
show version
|
|
||||||
show system
|
|
||||||
show inventory
|
|
||||||
show running-config
|
|
||||||
show bootvar
|
|
||||||
show cpu utilization
|
|
||||||
show cpu
|
|
||||||
show memory
|
|
||||||
show processes
|
|
||||||
show logging
|
|
||||||
|
|
||||||
! --- Interfaces (most important section) ------------------------------------
|
|
||||||
show interface status
|
|
||||||
show interface description
|
|
||||||
show interface counters
|
|
||||||
show interface counters errors
|
|
||||||
show interface counters detail
|
|
||||||
show interfaces status
|
|
||||||
show interfaces description
|
|
||||||
show interfaces counters
|
|
||||||
show interfaces counters errors
|
|
||||||
show interfaces configuration
|
|
||||||
|
|
||||||
! --- LACP / port-channel (the 4-port LAG to Rack 5) ------------------------
|
|
||||||
show port-channel summary
|
|
||||||
show port-channel detail
|
|
||||||
show port-channel 1
|
|
||||||
show port-channel 2
|
|
||||||
show port-channel 3
|
|
||||||
show port-channel 4
|
|
||||||
show interfaces port-channel
|
|
||||||
show lacp
|
|
||||||
show lacp neighbor
|
|
||||||
show lacp 1
|
|
||||||
show lag
|
|
||||||
|
|
||||||
! --- VLANs (VLAN1000 is the storage VLAN) ----------------------------------
|
|
||||||
show vlan
|
|
||||||
show vlan brief
|
|
||||||
show vlan id 1000
|
|
||||||
show vlan detail
|
|
||||||
show interfaces vlan 1000
|
|
||||||
show interfaces switchport
|
|
||||||
|
|
||||||
! --- Spanning tree ----------------------------------------------------------
|
|
||||||
show spanning-tree
|
|
||||||
show spanning-tree summary
|
|
||||||
show spanning-tree active
|
|
||||||
show spanning-tree detail
|
|
||||||
show spanning-tree mst
|
|
||||||
|
|
||||||
! --- Multicast / IGMP (storage traffic) -------------------------------------
|
|
||||||
show ip igmp snooping
|
|
||||||
show ip igmp snooping vlan 1000
|
|
||||||
show ip igmp snooping vlan 1
|
|
||||||
|
|
||||||
! --- QoS / CoS / DCB / PFC (matters for storage) ---------------------------
|
|
||||||
show qos
|
|
||||||
show qos interface
|
|
||||||
show class-of-service
|
|
||||||
show dcb
|
|
||||||
show dcb priority
|
|
||||||
show priority-flow-control
|
|
||||||
show pfc
|
|
||||||
|
|
||||||
! --- Jumbo / storm / MTU ----------------------------------------------------
|
|
||||||
show ports jumbo-frame
|
|
||||||
show storm-control
|
|
||||||
show interfaces mtu
|
|
||||||
|
|
||||||
! --- FDB / ARP / routing ----------------------------------------------------
|
|
||||||
show mac address-table
|
|
||||||
show mac-address-table
|
|
||||||
show arp
|
|
||||||
show ip route
|
|
||||||
|
|
||||||
! --- Environment / hardware / optics ---------------------------------------
|
|
||||||
show environment
|
|
||||||
show fan
|
|
||||||
show power supplies
|
|
||||||
show power
|
|
||||||
show hardware
|
|
||||||
show inventory
|
|
||||||
show transceiver
|
|
||||||
show interface transceiver
|
|
||||||
show media
|
|
||||||
|
|
||||||
! --- Flow control (important for storage) ----------------------------------
|
|
||||||
show flowcontrol
|
|
||||||
show interfaces flowcontrol
|
|
||||||
|
|
||||||
exit
|
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
! pfv-tor3-stor — Neyland 24T (Radlan-based, rack 3 storage TOR)
|
||||||
|
! Radlan CLI uses different keywords than DNOS
|
||||||
|
enable
|
||||||
|
show system
|
||||||
|
show inventory
|
||||||
|
show interfaces configuration
|
||||||
|
show interfaces description
|
||||||
|
show port-channel
|
||||||
|
show lag
|
||||||
|
show vlan database
|
||||||
|
show vlan
|
||||||
|
show running-config
|
||||||
Reference in New Issue
Block a user