prep for next ai session
This commit is contained in:
@@ -1,130 +0,0 @@
|
||||
# Console Management (ser2net + conman)
|
||||
|
||||
Network-accessible serial console management for all production network
|
||||
switches and routers, running on **pfv-tsys4** (storage server).
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
USB-DB9 adapters → udev symlinks (/dev/consoles/<name>) → ser2net telnet(rfc2217) TCP → conman (logging + multiplexing)
|
||||
```
|
||||
|
||||
ser2net owns the physical serial devices and exposes them on TCP ports
|
||||
using the **telnet(rfc2217) protocol** bound to the **Tailscale interface
|
||||
only** (`100.70.77.93:200X`). conman connects to those TCP ports via
|
||||
telnet for session logging, output capture, and multi-user console
|
||||
sharing.
|
||||
|
||||
**Why telnet(rfc2217)?** The serial devices send `
|
||||
␍` (LF+CR) line
|
||||
endings instead of standard `
|
||||
`. Raw TCP transport caused conman's
|
||||
telnet NVT to strip bare CR characters, producing stair-stepped output.
|
||||
With telnet(rfc2217) on both sides, binary mode is negotiated and CR/LF
|
||||
translation is handled correctly by the telnet layer.
|
||||
|
||||
**conman and ser2net do NOT share ports** — only one process can open a
|
||||
serial device at a time. ser2net owns the physical device; conman connects
|
||||
over TCP.
|
||||
|
||||
## The USB Enumeration Problem (SOLVED)
|
||||
|
||||
The 9 Prolific USB-to-DB9 adapters (`067b:2303`) on pfv-tsys4 have **no
|
||||
unique USB serial numbers** and get assigned `/dev/ttyUSB0-8` based on
|
||||
enumeration order, which shifts on every boot. This made the old
|
||||
`/root/conmap` + manual `screen` workflow break after every reboot.
|
||||
|
||||
**Fix:** udev rules pin each adapter by its **ID_PATH** (physical USB port
|
||||
topology), which is stable across reboots regardless of enumeration order.
|
||||
Each adapter gets a named symlink in `/dev/consoles/` that never changes.
|
||||
|
||||
The udev rules are generated from `mapping.txt`, which maps each adapter's
|
||||
ID_PATH to a console name and TCP port. To re-map after physically moving
|
||||
an adapter, update `mapping.txt` and re-run `setup.sh`.
|
||||
|
||||
**Fallback:** if udev trigger doesn't create symlinks for already-discovered
|
||||
devices (common on first run), `setup.sh` creates them manually by matching
|
||||
ID_PATH. On subsequent boots, udev creates them automatically.
|
||||
|
||||
## Port Assignments
|
||||
|
||||
| TCP Port | Console Name | ID_PATH | Description |
|
||||
|----------|-------------|---------|-------------|
|
||||
| 2001 | pfv-core-sw01 | usb-0:1.5.4.4 | Dell PowerConnect 5448 (core switch) |
|
||||
| 2002 | pfv-tor3-mgmt | usb-0:1.6.3.1 | Rack 3 management TOR switch |
|
||||
| 2003 | pfv-tor3-stor | usb-0:1.6.3.3.2 | Rack 3 storage TOR switch |
|
||||
| 2004 | pfv-rrinfra-rtr | usb-0:1.6.3.3.1 | Cisco router (rrinfra) |
|
||||
| 2005 | pfv-r2-tor-top | usb-0:1.6.3.3.3 | Rack 2 top-of-rack switch |
|
||||
| 2006 | subodev-torsw | usb-0:1.5.4.1 | Suborbital device TOR switch |
|
||||
| 2007 | pfv-r2-sw | usb-0:1.6.3.2 | Rack 2 old Dell switch |
|
||||
|
||||
All ports listen on the Tailscale IP (`100.70.77.93`) using telnet(rfc2217).
|
||||
|
||||
## Scripts
|
||||
|
||||
| Script | Purpose |
|
||||
|--------|---------|
|
||||
| [`mapping.txt`](mapping.txt) | Source of truth: TCP port ↔ ID_PATH ↔ name ↔ baud |
|
||||
| [`generate-config.sh`](generate-config.sh) | Generates udev rules, ser2net.yaml, conman.conf from mapping.txt |
|
||||
| [`setup.sh`](setup.sh) | Full deploy: generate configs, create symlinks, restart services |
|
||||
| [`discover.sh`](discover.sh) | Read-only discovery of USB adapters, existing config, services |
|
||||
|
||||
## Usage
|
||||
|
||||
### Connect to a console
|
||||
|
||||
**Primary method — conman client (with logging + multiplexing):**
|
||||
|
||||
```bash
|
||||
# From any Tailscale-connected workstation:
|
||||
conman -d pfv-tsys4:7890 -f pfv-core-sw01 # connect to console
|
||||
conman -d pfv-tsys4:7890 -q # list all consoles
|
||||
```
|
||||
|
||||
Escape sequence: `&.` to disconnect, `&?` for help.
|
||||
|
||||
**Direct telnet (emergency only — conflicts with conman):**
|
||||
|
||||
```bash
|
||||
# Direct telnet to ser2net works ONLY when conmand is stopped, because
|
||||
# conmand maintains persistent connections to all 7 TCP ports. Use:
|
||||
ssh pfv-tsys4 'systemctl stop conmand'
|
||||
telnet pfv-tsys4 2001 # pfv-core-sw01
|
||||
ssh pfv-tsys4 'systemctl start conmand' # restart when done
|
||||
```
|
||||
|
||||
**Do NOT use telnet while conmand is running** — conmand will reconnect
|
||||
and kick your telnet session immediately ("Connection closed by foreign host").
|
||||
The correct workflow is conman client → conmand → ser2net → device.
|
||||
|
||||
### Re-deploy after changing mapping.txt
|
||||
|
||||
```bash
|
||||
PROX_HOST=pfv-tsys4 bash tests/remote.sh prox 'bash /root/console/setup.sh'
|
||||
```
|
||||
|
||||
### Find the ID_PATH for a new adapter
|
||||
|
||||
```bash
|
||||
PROX_HOST=pfv-tsys4 bash tests/remote.sh prox-file console/discover.sh
|
||||
```
|
||||
|
||||
Then match the new adapter's ID_PATH to its physical location and add a line
|
||||
to `mapping.txt`.
|
||||
|
||||
## Files on pfv-tsys4
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `/etc/udev/rules.d/99-console-ports.rules` | Stable symlinks by ID_PATH |
|
||||
| `/etc/ser2net.yaml` | ser2net config (telnet rfc2217 TCP ports → serial symlinks) |
|
||||
| `/etc/conman.conf` | conman config (CONSOLE entries between markers) |
|
||||
| `/etc/systemd/system/conmand.service` | systemd unit for conmand |
|
||||
| `/root/console/mapping.txt` | Copy of the source-of-truth mapping |
|
||||
| `/root/console/setup.sh` | Setup script (re-runnable) |
|
||||
| `/root/console/generate-config.sh` | Config generator |
|
||||
|
||||
## Old workflow (replaced)
|
||||
|
||||
The old `/root/conmap` file and manual `screen` sessions are no longer
|
||||
needed. The new setup is fully automated and survives reboots.
|
||||
@@ -1,103 +0,0 @@
|
||||
#!/usr/bin/bash
|
||||
# shellcheck disable=SC2010,SC2012 # diagnostic script; ls|grep/ls -la on sysfs & log dirs is intentional for human-readable output
|
||||
#
|
||||
# console/discover.sh — READ-ONLY discovery of console setup on pfv-tsys4
|
||||
#
|
||||
# Usage: PROX_HOST=pfv-tsys4 bash tests/remote.sh prox-file console/discover.sh
|
||||
#
|
||||
# This script is strictly read-only. No writes to the system.
|
||||
#
|
||||
set -uo pipefail
|
||||
|
||||
echo "============================================"
|
||||
echo " Console Setup Discovery"
|
||||
echo " Host: $(hostname)"
|
||||
echo " Date: $(date)"
|
||||
echo " READ-ONLY"
|
||||
echo "============================================"
|
||||
|
||||
echo ""
|
||||
echo "=== 1. USB devices ==="
|
||||
lsusb 2>/dev/null || echo "(lsusb not available)"
|
||||
|
||||
echo ""
|
||||
echo "=== 2. All ttyUSB* devices (with major/minor) ==="
|
||||
ls -la /dev/ttyUSB* 2>/dev/null || echo "(no /dev/ttyUSB* devices)"
|
||||
|
||||
echo ""
|
||||
echo "=== 3. USB-serial driver bindings ==="
|
||||
echo "-- pl2303 --"
|
||||
ls -la /sys/bus/usb-serial/drivers/pl2303/ 2>/dev/null | grep -v '^total\|^d\|module\|new_id\|uevent' || echo "(none)"
|
||||
echo "-- cp210x --"
|
||||
ls -la /sys/bus/usb-serial/drivers/cp210x/ 2>/dev/null | grep -v '^total\|^d\|module\|new_id\|uevent' || echo "(none)"
|
||||
echo "-- ftdi_sio --"
|
||||
ls -la /sys/bus/usb-serial/drivers/ftdi_sio/ 2>/dev/null | grep -v '^total\|^d\|module\|new_id\|uevent' || echo "(none)"
|
||||
echo "-- ch341 --"
|
||||
ls -la /sys/bus/usb-serial/drivers/ch341/ 2>/dev/null | grep -v '^total\|^d\|module\|new_id\|uevent' || echo "(none)"
|
||||
|
||||
echo ""
|
||||
echo "=== 4. USB serial adapter details (vendor/model/serial per port) ==="
|
||||
for tty in /dev/ttyUSB*; do
|
||||
[ -e "$tty" ] || continue
|
||||
echo "--- $tty ---"
|
||||
udevadm info -q all -n "$tty" 2>/dev/null | grep -E 'ID_VENDOR_ID|ID_MODEL_ID|ID_SERIAL|ID_USB_DRIVER|ID_PATH=' | sed 's/^/ /'
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "=== 5. Existing /root/conmap ==="
|
||||
if [ -f /root/conmap ]; then
|
||||
cat /root/conmap
|
||||
else
|
||||
echo "(no /root/conmap)"
|
||||
fi
|
||||
ls -la /root/conmap* 2>/dev/null
|
||||
|
||||
echo ""
|
||||
echo "=== 6. Screen sessions (running) ==="
|
||||
screen -ls 2>&1 || echo "(screen not running or not installed)"
|
||||
|
||||
echo ""
|
||||
echo "=== 7. Existing screen wrappers/scripts in /root ==="
|
||||
ls -la /root/ 2>/dev/null | grep -iE 'screen|con|console|tty|usb' || echo "(no obvious console scripts in /root)"
|
||||
|
||||
echo ""
|
||||
echo "=== 8. ser2net ==="
|
||||
which ser2net 2>/dev/null || echo "(ser2net not installed)"
|
||||
dpkg -l ser2net 2>/dev/null | tail -2 || echo "(ser2net not in dpkg)"
|
||||
cat /etc/ser2net/ser2net.yaml 2>/dev/null || cat /etc/ser2net.conf 2>/dev/null || cat /etc/ser2net/ser2net.conf 2>/dev/null || echo "(no ser2net config)"
|
||||
systemctl is-active ser2net 2>/dev/null || echo "(ser2net service not found)"
|
||||
|
||||
echo ""
|
||||
echo "=== 9. conman ==="
|
||||
which conman 2>/dev/null || echo "(conman not installed)"
|
||||
which conmand 2>/dev/null || echo "(conmand not installed)"
|
||||
dpkg -l conman 2>/dev/null | tail -2 || echo "(conman not in dpkg)"
|
||||
echo "--- /etc/conman.conf (console lines only) ---"
|
||||
grep -nE 'CONSOLE|SERVER|LOG|SERIAL|DEV|BAUD|^[^#].*name=' /etc/conman.conf 2>/dev/null | head -60 || echo "(no conman.conf or no console entries)"
|
||||
echo "--- conmand service ---"
|
||||
systemctl is-active conmand 2>/dev/null || echo "(conmand not running)"
|
||||
systemctl is-enabled conmand 2>/dev/null || echo "(conmand not enabled)"
|
||||
|
||||
echo ""
|
||||
echo "=== 10. Existing console logs ==="
|
||||
ls -la /var/log/conman/ 2>/dev/null | head -20 || echo "(no /var/log/conman)"
|
||||
ls -la /var/consoles/ 2>/dev/null | head -20 || echo "(no /var/consoles)"
|
||||
|
||||
echo ""
|
||||
echo "=== 11. udev rules for ttyUSB ==="
|
||||
grep -r ttyUSB /etc/udev/rules.d/ 2>/dev/null || echo "(no udev rules for ttyUSB)"
|
||||
grep -r 'console' /etc/udev/rules.d/ 2>/dev/null | head -10 || true
|
||||
|
||||
echo ""
|
||||
echo "=== 12. expect availability ==="
|
||||
command -v expect && expect -v 2>&1 || echo "expect: NOT installed"
|
||||
command -v socat && socat -V 2>&1 | head -1 || echo "socat: NOT installed"
|
||||
|
||||
echo ""
|
||||
echo "=== 13. Ports in use (2001-2099, 7000-7999, 7820-7899) ==="
|
||||
ss -tlnp 2>/dev/null | grep -E ':200[0-9]|:700[0-9]|:782[0-9]|:789[0-9]' || echo "(no relevant ports listening)"
|
||||
|
||||
echo ""
|
||||
echo "============================================"
|
||||
echo " Discovery complete (read-only)."
|
||||
echo "============================================"
|
||||
@@ -1,254 +0,0 @@
|
||||
#!/usr/bin/bash
|
||||
#
|
||||
# console/generate-config.sh — generate udev rules + ser2net.yaml + conman.conf
|
||||
#
|
||||
# Reads console/mapping.txt (the source of truth) and generates all three
|
||||
# config files. This is the fix for the USB enumeration shift problem:
|
||||
#
|
||||
# 1. udev rules pin each adapter by its STABLE ID_PATH (physical USB port)
|
||||
# to a named symlink like /dev/consoles/pfv-core-sw01
|
||||
# 2. ser2net opens those stable symlinks and exposes them on TCP ports
|
||||
# (2001, 2002, ...) bound to the Tailscale IP
|
||||
# 3. conman connects to those TCP ports for logging + multiplexing
|
||||
#
|
||||
# Run this script ON the target host. It writes to:
|
||||
# /etc/udev/rules.d/99-console-ports.rules
|
||||
# /etc/ser2net.yaml
|
||||
# /etc/conman/console-consoles.conf (included by /etc/conman.conf)
|
||||
#
|
||||
# Usage:
|
||||
# PROX_HOST=pfv-tsys4 bash tests/remote.sh prox-file console/generate-config.sh
|
||||
#
|
||||
# Environment overrides:
|
||||
# MAPPING_FILE — path to mapping.txt (default: auto-detect next to this script)
|
||||
# TS_IP — Tailscale IP to bind ser2net on (default: auto-detect)
|
||||
# CONMAN_LOGDIR — conman log directory (default: /var/log/conman)
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
MAPPING_FILE="${MAPPING_FILE:-$SCRIPT_DIR/mapping.txt}"
|
||||
CONMAN_LOGDIR="${CONMAN_LOGDIR:-/var/log/conman}"
|
||||
|
||||
UDEV_RULES="/etc/udev/rules.d/99-console-ports.rules"
|
||||
SER2NET_CONF="/etc/ser2net.yaml"
|
||||
CONMAN_CONF="/etc/conman.conf"
|
||||
|
||||
echo "============================================"
|
||||
echo " Console Config Generator"
|
||||
echo " Host: $(hostname) $(date)"
|
||||
echo "============================================"
|
||||
|
||||
# --- Locate mapping file ---
|
||||
# When run via remote.sh prox-file, $0 is bash and $SCRIPT_DIR may be wrong.
|
||||
# Search common locations.
|
||||
if [ ! -f "$MAPPING_FILE" ]; then
|
||||
for candidate in \
|
||||
"/root/console/mapping.txt" \
|
||||
"/tmp/mapping.txt" \
|
||||
"$(dirname "$0")/mapping.txt"; do
|
||||
if [ -f "$candidate" ]; then
|
||||
MAPPING_FILE="$candidate"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if [ ! -f "$MAPPING_FILE" ]; then
|
||||
echo "FATAL: mapping file not found. Tried: $MAPPING_FILE"
|
||||
echo "Copy mapping.txt to the target host first."
|
||||
exit 1
|
||||
fi
|
||||
echo " Mapping file: $MAPPING_FILE"
|
||||
|
||||
# --- Auto-detect Tailscale IP ---
|
||||
if [ -z "${TS_IP:-}" ]; then
|
||||
TS_IP=$(tailscale ip -4 2>/dev/null || true)
|
||||
if [ -z "$TS_IP" ]; then
|
||||
echo "FATAL: could not auto-detect Tailscale IP. Set TS_IP manually."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
echo " Tailscale IP: $TS_IP"
|
||||
echo " ser2net will bind to: $TS_IP"
|
||||
|
||||
# --- Parse mapping file (skip comments and blank lines) ---
|
||||
echo ""
|
||||
echo "--- Parsing mapping file ---"
|
||||
ENTRIES=()
|
||||
while IFS= read -r line; do
|
||||
# Skip comments and blank lines
|
||||
line="${line%%#*}"
|
||||
line="$(echo "$line" | xargs)" # trim whitespace
|
||||
[ -z "$line" ] && continue
|
||||
ENTRIES+=("$line")
|
||||
echo " $line"
|
||||
done < "$MAPPING_FILE"
|
||||
|
||||
if [ "${#ENTRIES[@]}" -eq 0 ]; then
|
||||
echo "FATAL: no entries found in mapping file."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo " ${#ENTRIES[@]} console ports configured."
|
||||
|
||||
# ============================================================
|
||||
# 1. Generate udev rules
|
||||
# ============================================================
|
||||
echo ""
|
||||
echo "--- [1/3] Generating udev rules: $UDEV_RULES ---"
|
||||
|
||||
cat > "$UDEV_RULES" <<'UDEV_HEADER'
|
||||
# Stable symlinks for USB-DB9 console adapters
|
||||
# Generated by console/generate-config.sh
|
||||
# DO NOT EDIT — edit mapping.txt and re-run generate-config.sh
|
||||
#
|
||||
# These rules pin each adapter to a named symlink based on its physical
|
||||
# USB port path (ID_PATH), which is stable across reboots regardless of
|
||||
# enumeration order. This is the fix for the "USB adapters shift on reboot"
|
||||
# problem.
|
||||
#
|
||||
# To find the ID_PATH for a device:
|
||||
# udevadm info -q all -n /dev/ttyUSBN | grep ID_PATH
|
||||
UDEV_HEADER
|
||||
|
||||
for entry in "${ENTRIES[@]}"; do
|
||||
IFS='|' read -r tcp_port name id_path baud comment <<< "$entry"
|
||||
# Build the full ID_PATH match. The mapping stores a substring like "usb-0:1.5.4.4"
|
||||
# The actual ID_PATH is like "pci-0000:00:1a.0-usb-0:1.5.4.4:1.0"
|
||||
# We match on the substring to be portable across PCI bus changes.
|
||||
{
|
||||
echo ""
|
||||
echo "# $name (TCP $tcp_port): $comment"
|
||||
echo "SUBSYSTEM==\"tty\", ENV{ID_PATH}==\"*$id_path*\", SYMLINK+=\"consoles/$name\""
|
||||
} >> "$UDEV_RULES"
|
||||
done
|
||||
|
||||
echo " Written: $UDEV_RULES"
|
||||
echo " Symlinks: /dev/consoles/<name> for each device"
|
||||
|
||||
# ============================================================
|
||||
# 2. Generate ser2net.yaml
|
||||
# ============================================================
|
||||
echo ""
|
||||
echo "--- [2/3] Generating ser2net config: $SER2NET_CONF ---"
|
||||
|
||||
# Backup existing config if not already backed up
|
||||
if [ -f "$SER2NET_CONF" ] && [ ! -f "${SER2NET_CONF}.orig" ]; then
|
||||
cp "$SER2NET_CONF" "${SER2NET_CONF}.orig"
|
||||
echo " Backed up original to ${SER2NET_CONF}.orig"
|
||||
fi
|
||||
|
||||
{
|
||||
echo "%YAML 1.1"
|
||||
echo "---"
|
||||
echo "# ser2net configuration for pfv-tsys4 console ports"
|
||||
echo "# Generated by console/generate-config.sh on $(date)"
|
||||
echo "#"
|
||||
echo "# All ports use telnet(rfc2217) accepter so conman and telnet clients"
|
||||
echo "# negotiate proper telnet binary mode — this prevents CR stripping"
|
||||
printf '%s\n' "# and stair-stepping on devices that send \\n\\r (LF+CR) line endings."
|
||||
echo "# Ports bound to Tailscale IP ($TS_IP) for secure remote access."
|
||||
echo "#"
|
||||
echo "# Direct telnet: telnet $TS_IP 2001"
|
||||
echo "# Via conman: conman -f <name>"
|
||||
echo ""
|
||||
printf '%s\n' "define: &banner \\r\\nPFV console port \\p device \\d [\\B]\\r\\n\\r\\n"
|
||||
echo ""
|
||||
|
||||
for entry in "${ENTRIES[@]}"; do
|
||||
IFS='|' read -r tcp_port name id_path baud comment <<< "$entry"
|
||||
# ser2net connection block — telnet(rfc2217) accepter so conman and
|
||||
# telnet clients negotiate proper telnet binary mode. This prevents
|
||||
# CR stripping that occurs with raw TCP + conman's telnet NVT.
|
||||
echo "connection: &con${tcp_port}"
|
||||
echo " accepter: telnet(rfc2217),tcp,${TS_IP},${tcp_port}"
|
||||
echo " enable: on"
|
||||
echo " options:"
|
||||
echo " banner: *banner"
|
||||
echo " kickolduser: true"
|
||||
echo " telnet-brk-on-sync: true"
|
||||
echo " connector: serialdev,"
|
||||
echo " /dev/consoles/${name},"
|
||||
echo " ${baud},local"
|
||||
echo ""
|
||||
done
|
||||
} > "$SER2NET_CONF"
|
||||
|
||||
echo " Written: $SER2NET_CONF"
|
||||
echo " ${#ENTRIES[@]} TCP ports configured ($TS_IP:2001-20XX)"
|
||||
|
||||
# ============================================================
|
||||
# 3. Write conman console entries directly into conman.conf
|
||||
# ============================================================
|
||||
# conman 0.3.x does NOT support the 'include' directive, so we write
|
||||
# CONSOLE entries directly into /etc/conman.conf between idempotent markers.
|
||||
echo ""
|
||||
echo "--- [3/3] Writing conman consoles into $CONMAN_CONF ---"
|
||||
|
||||
# Ensure logdir exists
|
||||
mkdir -p "$CONMAN_LOGDIR" 2>/dev/null || true
|
||||
|
||||
# Ensure LOGDIR is set in conman.conf (server-level directive for log file paths)
|
||||
if ! grep -qiE '^\s*server\s+logdir\s*=' "$CONMAN_CONF" 2>/dev/null; then
|
||||
# Insert near the top, after the first SERVER directives
|
||||
sed -i "1i\\server logdir = \"$CONMAN_LOGDIR\"" "$CONMAN_CONF"
|
||||
echo " Added server logdir = \"$CONMAN_LOGDIR\" to $CONMAN_CONF"
|
||||
fi
|
||||
|
||||
# Ensure loopback=off so conmand is reachable over Tailscale (not localhost-only)
|
||||
if ! grep -qiE '^\s*server\s+loopback\s*=' "$CONMAN_CONF" 2>/dev/null; then
|
||||
sed -i "/^server logdir/a server loopback=off" "$CONMAN_CONF"
|
||||
echo " Added server loopback=off to $CONMAN_CONF (enables remote access)"
|
||||
fi
|
||||
|
||||
# Remove any previous auto-generated block (between markers)
|
||||
# Then append the new block
|
||||
MARKER_BEGIN="# BEGIN PFV CONSOLE DEFINITIONS (auto-generated — do not edit between markers)"
|
||||
MARKER_END="# END PFV CONSOLE DEFINITIONS"
|
||||
|
||||
# Strip old block if present
|
||||
if grep -q "$MARKER_BEGIN" "$CONMAN_CONF" 2>/dev/null; then
|
||||
sed -i "/$MARKER_BEGIN/,/$MARKER_END/d" "$CONMAN_CONF"
|
||||
echo " Removed previous console definitions."
|
||||
fi
|
||||
|
||||
# Append new block
|
||||
{
|
||||
echo ""
|
||||
echo "$MARKER_BEGIN"
|
||||
echo "# Generated by console/generate-config.sh on $(date)"
|
||||
echo "# Each console connects to a ser2net TCP port via telnet protocol."
|
||||
echo "# ser2net uses telnet(rfc2217) accepter so binary mode is negotiated"
|
||||
echo "# and CR/LF translation is handled correctly by the telnet NVT layer."
|
||||
echo "# Access: conman -f <name>"
|
||||
echo ""
|
||||
for entry in "${ENTRIES[@]}"; do
|
||||
IFS='|' read -r tcp_port name id_path baud comment <<< "$entry"
|
||||
echo "CONSOLE name=\"${name}\" dev=\"${TS_IP}:${tcp_port}\" log=\"${name}.log\" logopts=\"timestamp\""
|
||||
done
|
||||
echo "$MARKER_END"
|
||||
} >> "$CONMAN_CONF"
|
||||
|
||||
CONSOLE_COUNT=$(grep -c "^CONSOLE " "$CONMAN_CONF" 2>/dev/null || echo 0)
|
||||
echo " Written $CONSOLE_COUNT CONSOLE entries to $CONMAN_CONF"
|
||||
|
||||
# ============================================================
|
||||
# Summary
|
||||
# ============================================================
|
||||
echo ""
|
||||
echo "============================================"
|
||||
echo " Configuration generated successfully."
|
||||
echo ""
|
||||
echo " Files written:"
|
||||
echo " $UDEV_RULES ($(wc -l < "$UDEV_RULES") lines)"
|
||||
echo " $SER2NET_CONF ($(wc -l < "$SER2NET_CONF") lines)"
|
||||
echo " $CONMAN_CONF (CONSOLE entries appended between markers)"
|
||||
echo ""
|
||||
echo " Next steps:"
|
||||
echo " 1. Reload udev: udevadm control --reload-rules && udevadm trigger"
|
||||
echo " 2. Restart ser2net: systemctl restart ser2net"
|
||||
echo " 3. Start conman: systemctl enable --now conmand"
|
||||
echo " 4. Or run: bash $(basename "$0" .sh | sed 's/generate-config/setup/') .sh"
|
||||
echo "============================================"
|
||||
@@ -1,29 +0,0 @@
|
||||
# console/mapping.txt — Source of Truth for console port assignments
|
||||
#
|
||||
# Format: <tcp_port>|<name>|<id_path_substring>|<baud>|<comment>
|
||||
#
|
||||
# Delimiter is | (pipe) because ID_PATH values contain colons.
|
||||
#
|
||||
# - tcp_port: TCP port ser2net listens on (also the conman console name suffix)
|
||||
# - name: Device name (used for /dev/console/<name> symlink, conman console name)
|
||||
# - id_path_substring: Stable USB physical path from `udevadm info -q all -n /dev/ttyUSBN | grep ID_PATH`
|
||||
# These are STABLE across reboots as long as adapters aren't moved
|
||||
# to different physical USB ports.
|
||||
# - baud: Serial baud rate (9600n81 = 9600 8N1, no flow control)
|
||||
# - comment: Free-form description
|
||||
#
|
||||
# To RE-MAP after physically moving an adapter:
|
||||
# 1. Run: bash console/discover.sh (find the new ID_PATH for the device)
|
||||
# 2. Update the id_path_substring in this file
|
||||
# 3. Run: bash console/generate-config.sh && udevadm trigger && systemctl restart ser2net conmand
|
||||
#
|
||||
2001|pfv-core-sw01|usb-0:1.5.4.4|9600n81|Dell PowerConnect 5448 (core switch)
|
||||
2002|pfv-tor3-mgmt|usb-0:1.6.3.1|9600n81|Rack 3 management TOR switch
|
||||
2003|pfv-tor3-stor|usb-0:1.6.3.3.2|9600n81|Rack 3 storage TOR switch
|
||||
2004|pfv-rrinfra-rtr|usb-0:1.6.3.3.1|9600n81|Cisco router (rrinfra)
|
||||
2005|pfv-r2-tor-top|usb-0:1.6.3.3.3|9600n81|Rack 2 top-of-rack switch
|
||||
2006|subodev-torsw|usb-0:1.5.4.1|9600n81|Suborbital device TOR switch
|
||||
2007|pfv-r2-sw|usb-0:1.6.3.2|9600n81|Rack 2 old Dell switch
|
||||
# Unassigned (no device detected):
|
||||
# 2008|spare-1|usb-0:1.6.3.4|9600n81|Empty / spare
|
||||
# 2009|spare-2|usb-0:1.6.3.3.4|9600n81|Empty / spare
|
||||
@@ -1,62 +0,0 @@
|
||||
#!/usr/bin/bash
|
||||
#
|
||||
# console/query-remote.sh — install conman client and connect to a console
|
||||
# on pfv-tsys4 over Tailscale.
|
||||
#
|
||||
# Usage:
|
||||
# bash console/query-remote.sh # list consoles
|
||||
# bash console/query-remote.sh pfv-core-sw01 # connect to a console
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
REMOTE_HOST="${REMOTE_HOST:-pfv-tsys4}"
|
||||
REMOTE_PORT="${REMOTE_PORT:-7890}"
|
||||
|
||||
echo "============================================"
|
||||
echo " Conman Remote Console Access"
|
||||
echo " Server: ${REMOTE_HOST}:${REMOTE_PORT} (Tailscale)"
|
||||
echo "============================================"
|
||||
|
||||
# --- 1. Install conman client if missing ---
|
||||
if ! command -v conman >/dev/null 2>&1; then
|
||||
echo ""
|
||||
echo "--- Installing conman client ---"
|
||||
if sudo -n true 2>/dev/null; then
|
||||
sudo apt-get update -qq && sudo apt-get install -y -qq conman
|
||||
else
|
||||
echo " Passwordless sudo not available. Please run:"
|
||||
echo " sudo apt-get update && sudo apt-get install -y conman"
|
||||
echo " Then re-run this script."
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo " conman client already installed."
|
||||
fi
|
||||
|
||||
# --- 2. Verify connectivity ---
|
||||
echo ""
|
||||
echo "--- Connectivity check ---"
|
||||
if timeout 3 bash -c "echo > /dev/tcp/${REMOTE_HOST}/${REMOTE_PORT}" 2>/dev/null; then
|
||||
echo " [OK] ${REMOTE_HOST}:${REMOTE_PORT} reachable"
|
||||
else
|
||||
echo " [FAIL] Cannot reach ${REMOTE_HOST}:${REMOTE_PORT}"
|
||||
echo " Is Tailscale up? Is conmand running on ${REMOTE_HOST}?"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- 3. List or connect ---
|
||||
CONSOLE="${1:-}"
|
||||
if [ -z "$CONSOLE" ]; then
|
||||
echo ""
|
||||
echo "--- Available consoles ---"
|
||||
conman -d "${REMOTE_HOST}:${REMOTE_PORT}" -q
|
||||
echo ""
|
||||
echo "To connect: bash $0 <console-name>"
|
||||
echo " e.g: bash $0 pfv-core-sw01"
|
||||
else
|
||||
echo ""
|
||||
echo "--- Connecting to: $CONSOLE ---"
|
||||
echo " Escape sequence: &. (to disconnect)"
|
||||
echo ""
|
||||
conman -d "${REMOTE_HOST}:${REMOTE_PORT}" -f "$CONSOLE"
|
||||
fi
|
||||
@@ -1,217 +0,0 @@
|
||||
#!/usr/bin/bash
|
||||
# shellcheck disable=SC2010 # diagnostic; ls|grep on /dev listing is intentional
|
||||
#
|
||||
# console/setup.sh — deploy console management on pfv-tsys4
|
||||
#
|
||||
# Orchestrates the full setup:
|
||||
# 1. Ensures ser2net + conman are installed
|
||||
# 2. Copies mapping.txt to the target host (if running remotely)
|
||||
# 3. Runs generate-config.sh to produce udev rules + ser2net.yaml + conman.conf
|
||||
# 4. Reloads udev, creates /dev/consoles/ symlinks
|
||||
# 5. Restarts ser2net (TCP ports on Tailscale IP)
|
||||
# 6. Enables + starts conmand (logging + multiplexing)
|
||||
# 7. Verifies
|
||||
#
|
||||
# This script is IDEMPOTENT — safe to run multiple times.
|
||||
#
|
||||
# Usage:
|
||||
# PROX_HOST=pfv-tsys4 bash tests/remote.sh prox-file console/setup.sh
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
|
||||
echo "============================================"
|
||||
echo " Console Management Setup"
|
||||
echo " Host: $(hostname) $(date)"
|
||||
echo "============================================"
|
||||
|
||||
# --- 1. Install dependencies ---
|
||||
echo ""
|
||||
echo "--- [1/7] Checking dependencies ---"
|
||||
NEED_INSTALL=()
|
||||
dpkg -l ser2net 2>/dev/null | grep -q '^ii' && echo " ser2net: installed" || NEED_INSTALL+=(ser2net)
|
||||
dpkg -l conman 2>/dev/null | grep -q '^ii' && echo " conman: installed" || NEED_INSTALL+=(conman)
|
||||
|
||||
if [ "${#NEED_INSTALL[@]}" -gt 0 ]; then
|
||||
echo " Installing: ${NEED_INSTALL[*]}"
|
||||
apt-get update -qq
|
||||
apt-get install -y -qq "${NEED_INSTALL[@]}"
|
||||
else
|
||||
echo " All dependencies present."
|
||||
fi
|
||||
|
||||
# --- 2. Ensure mapping file is available ---
|
||||
echo ""
|
||||
echo "--- [2/7] Locating mapping file ---"
|
||||
|
||||
MAPPING_FILE=""
|
||||
for candidate in \
|
||||
"$SCRIPT_DIR/mapping.txt" \
|
||||
"$(dirname "$0")/mapping.txt" \
|
||||
"/root/console/mapping.txt" \
|
||||
"/tmp/mapping.txt"; do
|
||||
if [ -f "$candidate" ]; then
|
||||
MAPPING_FILE="$candidate"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "$MAPPING_FILE" ]; then
|
||||
echo "FATAL: mapping.txt not found. Copy it to the target host."
|
||||
exit 1
|
||||
fi
|
||||
echo " Using: $MAPPING_FILE"
|
||||
|
||||
# --- 3. Generate configs ---
|
||||
echo ""
|
||||
echo "--- [3/7] Generating configs ---"
|
||||
export MAPPING_FILE
|
||||
bash "$(dirname "$0")/generate-config.sh" 2>&1 || bash "$SCRIPT_DIR/generate-config.sh" 2>&1 || {
|
||||
echo "FATAL: generate-config.sh failed."
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- 4. Reload udev + create symlinks ---
|
||||
echo ""
|
||||
echo "--- [4/7] Reloading udev rules ---"
|
||||
udevadm control --reload-rules
|
||||
# Try trigger first (works on some systems)
|
||||
for tty in /sys/class/tty/ttyUSB*; do
|
||||
[ -e "$tty" ] && udevadm trigger --action=add "$tty" 2>/dev/null || true
|
||||
done
|
||||
# Also try writing to uevent (forces udev reprocessing)
|
||||
for tty in /sys/class/tty/ttyUSB*; do
|
||||
[ -e "$tty/uevent" ] && echo "add" > "$tty/uevent" 2>/dev/null || true
|
||||
done
|
||||
sleep 2
|
||||
|
||||
# FALLBACK: if udev symlinks don't exist (common when devices are already
|
||||
# discovered — udev trigger doesn't always re-create symlinks for existing
|
||||
# devices), create them manually by matching ID_PATH. The udev rules will
|
||||
# handle future boots/hotplugs automatically.
|
||||
if [ ! -d /dev/consoles ] || [ -z "$(ls /dev/consoles/ 2>/dev/null)" ]; then
|
||||
echo " udev trigger didn't create symlinks. Creating manually..."
|
||||
mkdir -p /dev/consoles
|
||||
while IFS= read -r line; do
|
||||
line="${line%%#*}"
|
||||
line="$(echo "$line" | xargs)"
|
||||
[ -z "$line" ] && continue
|
||||
IFS='|' read -r _ name id_path _ _ <<< "$line"
|
||||
# Find the ttyUSB whose ID_PATH contains the mapping's id_path substring
|
||||
for tty in /dev/ttyUSB*; do
|
||||
[ -e "$tty" ] || continue
|
||||
DEV_IDPATH=$(udevadm info -q property -n "$tty" 2>/dev/null | grep ^ID_PATH= | cut -d= -f2)
|
||||
if echo "$DEV_IDPATH" | grep -q "$id_path"; then
|
||||
ln -sf "$tty" "/dev/consoles/$name"
|
||||
echo " ln -s $tty -> /dev/consoles/$name"
|
||||
break
|
||||
fi
|
||||
done
|
||||
done < "$MAPPING_FILE"
|
||||
fi
|
||||
|
||||
echo " Stable symlinks:"
|
||||
ls -la /dev/consoles/ 2>/dev/null | grep -v '^total\|^d' | sed 's/^/ /' || echo " (none created)"
|
||||
|
||||
# Verify each symlink resolves
|
||||
echo ""
|
||||
echo " Symlink verification:"
|
||||
while IFS= read -r line; do
|
||||
line="${line%%#*}"
|
||||
line="$(echo "$line" | xargs)"
|
||||
[ -z "$line" ] && continue
|
||||
IFS='|' read -r _ name id_path _ _ <<< "$line"
|
||||
if [ -e "/dev/consoles/$name" ]; then
|
||||
TARGET=$(readlink -f "/dev/consoles/$name")
|
||||
echo " [OK] /dev/consoles/$name -> $TARGET"
|
||||
else
|
||||
echo " [MISSING] /dev/consoles/$name (adapter unplugged or ID_PATH changed)"
|
||||
fi
|
||||
done < "$MAPPING_FILE"
|
||||
|
||||
# --- 5. Restart ser2net ---
|
||||
echo ""
|
||||
echo "--- [5/7] Restarting ser2net ---"
|
||||
systemctl enable ser2net
|
||||
systemctl restart ser2net
|
||||
sleep 2
|
||||
|
||||
if systemctl is-active --quiet ser2net; then
|
||||
echo " ser2net is running (telnet rfc2217 accepters)."
|
||||
TS_IP=$(tailscale ip -4 2>/dev/null || echo "127.0.0.1")
|
||||
echo " Listening ports:"
|
||||
ss -tlnp | grep ser2net | grep -oE "${TS_IP}:[0-9]+" | sort -t: -k2 -n | sed 's/^/ /'
|
||||
else
|
||||
echo " WARNING: ser2net failed to start. Checking journal..."
|
||||
journalctl -u ser2net --no-pager -n 20
|
||||
fi
|
||||
|
||||
# --- 6. Enable + start conmand ---
|
||||
echo ""
|
||||
echo "--- [6/7] Starting conmand ---"
|
||||
|
||||
# conman package on Debian may not ship a systemd unit. Create one if missing.
|
||||
if ! systemctl cat conmand >/dev/null 2>&1; then
|
||||
echo " No systemd unit for conmand — creating one..."
|
||||
cat > /etc/systemd/system/conmand.service <<'CONMAND_UNIT'
|
||||
[Unit]
|
||||
Description=ConMan (Console Manager)
|
||||
After=network.target ser2net.service
|
||||
Requires=ser2net.service
|
||||
|
||||
[Service]
|
||||
Type=forking
|
||||
ExecStart=/usr/sbin/conmand -c /etc/conman.conf
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
CONMAND_UNIT
|
||||
systemctl daemon-reload
|
||||
echo " Created /etc/systemd/system/conmand.service"
|
||||
fi
|
||||
|
||||
# Kill any manually-started conmand first
|
||||
pkill -x conmand 2>/dev/null || true
|
||||
sleep 1
|
||||
|
||||
systemctl enable conmand 2>/dev/null || true
|
||||
systemctl restart conmand 2>/dev/null || true
|
||||
sleep 2
|
||||
|
||||
if systemctl is-active --quiet conmand; then
|
||||
echo " conmand is running."
|
||||
echo " Consoles:"
|
||||
conman -q 2>&1 | sed 's/^/ /' || true
|
||||
else
|
||||
echo " WARNING: conmand failed to start. Checking journal..."
|
||||
journalctl -u conmand --no-pager -n 20 2>/dev/null || true
|
||||
# Try manual start as fallback
|
||||
echo " Attempting manual start..."
|
||||
/usr/sbin/conmand -c /etc/conman.conf 2>&1 || true
|
||||
fi
|
||||
|
||||
# --- 7. Summary ---
|
||||
echo ""
|
||||
echo "--- [7/7] Setup complete ---"
|
||||
echo ""
|
||||
echo " ser2net + conman architecture (telnet rfc2217):"
|
||||
echo " ser2net owns serial devices, exposes telnet(rfc2217) TCP ports"
|
||||
echo " conman connects via telnet for logging + multiplexing"
|
||||
echo ""
|
||||
echo " Connect from any Tailscale workstation:"
|
||||
echo " conman -d pfv-tsys4:7890 -f pfv-core-sw01"
|
||||
echo " conman -d pfv-tsys4:7890 -q # list consoles"
|
||||
echo ""
|
||||
echo " Direct telnet (emergency, conflicts with conman):"
|
||||
echo " ssh pfv-tsys4 'systemctl stop conmand'"
|
||||
echo " telnet pfv-tsys4 2001"
|
||||
echo " ssh pfv-tsys4 'systemctl start conmand'"
|
||||
echo ""
|
||||
echo " To regenerate after changing mapping.txt:"
|
||||
echo " bash generate-config.sh"
|
||||
echo " udevadm trigger"
|
||||
echo " systemctl restart ser2net conmand"
|
||||
echo "============================================"
|
||||
@@ -1,89 +0,0 @@
|
||||
#!/usr/bin/bash
|
||||
# shellcheck disable=SC2012,SC2001 # diagnostic script; ls -la listings and sed line-prefixing are intentional
|
||||
#
|
||||
# console/validate-conman.sh — verify conman can actually reach devices via
|
||||
# ser2net TCP ports and is capturing log output to files.
|
||||
#
|
||||
# This tests the real data path: conman → TCP 200X → ser2net → /dev/consoles/X → device
|
||||
#
|
||||
set -uo pipefail
|
||||
|
||||
TS_IP=$(tailscale ip -4)
|
||||
LOGDIR="/var/log/conman"
|
||||
|
||||
echo "============================================"
|
||||
echo " Conman Data Path + Log Validation"
|
||||
echo " Host: $(hostname) TS IP: $TS_IP"
|
||||
echo "============================================"
|
||||
|
||||
echo ""
|
||||
echo "--- 1. conman.conf log settings ---"
|
||||
grep -E "logdir|LOGDIR|^GLOBAL LOG" /etc/conman.conf 2>/dev/null | grep -v "^#" || echo " (no explicit logdir — defaults to /var/log/conman)"
|
||||
echo " Log dir: $LOGDIR"
|
||||
ls -la "$LOGDIR"/ 2>/dev/null | head -15 || echo " ($LOGDIR does not exist yet)"
|
||||
|
||||
echo ""
|
||||
echo "--- 2. CONSOLE entries: each has a log= directive? ---"
|
||||
# Extract the auto-generated block and check each CONSOLE line has log=
|
||||
sed -n '/BEGIN PFV CONSOLE/,/END PFV CONSOLE/p' /etc/conman.conf | grep "^CONSOLE" | while read -r line; do
|
||||
name=$(echo "$line" | sed -n 's/.*name="\([^"]*\)".*/\1/p')
|
||||
if echo "$line" | grep -q 'log='; then
|
||||
logfile=$(echo "$line" | sed -n 's/.*log="\([^"]*\)".*/\1/p')
|
||||
echo " [OK] $name → log=$logfile"
|
||||
else
|
||||
echo " [FAIL] $name has NO log= directive"
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "--- 3. Trigger log capture: connect to each console briefly ---"
|
||||
# conman -e changes the escape char. We use -j (join, read-only) with a timeout.
|
||||
# Actually, conman doesn't have a built-in "connect for N seconds" — but conmand
|
||||
# connects to each device ON STARTUP and keeps the connection open for logging.
|
||||
# The log files should already be created. Let's check timestamps.
|
||||
|
||||
echo " conmand connects to all consoles on startup. Checking if logs exist..."
|
||||
echo ""
|
||||
echo "--- 4. Log file inventory ---"
|
||||
for name in pfv-core-sw01 pfv-tor3-mgmt pfv-tor3-stor pfv-rrinfra-rtr pfv-r2-tor-top subodev-torsw pfv-r2-sw; do
|
||||
logfile="$LOGDIR/${name}.log"
|
||||
if [ -f "$logfile" ]; then
|
||||
SIZE=$(stat -c%s "$logfile" 2>/dev/null || echo 0)
|
||||
MTIME=$(stat -c%y "$logfile" 2>/dev/null | cut -d. -f1)
|
||||
echo " [OK] $logfile ($SIZE bytes, modified $MTIME)"
|
||||
else
|
||||
echo " [MISSING] $logfile — conmand may not be writing yet"
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "--- 5. conmand connection status (journal) ---"
|
||||
# conmand logs connection attempts/errors to syslog
|
||||
journalctl -u conmand --no-pager -n 50 2>/dev/null | grep -iE "connect|error|fail|console|refused|timeout" | tail -15 || echo " (no relevant journal entries)"
|
||||
|
||||
echo ""
|
||||
echo "--- 6. Verify ser2net is proxying data (telnet rfc2217) ---"
|
||||
echo " Probing TCP $TS_IP:2007 for data..."
|
||||
RESPONSE=$(timeout 3 bash -c "printf '\r\r' | nc -w 2 $TS_IP 2007 2>/dev/null" | tr -cd '[:print:][:space:]' | head -5)
|
||||
if [ -n "$RESPONSE" ]; then
|
||||
echo " [OK] Data flowing through ser2net TCP 2007:"
|
||||
echo "$RESPONSE" | sed 's/^/ /'
|
||||
else
|
||||
echo " (no immediate response — device may need more interaction)"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "--- 7. Check if conmand has open connections to ser2net ports ---"
|
||||
CONMAND_PID=$(pgrep -x conmand 2>/dev/null || echo "")
|
||||
if [ -n "$CONMAND_PID" ]; then
|
||||
echo " conmand PID: $CONMAND_PID"
|
||||
echo " Open connections to ser2net (expect 7 to 100.x:200X):"
|
||||
ss -tnp 2>/dev/null | grep "pid=$CONMAND_PID" | grep -oE "100\.[0-9.]+:200[0-9]" | sort | sed 's/^/ /'
|
||||
COUNT=$(ss -tnp 2>/dev/null | grep "pid=$CONMAND_PID" | grep -c ":200")
|
||||
echo " Total conmand→ser2net connections: $COUNT (expect 7)"
|
||||
else
|
||||
echo " [FAIL] conmand not running"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "============================================"
|
||||
@@ -1,183 +0,0 @@
|
||||
# Technitium DNS Cluster Setup
|
||||
|
||||
Replicates the production Technitium DNS Server from `tailscale-router` to the
|
||||
`pfv-netinfra-01/02` pair and configures them as a primary/secondary cluster
|
||||
with automatic zone transfers.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
tailscale-router (PRODUCTION — READ ONLY)
|
||||
└─ tsys-dns container (technitium/dns-server)
|
||||
└─ 124 zones (knel.net + reverse DNS)
|
||||
└─ Users + 2FA in auth.config
|
||||
│
|
||||
docker cp (export)
|
||||
│
|
||||
▼
|
||||
┌─ pfv-netinfra-01 (192.168.3.252) ──── PRIMARY ──────────┐
|
||||
│ tsys-dns container (Technitium on :5300) │
|
||||
│ pihole container (Pi-hole on :53 → Technitium :5300) │
|
||||
│ All zones are Primary │
|
||||
│ Zone transfer allowed from 192.168.3.253 │
|
||||
└──────────────────────────────────────────────────────────┘
|
||||
│
|
||||
AXFR / IXFR + NOTIFY (DNS zone transfer, port 5300)
|
||||
│
|
||||
▼
|
||||
┌─ pfv-netinfra-02 (192.168.3.253) ─── SECONDARY ────────┐
|
||||
│ tsys-dns container (Technitium on :5300) │
|
||||
│ pihole container (Pi-hole on :53 → Technitium :5300) │
|
||||
│ All zones are Secondary (AXFR from 01) │
|
||||
└──────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### How clustering works
|
||||
|
||||
Technitium uses standard DNS zone transfers (AXFR/IXFR) for primary/secondary
|
||||
replication, not a proprietary protocol:
|
||||
|
||||
1. **Primary (01)** holds all zones as authoritative primary zones.
|
||||
2. **Secondary (02)** holds each zone as a secondary zone configured with
|
||||
`primaryServer=192.168.3.252:5300`.
|
||||
3. On startup, the secondary immediately AXFRs the full zone from the primary.
|
||||
4. On subsequent record changes, the primary sends a **DNS NOTIFY** to the
|
||||
secondary, which triggers an **IXFR** (incremental transfer).
|
||||
5. If the primary is down, the secondary continues serving the last-known zone
|
||||
data independently.
|
||||
|
||||
### Credentials and 2FA
|
||||
|
||||
The production `auth.config` (containing all user accounts, passwords, and 2FA
|
||||
secrets) is copied verbatim to both nodes. This means:
|
||||
|
||||
- The **same username, password, and 2FA device** work on all three servers.
|
||||
- The web console is at `http://<host>:5380/` on each node.
|
||||
- No credential changes are needed.
|
||||
|
||||
During the clustering configuration step, a temporary admin password is used
|
||||
briefly (to access the API without 2FA), then the production `auth.config` is
|
||||
restored. See "Security notes" below.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- SSH key access to all hosts as `localuser` with passwordless sudo.
|
||||
- The `remote-dns.sh` wrapper must be able to reach all hosts via Tailscale FQDN.
|
||||
- Docker + Docker Compose on netinfra-01/02 (already installed).
|
||||
- The production Technitium on tailscale-router must be running.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
cd dns-cluster-setup/
|
||||
|
||||
# Step-by-step (recommended for first run):
|
||||
./setup.sh export # 1. Export config from tailscale-router (READ-ONLY)
|
||||
./setup.sh deploy01 # 2. Deploy to netinfra-01 as primary
|
||||
./setup.sh deploy02 # 3. Deploy to netinfra-02 as secondary clone
|
||||
./setup.sh cluster # 4. Configure clustering (01→02 zone transfers)
|
||||
./setup.sh verify # 5. Run all verification tests
|
||||
|
||||
# Or all at once:
|
||||
./setup.sh all
|
||||
```
|
||||
|
||||
### Configuration overrides
|
||||
|
||||
All defaults can be overridden via environment variables:
|
||||
|
||||
| Variable | Default | Description |
|
||||
|---|---|---|
|
||||
| `PRIMARY_IP` | `192.168.3.252` | netinfra-01 LAN IP |
|
||||
| `SECONDARY_IP` | `192.168.3.253` | netinfra-02 LAN IP |
|
||||
| `TECH_PORT` | `5300` | Technitium DNS port on host (from compose mapping) |
|
||||
| `CONFIG_DIR` | `/home/localuser/services/technitium/config` | Config bind-mount dir |
|
||||
| `COMPOSE_FILE` | `/home/localuser/services/technitium/docker-compose.yml` | Compose file |
|
||||
| `TEMP_ADMIN_PW` | `KnelClusterSetup!2026` | Temp admin password (used only during clustering, then discarded) |
|
||||
|
||||
## Scripts
|
||||
|
||||
| Script | Purpose |
|
||||
|---|---|
|
||||
| `remote-dns.sh` | SSH/SCP chokepoint for all DNS host access (tsrouter, netinfra01, netinfra02, netboot, sandbox) |
|
||||
| `setup.sh` | Master orchestrator: export → deploy → cluster → verify |
|
||||
| `verify.sh` | Comprehensive 10-section verification suite |
|
||||
| `discover*.sh` | Read-only discovery probes (used during development, safe to keep) |
|
||||
|
||||
## What gets copied
|
||||
|
||||
From production `/etc/dns/` (inside the container), **excluding** runtime data:
|
||||
|
||||
| Copied (configuration) | Excluded (runtime) |
|
||||
|---|---|
|
||||
| `auth.config` (users, passwords, 2FA) | `cache.bin` (DNS cache) |
|
||||
| `dns.config` (server settings) | `stats/` (query statistics) |
|
||||
| `webservice.config` (web console) | `logs/` (log files) |
|
||||
| `allowed.config` (zone transfer ACL) | |
|
||||
| `blocked.config` (blocked domains) | |
|
||||
| `blocklist.config` (blocklist settings) | |
|
||||
| `blocklists/` (blocklist data) | |
|
||||
| `zones/` (all 124 zone files) | |
|
||||
| `scopes/` (DHCP scopes) | |
|
||||
| `apps/` (Technitium apps) | |
|
||||
|
||||
## Verification tests
|
||||
|
||||
The `verify.sh` script runs 10 categories of tests:
|
||||
|
||||
1. **Container health** — both Technitium containers are Up
|
||||
2. **API responds** — web console API is reachable on both nodes
|
||||
3. **Zone count** — primary matches production; secondary matches primary
|
||||
4. **Forward DNS** — known knel.net records resolve identically on both nodes
|
||||
5. **External DNS** — both nodes can resolve external domains (github.com)
|
||||
6. **Zone transfer (AXFR)** — secondary can AXFR knel.net from primary
|
||||
7. **Reverse DNS** — PTR zones have SOA records on both nodes
|
||||
8. **Production untouched** — container still running, zone count unchanged
|
||||
9. **Failover** — secondary serves SOA independently (no primary dependency)
|
||||
10. **Credentials** — `auth.config` byte-size matches across all three nodes
|
||||
|
||||
## Security notes
|
||||
|
||||
- **tailscale-router is never modified.** The only operation is `docker cp`
|
||||
(read) to export the config. No writes, no restarts, no config changes.
|
||||
- The temporary admin password (`TEMP_ADMIN_PW`) exists only during the
|
||||
clustering step. After configuration, the production `auth.config` (with 2FA)
|
||||
is restored. The temp password is never persisted.
|
||||
- The export tarball (`.export/technitium-production-config.tar.gz`) contains
|
||||
production credentials. It is in `.gitignore` and should be deleted after
|
||||
setup: `rm -rf dns-cluster-setup/.export/`
|
||||
- Each node's existing config is backed up to `config.backup-<timestamp>` before
|
||||
replacement, so the change is reversible.
|
||||
|
||||
## Recovery
|
||||
|
||||
If something goes wrong, each node has a backup:
|
||||
|
||||
```bash
|
||||
# On netinfra-01 or netinfra-02:
|
||||
cd /home/localuser/services/technitium/
|
||||
docker compose down
|
||||
mv config config.failed
|
||||
mv config.backup-<timestamp> config
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
## Validation on sandbox
|
||||
|
||||
After cluster setup, validate that client hosts use the pair correctly:
|
||||
|
||||
```bash
|
||||
# From sectestbed-sandbox (or any client):
|
||||
# Query primary directly:
|
||||
dig @192.168.3.252 pfv-netinfra-01.knel.net
|
||||
|
||||
# Query secondary directly:
|
||||
dig @192.168.3.253 pfv-netinfra-01.knel.net
|
||||
|
||||
# Both should return the same answer.
|
||||
```
|
||||
|
||||
The KNELServerBuild provisioning code (`provisioning/ConfigFiles/NTP/ntp.conf`
|
||||
and `provisioning/ConfigFiles/Resolv/resolv.conf`) points clients at both
|
||||
servers for DNS and NTP redundancy. See `docs/server-build/tailscale.md` for the
|
||||
full DNS architecture analysis.
|
||||
@@ -1,90 +0,0 @@
|
||||
#!/usr/bin/bash
|
||||
#
|
||||
# remote-dns.sh
|
||||
#
|
||||
# Single chokepoint for ALL ssh/scp access to the DNS infrastructure hosts.
|
||||
# Every other script in dns-cluster-setup/ MUST route through this wrapper.
|
||||
# Never call ssh/scp directly.
|
||||
#
|
||||
# WHY: one place to configure host aliases/users/keys, one place to audit,
|
||||
# and the command scanner only permits ssh when invoked indirectly via a
|
||||
# script. Mirrors the pattern of tests/remote.sh.
|
||||
#
|
||||
# HOSTS (override IPs via env if needed):
|
||||
# tsrouter tailscale-router.knel.net (PRODUCTION — READ-ONLY here)
|
||||
# netinfra01 pfv-netinfra-01.knel.net (Technitium primary target)
|
||||
# netinfra02 pfv-netinfra-02.knel.net (Technitium secondary target)
|
||||
# netboot pfv-netboot.knel.net (reference / validation client)
|
||||
# sandbox sectestbed-sandbox.knel.net (validation client)
|
||||
#
|
||||
# All hosts are accessed as $VM_USER (default: localuser) over SSH with key auth
|
||||
# and passwordless sudo.
|
||||
#
|
||||
# USAGE:
|
||||
# remote-dns.sh <host-alias> <cmd...> run command on host
|
||||
# remote-dns.sh <host-alias>-root <cmd...> run command on host as root (sudo)
|
||||
# remote-dns.sh <host-alias>-file <script> run a local script file on host (bash -s)
|
||||
# remote-dns.sh <host-alias>-copy <local> <remote-dest> copy a file to host
|
||||
#
|
||||
# e.g.
|
||||
# remote-dns.sh tsrouter 'hostname; whoami'
|
||||
# remote-dns.sh netinfra01-root 'systemctl status dnsServer'
|
||||
# remote-dns.sh tsrouter-file ./probe.sh
|
||||
#
|
||||
set -uo pipefail
|
||||
|
||||
VM_USER="${VM_USER:-localuser}"
|
||||
# Hostname -> FQDN map. Override individual IPs via env if a host moves.
|
||||
TSROUTER_HOST="${TSROUTER_HOST:-tailscale-router.knel.net}"
|
||||
NETINFRA01_HOST="${NETINFRA01_HOST:-pfv-netinfra-01.knel.net}"
|
||||
NETINFRA02_HOST="${NETINFRA02_HOST:-pfv-netinfra-02.knel.net}"
|
||||
NETBOOT_HOST="${NETBOOT_HOST:-pfv-netboot.knel.net}"
|
||||
SANDBOX_HOST="${SANDBOX_HOST:-sectestbed-sandbox.knel.net}"
|
||||
|
||||
SSH_OPTS=(-o BatchMode=yes -o StrictHostKeyChecking=accept-new -o ConnectTimeout=15)
|
||||
|
||||
die() { echo "remote-dns.sh: $*" >&2; exit 1; }
|
||||
|
||||
host_fqdn() {
|
||||
case "$1" in
|
||||
tsrouter) printf '%s' "$TSROUTER_HOST" ;;
|
||||
netinfra01) printf '%s' "$NETINFRA01_HOST" ;;
|
||||
netinfra02) printf '%s' "$NETINFRA02_HOST" ;;
|
||||
netboot) printf '%s' "$NETBOOT_HOST" ;;
|
||||
sandbox) printf '%s' "$SANDBOX_HOST" ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
_run() { ssh "${SSH_OPTS[@]}" "${VM_USER}@$1" "$2"; }
|
||||
_run_root() { ssh "${SSH_OPTS[@]}" "${VM_USER}@$1" "sudo -n bash -c $(printf '%q' "$2")"; }
|
||||
_run_file() { ssh "${SSH_OPTS[@]}" "${VM_USER}@$1" "bash -s" < "$2"; }
|
||||
_copy() {
|
||||
local fqdn="$1" local="$2" dest="$3"
|
||||
if command -v rsync >/dev/null 2>&1 \
|
||||
&& ssh "${SSH_OPTS[@]}" "${VM_USER}@${fqdn}" 'command -v rsync' >/dev/null 2>&1; then
|
||||
rsync -az -e "ssh ${SSH_OPTS[*]}" "$local" "${VM_USER}@${fqdn}:${dest}"
|
||||
else
|
||||
ssh "${SSH_OPTS[@]}" "${VM_USER}@${fqdn}" "cat > '$dest'" < "$local"
|
||||
fi
|
||||
}
|
||||
|
||||
spec="${1:-}"; shift || true
|
||||
# Split host alias from mode: "netinfra01", "netinfra01-root", "netinfra01-file", "netinfra01-copy"
|
||||
mode="run"
|
||||
alias="$spec"
|
||||
case "$spec" in
|
||||
*-root) mode="root"; alias="${spec%-root}" ;;
|
||||
*-file) mode="file"; alias="${spec%-file}" ;;
|
||||
*-copy) mode="copy"; alias="${spec%-copy}" ;;
|
||||
esac
|
||||
|
||||
fqdn="$(host_fqdn "$alias")" || die "unknown host alias '$alias' (try: tsrouter|netinfra01|netinfra02|netboot|sandbox)"
|
||||
|
||||
case "$mode" in
|
||||
run) _run "$fqdn" "$*" ;;
|
||||
root) [ "$#" -ge 1 ] || die "need command"; _run_root "$fqdn" "$*" ;;
|
||||
file) [ -f "${1:-}" ] || die "need local script file"; _run_file "$fqdn" "$1" ;;
|
||||
copy) [ -f "${1:-}" ] || die "need local file"; _copy "$fqdn" "$1" "${2:-}" ;;
|
||||
*) die "bad mode" ;;
|
||||
esac
|
||||
@@ -1,474 +0,0 @@
|
||||
#!/usr/bin/bash
|
||||
#
|
||||
# setup.sh — Technitium DNS Cluster Setup
|
||||
#
|
||||
# Replicates the production Technitium DNS Server config from tailscale-router
|
||||
# to the pfv-netinfra-01/02 pair, then configures 01 as primary and 02 as
|
||||
# secondary with automatic zone transfers (AXFR).
|
||||
#
|
||||
# PRODUCTION SAFETY: tailscale-router is accessed READ-ONLY. No file on it is
|
||||
# modified. The only operation is a docker cp (read) to export the config.
|
||||
#
|
||||
# ARCHITECTURE AFTER SETUP:
|
||||
#
|
||||
# pfv-netinfra-01 (192.168.3.252) — PRIMARY
|
||||
# Pi-hole (:53) → Technitium (:5300 inside container)
|
||||
# All zones are Primary; zone transfer allowed from 02
|
||||
#
|
||||
# pfv-netinfra-02 (192.168.3.253) — SECONDARY
|
||||
# Pi-hole (:53) → Technitium (:5300 inside container)
|
||||
# All zones are Secondary; AXFR from 01 on changes
|
||||
#
|
||||
# tailscale-router — PRODUCTION (untouched, read-only source of truth)
|
||||
#
|
||||
# CLUSTERING MECHANISM:
|
||||
# Technitium primary/secondary via DNS zone transfers (AXFR/IXFR + NOTIFY).
|
||||
# 01 serves all zones as Primary. 02 fetches them as Secondary from
|
||||
# 01's address (192.168.3.252:5300). When a record changes on 01, it sends
|
||||
# a DNS NOTIFY to 02, which immediately pulls the update via IXFR.
|
||||
#
|
||||
# CREDENTIALS:
|
||||
# The production auth.config (users + 2FA) is copied to both targets, so
|
||||
# the existing admin username, password, and 2FA device work identically on
|
||||
# all three servers.
|
||||
#
|
||||
# USAGE:
|
||||
# ./setup.sh export # Step 1: read-only export from tailscale-router
|
||||
# ./setup.sh deploy01 # Step 2: deploy config to netinfra-01 (primary)
|
||||
# ./setup.sh deploy02 # Step 3: deploy config to netinfra-02 (secondary)
|
||||
# ./setup.sh cluster # Step 4: configure clustering (01 primary, 02 secondary)
|
||||
# ./setup.sh verify # Step 5: test everything
|
||||
# ./setup.sh all # Steps 1-5 in sequence
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REMOTE="$HERE/remote-dns.sh"
|
||||
|
||||
# Host aliases (defined in remote-dns.sh)
|
||||
PROD="tsrouter" # tailscale-router (READ-ONLY)
|
||||
PRIMARY="netinfra01" # pfv-netinfra-01
|
||||
SECONDARY="netinfra02" # pfv-netinfra-02
|
||||
|
||||
# Network addresses for zone transfer
|
||||
PRIMARY_IP="${PRIMARY_IP:-192.168.3.252}"
|
||||
SECONDARY_IP="${SECONDARY_IP:-192.168.3.253}"
|
||||
# Technitium DNS port on the host (from docker-compose port mapping)
|
||||
TECH_PORT="${TECH_PORT:-5300}"
|
||||
|
||||
# Config directory on the netinfra hosts (bind mount target)
|
||||
CONFIG_DIR="${CONFIG_DIR:-/home/localuser/services/technitium/config}"
|
||||
COMPOSE_FILE="${COMPOSE_FILE:-/home/localuser/services/technitium/docker-compose.yml}"
|
||||
|
||||
# Temporary admin password used ONLY during clustering API calls.
|
||||
# After configuration, the production auth.config (with 2FA) is restored.
|
||||
TEMP_ADMIN_PW="${TEMP_ADMIN_PW:-KnelCluster2026}"
|
||||
|
||||
# Local working directory for exports
|
||||
WORK_DIR="$HERE/.export"
|
||||
mkdir -p "$WORK_DIR"
|
||||
|
||||
# Files/dirs to EXCLUDE from the config copy (runtime data, not configuration)
|
||||
EXCLUDE_PATTERNS=(cache.bin stats logs)
|
||||
|
||||
log() { printf '\033[0;36m[%s]\033[0m %s\n' "$(date +%H:%M:%S)" "$*"; }
|
||||
die() { log "ERROR: $*"; exit 1; }
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# Build an exclude-args string for tar
|
||||
exclude_args() {
|
||||
local args=""
|
||||
for p in "${EXCLUDE_PATTERNS[@]}"; do
|
||||
args+=" --exclude=$p"
|
||||
done
|
||||
printf '%s' "$args"
|
||||
}
|
||||
|
||||
# Run a command on a host as root via the wrapper
|
||||
run_root() { bash "$REMOTE" "$1-root" "${@:2}"; }
|
||||
run() { bash "$REMOTE" "$1" "${@:2}"; }
|
||||
|
||||
# Get a Technitium API token on a host (temporary admin, no 2FA)
|
||||
# Uses root to avoid PATH issues with non-interactive SSH sessions.
|
||||
# Usage: get_token <host-alias>
|
||||
get_token() {
|
||||
local host="$1"
|
||||
local resp
|
||||
resp=$(run_root "$host" "curl -sk --max-time 10 -X POST http://127.0.0.1:5380/api/user/login -d 'user=admin&pass=${TEMP_ADMIN_PW}'" 2>/dev/null || true)
|
||||
local token
|
||||
token=$(echo "$resp" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('token',''))" 2>/dev/null || true)
|
||||
printf '%s' "$token"
|
||||
}
|
||||
|
||||
# API call helper (uses root for reliable curl access)
|
||||
# Usage: api_call <host> <token> <endpoint> [param=value ...]
|
||||
api_call() {
|
||||
local host="$1" token="$2" endpoint="$3"; shift 3
|
||||
local url="http://127.0.0.1:5380/api/${endpoint}?token=${token}"
|
||||
local p
|
||||
for p in "$@"; do url+="&${p}"; done
|
||||
run_root "$host" "curl -sk --max-time 10 '$url'" 2>/dev/null || true
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Step 1: Export production config (READ-ONLY on tailscale-router)
|
||||
# -----------------------------------------------------------------------------
|
||||
do_export() {
|
||||
log "=== STEP 1: Exporting production config from $PROD (READ-ONLY) ==="
|
||||
|
||||
local export_tar="$WORK_DIR/technitium-production-config.tar.gz"
|
||||
|
||||
log "Exporting config volume from $PROD (piped, no disk writes on prod)..."
|
||||
# Read the Docker volume directory directly from the host filesystem.
|
||||
# No docker exec needed (avoids /tmp space issues on the prod host).
|
||||
# Pipe tar → ssh → local file. Nothing is written on production's disk.
|
||||
local vol_path
|
||||
vol_path=$(bash "$REMOTE" "$PROD-root" \
|
||||
"docker volume inspect -f '{{.Mountpoint}}' dns_tsys-dns-config 2>/dev/null" \
|
||||
| tr -d '[:space:]')
|
||||
[ -n "$vol_path" ] || die "Could not find Docker volume path on $PROD."
|
||||
log "Volume path: $vol_path"
|
||||
|
||||
bash "$REMOTE" "$PROD-root" \
|
||||
"tar czf - -C '$vol_path' --exclude=cache.bin --exclude=stats --exclude=logs ." \
|
||||
> "$export_tar" 2>/dev/null || die "Export pipe failed."
|
||||
|
||||
[ -s "$export_tar" ] || die "Export tarball is empty."
|
||||
|
||||
# Inspect
|
||||
local zone_count
|
||||
zone_count=$(tar tzf "$export_tar" | grep -c '\.zone$' || true)
|
||||
log "Export complete: $(du -h "$export_tar" | cut -f1), $zone_count zones."
|
||||
|
||||
# Save the zone name list for clustering
|
||||
tar tzf "$export_tar" | grep '\.zone$' | sed 's|^\./||; s|^zones/||; s|\.zone$||' | sort > "$WORK_DIR/zones.txt"
|
||||
log "Zone list saved ($zone_count zones): $(head -5 "$WORK_DIR/zones.txt" | tr '\n' ' ')..."
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Step 2: Deploy to netinfra-01 (PRIMARY)
|
||||
# -----------------------------------------------------------------------------
|
||||
do_deploy_primary() {
|
||||
log "=== STEP 2: Deploying PRIMARY to $PRIMARY ==="
|
||||
_deploy "$PRIMARY" "primary"
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Step 3: Deploy to netinfra-02 (SECONDARY — initial clone, clustering in step 4)
|
||||
# -----------------------------------------------------------------------------
|
||||
do_deploy_secondary() {
|
||||
log "=== STEP 3: Deploying SECONDARY to $SECONDARY ==="
|
||||
_deploy "$SECONDARY" "secondary"
|
||||
}
|
||||
|
||||
# Shared deploy logic
|
||||
# Usage: _deploy <host-alias> <role>
|
||||
_deploy() {
|
||||
local host="$1" role="$2"
|
||||
local export_tar="$WORK_DIR/technitium-production-config.tar.gz"
|
||||
[ -f "$export_tar" ] || die "No export found. Run '$0 export' first."
|
||||
|
||||
log "Stopping Technitium on $host..."
|
||||
run_root "$host" "cd $CONFIG_DIR/.. && docker compose down" 2>/dev/null \
|
||||
|| run_root "$host" "docker stop tsys-dns" 2>/dev/null || true
|
||||
|
||||
log "Backing up existing config on $host..."
|
||||
run_root "$host" "
|
||||
if [ -d '$CONFIG_DIR' ]; then
|
||||
mv '$CONFIG_DIR' '${CONFIG_DIR}.backup-$(date +%Y%m%d-%H%M%S)'
|
||||
fi
|
||||
mkdir -p '$CONFIG_DIR'
|
||||
" || die "Backup failed."
|
||||
|
||||
log "Uploading production config to $host..."
|
||||
bash "$REMOTE" "$host-root" "cat > /tmp/technitium-config.tar.gz" < "$export_tar" \
|
||||
|| die "Upload failed."
|
||||
|
||||
log "Extracting config on $host..."
|
||||
run_root "$host" "
|
||||
cd '$CONFIG_DIR'
|
||||
tar xzf /tmp/technitium-config.tar.gz
|
||||
rm -f /tmp/technitium-config.tar.gz
|
||||
chown -R 1654:1654 '$CONFIG_DIR' 2>/dev/null || true
|
||||
ls -la '$CONFIG_DIR/' | head -20
|
||||
" || die "Extract failed."
|
||||
|
||||
# Update compose with production env vars
|
||||
log "Updating docker-compose env on $host ($role)..."
|
||||
run_root "$host" "
|
||||
cat > /tmp/compose-patch.py << 'PYEOF'
|
||||
import re, sys
|
||||
f = sys.argv[1]
|
||||
with open(f) as fh: c = fh.read()
|
||||
# Ensure DNS_SERVER_DOMAIN and web service env vars are set
|
||||
if 'DNS_SERVER_DOMAIN' not in c:
|
||||
c = re.sub(r'(image:.*\n)', r'\1 environment:\n - DNS_SERVER_DOMAIN=knel.net\n', c, count=1)
|
||||
print(c)
|
||||
PYEOF
|
||||
python3 /tmp/compose-patch.py '$COMPOSE_FILE' > '${COMPOSE_FILE}.new' 2>/dev/null && mv '${COMPOSE_FILE}.new' '$COMPOSE_FILE' || true
|
||||
rm -f /tmp/compose-patch.py
|
||||
" || log "WARN: compose patch skipped (non-critical)."
|
||||
|
||||
log "Starting Technitium on $host..."
|
||||
run_root "$host" "cd $CONFIG_DIR/.. && docker compose up -d" 2>/dev/null \
|
||||
|| run_root "$host" "docker start tsys-dns" || die "Start failed."
|
||||
|
||||
log "Waiting for Technitium to come up on $host..."
|
||||
local i
|
||||
for i in $(seq 1 20); do
|
||||
if run "$host" "curl -sk --max-time 3 http://127.0.0.1:5380/api/config/getVersion 2>/dev/null | head -c 50" 2>/dev/null | grep -qE 'token|error'; then
|
||||
log "Technitium is up on $host (after ${i}s)."
|
||||
return 0
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
die "Technitium did not come up on $host within 40s."
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Step 4: Configure clustering
|
||||
#
|
||||
# On PRIMARY (01): enable zone transfer for SECONDARY's IP on all zones.
|
||||
# On SECONDARY (02): replace all primary zones with secondary zones pointing
|
||||
# to PRIMARY's address. Uses a temporary admin (no 2FA) for API access,
|
||||
# then restores the production auth.config.
|
||||
# -----------------------------------------------------------------------------
|
||||
do_cluster() {
|
||||
log "=== STEP 4: Configuring clustering ($PRIMARY → $SECONDARY) ==="
|
||||
|
||||
# --- 4a: On PRIMARY, enable zone transfer (for manual AXFR if needed) ---
|
||||
log "4a: Enabling zone transfer on $PRIMARY..."
|
||||
_with_temp_admin "$PRIMARY" "_cluster_enable_transfer"
|
||||
log "Zone transfers enabled on primary."
|
||||
|
||||
# --- 4b: Install rsync-based zone replication on SECONDARY ---
|
||||
log "4b: Installing rsync-based zone replication on $SECONDARY..."
|
||||
_install_rsync_replication
|
||||
log "Replication installed."
|
||||
}
|
||||
|
||||
# Install rsync-based zone sync on the secondary as a systemd timer.
|
||||
_install_rsync_replication() {
|
||||
local sync_script="$HERE/sync-zones.sh"
|
||||
[ -f "$sync_script" ] || die "sync-zones.sh not found."
|
||||
|
||||
# Upload the sync script (copy to /tmp first, then move as root since
|
||||
# the services dir may be root-owned from docker operations)
|
||||
bash "$REMOTE" "$SECONDARY-copy" "$sync_script" "/tmp/sync-zones.sh" \
|
||||
|| die "Could not copy sync-zones.sh to /tmp."
|
||||
run_root "$SECONDARY" "cp /tmp/sync-zones.sh /home/localuser/services/technitium/sync-zones.sh && chmod +x /home/localuser/services/technitium/sync-zones.sh && chown localuser:localuser /home/localuser/services/technitium/sync-zones.sh && rm /tmp/sync-zones.sh" \
|
||||
|| die "Could not install sync-zones.sh."
|
||||
|
||||
# Set up SSH key for rsync from secondary → primary (passwordless)
|
||||
log "Setting up SSH key for rsync (secondary → primary)..."
|
||||
run_root "$SECONDARY" "
|
||||
if [ ! -f /home/localuser/.ssh/id_ed25519 ]; then
|
||||
sudo -u localuser ssh-keygen -t ed25519 -N '' -f /home/localuser/.ssh/id_ed25519 -q
|
||||
fi
|
||||
cat /home/localuser/.ssh/id_ed25519.pub
|
||||
" 2>/dev/null | grep -E 'ssh-ed25519' | while read -r pubkey; do
|
||||
log "Adding secondary's SSH key to primary's authorized_keys..."
|
||||
run_root "$PRIMARY" "mkdir -p /home/localuser/.ssh && echo '$pubkey' >> /home/localuser/.ssh/authorized_keys && chmod 600 /home/localuser/.ssh/authorized_keys" \
|
||||
2>/dev/null || log "WARN: could not add key to primary"
|
||||
done
|
||||
|
||||
# Install systemd timer for periodic sync
|
||||
run_root "$SECONDARY" "
|
||||
cat > /etc/systemd/system/technitium-zone-sync.service << 'SVCEOF'
|
||||
[Unit]
|
||||
Description=Technitium Zone Sync (primary → secondary)
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
User=localuser
|
||||
ExecStart=/home/localuser/services/technitium/sync-zones.sh
|
||||
SVCEOF
|
||||
|
||||
cat > /etc/systemd/system/technitium-zone-sync.timer << 'TMREOF'
|
||||
[Unit]
|
||||
Description=Run Technitium Zone Sync every minute
|
||||
|
||||
[Timer]
|
||||
OnBootSec=30
|
||||
OnUnitActiveSec=60
|
||||
AccuracySec=10
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
TMREOF
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now technitium-zone-sync.timer
|
||||
echo 'timer installed'
|
||||
" 2>/dev/null || die "Could not install systemd timer."
|
||||
|
||||
# Trigger an immediate sync
|
||||
log "Triggering initial sync..."
|
||||
run_root "$SECONDARY" "sudo -u localuser /home/localuser/services/technitium/sync-zones.sh 2>&1" 2>/dev/null || true
|
||||
sleep 3
|
||||
|
||||
# Check result
|
||||
local zones
|
||||
zones=$(run_root "$SECONDARY" "ls /home/localuser/services/technitium/config/zones/ 2>/dev/null | wc -l" 2>/dev/null | tr -d '[:space:]')
|
||||
log "Secondary now has $zones zones."
|
||||
}
|
||||
|
||||
# Enable zone transfer for the secondary IP on all primary zones.
|
||||
# Runs inside _with_temp_admin, so $1 = host.
|
||||
_cluster_enable_transfer() {
|
||||
local host="$1"
|
||||
local token; token="$(get_token "$host")"
|
||||
[ -n "$token" ] || die "Cannot get API token on $host."
|
||||
|
||||
# Set global zone transfer allow list to include the secondary.
|
||||
# Technitium per-zone "allow zone transfer" — use the API to set it.
|
||||
local zone
|
||||
while IFS= read -r zone <&3; do
|
||||
[ -z "$zone" ] && continue
|
||||
# Set zone transfer to AllowAnyone so the secondary can AXFR.
|
||||
# Technitium API param: zoneTransfer (not allowZoneTransfer).
|
||||
api_call "$host" "$token" "zones/options/set" \
|
||||
"zone=$zone" "zoneTransfer=Allow" \
|
||||
>/dev/null 2>&1 || true
|
||||
done 3< "$WORK_DIR/zones.txt"
|
||||
log "Zone transfer set to AllowAnyone for ${SECONDARY_IP} on all zones."
|
||||
}
|
||||
|
||||
# Delete all primary zones and recreate as secondary zones.
|
||||
# Runs inside _with_temp_admin, so $1 = host.
|
||||
_cluster_make_secondary() {
|
||||
local host="$1"
|
||||
local token; token="$(get_token "$host")"
|
||||
[ -n "$token" ] || die "Cannot get API token on $host."
|
||||
|
||||
local zone total
|
||||
total=$(wc -l < "$WORK_DIR/zones.txt")
|
||||
local n=0
|
||||
# Use FD 3 so SSH (called by api_call/run_root) doesn't consume the loop's
|
||||
# stdin (a classic bash pitfall: ssh inherits and reads from FD 0).
|
||||
while IFS= read -r zone <&3; do
|
||||
[ -z "$zone" ] && continue
|
||||
n=$((n + 1))
|
||||
# Delete the existing (primary) zone
|
||||
api_call "$host" "$token" "zones/delete" "zone=$zone" >/dev/null 2>&1 || true
|
||||
# Create as secondary zone pointing to primary
|
||||
api_call "$host" "$token" "zones/create" \
|
||||
"zone=$zone" "type=Secondary" "primaryServer=${PRIMARY_IP}%3A${TECH_PORT}" \
|
||||
>/dev/null 2>&1 || true
|
||||
[ $((n % 20)) -eq 0 ] && log " ...converted $n/$total zones"
|
||||
done 3< "$WORK_DIR/zones.txt"
|
||||
log "Converted $n zones to secondary (AXFR from ${PRIMARY_IP}:${TECH_PORT})."
|
||||
|
||||
# Give Technitium a moment to AXFR
|
||||
log "Waiting 10s for initial zone transfer..."
|
||||
sleep 10
|
||||
}
|
||||
|
||||
# Helper: temporarily replace auth.config with a fresh admin (no 2FA),
|
||||
# run a function, then restore the original auth.config.
|
||||
# Uses a docker-compose.override.yml (auto-merged by compose) so the original
|
||||
# compose file is never modified.
|
||||
# Usage: _with_temp_admin <host> <function_name>
|
||||
_with_temp_admin() {
|
||||
local host="$1" func="$2"
|
||||
log "Temporarily resetting admin on $host for API access (will restore after)..."
|
||||
|
||||
local svc_dir; svc_dir="$(dirname "$CONFIG_DIR")"
|
||||
|
||||
# Stop the container FIRST (otherwise it recreates auth.config from memory
|
||||
# before we can delete it), then back up + delete auth.config, then create
|
||||
# the override file, then restart.
|
||||
log "Stopping Technitium on $host..."
|
||||
run_root "$host" "cd '$svc_dir' && docker compose down 2>/dev/null || docker stop tsys-dns 2>/dev/null || true" \
|
||||
|| die "Could not stop Technitium on $host."
|
||||
|
||||
# Back up production auth.config, then remove it so Technitium creates a
|
||||
# fresh admin on next start.
|
||||
run_root "$host" "
|
||||
cp '$CONFIG_DIR/auth.config' '$CONFIG_DIR/auth.config.production'
|
||||
rm -f '$CONFIG_DIR/auth.config'
|
||||
" || die "Could not back up/remove auth.config on $host."
|
||||
|
||||
# Create a compose override that injects the temp admin password.
|
||||
run_root "$host" "
|
||||
printf 'services:\\n technitium:\\n environment:\\n - DNS_SERVER_ADMIN_PASSWORD=${TEMP_ADMIN_PW}\\n' \
|
||||
> '$svc_dir/docker-compose.override.yml'
|
||||
" || die "Could not create compose override on $host."
|
||||
|
||||
# Restart with override in effect
|
||||
run_root "$host" "cd '$svc_dir' && docker compose up -d" \
|
||||
2>/dev/null || die "Could not restart with temp admin on $host."
|
||||
|
||||
# Wait for API to come up (check with root to avoid PATH issues)
|
||||
local i
|
||||
for i in $(seq 1 20); do
|
||||
if run_root "$host" "curl -sk --max-time 3 http://127.0.0.1:5380/api/config/getVersion 2>/dev/null" 2>/dev/null | grep -q .; then
|
||||
log "Temp admin API is up on $host."
|
||||
# Give the auth subsystem a few seconds to finish creating the admin user.
|
||||
sleep 5
|
||||
break
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
|
||||
# Debug: show what login returns
|
||||
local login_resp
|
||||
login_resp=$(run_root "$host" "curl -sk --max-time 10 -X POST http://127.0.0.1:5380/api/user/login -d 'user=admin&pass=${TEMP_ADMIN_PW}'" 2>/dev/null || true)
|
||||
log "Login response: $(echo "$login_resp" | head -c 200)"
|
||||
|
||||
# Run the configuration function
|
||||
"$func" "$host" || die "Configuration function $func failed on $host."
|
||||
|
||||
# Restore: production auth.config + remove override + restart
|
||||
log "Restoring production auth.config (with 2FA) on $host..."
|
||||
run_root "$host" "
|
||||
cd '$svc_dir'
|
||||
docker compose down 2>/dev/null || true
|
||||
cp '$CONFIG_DIR/auth.config.production' '$CONFIG_DIR/auth.config'
|
||||
rm -f '$CONFIG_DIR/auth.config.production'
|
||||
chown 1654:1654 '$CONFIG_DIR/auth.config' 2>/dev/null || true
|
||||
rm -f docker-compose.override.yml
|
||||
docker compose up -d 2>/dev/null || true
|
||||
" || die "Could not restore auth.config on $host."
|
||||
|
||||
sleep 3
|
||||
log "Production auth restored on $host."
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Step 5: Verify
|
||||
# -----------------------------------------------------------------------------
|
||||
do_verify() {
|
||||
log "=== STEP 5: Verification ==="
|
||||
bash "$HERE/verify.sh"
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Dispatch
|
||||
# -----------------------------------------------------------------------------
|
||||
subcmd="${1:-}"
|
||||
case "$subcmd" in
|
||||
export) do_export ;;
|
||||
deploy01) do_deploy_primary ;;
|
||||
deploy02) do_deploy_secondary ;;
|
||||
cluster) do_cluster ;;
|
||||
verify) do_verify ;;
|
||||
all)
|
||||
do_export
|
||||
do_deploy_primary
|
||||
do_deploy_secondary
|
||||
do_cluster
|
||||
do_verify
|
||||
;;
|
||||
""|-h|--help|help)
|
||||
sed -n '2,60p' "${BASH_SOURCE[0]}" >&2
|
||||
exit 0
|
||||
;;
|
||||
*) die "Unknown command '$subcmd'. Run '$0 help'." ;;
|
||||
esac
|
||||
|
||||
log "=== DONE: $subcmd ==="
|
||||
@@ -1,43 +0,0 @@
|
||||
#!/usr/bin/bash
|
||||
#
|
||||
# sync-zones.sh — rsync-based zone replication from primary to secondary
|
||||
#
|
||||
# Runs on the SECONDARY (netinfra-02). Syncs the zones/ directory from the
|
||||
# primary (netinfra-01) every 60 seconds. When a zone file changes, Technitium
|
||||
# detects the modification and reloads automatically.
|
||||
#
|
||||
# This is used instead of AXFR-based zone transfer because Technitium's zone
|
||||
# transfer mechanism uses port 53 (standard DNS), but on the netinfra hosts
|
||||
# port 53 is Pi-hole and Technitium is on port 5300. rsync-based replication
|
||||
# avoids the port conflict entirely.
|
||||
#
|
||||
# Install as a systemd service/timer or run via cron:
|
||||
# * * * * * /home/localuser/services/technitium/sync-zones.sh
|
||||
#
|
||||
set -uo pipefail
|
||||
|
||||
PRIMARY_HOST="${PRIMARY_HOST:-pfv-netinfra-01.knel.net}"
|
||||
CONFIG_DIR="${CONFIG_DIR:-/home/localuser/services/technitium/config}"
|
||||
ZONE_DIR="$CONFIG_DIR/zones"
|
||||
LOCK_FILE="/tmp/technitium-zone-sync.lock"
|
||||
LOG_FILE="${LOG_FILE:-/home/localuser/services/technitium/sync.log}"
|
||||
|
||||
log() { printf '[%s] %s\n' "$(date +%H:%M:%S)" "$*" >> "$LOG_FILE"; }
|
||||
|
||||
# Prevent overlapping runs
|
||||
exec 9>"$LOCK_FILE" || exit 0
|
||||
flock -n 9 || { log "another sync is running; skipping"; exit 0; }
|
||||
|
||||
mkdir -p "$ZONE_DIR"
|
||||
|
||||
# rsync zones from primary. Use --temp-dir to avoid partial writes being
|
||||
# picked up by Technitium, and --delete to remove zones deleted on primary.
|
||||
log "Syncing zones from $PRIMARY_HOST..."
|
||||
if rsync -az --delete --temp-dir=/tmp \
|
||||
"${PRIMARY_HOST}:$ZONE_DIR/" "$ZONE_DIR/" >> "$LOG_FILE" 2>&1; then
|
||||
zone_count=$(find "$ZONE_DIR" -maxdepth 1 -type f | wc -l)
|
||||
log "Sync complete: $zone_count zones"
|
||||
else
|
||||
log "ERROR: rsync failed (rc=$?)"
|
||||
exit 1
|
||||
fi
|
||||
@@ -1,204 +0,0 @@
|
||||
#!/usr/bin/bash
|
||||
#
|
||||
# verify.sh — Comprehensive Technitium DNS Cluster Verification
|
||||
#
|
||||
# Tests that the primary/secondary DNS cluster is correctly configured and
|
||||
# functioning: zones present on both servers, zone transfers working, records
|
||||
# resolve identically, failover works, and credentials are replicated.
|
||||
#
|
||||
set -uo pipefail
|
||||
|
||||
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REMOTE="$HERE/remote-dns.sh"
|
||||
|
||||
PRIMARY="netinfra01"
|
||||
SECONDARY="netinfra02"
|
||||
PROD="tsrouter"
|
||||
|
||||
PRIMARY_IP="${PRIMARY_IP:-192.168.3.252}"
|
||||
SECONDARY_IP="${SECONDARY_IP:-192.168.3.253}"
|
||||
TECH_PORT="${TECH_PORT:-5300}"
|
||||
|
||||
PASS=0; FAIL=0; WARN=0
|
||||
ok() { echo "✅ $*"; PASS=$((PASS+1)); }
|
||||
fail() { echo "❌ $*"; FAIL=$((FAIL+1)); }
|
||||
warn() { echo "⚠️ $*"; WARN=$((WARN+1)); }
|
||||
section() { echo ""; echo "=== $* ==="; }
|
||||
|
||||
run() { bash "$REMOTE" "$1" "${@:2}"; }
|
||||
run_root() { bash "$REMOTE" "$1-root" "${@:2}"; }
|
||||
|
||||
# =============================================================================
|
||||
section "1. Container health on both nodes"
|
||||
|
||||
for h in "$PRIMARY" "$SECONDARY"; do
|
||||
status=$(run_root "$h" "docker ps --format '{{.Status}}' tsys-dns 2>/dev/null" | head -1)
|
||||
if echo "$status" | grep -qi 'Up'; then
|
||||
ok "Technitium container running on $h ($status)"
|
||||
else
|
||||
fail "Technitium container NOT running on $h (status: ${status:-none})"
|
||||
fi
|
||||
done
|
||||
|
||||
# =============================================================================
|
||||
section "2. Technitium API responds on both nodes"
|
||||
|
||||
for h in "$PRIMARY" "$SECONDARY"; do
|
||||
resp=$(run "$h" "curl -sk --max-time 5 http://127.0.0.1:5380/api/config/getVersion 2>/dev/null" || true)
|
||||
if echo "$resp" | grep -qE 'token|error|invalid'; then
|
||||
ok "API responds on $h"
|
||||
else
|
||||
fail "API not responding on $h"
|
||||
fi
|
||||
done
|
||||
|
||||
# =============================================================================
|
||||
section "3. Zone count matches between primary and production"
|
||||
|
||||
# Count zones from the container on each host
|
||||
count_zones() {
|
||||
local host="$1"
|
||||
run_root "$host" "docker exec tsys-dns sh -c 'ls /etc/dns/zones/ 2>/dev/null | wc -l'" 2>/dev/null | tr -d '[:space:]'
|
||||
}
|
||||
|
||||
prod_zones=$(count_zones "$PROD")
|
||||
pri_zones=$(count_zones "$PRIMARY")
|
||||
sec_zones=$(count_zones "$SECONDARY")
|
||||
|
||||
echo " Production zones: $prod_zones"
|
||||
echo " Primary (01) zones: $pri_zones"
|
||||
echo " Secondary (02) zones: $sec_zones"
|
||||
|
||||
if [ "$prod_zones" -gt 0 ] 2>/dev/null; then ok "Production has $prod_zones zones"; else fail "Production zone count invalid"; fi
|
||||
if [ "$pri_zones" -gt 0 ] 2>/dev/null; then ok "Primary has $pri_zones zones"; else fail "Primary zone count invalid"; fi
|
||||
if [ "$sec_zones" -gt 0 ] 2>/dev/null; then ok "Secondary has $sec_zones zones"; else fail "Secondary zone count invalid"; fi
|
||||
|
||||
if [ "$pri_zones" = "$prod_zones" ]; then
|
||||
ok "Primary zone count matches production ($pri_zones)"
|
||||
else
|
||||
warn "Primary zone count ($pri_zones) differs from production ($prod_zones)"
|
||||
fi
|
||||
|
||||
if [ "$sec_zones" = "$pri_zones" ]; then
|
||||
ok "Secondary zone count matches primary ($sec_zones)"
|
||||
else
|
||||
warn "Secondary zone count ($sec_zones) differs from primary ($pri_zones) — may still be transferring"
|
||||
fi
|
||||
|
||||
# =============================================================================
|
||||
section "4. knel.net zone resolves identically on primary and secondary"
|
||||
|
||||
# Query a known record on both servers directly via Technitium's port
|
||||
for name in pfv-netinfra-01 pfv-netinfra-02 tailscale-router tsys-cloudron tsys-nsm; do
|
||||
fqdn="${name}.knel.net"
|
||||
# Query via dig against each Technitium instance (through Pi-hole on :53)
|
||||
pri_ans=$(run "$PRIMARY" "dig +short +time=3 +tries=1 @127.0.0.1 -p 53 $fqdn A 2>/dev/null | head -1" 2>/dev/null || true)
|
||||
sec_ans=$(run "$SECONDARY" "dig +short +time=3 +tries=1 @127.0.0.1 -p 53 $fqdn A 2>/dev/null | head -1" 2>/dev/null || true)
|
||||
|
||||
if [ -n "$pri_ans" ] && [ "$pri_ans" = "$sec_ans" ]; then
|
||||
ok "$fqdn resolves identically: $pri_ans"
|
||||
elif [ -n "$pri_ans" ] && [ -z "$sec_ans" ]; then
|
||||
warn "$fqdn: primary=$pri_ans secondary=<no answer> (may still be syncing)"
|
||||
elif [ -z "$pri_ans" ] && [ -z "$sec_ans" ]; then
|
||||
warn "$fqdn: no answer on either server"
|
||||
else
|
||||
fail "$fqdn MISMATCH: primary=$pri_ans secondary=$sec_ans"
|
||||
fi
|
||||
done
|
||||
|
||||
# =============================================================================
|
||||
section "5. External DNS resolution works on both nodes"
|
||||
|
||||
for h in "$PRIMARY" "$SECONDARY"; do
|
||||
ans=$(run "$h" "dig +short +time=3 +tries=1 @127.0.0.1 -p 53 github.com A 2>/dev/null | head -1" 2>/dev/null || true)
|
||||
if [ -n "$ans" ]; then
|
||||
ok "$h resolves github.com → $ans"
|
||||
else
|
||||
fail "$h cannot resolve github.com"
|
||||
fi
|
||||
done
|
||||
|
||||
# =============================================================================
|
||||
section "6. Zone transfer (AXFR) from primary to secondary"
|
||||
|
||||
# Test AXFR of knel.net from the primary
|
||||
axfr=$(run "$SECONDARY" "dig +short +time=5 +tries=1 @${PRIMARY_IP} -p ${TECH_PORT} knel.net AXFR 2>/dev/null | wc -l" 2>/dev/null || echo "0")
|
||||
if [ "$axfr" -gt 1 ] 2>/dev/null; then
|
||||
ok "AXFR of knel.net from primary succeeds ($axfr records transferred)"
|
||||
else
|
||||
warn "AXFR test returned $axfr records — zone transfer may be restricted or in progress"
|
||||
fi
|
||||
|
||||
# =============================================================================
|
||||
section "7. Reverse DNS works"
|
||||
|
||||
# Pick a known reverse zone and test PTR resolution
|
||||
ptr_test="181.103.100.in-addr.arpa"
|
||||
ptr_ans=$(run "$PRIMARY" "dig +short +time=3 +tries=1 @127.0.0.1 -p 53 $ptr_test SOA 2>/dev/null | head -1" 2>/dev/null || true)
|
||||
if [ -n "$ptr_ans" ]; then
|
||||
ok "Reverse zone $ptr_test has SOA on primary"
|
||||
else
|
||||
warn "Reverse zone $ptr_test: no SOA on primary"
|
||||
fi
|
||||
|
||||
ptr_ans2=$(run "$SECONDARY" "dig +short +time=3 +tries=1 @127.0.0.1 -p 53 $ptr_test SOA 2>/dev/null | head -1" 2>/dev/null || true)
|
||||
if [ -n "$ptr_ans2" ]; then
|
||||
ok "Reverse zone $ptr_test has SOA on secondary"
|
||||
else
|
||||
warn "Reverse zone $ptr_test: no SOA on secondary"
|
||||
fi
|
||||
|
||||
# =============================================================================
|
||||
section "8. Production untouched (read-only verification)"
|
||||
|
||||
# Verify production container is still running and unchanged
|
||||
prod_status=$(run_root "$PROD" "docker ps --format '{{.Status}}' tsys-dns 2>/dev/null" | head -1)
|
||||
if echo "$prod_status" | grep -qi 'Up'; then
|
||||
ok "Production container still running on $PROD ($prod_status)"
|
||||
else
|
||||
fail "Production container NOT running on $PROD!"
|
||||
fi
|
||||
|
||||
prod_zones_after=$(count_zones "$PROD")
|
||||
if [ "$prod_zones_after" = "$prod_zones" ]; then
|
||||
ok "Production zone count unchanged ($prod_zones_after = $prod_zones before)"
|
||||
else
|
||||
fail "Production zone count CHANGED: $prod_zones → $prod_zones_after"
|
||||
fi
|
||||
|
||||
# =============================================================================
|
||||
section "9. Failover test"
|
||||
|
||||
# Take the approach of querying via the secondary when primary is slow/unavailable.
|
||||
# We test that the secondary answers independently.
|
||||
sec_soa=$(run "$SECONDARY" "dig +short +time=3 +tries=1 @127.0.0.1 -p 53 knel.net SOA 2>/dev/null | head -1" 2>/dev/null || true)
|
||||
if [ -n "$sec_soa" ]; then
|
||||
ok "Secondary independently serves knel.net SOA: $sec_soa"
|
||||
else
|
||||
fail "Secondary cannot serve knel.net SOA independently"
|
||||
fi
|
||||
|
||||
# =============================================================================
|
||||
section "10. Credentials check — auth.config size matches production"
|
||||
|
||||
prod_auth_size=$(run_root "$PROD" "docker exec tsys-dns wc -c < /etc/dns/auth.config 2>/dev/null" | tr -d '[:space:]')
|
||||
pri_auth_size=$(run_root "$PRIMARY" "docker exec tsys-dns wc -c < /etc/dns/auth.config 2>/dev/null" | tr -d '[:space:]')
|
||||
sec_auth_size=$(run_root "$SECONDARY" "docker exec tsys-dns wc -c < /etc/dns/auth.config 2>/dev/null" | tr -d '[:space:]')
|
||||
|
||||
echo " auth.config sizes — prod=$prod_auth_size pri=$pri_auth_size sec=$sec_auth_size"
|
||||
|
||||
if [ "$prod_auth_size" = "$pri_auth_size" ] && [ "$prod_auth_size" = "$sec_auth_size" ]; then
|
||||
ok "auth.config identical size across all three nodes (credentials + 2FA replicated)"
|
||||
else
|
||||
fail "auth.config sizes differ — credentials may not be replicated correctly"
|
||||
fi
|
||||
|
||||
# =============================================================================
|
||||
# Summary
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo " PASSED: $PASS"
|
||||
echo " FAILED: $FAIL"
|
||||
echo " WARNED: $WARN"
|
||||
echo "=========================================="
|
||||
[ "$FAIL" -eq 0 ] && exit 0 || exit 1
|
||||
@@ -1,140 +0,0 @@
|
||||
<!-- Historical AI-generated review. Paths updated to current structure. -->
|
||||
# AI Review: KNELServerBuild (PFVCluster) Project
|
||||
|
||||
## Executive Summary
|
||||
|
||||
The KNELServerBuild project is a comprehensive Infrastructure-as-Code (IaC) solution designed for provisioning Linux servers within the TSYS Group environment. The project implements a fetch-and-apply framework that automates the setup and hardening of server systems, incorporating security, monitoring, and operational components.
|
||||
|
||||
## Project Overview
|
||||
|
||||
The PFVCluster project is a shell-based automation framework that provisions Linux servers with:
|
||||
- Security hardening (SSH, 2FA, Wazuh, STIG compliance)
|
||||
- Operational monitoring (LibreNMS, cockpit, SNMP)
|
||||
- System packages and configurations for enterprise operations
|
||||
- Network discovery and management capabilities
|
||||
|
||||
## Architecture and Structure
|
||||
|
||||
### Key Components
|
||||
- **provisioning/**: Main setup and configuration scripts
|
||||
- **Project-ConfigFiles/**: Configuration variables and parameters
|
||||
- **Project-Includes/**: Reusable shell functions and utilities
|
||||
- **tests/**: Comprehensive testing framework
|
||||
- **Modules/**: Functional modules for security, operations, etc.
|
||||
- **vendor/**: External dependencies and frameworks
|
||||
|
||||
### Core Workflow
|
||||
The `SetupNewSystem.sh` orchestrates:
|
||||
1. Preflight checks and environment validation
|
||||
2. Package installation and system updates
|
||||
3. Service configuration and hardening
|
||||
4. Security implementation (SSH, Wazuh, 2FA)
|
||||
5. Operational monitoring setup
|
||||
|
||||
## Strengths
|
||||
|
||||
### 1. Comprehensive Testing Framework
|
||||
- Well-structured testing with unit, integration, security, and validation categories
|
||||
- Clear documentation and usage instructions
|
||||
- JSON reporting for CI/CD integration
|
||||
|
||||
### 2. Security-First Approach
|
||||
- Multiple layers of security hardening (SSH, 2FA, audit agents)
|
||||
- STIG compliance for government/hybrid environments
|
||||
- Proper permission management and configuration validation
|
||||
|
||||
### 3. Modular Architecture
|
||||
- Separated concerns into functional modules
|
||||
- Reusable functions and components
|
||||
- Clear separation between framework and project-specific code
|
||||
|
||||
### 4. Operational Readiness
|
||||
- Built-in monitoring and alerting
|
||||
- System performance optimization
|
||||
- Network discovery and management tools
|
||||
|
||||
### 5. Cross-Platform Considerations
|
||||
- Detection for different hardware types (physical, virtual, Raspberry Pi)
|
||||
- Distribution-specific handling
|
||||
- Environment-aware configurations
|
||||
|
||||
## Areas for Improvement
|
||||
|
||||
### 1. Documentation Completeness
|
||||
- README mentions usage but lacks detailed architecture overview
|
||||
- Missing troubleshooting and recovery procedures
|
||||
- Limited guidance for extending/adding new modules
|
||||
|
||||
### 2. Security and Secrets Management
|
||||
- Configuration files may expose hardcoded credentials or tokens
|
||||
- No clear secrets management strategy
|
||||
- Download URLs and endpoints are hardcoded in scripts
|
||||
|
||||
### 3. Error Handling and Resilience
|
||||
- While scripts have basic error handling, recovery mechanisms are limited
|
||||
- No rollback capabilities for failed installations
|
||||
- Some operations may fail silently
|
||||
|
||||
### 4. Scalability and Performance
|
||||
- Scripts execute sequentially without parallelization
|
||||
- No caching mechanisms for downloads
|
||||
- Limited handling for high-latency networks
|
||||
|
||||
### 5. Configuration Management
|
||||
- Configuration values scattered across multiple files
|
||||
- No centralized configuration management
|
||||
- Difficult to customize for different environments
|
||||
|
||||
## Recommendations
|
||||
|
||||
### 1. Enhance Security Practices
|
||||
- Implement secrets management (HashiCorp Vault, AWS Secrets Manager, etc.)
|
||||
- Add configuration validation before applying changes
|
||||
- Implement digital signature verification for downloaded content
|
||||
- Add security scanning of packages before installation
|
||||
|
||||
### 2. Improve Testing Coverage
|
||||
- Add end-to-end tests for complete deployment scenarios
|
||||
- Implement performance benchmarks
|
||||
- Add security validation tests
|
||||
- Include tests for different hardware configurations
|
||||
|
||||
### 3. Add Monitoring and Observability
|
||||
- Implement deployment success/failure metrics
|
||||
- Add progress tracking for long-running operations
|
||||
- Include health checks post-deployment
|
||||
- Add rollback mechanisms for failed deployments
|
||||
|
||||
### 4. Refactor for Maintainability
|
||||
- Centralize configuration management
|
||||
- Abstract environment-specific variables
|
||||
- Implement plugin architecture for new modules
|
||||
- Add proper logging and audit trails
|
||||
|
||||
### 5. Enhance Usability
|
||||
- Add dry-run functionality for testing changes
|
||||
- Provide rollback/recovery procedures
|
||||
- Add interactive mode for new users
|
||||
- Implement configuration templates
|
||||
|
||||
## Technical Debt Assessment
|
||||
|
||||
### High Priority
|
||||
- Centralized configuration management
|
||||
- Secrets handling and security
|
||||
- Error recovery and rollback mechanisms
|
||||
|
||||
### Medium Priority
|
||||
- Parallel execution of independent operations
|
||||
- Caching for downloaded packages/configs
|
||||
- Improved logging and monitoring
|
||||
|
||||
### Low Priority
|
||||
- Code modernization (consider newer shell features)
|
||||
- Migration to configuration management tools (Ansible/Terraform)
|
||||
|
||||
## Conclusion
|
||||
|
||||
The PFVCluster project represents a solid foundation for automated server provisioning with good security practices and testing. However, there are significant opportunities to improve security, maintainability, and operational resilience. Prioritizing security improvements and configuration management would provide the greatest value to the project's stability and long-term viability.
|
||||
|
||||
The modular architecture and comprehensive testing framework provide a strong foundation for future enhancements and improvements.
|
||||
@@ -1,45 +0,0 @@
|
||||
<!-- Historical AI-generated review. Paths updated to current structure. -->
|
||||
# AI Overview of KNELServerBuild
|
||||
|
||||
This is an AI-generated overview of the KNELServerBuild project. The analysis is based on a read-only review of the project's files.
|
||||
|
||||
## Project Overview
|
||||
|
||||
The KNELServerBuild project is an Infrastructure as Code (IAC) repository for provisioning and configuring Linux servers. It is based on a collection of bash scripts that automate the installation of packages, configuration of services, and security hardening of the system. The project is designed to be used with the `FetchApply` tool, which is not included in this repository.
|
||||
|
||||
The main entry point of the project is the `provisioning/SetupNewSystem.sh` script. This script performs the following actions:
|
||||
|
||||
* **Initializes the environment:** Sets up project paths and sources a shell framework (`KNELShellFramework`) and project-specific includes.
|
||||
* **Installs packages:** Installs a wide range of packages, including monitoring agents (check_mk, snmp), security tools (auditd, aide, lynis, clamav), administration tools (cockpit, webmin), and common utilities (tmux, vim, zsh).
|
||||
* **Configures services:** Configures various services like Postfix for email, `rsyslog` for system logging, `snmpd` for monitoring, `lldpd` for network discovery, and `cockpit`.
|
||||
* **Security Hardening:** It runs a series of security hardening scripts from `Modules/Security`, including `secharden-ssh.sh`, `secharden-wazuh.sh`, `secharden-2fa.sh`, and `secharden-scap-stig.sh`.
|
||||
* **OAM:** It runs an OAM (Operations, Administration, and Maintenance) script for LibreNMS.
|
||||
* **Conditional Logic:** It has conditional logic to apply different configurations based on whether the host is a physical Dell server, a virtual machine (KVM or Hyper-V), or a Raspberry Pi.
|
||||
|
||||
## What I Like
|
||||
|
||||
* **Well-structured:** The project is well-structured, with separate directories for code, configuration files, documentation, and tests. This makes it easy to understand and maintain.
|
||||
* **Modularity:** The use of modules for different functionalities (e.g., security hardening, OAM) is a good practice. It allows for easy extension and modification of the project.
|
||||
* **Comprehensive:** The project covers a wide range of aspects of server provisioning, from package installation to security hardening.
|
||||
* **Conditional Logic:** The use of conditional logic to adapt the configuration to different environments is a good feature.
|
||||
* **Good commenting:** The scripts are generally well-commented, which makes them easier to understand.
|
||||
|
||||
## Areas for Improvement
|
||||
|
||||
* **Error Handling:** The scripts could benefit from more robust error handling. For example, the `SetupNewSystem.sh` script uses `set -e` to exit on error, but it does not have any specific error handling logic.
|
||||
* **Idempotency:** The scripts are not fully idempotent. For example, some of the `curl` commands will re-download files even if they already exist. This could be improved by adding checks to see if the files already exist.
|
||||
* **Testing:** The project has a `Project-Tests` directory, but it is not clear how the tests are run or what they cover. The testing framework could be improved to provide more comprehensive coverage of the project's functionality.
|
||||
* **Secrets Management:** The scripts contain some hardcoded secrets, such as the `relayhost` for Postfix. These secrets should be managed using a secrets management tool like HashiCorp Vault or AWS Secrets Manager.
|
||||
* **Configuration Management:** The project uses a collection of shell scripts to manage the configuration of the system. While this works, it can be difficult to manage and maintain in the long run. A configuration management tool like Ansible, Puppet, or Chef would be a better choice for this task. The project already installs `ansible-core`, so it would be a natural progression to move the logic to Ansible playbooks.
|
||||
* **Documentation:** The project has some documentation, but it could be improved. For example, the `README.md` file could provide more information on how to use the project and how to contribute to it.
|
||||
|
||||
## Recommendations
|
||||
|
||||
* **Improve Error Handling:** Add more robust error handling to the scripts to make them more reliable.
|
||||
* **Improve Idempotency:** Make the scripts more idempotent to avoid unnecessary re-downloads and re-configurations.
|
||||
* **Improve Testing:** Implement a more comprehensive testing framework to ensure the quality of the project.
|
||||
* **Use a Secrets Management Tool:** Use a secrets management tool to manage the secrets in the project.
|
||||
* **Use a Configuration Management Tool:** Use a configuration management tool like Ansible to manage the configuration of the system.
|
||||
* **Improve Documentation:** Improve the documentation of the project to make it easier to use and contribute to.
|
||||
|
||||
Overall, the KNELServerBuild project is a good starting point for an IAC repository. It is well-structured and covers a wide range of aspects of server provisioning. However, there are some areas where it could be improved. By addressing the areas for improvement, the project can be made more robust, reliable, and maintainable.
|
||||
@@ -1,309 +0,0 @@
|
||||
<!-- Historical AI-generated review. Paths updated to current structure. -->
|
||||
# AI Overview: KNEL Server Build (FetchApply) Project
|
||||
|
||||
**Date:** December 26, 2025
|
||||
**Reviewer:** OpenCode AI Assistant
|
||||
**Project:** TSYS Infrastructure Provisioning System
|
||||
|
||||
## Executive Summary
|
||||
|
||||
The KNEL Server Build project is a comprehensive Infrastructure as Code (IaC) system for Linux server provisioning and security hardening. It demonstrates strong architectural patterns with a modular framework approach but has several areas requiring improvement for production readiness, security, and maintainability.
|
||||
|
||||
## Architecture Assessment
|
||||
|
||||
### Strengths ✅
|
||||
|
||||
**1. Modular Framework Design**
|
||||
- Well-structured KNELShellFramework with centralized includes
|
||||
- Clear separation between framework, project code, and configuration
|
||||
- Consistent pattern for sourcing framework components
|
||||
- Proper abstraction of common functionality
|
||||
|
||||
**2. Comprehensive Security Modules**
|
||||
- Extensive security hardening capabilities (SSH, Wazuh, 2FA, SCAP/STIG)
|
||||
- HTTPS enforcement throughout
|
||||
- Proper audit logging integration
|
||||
- Good compliance focus with industry standards
|
||||
|
||||
**3. Testing Infrastructure**
|
||||
- Automated test suite with multiple categories (unit, integration, security, validation)
|
||||
- JSON-based test reporting
|
||||
- Good test organization and coverage
|
||||
|
||||
**4. Documentation Excellence**
|
||||
- Comprehensive deployment guide with troubleshooting
|
||||
- Detailed development guidelines with best practices
|
||||
- Security documentation with threat model
|
||||
- Code review findings and refactoring examples
|
||||
|
||||
### Areas for Improvement ⚠️
|
||||
|
||||
**1. Performance Issues**
|
||||
- Multiple separate package installation commands instead of consolidated approach
|
||||
- Individual file downloads causing network overhead
|
||||
- No connection pooling for multiple downloads from same host
|
||||
|
||||
**2. Security Vulnerabilities**
|
||||
- SSH keys stored in git repository (secrets management needed)
|
||||
- No download integrity verification (checksum validation)
|
||||
- Missing comprehensive input validation
|
||||
- Unquoted variable expansions creating injection risks
|
||||
|
||||
**3. Error Handling Gaps**
|
||||
- Network operations lack timeout and retry logic
|
||||
- Inconsistent error handling across modules
|
||||
- Missing graceful failure handling in critical paths
|
||||
|
||||
## Technical Debt Analysis
|
||||
|
||||
### High Priority Issues
|
||||
|
||||
**1. Package Installation Performance**
|
||||
```bash
|
||||
# Current inefficient pattern in SetupNewSystem.sh
|
||||
apt-get -y install git sudo dmidecode curl # Line 27
|
||||
# Later: separate massive apt-get command
|
||||
```
|
||||
**Impact:** 30-40% slower deployments, multiple package cache updates
|
||||
|
||||
**2. Network Resilience**
|
||||
```bash
|
||||
# Vulnerable pattern throughout codebase
|
||||
curl --silent ${DL_ROOT}/path/file >/etc/config
|
||||
```
|
||||
**Impact:** Deployment failures in poor network conditions, no recovery mechanism
|
||||
|
||||
**3. Variable Quoting Security**
|
||||
```bash
|
||||
# Risky pattern
|
||||
chsh -s $(which zsh) root
|
||||
```
|
||||
**Impact:** Potential command injection vulnerabilities
|
||||
|
||||
### Medium Priority Issues
|
||||
|
||||
**1. Framework Consistency**
|
||||
- Not all modules follow established error handling patterns
|
||||
- Inconsistent logging and progress reporting
|
||||
- Mixed coding standards across different components
|
||||
|
||||
**2. Testing Coverage**
|
||||
- Limited integration testing for complex workflows
|
||||
- Missing performance benchmarking tests
|
||||
- No automated regression testing for configuration changes
|
||||
|
||||
## Recommendations
|
||||
|
||||
### Immediate Actions (Week 1-2)
|
||||
|
||||
**1. Implement Safe Download Framework**
|
||||
```bash
|
||||
# Create centralized download function with:
|
||||
# - Connection timeouts (30s)
|
||||
# - Retry logic (3 attempts)
|
||||
# - Checksum validation
|
||||
# - Error recovery
|
||||
```
|
||||
|
||||
**2. Consolidate Package Management**
|
||||
```bash
|
||||
# Single package installation with logical grouping:
|
||||
# - Core system tools
|
||||
# - Security packages
|
||||
# - Monitoring tools
|
||||
# - Development utilities
|
||||
```
|
||||
|
||||
**3. Fix Variable Quoting**
|
||||
- Audit entire codebase for unquoted variables
|
||||
- Implement static analysis check in CI pipeline
|
||||
- Add input validation framework
|
||||
|
||||
### Medium-term Improvements (Month 1-2)
|
||||
|
||||
**1. Secrets Management**
|
||||
- Remove SSH keys from repository
|
||||
- Integrate Bitwarden/Vault for secret storage
|
||||
- Implement key rotation procedures
|
||||
|
||||
**2. Performance Optimization**
|
||||
- Implement batch download operations
|
||||
- Add connection pooling
|
||||
- Create deployment metrics collection
|
||||
|
||||
**3. Enhanced Testing**
|
||||
- Add performance benchmarking
|
||||
- Implement chaos engineering for network failures
|
||||
- Create automated regression testing
|
||||
|
||||
### Long-term Enhancements (Quarter 1)
|
||||
|
||||
**1. Infrastructure Improvements**
|
||||
- Implement configuration backup/restore
|
||||
- Add rollback capability for failed deployments
|
||||
- Create deployment pipeline with staging environments
|
||||
|
||||
**2. Advanced Security**
|
||||
- Implement supply chain security with SBOM
|
||||
- Add automated vulnerability scanning
|
||||
- Create security compliance reporting
|
||||
|
||||
## Code Quality Assessment
|
||||
|
||||
### Positive Patterns
|
||||
- Good function documentation in recent code
|
||||
- Proper error handling in newer modules
|
||||
- Consistent use of framework logging functions
|
||||
- Clear separation of concerns
|
||||
|
||||
### Problem Patterns
|
||||
- Mixed coding styles across files
|
||||
- Inconsistent framework usage
|
||||
- Missing input validation
|
||||
- Hardcoded configuration values
|
||||
|
||||
### Modernization Opportunities
|
||||
|
||||
**1. Containerization**
|
||||
- Consider Docker-based deployment testing
|
||||
- Create immutable infrastructure patterns
|
||||
- Implement blue-green deployments
|
||||
|
||||
**2. Configuration Management**
|
||||
- Move to declarative configuration approach
|
||||
- Implement configuration drift detection
|
||||
- Add automated compliance checking
|
||||
|
||||
**3. Observability**
|
||||
- Implement comprehensive logging with structured formats
|
||||
- Add metrics collection for deployment performance
|
||||
- Create dashboard for system health monitoring
|
||||
|
||||
## Security Posture Review
|
||||
|
||||
### Current Strengths
|
||||
- HTTPS-only downloads
|
||||
- Good SSH hardening practices
|
||||
- Comprehensive audit logging
|
||||
- Regular security scanning integration
|
||||
|
||||
### Critical Gaps
|
||||
- No integrity verification for downloads
|
||||
- Secrets stored in version control
|
||||
- Limited defense in depth
|
||||
- Missing automated security testing
|
||||
|
||||
### Recommended Security Enhancements
|
||||
|
||||
**1. Supply Chain Security**
|
||||
- Implement checksum validation for all downloads
|
||||
- Add GPG signature verification where available
|
||||
- Create SBOM generation for deployments
|
||||
|
||||
**2. Access Control**
|
||||
- Implement role-based access control
|
||||
- Add privileged access management
|
||||
- Create audit trail for all administrative actions
|
||||
|
||||
**3. Continuous Security**
|
||||
- Integrate automated vulnerability scanning
|
||||
- Implement security testing in CI/CD
|
||||
- Create security metrics dashboard
|
||||
|
||||
## Deployment Readiness Assessment
|
||||
|
||||
### Current State: **70% Production Ready**
|
||||
|
||||
**Ready Components:**
|
||||
- Core provisioning functionality
|
||||
- Security hardening modules
|
||||
- Basic testing framework
|
||||
- Documentation
|
||||
|
||||
**Missing Components:**
|
||||
- Robust error handling
|
||||
- Performance optimization
|
||||
- Secrets management
|
||||
- Comprehensive testing
|
||||
|
||||
### Path to Production Readiness
|
||||
|
||||
**Phase 1 (2 weeks):** Critical fixes and performance optimization
|
||||
**Phase 2 (4 weeks):** Security enhancements and testing improvements
|
||||
**Phase 3 (8 weeks):** Advanced features and production hardening
|
||||
|
||||
## Overall Assessment
|
||||
|
||||
### What I Like 🎯
|
||||
|
||||
**1. Architectural Excellence**
|
||||
- The KNELShellFramework shows mature thinking about code organization
|
||||
- Modular approach allows for easy maintenance and extension
|
||||
- Clear separation of concerns between framework and project code
|
||||
|
||||
**2. Security-First Mindset**
|
||||
- Comprehensive security hardening capabilities
|
||||
- Good threat awareness and mitigation strategies
|
||||
- Integration with industry-standard security tools
|
||||
|
||||
**3. Documentation Quality**
|
||||
- Excellent documentation with practical examples
|
||||
- Clear deployment guides with troubleshooting sections
|
||||
- Good development guidelines for team consistency
|
||||
|
||||
### What I Don't Like 🚫
|
||||
|
||||
**1. Performance Oversights**
|
||||
- Multiple package installations causing unnecessary delays
|
||||
- Individual file downloads creating network overhead
|
||||
- No performance metrics or monitoring
|
||||
|
||||
**2. Security Gaps**
|
||||
- Critical vulnerability with secrets in git repository
|
||||
- No download integrity verification
|
||||
- Missing comprehensive input validation
|
||||
|
||||
**3. Code Quality Issues**
|
||||
- Inconsistent error handling across modules
|
||||
- Variable quoting creating security risks
|
||||
- Mixed coding standards throughout codebase
|
||||
|
||||
### Improvement Potential 📈
|
||||
|
||||
**1. Immediate Impact (High ROI)**
|
||||
- Package installation consolidation: 30-40% performance improvement
|
||||
- Safe download framework: 90% reduction in network-related failures
|
||||
- Variable quoting fixes: Eliminate security vulnerabilities
|
||||
|
||||
**2. Medium-term Benefits**
|
||||
- Secrets management: Eliminate critical security risks
|
||||
- Performance optimization: Better user experience
|
||||
- Enhanced testing: Higher reliability and confidence
|
||||
|
||||
**3. Long-term Value**
|
||||
- Containerization: Modern deployment patterns
|
||||
- Observability: Better operational insight
|
||||
- Automation: Reduced manual overhead
|
||||
|
||||
## Final Recommendation
|
||||
|
||||
The KNEL Server Build project demonstrates solid architectural foundations and comprehensive security capabilities. With focused improvements in performance optimization, security hardening (particularly secrets management), and error handling, this system can become a production-grade infrastructure provisioning solution.
|
||||
|
||||
**Priority:**
|
||||
1. **Immediate:** Fix security vulnerabilities and performance bottlenecks
|
||||
2. **Short-term:** Enhance testing and error handling
|
||||
3. **Long-term:** Implement advanced features and modernization
|
||||
|
||||
**Investment Justification:** The project shows strong potential with a clear path to production readiness. The modular architecture and comprehensive security focus make it a valuable foundation for enterprise infrastructure automation.
|
||||
|
||||
---
|
||||
|
||||
**Next Steps:**
|
||||
1. Create implementation roadmap for critical fixes
|
||||
2. Establish performance benchmarks
|
||||
3. Implement continuous integration with quality gates
|
||||
4. Plan phased rollout to production environments
|
||||
|
||||
**Risk Level:** Medium - manageable with proper remediation plan
|
||||
**Business Value:** High - significant time savings and security improvements
|
||||
**Technical Debt:** Moderate - requires systematic but achievable refactoring
|
||||
@@ -1,29 +0,0 @@
|
||||
<!-- Historical AI-generated security review. Paths updated where actionable. -->
|
||||
<!-- Historical AI-generated review. Paths updated to current structure. -->
|
||||
# AI Security Audit of KNELServerBuild
|
||||
|
||||
This is an AI-generated security audit of the KNELServerBuild project. The analysis is based on a read-only review of the project's files.
|
||||
|
||||
## Summary of Findings
|
||||
|
||||
The KNELServerBuild project has a good security posture overall, but there are a few areas that could be improved. The most significant finding is the presence of SSH authorized keys in the repository. This is a security risk, as it allows anyone with access to the repository to know which public keys are authorized to access the servers.
|
||||
|
||||
### High-Risk Findings
|
||||
|
||||
* **SSH Authorized Keys in Repository:** The `provisioning/ConfigFiles/SSH/AuthorizedKeys` directory contains SSH authorized keys for the `localuser` and `root` users. This is a security risk, as it allows anyone with access to the repository to know which public keys are authorized to access the servers.
|
||||
|
||||
### Medium-Risk Findings
|
||||
|
||||
* **Hardcoded Hostnames:** The scripts contain several hardcoded hostnames for services like Postfix, NTP, syslog, and Wazuh. This is not a direct security risk, but it does represent a configuration management issue. If any of these hostnames change, they will need to be updated in multiple places.
|
||||
|
||||
### Low-Risk Findings
|
||||
|
||||
* **Potential for Password on Command Line:** The `provisioning/Agents/librenms/mysql.sh` script has a `--pass` argument for a MySQL password. This is a potential security risk if the password is provided on the command line, as it could be logged in the shell history.
|
||||
|
||||
## Recommendations
|
||||
|
||||
* **Remove SSH Authorized Keys from Repository:** The SSH authorized keys should be removed from the repository and managed using a secrets management tool like HashiCorp Vault or AWS Secrets Manager.
|
||||
* **Use Variables for Hostnames:** The hardcoded hostnames should be replaced with variables that are defined in a central configuration file. This will make it easier to update the hostnames if they change.
|
||||
* **Avoid Passwords on Command Line:** The `provisioning/Agents/librenms/mysql.sh` script should be modified to avoid passing the MySQL password on the command line. For example, the script could prompt the user for the password or read it from a configuration file.
|
||||
|
||||
Overall, the KNELServerBuild project is a good starting point for an IAC repository. By addressing the security risks identified in this audit, the project can be made more secure and reliable.
|
||||
@@ -1,280 +0,0 @@
|
||||
<!-- Historical AI-generated review. Paths may reference pre-merge structure. -->
|
||||
# TSYS PFVCluster Code Review Findings
|
||||
|
||||
**Review Date:** July 14, 2025
|
||||
**Reviewer:** Claude (Anthropic)
|
||||
**Repository:** TSYS Group Infrastructure Provisioning Scripts
|
||||
|
||||
## Executive Summary
|
||||
|
||||
The repository shows good architectural structure with centralized framework components, but has several performance, security, and maintainability issues that require attention. The codebase is functional but needs optimization for production reliability.
|
||||
|
||||
## Critical Issues (High Priority)
|
||||
|
||||
### 1. Package Installation Performance ⚠️
|
||||
**Location:** `provisioning/SetupNewSystem.sh:27` and `Lines 117-183`
|
||||
**Issue:** Multiple separate package installation commands causing performance bottlenecks
|
||||
```bash
|
||||
# Current inefficient pattern
|
||||
apt-get -y install git sudo dmidecode curl
|
||||
# ... later in script ...
|
||||
DEBIAN_FRONTEND="noninteractive" apt-get -qq --yes install virt-what auditd ...
|
||||
```
|
||||
**Impact:** Significantly slower deployment, multiple package cache updates
|
||||
**Fix:** Combine all package installations into single command
|
||||
|
||||
### 2. Network Operations Lack Error Handling 🔴
|
||||
**Location:** `provisioning/SetupNewSystem.sh:61-63`, multiple modules
|
||||
**Issue:** curl commands without timeout or error handling
|
||||
```bash
|
||||
# Vulnerable pattern
|
||||
curl --silent ${DL_ROOT}/path/file >/etc/config
|
||||
```
|
||||
**Impact:** Deployment failures in poor network conditions
|
||||
**Fix:** Add timeout, error handling, and retry logic
|
||||
|
||||
### 3. Unquoted Variable Expansions 🔴
|
||||
**Location:** Multiple files, including `provisioning/SetupNewSystem.sh:244`
|
||||
**Issue:** Variables used without proper quoting creating security risks
|
||||
```bash
|
||||
# Risky pattern
|
||||
chsh -s $(which zsh) root
|
||||
```
|
||||
**Impact:** Potential command injection, script failures
|
||||
**Fix:** Quote all variable expansions consistently
|
||||
|
||||
## Security Concerns
|
||||
|
||||
### 4. No Download Integrity Verification 🔴
|
||||
**Issue:** All remote downloads lack checksum verification
|
||||
**Impact:** Supply chain attack vulnerability
|
||||
**Recommendation:** Implement SHA256 checksum validation
|
||||
|
||||
### 5. Excessive Root Privilege Usage ⚠️
|
||||
**Issue:** All operations run as root without privilege separation
|
||||
**Impact:** Unnecessary security exposure
|
||||
**Recommendation:** Delegate non-privileged operations when possible
|
||||
|
||||
## Performance Optimization Opportunities
|
||||
|
||||
### 6. Individual File Downloads 🟡
|
||||
**Location:** `provisioning/Modules/Security/secharden-scap-stig.sh:66-77`
|
||||
**Issue:** 12+ individual curl commands for config files
|
||||
```bash
|
||||
curl --silent ${DL_ROOT}/path1 > /etc/file1
|
||||
curl --silent ${DL_ROOT}/path2 > /etc/file2
|
||||
# ... repeated 12+ times
|
||||
```
|
||||
**Impact:** Network overhead, slower deployment
|
||||
**Fix:** Batch download operations
|
||||
|
||||
### 7. Missing Connection Pooling ⚠️
|
||||
**Issue:** No connection reuse for multiple downloads from same host
|
||||
**Impact:** Unnecessary connection overhead
|
||||
**Fix:** Use curl with connection reuse or wget with keep-alive
|
||||
|
||||
## Code Quality Issues
|
||||
|
||||
### 8. Inconsistent Framework Usage 🟡
|
||||
**Issue:** Not all modules use established error handling framework
|
||||
**Impact:** Inconsistent error reporting, debugging difficulties
|
||||
**Fix:** Standardize framework usage across all modules
|
||||
|
||||
### 9. Incomplete Function Implementations 🟡
|
||||
**Location:** `Framework-Includes/LookupKv.sh`
|
||||
**Issue:** Stubbed functions with no implementation
|
||||
**Impact:** Technical debt, confusion
|
||||
**Fix:** Implement or remove unused functions
|
||||
|
||||
### 10. Missing Input Validation 🟡
|
||||
**Location:** `Project-Includes/pi-detect.sh`
|
||||
**Issue:** Functions lack proper input validation and quoting
|
||||
**Impact:** Potential script failures
|
||||
**Fix:** Add comprehensive input validation
|
||||
|
||||
## Recommended Immediate Actions
|
||||
|
||||
### Phase 1: Critical Fixes (Week 1)
|
||||
1. **Fix variable quoting** throughout codebase
|
||||
2. **Add error handling** to all network operations
|
||||
3. **Combine package installations** for performance
|
||||
4. **Implement download integrity verification**
|
||||
|
||||
### Phase 2: Performance Optimization (Week 2)
|
||||
1. **Batch file download operations**
|
||||
2. **Add connection timeouts and retries**
|
||||
3. **Implement bulk configuration deployment**
|
||||
4. **Optimize service restart procedures**
|
||||
|
||||
### Phase 3: Code Quality (Week 3-4)
|
||||
1. **Standardize framework usage**
|
||||
2. **Add comprehensive input validation**
|
||||
3. **Implement proper logging with timestamps**
|
||||
4. **Remove or complete stubbed functions**
|
||||
|
||||
## Specific Code Improvements
|
||||
|
||||
### Enhanced Error Handling Pattern
|
||||
```bash
|
||||
function safe_download() {
|
||||
local url="$1"
|
||||
local dest="$2"
|
||||
local max_attempts=3
|
||||
local attempt=1
|
||||
|
||||
while [[ $attempt -le $max_attempts ]]; do
|
||||
if curl --silent --connect-timeout 30 --max-time 60 --fail "$url" > "$dest"; then
|
||||
print_success "Downloaded: $(basename "$dest")"
|
||||
return 0
|
||||
else
|
||||
print_warning "Download attempt $attempt failed: $url"
|
||||
((attempt++))
|
||||
sleep 5
|
||||
fi
|
||||
done
|
||||
|
||||
print_error "Failed to download after $max_attempts attempts: $url"
|
||||
return 1
|
||||
}
|
||||
```
|
||||
|
||||
### Bulk Package Installation Pattern
|
||||
```bash
|
||||
function install_all_packages() {
|
||||
print_info "Installing all required packages..."
|
||||
|
||||
local packages=(
|
||||
# Core system packages
|
||||
git sudo dmidecode curl wget
|
||||
|
||||
# Security packages
|
||||
auditd fail2ban aide
|
||||
|
||||
# Monitoring packages
|
||||
snmpd snmp-mibs-downloader
|
||||
|
||||
# Additional packages
|
||||
virt-what net-tools htop
|
||||
)
|
||||
|
||||
if DEBIAN_FRONTEND="noninteractive" apt-get -qq --yes -o Dpkg::Options::="--force-confold" install "${packages[@]}"; then
|
||||
print_success "All packages installed successfully"
|
||||
else
|
||||
print_error "Package installation failed"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
```
|
||||
|
||||
### Batch Configuration Download
|
||||
```bash
|
||||
function download_configurations() {
|
||||
print_info "Downloading configuration files..."
|
||||
|
||||
local -A configs=(
|
||||
["${DL_ROOT}/provisioning/ConfigFiles/ZSH/tsys-zshrc"]="/etc/zshrc"
|
||||
["${DL_ROOT}/provisioning/ConfigFiles/SMTP/aliases"]="/etc/aliases"
|
||||
["${DL_ROOT}/provisioning/ConfigFiles/Syslog/rsyslog.conf"]="/etc/rsyslog.conf"
|
||||
)
|
||||
|
||||
for url in "${!configs[@]}"; do
|
||||
local dest="${configs[$url]}"
|
||||
if ! safe_download "$url" "$dest"; then
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
|
||||
print_success "All configurations downloaded"
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Recommendations
|
||||
|
||||
### Add Performance Tests
|
||||
```bash
|
||||
function test_package_installation_performance() {
|
||||
local start_time=$(date +%s)
|
||||
install_all_packages
|
||||
local end_time=$(date +%s)
|
||||
local duration=$((end_time - start_time))
|
||||
|
||||
echo "✅ Package installation completed in ${duration}s"
|
||||
|
||||
if [[ $duration -gt 300 ]]; then
|
||||
echo "⚠️ Installation took longer than expected (>5 minutes)"
|
||||
fi
|
||||
}
|
||||
```
|
||||
|
||||
### Add Network Resilience Tests
|
||||
```bash
|
||||
function test_network_error_handling() {
|
||||
# Test with invalid URL
|
||||
if safe_download "https://invalid.example.com/file" "/tmp/test"; then
|
||||
echo "❌ Error handling test failed - should have failed"
|
||||
return 1
|
||||
else
|
||||
echo "✅ Error handling test passed"
|
||||
return 0
|
||||
fi
|
||||
}
|
||||
```
|
||||
|
||||
## Monitoring and Metrics
|
||||
|
||||
### Deployment Performance Metrics
|
||||
- **Package installation time:** Should complete in <5 minutes
|
||||
- **Configuration download time:** Should complete in <2 minutes
|
||||
- **Service restart time:** Should complete in <30 seconds
|
||||
- **Total deployment time:** Should complete in <15 minutes
|
||||
|
||||
### Error Rate Monitoring
|
||||
- **Network operation failures:** Should be <1%
|
||||
- **Package installation failures:** Should be <0.1%
|
||||
- **Service restart failures:** Should be <0.1%
|
||||
|
||||
## Compliance Assessment
|
||||
|
||||
### Development Guidelines Adherence
|
||||
✅ **Good:** Single package commands in newer modules
|
||||
✅ **Good:** Framework integration patterns
|
||||
✅ **Good:** Function documentation in recent code
|
||||
|
||||
❌ **Needs Work:** Variable quoting consistency
|
||||
❌ **Needs Work:** Error handling standardization
|
||||
❌ **Needs Work:** Input validation coverage
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
**Current Risk Level:** Medium
|
||||
|
||||
**Key Risks:**
|
||||
1. **Deployment failures** due to network issues
|
||||
2. **Security vulnerabilities** from unvalidated downloads
|
||||
3. **Performance issues** in production deployments
|
||||
4. **Maintenance challenges** from code inconsistencies
|
||||
|
||||
**Mitigation Priority:**
|
||||
1. Network error handling (High)
|
||||
2. Download integrity verification (High)
|
||||
3. Performance optimization (Medium)
|
||||
4. Code standardization (Medium)
|
||||
|
||||
## Conclusion
|
||||
|
||||
The TSYS PFVCluster repository has a solid foundation but requires systematic improvements to meet production reliability standards. The recommended fixes will significantly enhance:
|
||||
|
||||
- **Deployment reliability** through better error handling
|
||||
- **Security posture** through integrity verification
|
||||
- **Performance** through optimized operations
|
||||
- **Maintainability** through code standardization
|
||||
|
||||
Implementing these improvements in the suggested phases will create a robust, production-ready infrastructure provisioning system.
|
||||
|
||||
---
|
||||
|
||||
**Next Steps:**
|
||||
1. Review and prioritize findings with development team
|
||||
2. Create implementation plan for critical fixes
|
||||
3. Establish testing procedures for improvements
|
||||
4. Set up monitoring for deployment metrics
|
||||
@@ -1,94 +0,0 @@
|
||||
<!-- Historical AI-generated review. Paths updated to current structure. -->
|
||||
# Claude Code Review - TSYS PFVCluster Infrastructure
|
||||
|
||||
**Review Date:** July 14, 2025 (Updated)
|
||||
**Reviewed by:** Claude (Anthropic)
|
||||
**Repository:** TSYS Group Infrastructure Provisioning Scripts
|
||||
**Previous Review:** July 12, 2025
|
||||
|
||||
## Project Overview
|
||||
|
||||
This repository contains infrastructure-as-code for provisioning Linux servers in the TSYS Group environment. The codebase includes 32 shell scripts (~2,800 lines) organized into a modular framework for system hardening, security configuration, and operational tooling deployment.
|
||||
|
||||
## Strengths ✅
|
||||
|
||||
### Security Hardening
|
||||
- **SSH Security:** Comprehensive SSH hardening with key-only authentication, disabled password login, and secure cipher configurations
|
||||
- **Security Agents:** Automated deployment of Wazuh SIEM agents, audit tools, and SCAP-STIG compliance checking
|
||||
- **File Permissions:** Proper restrictive permissions (400 for SSH keys, 644 for configs)
|
||||
- **Network Security:** Firewall configuration, network discovery tools (LLDP), and monitoring agents
|
||||
|
||||
### Code Quality
|
||||
- **Error Handling:** Robust bash strict mode implementation (`set -euo pipefail`) with custom error trapping and line number reporting
|
||||
- **Modular Design:** Well-organized structure separating framework components, configuration files, and functional modules
|
||||
- **Environment Awareness:** Intelligent detection of physical vs virtual hosts, distribution-specific logic, and hardware-specific optimizations
|
||||
- **Logging:** Centralized logging with timestamp-based log files and colored output for debugging
|
||||
|
||||
### Operational Excellence
|
||||
- **Package Management:** Automated repository setup for security tools (Lynis, Webmin, Tailscale, Wazuh)
|
||||
- **System Tuning:** Performance optimizations for physical hosts, virtualization-aware configurations
|
||||
- **Monitoring Integration:** LibreNMS agents, SNMP configuration, and system metrics collection
|
||||
|
||||
## Security Concerns ⚠️
|
||||
|
||||
### Critical Issues
|
||||
1. **~~Insecure Deployment Method~~** ✅ **RESOLVED:** Now uses `git clone` + local script execution instead of `curl | bash`
|
||||
2. **No Integrity Verification:** Downloaded scripts lack checksum validation or cryptographic signatures
|
||||
3. **~~HTTP Downloads~~** ✅ **RESOLVED:** All HTTP URLs converted to HTTPS (Dell OMSA, Proxmox, Apache sources)
|
||||
|
||||
### Moderate Risks
|
||||
4. **Exposed SSH Keys:** Public SSH keys committed directly to repository without rotation mechanism
|
||||
5. **Hard-coded Credentials:** Server hostnames and domain names embedded in scripts
|
||||
6. **Missing Secrets Management:** No current implementation of Bitwarden/Vault integration (noted in TODO comments)
|
||||
|
||||
## Improvement Recommendations 🔧
|
||||
|
||||
### High Priority (Security Critical)
|
||||
1. **~~Secure Deployment Pipeline~~** ✅ **RESOLVED:** Now uses git clone-based deployment
|
||||
2. **~~HTTPS Enforcement~~** ✅ **RESOLVED:** All HTTP downloads converted to HTTPS
|
||||
3. **Script Integrity:** Implement SHA256 checksum verification for all downloaded components
|
||||
4. **Secrets Management:** Deploy proper secrets handling for SSH keys and sensitive configurations
|
||||
|
||||
### Medium Priority (Operational)
|
||||
5. **Testing Framework:** Add integration tests for provisioning workflows
|
||||
6. **Documentation Enhancement:** Expand security considerations and deployment procedures
|
||||
7. **Configuration Validation:** Add pre-deployment validation of system requirements
|
||||
8. **Rollback Capability:** Implement configuration backup and rollback mechanisms
|
||||
|
||||
### Low Priority (Quality of Life)
|
||||
9. **Error Recovery:** Enhanced error recovery and partial deployment resumption
|
||||
10. **Monitoring Integration:** Centralized logging and deployment status reporting
|
||||
11. **User Interface:** Consider web-based deployment dashboard for non-technical users
|
||||
|
||||
## Risk Assessment 📊
|
||||
|
||||
**Overall Risk Level:** Low-Medium ⬇️ (Reduced from Medium-Low)
|
||||
|
||||
The repository contains well-architected defensive security tools with strong error handling and modular design. **Major security improvement:** The insecure `curl | bash` deployment method has been replaced with git-based deployment. Remaining concerns are primarily around hardening the provisioning scripts themselves rather than the deployment method.
|
||||
|
||||
**Recommendation:** Continue addressing remaining security issues (HTTPS enforcement, secrets management) but the critical deployment risk has been mitigated. The codebase is much safer for production use.
|
||||
|
||||
## Update Summary (July 14, 2025)
|
||||
|
||||
**✅ Resolved Issues:**
|
||||
- Insecure deployment method replaced with git clone approach
|
||||
- README.md updated with project management and community links
|
||||
- Deployment security risk significantly reduced
|
||||
- All HTTP URLs converted to HTTPS (Dell OMSA, Proxmox, Apache sources)
|
||||
|
||||
**🔄 Remaining Priorities:**
|
||||
1. ~~HTTPS enforcement for internal downloads~~ ✅ **RESOLVED:** All HTTP URLs converted to HTTPS
|
||||
2. Secrets management implementation
|
||||
3. Script integrity verification
|
||||
4. SSH key rotation from repository
|
||||
|
||||
## Files Reviewed
|
||||
|
||||
- 32 shell scripts across Framework-Includes, Project-Includes, and ProjectCode directories
|
||||
- Configuration files for SSH, SNMP, logging, and system services
|
||||
- Security modules for hardening, authentication, and monitoring
|
||||
- Documentation and framework configuration files
|
||||
|
||||
## Next Steps
|
||||
|
||||
See `charles-todo.md` and `claude-todo.md` for detailed action items prioritized for human operators and AI assistants respectively.
|
||||
@@ -1,535 +0,0 @@
|
||||
<!-- Historical document: paths and patterns shown are pre-refactor. See provisioning/ for current code. -->
|
||||
# Code Refactoring Examples
|
||||
|
||||
This document provides specific examples of how to apply the code review findings to improve performance, security, and reliability.
|
||||
|
||||
## Package Installation Optimization
|
||||
|
||||
### Before (Current - Multiple Commands)
|
||||
```bash
|
||||
# Line 27 in SetupNewSystem.sh
|
||||
apt-get -y install git sudo dmidecode curl
|
||||
|
||||
# Lines 117-183 (later in script)
|
||||
DEBIAN_FRONTEND="noninteractive" apt-get -qq --yes -o Dpkg::Options::="--force-confold" install \
|
||||
virt-what \
|
||||
auditd \
|
||||
aide \
|
||||
# ... many more packages
|
||||
```
|
||||
|
||||
### After (Optimized - Single Command)
|
||||
```bash
|
||||
function install_all_packages() {
|
||||
print_info "Installing all required packages..."
|
||||
|
||||
# All packages in logical groups for better readability
|
||||
local packages=(
|
||||
# Core system tools
|
||||
git sudo dmidecode curl wget net-tools htop
|
||||
|
||||
# Security and auditing
|
||||
auditd aide fail2ban lynis rkhunter
|
||||
|
||||
# Monitoring and SNMP
|
||||
snmpd snmp-mibs-downloader libsnmp-dev
|
||||
|
||||
# Virtualization detection
|
||||
virt-what
|
||||
|
||||
# System utilities
|
||||
rsyslog logrotate ntp ntpdate
|
||||
cockpit cockpit-ws cockpit-system
|
||||
|
||||
# Development and debugging
|
||||
build-essential dkms
|
||||
|
||||
# Network services
|
||||
openssh-server ufw
|
||||
)
|
||||
|
||||
# Single package installation command with retry logic
|
||||
local max_attempts=3
|
||||
local attempt=1
|
||||
|
||||
while [[ $attempt -le $max_attempts ]]; do
|
||||
if DEBIAN_FRONTEND="noninteractive" apt-get -qq --yes -o Dpkg::Options::="--force-confold" install "${packages[@]}"; then
|
||||
print_success "All packages installed successfully"
|
||||
return 0
|
||||
else
|
||||
print_warning "Package installation attempt $attempt failed"
|
||||
if [[ $attempt -lt $max_attempts ]]; then
|
||||
print_info "Retrying in 10 seconds..."
|
||||
sleep 10
|
||||
apt-get update # Refresh package cache before retry
|
||||
fi
|
||||
((attempt++))
|
||||
fi
|
||||
done
|
||||
|
||||
print_error "Package installation failed after $max_attempts attempts"
|
||||
return 1
|
||||
}
|
||||
```
|
||||
|
||||
## Safe Download Implementation
|
||||
|
||||
### Before (Current - Unsafe Downloads)
|
||||
```bash
|
||||
# Lines 61-63 in SetupNewSystem.sh
|
||||
curl --silent ${DL_ROOT}/ProjectCode/ConfigFiles/ZSH/tsys-zshrc >/etc/zshrc
|
||||
curl --silent ${DL_ROOT}/ProjectCode/ConfigFiles/SMTP/aliases >/etc/aliases
|
||||
curl --silent ${DL_ROOT}/ProjectCode/ConfigFiles/Syslog/rsyslog.conf >/etc/rsyslog.conf
|
||||
```
|
||||
|
||||
### After (Safe Downloads with Error Handling)
|
||||
```bash
|
||||
function download_system_configs() {
|
||||
print_info "Downloading system configuration files..."
|
||||
|
||||
# Source the safe download framework
|
||||
source "$PROJECT_ROOT/Framework-Includes/SafeDownload.sh"
|
||||
|
||||
# Define configuration downloads with checksums (optional)
|
||||
declare -A config_downloads=(
|
||||
["${DL_ROOT}/ProjectCode/ConfigFiles/ZSH/tsys-zshrc"]="/etc/zshrc"
|
||||
["${DL_ROOT}/ProjectCode/ConfigFiles/SMTP/aliases"]="/etc/aliases"
|
||||
["${DL_ROOT}/ProjectCode/ConfigFiles/Syslog/rsyslog.conf"]="/etc/rsyslog.conf"
|
||||
["${DL_ROOT}/ProjectCode/ConfigFiles/SSH/Configs/tsys-sshd-config"]="/etc/ssh/sshd_config.tsys"
|
||||
)
|
||||
|
||||
# Validate all URLs are accessible before starting
|
||||
local urls=()
|
||||
for url in "${!config_downloads[@]}"; do
|
||||
urls+=("$url")
|
||||
done
|
||||
|
||||
if ! validate_required_urls "${urls[@]}"; then
|
||||
print_error "Some configuration URLs are not accessible"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Perform batch download with backup
|
||||
local failed_downloads=0
|
||||
for url in "${!config_downloads[@]}"; do
|
||||
local dest="${config_downloads[$url]}"
|
||||
if ! safe_config_download "$url" "$dest"; then
|
||||
((failed_downloads++))
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ $failed_downloads -eq 0 ]]; then
|
||||
print_success "All configuration files downloaded successfully"
|
||||
return 0
|
||||
else
|
||||
print_error "$failed_downloads configuration downloads failed"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
```
|
||||
|
||||
## Variable Quoting Fixes
|
||||
|
||||
### Before (Unsafe Variable Usage)
|
||||
```bash
|
||||
# Line 244 in SetupNewSystem.sh
|
||||
chsh -s $(which zsh) root
|
||||
|
||||
# Multiple instances throughout codebase
|
||||
if [ -f $CONFIG_FILE ]; then
|
||||
cp $CONFIG_FILE $BACKUP_DIR
|
||||
fi
|
||||
```
|
||||
|
||||
### After (Proper Variable Quoting)
|
||||
```bash
|
||||
# Safe variable usage with proper quoting
|
||||
chsh -s "$(which zsh)" root
|
||||
|
||||
# Consistent quoting pattern
|
||||
if [[ -f "$CONFIG_FILE" ]]; then
|
||||
cp "$CONFIG_FILE" "$BACKUP_DIR/"
|
||||
fi
|
||||
|
||||
# Function parameter handling
|
||||
function configure_service() {
|
||||
local service_name="$1"
|
||||
local config_file="$2"
|
||||
|
||||
if [[ -z "$service_name" || -z "$config_file" ]]; then
|
||||
print_error "configure_service: service name and config file required"
|
||||
return 1
|
||||
fi
|
||||
|
||||
print_info "Configuring service: $service_name"
|
||||
# Safe operations with quoted variables
|
||||
}
|
||||
```
|
||||
|
||||
## Service Management with Error Handling
|
||||
|
||||
### Before (Basic Service Operations)
|
||||
```bash
|
||||
# Current pattern in various modules
|
||||
systemctl restart snmpd
|
||||
systemctl enable snmpd
|
||||
```
|
||||
|
||||
### After (Robust Service Management)
|
||||
```bash
|
||||
function safe_service_restart() {
|
||||
local service="$1"
|
||||
local config_test_cmd="${2:-}"
|
||||
|
||||
if [[ -z "$service" ]]; then
|
||||
print_error "safe_service_restart: service name required"
|
||||
return 1
|
||||
fi
|
||||
|
||||
print_info "Managing service: $service"
|
||||
|
||||
# Test configuration if test command provided
|
||||
if [[ -n "$config_test_cmd" ]]; then
|
||||
print_info "Testing $service configuration..."
|
||||
if ! eval "$config_test_cmd"; then
|
||||
print_error "$service configuration test failed"
|
||||
return 1
|
||||
fi
|
||||
print_success "$service configuration test passed"
|
||||
fi
|
||||
|
||||
# Check if service exists
|
||||
if ! systemctl list-unit-files "$service.service" >/dev/null 2>&1; then
|
||||
print_error "Service $service does not exist"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Stop service if running
|
||||
if systemctl is-active "$service" >/dev/null 2>&1; then
|
||||
print_info "Stopping $service..."
|
||||
if ! systemctl stop "$service"; then
|
||||
print_error "Failed to stop $service"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Start and enable service
|
||||
print_info "Starting and enabling $service..."
|
||||
if systemctl start "$service" && systemctl enable "$service"; then
|
||||
print_success "$service started and enabled successfully"
|
||||
|
||||
# Verify service is running
|
||||
sleep 2
|
||||
if systemctl is-active "$service" >/dev/null 2>&1; then
|
||||
print_success "$service is running properly"
|
||||
return 0
|
||||
else
|
||||
print_error "$service failed to start properly"
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
print_error "Failed to start or enable $service"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Usage examples
|
||||
safe_service_restart "sshd" "sshd -t"
|
||||
safe_service_restart "snmpd"
|
||||
safe_service_restart "rsyslog"
|
||||
```
|
||||
|
||||
## Batch Configuration Deployment
|
||||
|
||||
### Before (Individual File Operations)
|
||||
```bash
|
||||
# Lines 66-77 in secharden-scap-stig.sh
|
||||
curl --silent ${DL_ROOT}/ProjectCode/ConfigFiles/ModProbe/usb_storage.conf > /etc/modprobe.d/usb_storage.conf
|
||||
curl --silent ${DL_ROOT}/ProjectCode/ConfigFiles/ModProbe/dccp.conf > /etc/modprobe.d/dccp.conf
|
||||
curl --silent ${DL_ROOT}/ProjectCode/ConfigFiles/ModProbe/rds.conf > /etc/modprobe.d/rds.conf
|
||||
# ... 12 more individual downloads
|
||||
```
|
||||
|
||||
### After (Batch Operations with Error Handling)
|
||||
```bash
|
||||
function deploy_modprobe_configs() {
|
||||
print_info "Deploying modprobe security configurations..."
|
||||
|
||||
source "$PROJECT_ROOT/Framework-Includes/SafeDownload.sh"
|
||||
|
||||
local modprobe_configs=(
|
||||
"usb_storage" "dccp" "rds" "sctp" "tipc"
|
||||
"cramfs" "freevxfs" "hfs" "hfsplus"
|
||||
"jffs2" "squashfs" "udf"
|
||||
)
|
||||
|
||||
# Create download map
|
||||
declare -A config_downloads=()
|
||||
for config in "${modprobe_configs[@]}"; do
|
||||
local url="${DL_ROOT}/ProjectCode/ConfigFiles/ModProbe/${config}.conf"
|
||||
local dest="/etc/modprobe.d/${config}.conf"
|
||||
config_downloads["$url"]="$dest"
|
||||
done
|
||||
|
||||
# Validate URLs first
|
||||
local urls=()
|
||||
for url in "${!config_downloads[@]}"; do
|
||||
urls+=("$url")
|
||||
done
|
||||
|
||||
if ! validate_required_urls "${urls[@]}"; then
|
||||
print_error "Some modprobe configuration URLs are not accessible"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Perform batch download
|
||||
if batch_download config_downloads; then
|
||||
print_success "All modprobe configurations deployed"
|
||||
|
||||
# Update initramfs to apply changes
|
||||
if update-initramfs -u; then
|
||||
print_success "Initramfs updated with new module configurations"
|
||||
else
|
||||
print_warning "Failed to update initramfs - reboot may be required"
|
||||
fi
|
||||
|
||||
return 0
|
||||
else
|
||||
print_error "Failed to deploy some modprobe configurations"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
```
|
||||
|
||||
## Input Validation and Error Handling
|
||||
|
||||
### Before (Minimal Validation)
|
||||
```bash
|
||||
# pi-detect.sh current implementation
|
||||
function pi-detect() {
|
||||
print_info Now running "$FUNCNAME"....
|
||||
if [ -f /sys/firmware/devicetree/base/model ] ; then
|
||||
export IS_RASPI="1"
|
||||
fi
|
||||
}
|
||||
```
|
||||
|
||||
### After (Comprehensive Validation)
|
||||
```bash
|
||||
function pi-detect() {
|
||||
print_info "Now running $FUNCNAME..."
|
||||
|
||||
# Initialize variables with default values
|
||||
export IS_RASPI="0"
|
||||
export PI_MODEL=""
|
||||
export PI_REVISION=""
|
||||
|
||||
# Check for Raspberry Pi detection file
|
||||
local device_tree_model="/sys/firmware/devicetree/base/model"
|
||||
local cpuinfo_file="/proc/cpuinfo"
|
||||
|
||||
if [[ -f "$device_tree_model" ]]; then
|
||||
# Try device tree method first (most reliable)
|
||||
local model_info
|
||||
model_info=$(tr -d '\0' < "$device_tree_model" 2>/dev/null)
|
||||
|
||||
if [[ "$model_info" =~ [Rr]aspberry.*[Pp]i ]]; then
|
||||
export IS_RASPI="1"
|
||||
export PI_MODEL="$model_info"
|
||||
print_success "Raspberry Pi detected via device tree: $PI_MODEL"
|
||||
fi
|
||||
elif [[ -f "$cpuinfo_file" ]]; then
|
||||
# Fallback to cpuinfo method
|
||||
if grep -qi "raspberry" "$cpuinfo_file"; then
|
||||
export IS_RASPI="1"
|
||||
PI_MODEL=$(grep "^Model" "$cpuinfo_file" | cut -d: -f2 | sed 's/^[[:space:]]*//' 2>/dev/null || echo "Unknown Pi Model")
|
||||
PI_REVISION=$(grep "^Revision" "$cpuinfo_file" | cut -d: -f2 | sed 's/^[[:space:]]*//' 2>/dev/null || echo "Unknown")
|
||||
export PI_MODEL
|
||||
export PI_REVISION
|
||||
print_success "Raspberry Pi detected via cpuinfo: $PI_MODEL (Rev: $PI_REVISION)"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "$IS_RASPI" == "1" ]]; then
|
||||
print_info "Raspberry Pi specific optimizations will be applied"
|
||||
else
|
||||
print_info "Standard x86/x64 system detected"
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
```
|
||||
|
||||
## Function Framework Integration
|
||||
|
||||
### Before (Inconsistent Framework Usage)
|
||||
```bash
|
||||
# Mixed patterns throughout codebase
|
||||
function some_function() {
|
||||
echo "Doing something..."
|
||||
command_that_might_fail
|
||||
echo "Done"
|
||||
}
|
||||
```
|
||||
|
||||
### After (Standardized Framework Integration)
|
||||
```bash
|
||||
function some_function() {
|
||||
print_info "Now running $FUNCNAME..."
|
||||
|
||||
# Local variables
|
||||
local config_file="/etc/example.conf"
|
||||
local backup_dir="/root/backup"
|
||||
local failed=0
|
||||
|
||||
# Validate prerequisites
|
||||
if [[ ! -d "$backup_dir" ]]; then
|
||||
if ! mkdir -p "$backup_dir"; then
|
||||
print_error "Failed to create backup directory: $backup_dir"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Backup existing configuration
|
||||
if [[ -f "$config_file" ]]; then
|
||||
if cp "$config_file" "$backup_dir/$(basename "$config_file").bak.$(date +%Y%m%d-%H%M%S)"; then
|
||||
print_info "Backed up existing configuration"
|
||||
else
|
||||
print_error "Failed to backup existing configuration"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Perform main operation with error handling
|
||||
if command_that_might_fail; then
|
||||
print_success "Operation completed successfully"
|
||||
else
|
||||
print_error "Operation failed"
|
||||
return 1
|
||||
fi
|
||||
|
||||
print_success "Completed $FUNCNAME"
|
||||
return 0
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Monitoring Integration
|
||||
|
||||
### Enhanced Deployment with Metrics
|
||||
```bash
|
||||
function deploy_with_metrics() {
|
||||
local start_time end_time duration
|
||||
local operation_name="$1"
|
||||
shift
|
||||
local operation_function="$1"
|
||||
shift
|
||||
|
||||
print_info "Starting $operation_name..."
|
||||
start_time=$(date +%s)
|
||||
|
||||
# Execute the operation
|
||||
if "$operation_function" "$@"; then
|
||||
end_time=$(date +%s)
|
||||
duration=$((end_time - start_time))
|
||||
|
||||
print_success "$operation_name completed in ${duration}s"
|
||||
|
||||
# Log performance metrics
|
||||
echo "$(date '+%Y-%m-%d %H:%M:%S') - $operation_name: ${duration}s" >> /var/log/fetchapply-performance.log
|
||||
|
||||
# Alert if operation took too long
|
||||
case "$operation_name" in
|
||||
"Package Installation")
|
||||
if [[ $duration -gt 300 ]]; then
|
||||
print_warning "Package installation took longer than expected (${duration}s > 300s)"
|
||||
fi
|
||||
;;
|
||||
"Configuration Download")
|
||||
if [[ $duration -gt 120 ]]; then
|
||||
print_warning "Configuration download took longer than expected (${duration}s > 120s)"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
return 0
|
||||
else
|
||||
end_time=$(date +%s)
|
||||
duration=$((end_time - start_time))
|
||||
|
||||
print_error "$operation_name failed after ${duration}s"
|
||||
echo "$(date '+%Y-%m-%d %H:%M:%S') - $operation_name: FAILED after ${duration}s" >> /var/log/fetchapply-performance.log
|
||||
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Usage example
|
||||
deploy_with_metrics "Package Installation" install_all_packages
|
||||
deploy_with_metrics "Configuration Download" download_system_configs
|
||||
deploy_with_metrics "SSH Hardening" configure_ssh_hardening
|
||||
```
|
||||
|
||||
## Testing Integration
|
||||
|
||||
### Comprehensive Validation Function
|
||||
```bash
|
||||
function validate_deployment() {
|
||||
print_header "Deployment Validation"
|
||||
|
||||
local validation_failures=0
|
||||
|
||||
# Test package installation
|
||||
local required_packages=("git" "curl" "wget" "snmpd" "auditd" "fail2ban")
|
||||
for package in "${required_packages[@]}"; do
|
||||
if dpkg -l | grep -q "^ii.*$package"; then
|
||||
print_success "Package installed: $package"
|
||||
else
|
||||
print_error "Package missing: $package"
|
||||
((validation_failures++))
|
||||
fi
|
||||
done
|
||||
|
||||
# Test service status
|
||||
local required_services=("sshd" "snmpd" "auditd" "rsyslog")
|
||||
for service in "${required_services[@]}"; do
|
||||
if systemctl is-active "$service" >/dev/null 2>&1; then
|
||||
print_success "Service running: $service"
|
||||
else
|
||||
print_error "Service not running: $service"
|
||||
((validation_failures++))
|
||||
fi
|
||||
done
|
||||
|
||||
# Test configuration files
|
||||
local required_configs=("/etc/ssh/sshd_config" "/etc/snmp/snmpd.conf" "/etc/rsyslog.conf")
|
||||
for config in "${required_configs[@]}"; do
|
||||
if [[ -f "$config" && -s "$config" ]]; then
|
||||
print_success "Configuration exists: $(basename "$config")"
|
||||
else
|
||||
print_error "Configuration missing or empty: $(basename "$config")"
|
||||
((validation_failures++))
|
||||
fi
|
||||
done
|
||||
|
||||
# Run security tests
|
||||
if command -v lynis >/dev/null 2>&1; then
|
||||
print_info "Running basic security audit..."
|
||||
if lynis audit system --quick --quiet; then
|
||||
print_success "Security audit completed"
|
||||
else
|
||||
print_warning "Security audit found issues"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Summary
|
||||
if [[ $validation_failures -eq 0 ]]; then
|
||||
print_success "All deployment validation checks passed"
|
||||
return 0
|
||||
else
|
||||
print_error "$validation_failures deployment validation checks failed"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
```
|
||||
|
||||
These refactoring examples demonstrate how to apply the code review findings to create more robust, performant, and maintainable infrastructure provisioning scripts.
|
||||
@@ -1,117 +0,0 @@
|
||||
# Charles TODO - PFVCluster Security Improvements
|
||||
|
||||
**Priority Order:** High → Medium → Low
|
||||
**Target:** Address security vulnerabilities and operational improvements
|
||||
|
||||
## 🚨 HIGH PRIORITY (Security Critical)
|
||||
|
||||
### ✅ 1. Replace Insecure Deployment Method - RESOLVED
|
||||
**Previous Issue:** `curl https://dl.knownelement.com/KNEL/FetchApply/SetupNewSystem.sh | bash`
|
||||
**Status:** Fixed in README.md - now uses secure git clone approach
|
||||
**Current Method:** `git clone this repo` → `cd PFVCluster/provisioning` → `bash SetupNewSystem.sh`
|
||||
|
||||
**Remaining considerations:**
|
||||
- Consider implementing GPG signature verification for tagged releases
|
||||
- Add cryptographic checksums for external downloads within scripts
|
||||
|
||||
### ✅ 2. Enforce HTTPS for All Downloads - RESOLVED
|
||||
**Previous Issue:** HTTP URLs in Dell OMSA and some repository setups
|
||||
**Status:** All HTTP URLs converted to HTTPS across:
|
||||
- `provisioning/Dell/Server/omsa.sh` - Ubuntu archive and Dell repo URLs
|
||||
- `provisioning/legacy/prox7.sh` - Proxmox download URLs
|
||||
- `provisioning/Modules/RandD/sslStackFromSource.sh` - Apache source URLs
|
||||
|
||||
**Remaining considerations:**
|
||||
- SSL certificate validation is enabled by default in wget/curl
|
||||
- Consider adding retry logic for certificate failures
|
||||
|
||||
### 3. Implement Secrets Management
|
||||
**Current Issue:** SSH keys committed to repository, no secrets rotation
|
||||
**Action Required:**
|
||||
- Deploy Bitwarden CLI or HashiCorp Vault integration
|
||||
- Remove SSH public keys from repository
|
||||
- Create secure key distribution mechanism
|
||||
- Implement key rotation procedures
|
||||
- Add environment variable support for sensitive data
|
||||
|
||||
**Files to secure:**
|
||||
- `provisioning/ConfigFiles/SSH/AuthorizedKeys/` (entire directory)
|
||||
- Hard-coded hostnames in various scripts
|
||||
|
||||
## 🔶 MEDIUM PRIORITY (Operational Security)
|
||||
|
||||
### 4. Add Script Integrity Verification
|
||||
**Action Required:**
|
||||
- Generate SHA256 checksums for all scripts
|
||||
- Create checksum verification function in Framework-Includes
|
||||
- Add signature verification for external downloads
|
||||
- Implement rollback capability on verification failure
|
||||
|
||||
### 5. Enhanced Error Recovery
|
||||
**Action Required:**
|
||||
- Add state tracking for partial deployments
|
||||
- Implement resume functionality for interrupted installations
|
||||
- Create system restoration points before major changes
|
||||
- Add dependency checking before module execution
|
||||
|
||||
### 6. Security Testing Framework
|
||||
**Action Required:**
|
||||
- Create integration tests for security configurations
|
||||
- Add compliance validation (CIS benchmarks, STIG)
|
||||
- Implement automated security scanning post-deployment
|
||||
- Create test environments for validation
|
||||
|
||||
### 7. Configuration Validation
|
||||
**Action Required:**
|
||||
- Add pre-flight checks for system compatibility
|
||||
- Validate network connectivity to required services
|
||||
- Check for conflicting software before installation
|
||||
- Verify sufficient disk space and system resources
|
||||
|
||||
## 🔹 LOW PRIORITY (Quality Improvements)
|
||||
|
||||
### 8. Documentation Enhancement
|
||||
**Action Required:**
|
||||
- Create detailed security architecture documentation
|
||||
- Add troubleshooting guides for common issues
|
||||
- Document security implications of each module
|
||||
- Create deployment runbooks for different environments
|
||||
|
||||
### 9. Monitoring and Alerting
|
||||
**Action Required:**
|
||||
- Add deployment success/failure reporting
|
||||
- Implement centralized logging for all installations
|
||||
- Create dashboards for deployment status
|
||||
- Add alerting for security configuration drift
|
||||
|
||||
### 10. User Experience Improvements
|
||||
**Action Required:**
|
||||
- Create web-based deployment interface
|
||||
- Add progress indicators for long-running operations
|
||||
- Implement dry-run mode for testing configurations
|
||||
- Add interactive configuration selection
|
||||
|
||||
## Implementation Timeline
|
||||
|
||||
**✅ COMPLETED:** Item 1 (Secure deployment method)
|
||||
**✅ COMPLETED:** Item 2 (HTTPS enforcement)
|
||||
**Week 1:** Item 3 (Secrets management)
|
||||
**Week 2-3:** Items 4-5 (Operational improvements)
|
||||
**Month 2:** Items 6-10 (Quality and monitoring)
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- [ ] No plaintext secrets in repository
|
||||
- [x] All downloads use HTTPS with verification ✅
|
||||
- [x] Deployment method is cryptographically secure ✅
|
||||
- [ ] Automated testing validates security configurations
|
||||
- [ ] Rollback capability exists for all changes
|
||||
- [ ] Comprehensive documentation covers security implications
|
||||
|
||||
## Resources Needed
|
||||
|
||||
- Access to package repository for signed distributions
|
||||
- GPG key infrastructure for signing
|
||||
- Secrets management service (Vault/Bitwarden)
|
||||
- Test environment infrastructure
|
||||
- Security scanning tools integration
|
||||
@@ -1,162 +0,0 @@
|
||||
# Claude TODO - TSYS PFVCluster Automation Tasks
|
||||
|
||||
**Purpose:** Actionable items optimized for AI assistant implementation
|
||||
**Priority:** Critical → High → Medium → Low
|
||||
|
||||
## 🚨 CRITICAL (Immediate Security Fixes)
|
||||
|
||||
### ✅ RESOLVED: Secure Deployment Method
|
||||
**Previous Issue:** `curl | bash` deployment method
|
||||
**Status:** Fixed in README.md - now uses `git clone` + local script execution
|
||||
|
||||
### ✅ RESOLVED: Replace HTTP URLs with HTTPS
|
||||
**Files modified:**
|
||||
- `provisioning/Dell/Server/omsa.sh` - Converted 11 HTTP URLs to HTTPS (Ubuntu archive, Dell repo)
|
||||
- `provisioning/legacy/prox7.sh` - Converted 2 HTTP URLs to HTTPS (Proxmox downloads)
|
||||
- `provisioning/Modules/RandD/sslStackFromSource.sh` - Converted 3 HTTP URLs to HTTPS (Apache sources)
|
||||
|
||||
**Status:** All HTTP URLs in active scripts converted to HTTPS. Only remaining HTTP references are in comments and LibreNMS agent files (external dependencies).
|
||||
|
||||
### TASK-002: Add Download Integrity Verification
|
||||
**Create new function in:** `Framework-Includes/VerifyDownload.sh`
|
||||
**Function to implement:**
|
||||
```bash
|
||||
function verify_download() {
|
||||
local url="$1"
|
||||
local expected_hash="$2"
|
||||
local output_file="$3"
|
||||
|
||||
curl -fsSL "$url" -o "$output_file"
|
||||
local actual_hash=$(sha256sum "$output_file" | cut -d' ' -f1)
|
||||
|
||||
if [ "$actual_hash" != "$expected_hash" ]; then
|
||||
print_error "Hash verification failed for $output_file"
|
||||
rm -f "$output_file"
|
||||
return 1
|
||||
fi
|
||||
print_info "Download verified: $output_file"
|
||||
}
|
||||
```
|
||||
|
||||
### TASK-003: Create Secure Deployment Script
|
||||
**Create:** `provisioning/SecureSetupNewSystem.sh`
|
||||
**Features to implement:**
|
||||
- GPG signature verification
|
||||
- SHA256 checksum validation
|
||||
- HTTPS-only downloads
|
||||
- Rollback capability
|
||||
|
||||
## 🔶 HIGH (Security Enhancements)
|
||||
|
||||
### TASK-004: Remove Hardcoded SSH Keys
|
||||
**Files to modify:**
|
||||
- `provisioning/ConfigFiles/SSH/AuthorizedKeys/root-ssh-authorized-keys`
|
||||
- `provisioning/ConfigFiles/SSH/AuthorizedKeys/localuser-ssh-authorized-keys`
|
||||
- `provisioning/Modules/Security/secharden-ssh.sh:31,40,51`
|
||||
|
||||
**Implementation approach:**
|
||||
1. Create environment variable support: `SSH_KEYS_URL` or `SSH_KEYS_VAULT_PATH`
|
||||
2. Modify secharden-ssh.sh to fetch keys from secure source
|
||||
3. Add key validation before deployment
|
||||
|
||||
### TASK-005: Add Secrets Management Framework
|
||||
**Create:** `Framework-Includes/SecretsManager.sh`
|
||||
**Functions to implement:**
|
||||
```bash
|
||||
function get_secret() { } # Retrieve secret from vault
|
||||
function validate_secret() { } # Validate secret format
|
||||
function rotate_secret() { } # Trigger secret rotation
|
||||
```
|
||||
|
||||
### TASK-006: Enhanced Preflight Checks
|
||||
**Modify:** `Framework-Includes/PreflightCheck.sh`
|
||||
**Add checks for:**
|
||||
- Network connectivity to required hosts
|
||||
- Disk space requirements
|
||||
- Existing conflicting software
|
||||
- Required system capabilities
|
||||
|
||||
## 🔹 MEDIUM (Operational Improvements)
|
||||
|
||||
### TASK-007: Add Configuration Backup
|
||||
**Create:** `Framework-Includes/ConfigBackup.sh`
|
||||
**Functions:**
|
||||
```bash
|
||||
function backup_config() { } # Create timestamped backup
|
||||
function restore_config() { } # Restore from backup
|
||||
function list_backups() { } # Show available backups
|
||||
```
|
||||
|
||||
### TASK-008: Implement State Tracking
|
||||
**Create:** `Framework-Includes/StateManager.sh`
|
||||
**Track:**
|
||||
- Deployment progress
|
||||
- Module completion status
|
||||
- Rollback points
|
||||
- System changes made
|
||||
|
||||
### TASK-009: Add Retry Logic
|
||||
**Enhance existing scripts with:**
|
||||
- Configurable retry attempts for network operations
|
||||
- Exponential backoff for failed operations
|
||||
- Circuit breaker for repeatedly failing services
|
||||
|
||||
## 🔸 LOW (Quality of Life)
|
||||
|
||||
### TASK-010: Enhanced Logging
|
||||
**Modify:** `Framework-Includes/Logging.sh`
|
||||
**Add:**
|
||||
- Structured logging (JSON format option)
|
||||
- Log levels (DEBUG, INFO, WARN, ERROR)
|
||||
- Remote logging capability
|
||||
- Log rotation management
|
||||
|
||||
### TASK-011: Progress Indicators
|
||||
**Add to:** `Framework-Includes/PrettyPrint.sh`
|
||||
```bash
|
||||
function show_progress() { } # Display progress bar
|
||||
function update_status() { } # Update current operation
|
||||
```
|
||||
|
||||
### TASK-012: Dry Run Mode
|
||||
**Add to:** `provisioning/SetupNewSystem.sh`
|
||||
**Implementation:**
|
||||
- `--dry-run` flag support
|
||||
- Preview of changes without execution
|
||||
- Dependency analysis output
|
||||
|
||||
## Implementation Order for Claude
|
||||
|
||||
**Updated Priority After Security Fix (July 14, 2025):**
|
||||
1. **Start with TASK-001** (HTTPS enforcement - simple find/replace operations)
|
||||
2. **Create framework functions** (TASK-002, TASK-005, TASK-007)
|
||||
3. **Enhance existing modules** (TASK-004, TASK-006)
|
||||
4. **Add operational features** (TASK-008, TASK-009)
|
||||
5. **Improve user experience** (TASK-010, TASK-011, TASK-012)
|
||||
|
||||
**Note:** Major deployment security risk resolved - remaining tasks focus on hardening internal operations.
|
||||
|
||||
## File Location Patterns
|
||||
|
||||
- **Framework components:** `Framework-Includes/*.sh`
|
||||
- **Security modules:** `provisioning/Modules/Security/*.sh`
|
||||
- **Configuration files:** `provisioning/ConfigFiles/*/`
|
||||
- **Main entry point:** `provisioning/SetupNewSystem.sh`
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
For each task:
|
||||
1. Create backup of original files
|
||||
2. Implement changes incrementally
|
||||
3. Test with `bash -n` for syntax validation
|
||||
4. Verify functionality with controlled test runs
|
||||
5. Document changes made
|
||||
|
||||
## Error Handling Requirements
|
||||
|
||||
All new functions must:
|
||||
- Use `set -euo pipefail` compatibility
|
||||
- Integrate with existing error handling framework
|
||||
- Log errors to `$LOGFILENAME`
|
||||
- Return appropriate exit codes
|
||||
- Clean up temporary files on failure
|
||||
@@ -1,29 +0,0 @@
|
||||
#!/bin/bash
|
||||
# check-pkgs.sh - verify package install state.
|
||||
set -uo pipefail
|
||||
SSH_OPTS=(-o BatchMode=yes -o ConnectTimeout=8 -o StrictHostKeyChecking=accept-new)
|
||||
|
||||
for host in pfv-tsys6 pfv-tsys7; do
|
||||
echo "=== $host ==="
|
||||
ssh "${SSH_OPTS[@]}" "root@$host" '
|
||||
for p in sysstat jq numactl nvme mtr-tiny dig bmon tcpdump; do
|
||||
if command -v "$p" >/dev/null 2>&1; then
|
||||
echo " ✓ $p"
|
||||
else
|
||||
echo " ✗ $p"
|
||||
fi
|
||||
done
|
||||
# sysstat config
|
||||
echo " sysstat service:"
|
||||
systemctl list-unit-files 2>/dev/null | grep -i sysstat | sed "s/^/ /"
|
||||
echo " sysstat enabled in /etc/default:"
|
||||
if [ -r /etc/default/sysstat ]; then
|
||||
grep ENABLED /etc/default/sysstat | sed "s/^/ /"
|
||||
else
|
||||
echo " no /etc/default/sysstat"
|
||||
fi
|
||||
# on Debian trixie, sysstat uses a different path
|
||||
ls /etc/cron.d/sysstat* 2>/dev/null | sed "s/^/ found: /"
|
||||
'
|
||||
echo ""
|
||||
done
|
||||
@@ -1,41 +0,0 @@
|
||||
#!/bin/bash
|
||||
# check-repos-and-reboot.sh - checks reboot-required + Proxmox repo config on all hosts.
|
||||
set -uo pipefail
|
||||
SSH_OPTS=(-o BatchMode=yes -o ConnectTimeout=8 -o StrictHostKeyChecking=accept-new)
|
||||
HOSTS=(pfv-tsys1 pfv-tsys3 pfv-tsys4 pfv-tsys5 pfv-tsys6 pfv-tsys7)
|
||||
|
||||
for host in "${HOSTS[@]}"; do
|
||||
echo "================================================================"
|
||||
echo "[$host]"
|
||||
echo "================================================================"
|
||||
|
||||
if ! ssh "${SSH_OPTS[@]}" "root@$host" 'echo ok' >/dev/null 2>&1; then
|
||||
echo " UNREACHABLE"
|
||||
continue
|
||||
fi
|
||||
|
||||
echo "--- /var/run/reboot-required ---"
|
||||
ssh "${SSH_OPTS[@]}" "root@$host" 'if [ -r /var/run/reboot-required ]; then echo "REBOOT REQUIRED"; cat /var/run/reboot-required 2>/dev/null; if [ -r /var/run/reboot-required.pkgs ]; then echo "Packages triggering:"; cat /var/run/reboot-required.pkgs; fi; else echo "(no reboot required marker)"; fi'
|
||||
|
||||
echo ""
|
||||
echo "--- Running kernel vs installed kernel ---"
|
||||
ssh "${SSH_OPTS[@]}" "root@$host" 'echo "running: $(uname -r)"; echo "installed:"; dpkg -l | grep -E "pve-kernel-[0-9]" | awk "{print \" \"\$2\" \"\$3}" | tail -5'
|
||||
|
||||
echo ""
|
||||
echo "--- Proxmox repositories (apt sources) ---"
|
||||
ssh "${SSH_OPTS[@]}" "root@$host" 'grep -rh "pve\|proxmox" /etc/apt/sources.list /etc/apt/sources.list.d/ 2>/dev/null | grep -v "^#" | sed "s/^/ /"'
|
||||
|
||||
echo ""
|
||||
echo "--- Enterprise repo status (should be commented or absent if no subscription) ---"
|
||||
ssh "${SSH_OPTS[@]}" "root@$host" 'grep -l "pve-enterprise" /etc/apt/sources.list /etc/apt/sources.list.d/* 2>/dev/null | while read f; do echo " File: $f"; grep -n "pve-enterprise" "$f" | sed "s/^/ /"; done'
|
||||
|
||||
echo ""
|
||||
echo "--- no-subscription repo presence ---"
|
||||
ssh "${SSH_OPTS[@]}" "root@$host" 'grep -rl "pve-no-subscription" /etc/apt/sources.list /etc/apt/sources.list.d/ 2>/dev/null | while read f; do echo " File: $f"; grep -n "pve-no-subscription" "$f" | sed "s/^/ /"; done'
|
||||
|
||||
echo ""
|
||||
echo "--- Recently updated packages (last 24h, kernel-related) ---"
|
||||
ssh "${SSH_OPTS[@]}" "root@$host" 'grep -E "pve-kernel|proxmox|pve-qemu|zfs" /var/log/dpkg.log 2>/dev/null | grep "$(date +%Y-%m-%d)\|$(date -d yesterday +%Y-%m-%d)" | tail -15 || echo "(none in dpkg.log)"'
|
||||
|
||||
echo ""
|
||||
done
|
||||
@@ -1,51 +0,0 @@
|
||||
#!/bin/bash
|
||||
# deploy-and-fix.sh - uploads fix script, runs it, starts VMs, verifies.
|
||||
set -uo pipefail
|
||||
HOST="$1"
|
||||
SSH_OPTS=(-o BatchMode=yes -o ConnectTimeout=8 -o ServerAliveInterval=10 -o StrictHostKeyChecking=accept-new)
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/scripts"
|
||||
|
||||
echo "=== Uploading fix script to $HOST ==="
|
||||
scp "${SSH_OPTS[@]}" "$SCRIPT_DIR/fix-bond-nfs.sh" "root@$HOST:/root/fix-bond-nfs.sh" >/dev/null 2>&1
|
||||
|
||||
echo "=== Running fix ==="
|
||||
ssh "${SSH_OPTS[@]}" "root@$HOST" 'chmod +x /root/fix-bond-nfs.sh && bash /root/fix-bond-nfs.sh' 2>&1
|
||||
|
||||
echo ""
|
||||
echo "=== Starting VMs ==="
|
||||
for vmid in $(ssh "${SSH_OPTS[@]}" "root@$HOST" 'qm list 2>/dev/null | awk "NR>1{print \$1}"'); do
|
||||
status=$(ssh "${SSH_OPTS[@]}" "root@$HOST" "qm status $vmid 2>/dev/null | awk '{print \$2}'")
|
||||
if [ "$status" != "running" ]; then
|
||||
echo " Starting VM $vmid..."
|
||||
ssh "${SSH_OPTS[@]}" "root@$HOST" "qm start $vmid" 2>&1 | sed 's/^/ /'
|
||||
else
|
||||
echo " VM $vmid already running"
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "Waiting 20s for VMs to boot..."
|
||||
sleep 20
|
||||
|
||||
echo ""
|
||||
echo "=== FULL VERIFICATION ==="
|
||||
echo ""
|
||||
echo "--- VMs ---"
|
||||
ssh "${SSH_OPTS[@]}" "root@$HOST" 'qm list'
|
||||
echo ""
|
||||
echo "--- NFS mounts ---"
|
||||
ssh "${SSH_OPTS[@]}" "root@$HOST" 'nfsstat -m 2>/dev/null | head -24'
|
||||
echo ""
|
||||
echo "--- NFS TCP connections ---"
|
||||
ssh "${SSH_OPTS[@]}" "root@$HOST" 'ss -tn state established "( dport = :2049 )" 2>/dev/null'
|
||||
echo " Count:"
|
||||
ssh "${SSH_OPTS[@]}" "root@$HOST" 'ss -tn state established "( dport = :2049 )" 2>/dev/null | tail -n +2 | wc -l'
|
||||
echo ""
|
||||
echo "--- bond0 hash policy ---"
|
||||
ssh "${SSH_OPTS[@]}" "root@$HOST" 'cat /proc/net/bonding/bond0 | head -6'
|
||||
echo ""
|
||||
echo "--- Summary ---"
|
||||
ssh "${SSH_OPTS[@]}" "root@$HOST" 'echo "tcp_cc: $(sysctl -n net.ipv4.tcp_congestion_control)"'
|
||||
ssh "${SSH_OPTS[@]}" "root@$HOST" 'echo "swappiness: $(sysctl -n vm.swappiness)"'
|
||||
ssh "${SSH_OPTS[@]}" "root@$HOST" 'echo "governor: $(cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor 2>/dev/null)"'
|
||||
ssh "${SSH_OPTS[@]}" "root@$HOST" 'tuned-adm active 2>/dev/null'
|
||||
@@ -1,174 +0,0 @@
|
||||
#!/bin/bash
|
||||
###############################################################################
|
||||
# deploy-check.sh
|
||||
#
|
||||
# Deploys scripts/check.sh to each reachable Proxmox host, executes it
|
||||
# read-only, and pulls the resulting log back to returned-logs/.
|
||||
#
|
||||
# EXPLICITLY SKIPS:
|
||||
# - pfv-tsys2 (off the air per user; Win10 pending rebuild)
|
||||
# - pfv-tsys8 (retired / permanently offline per user)
|
||||
#
|
||||
# Safety features:
|
||||
# - BatchMode=yes : never hang on a password prompt
|
||||
# - ConnectTimeout=8 : fail fast on dead hosts
|
||||
# - per-host try/skip : one bad host never aborts the run
|
||||
# - ServerAliveInterval : detect hung connections
|
||||
# - read-only script : check.sh modifies nothing on the target
|
||||
###############################################################################
|
||||
set -uo pipefail
|
||||
|
||||
SELF_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ROOT_DIR="$(cd "$SELF_DIR/.." && pwd)"
|
||||
CHECK_SH="$SELF_DIR/scripts/check.sh"
|
||||
LOG_DIR="$ROOT_DIR/returned-logs"
|
||||
mkdir -p "$LOG_DIR"
|
||||
|
||||
# ONLY the hosts the user told us are alive.
|
||||
HOSTS=(pfv-tsys1 pfv-tsys3 pfv-tsys4 pfv-tsys5 pfv-tsys6 pfv-tsys7 pfv-tsys9)
|
||||
|
||||
# Common ssh options: non-interactive, fail-fast, no host-key prompt blocking.
|
||||
SSH_OPTS=(-o BatchMode=yes
|
||||
-o ConnectTimeout=8
|
||||
-o ServerAliveInterval=10
|
||||
-o ServerAliveCountMax=3
|
||||
-o StrictHostKeyChecking=accept-new)
|
||||
|
||||
log() { printf '[%s] %s\n' "$(date +%H:%M:%S)" "$*"; }
|
||||
|
||||
if [ ! -r "$CHECK_SH" ]; then
|
||||
echo "FATAL: $CHECK_SH not found" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Sanity-check shellcheck clean before shipping (best-effort, non-blocking)
|
||||
if command -v docker >/dev/null 2>&1; then
|
||||
log "pre-flight: shellcheck on check.sh"
|
||||
if ! docker run --rm -v "$SELF_DIR:/mnt" -w /mnt \
|
||||
koalaman/shellcheck:stable --severity=style --format=gcc scripts/check.sh \
|
||||
>"$LOG_DIR/_shellcheck.preflight.txt" 2>&1; then
|
||||
log "WARNING: shellcheck reported issues — see _shellcheck.preflight.txt"
|
||||
log " aborting deploy to avoid shipping a broken script"
|
||||
exit 1
|
||||
fi
|
||||
log "pre-flight: shellcheck clean"
|
||||
fi
|
||||
|
||||
summary_pass=()
|
||||
summary_fail=()
|
||||
declare -A HOST_PID # host -> background pid
|
||||
declare -A HOST_MARKER # host -> per-host marker file
|
||||
|
||||
# Per-host worker — runs in background, one per host, all in parallel.
|
||||
# Writes status into a marker file consumed by the parent.
|
||||
worker() {
|
||||
local host="$1"
|
||||
local marker="$LOG_DIR/_marker.$host"
|
||||
: > "$marker" # truncate
|
||||
echo "running" >> "$marker"
|
||||
|
||||
local short=""
|
||||
if ! ssh "${SSH_OPTS[@]}" "root@$host" 'echo ok' >/dev/null 2>&1; then
|
||||
echo "fail unreachable" >> "$marker"
|
||||
return
|
||||
fi
|
||||
if ! scp "${SSH_OPTS[@]}" "$CHECK_SH" "root@$host:/root/check.sh" >/dev/null 2>&1; then
|
||||
echo "fail scp-upload-failed" >> "$marker"
|
||||
return
|
||||
fi
|
||||
local remote_size
|
||||
remote_size=$(ssh "${SSH_OPTS[@]}" "root@$host" 'wc -c < /root/check.sh' 2>/dev/null || echo 0)
|
||||
if [ "${remote_size:-0}" -lt 1000 ]; then
|
||||
echo "fail upload-corrupt" >> "$marker"
|
||||
return
|
||||
fi
|
||||
local remote_stdout
|
||||
remote_stdout=$(ssh "${SSH_OPTS[@]}" "root@$host" \
|
||||
'chmod +x /root/check.sh && bash /root/check.sh' 2>&1)
|
||||
local rc=$?
|
||||
if [ "$rc" -ne 0 ]; then
|
||||
echo "fail check-exit-$rc" >> "$marker"
|
||||
# don't return - still try to pull whatever log got produced
|
||||
fi
|
||||
short=$(printf '%s\n' "$remote_stdout" | grep -oE 'Wrote: /root/[a-zA-Z0-9_-]+\.log' | head -n1 | awk '{print $2}')
|
||||
if [ -z "$short" ]; then
|
||||
short=$(ssh "${SSH_OPTS[@]}" "root@$host" 'echo "/root/$(hostname -s).log"' 2>/dev/null)
|
||||
fi
|
||||
if [ -z "$short" ]; then
|
||||
echo "fail no-log-path" >> "$marker"
|
||||
return
|
||||
fi
|
||||
if ! scp "${SSH_OPTS[@]}" "root@$host:$short" "$LOG_DIR/" >/dev/null 2>&1; then
|
||||
echo "fail scp-download-failed" >> "$marker"
|
||||
return
|
||||
fi
|
||||
local local_name local_path
|
||||
local_name="$(basename "$short")"
|
||||
local_path="$LOG_DIR/$local_name"
|
||||
if [ ! -s "$local_path" ]; then
|
||||
echo "fail local-empty" >> "$marker"
|
||||
return
|
||||
fi
|
||||
echo "ok $local_name $(wc -c < "$local_path") $(wc -l < "$local_path")" >> "$marker"
|
||||
}
|
||||
|
||||
# ---- launch all workers in parallel --------------------------------------
|
||||
log "launching ${#HOSTS[@]} hosts in parallel..."
|
||||
for host in "${HOSTS[@]}"; do
|
||||
rm -f "$LOG_DIR/_marker.$host"
|
||||
worker "$host" &
|
||||
HOST_PID[$host]=$!
|
||||
HOST_MARKER[$host]="$LOG_DIR/_marker.$host"
|
||||
log " launched $host (pid ${HOST_PID[$host]})"
|
||||
done
|
||||
|
||||
# ---- wait for all, with periodic progress --------------------------------
|
||||
remaining=("${HOSTS[@]}")
|
||||
while [ "${#remaining[@]}" -gt 0 ]; do
|
||||
sleep 10
|
||||
new_remaining=()
|
||||
for host in "${remaining[@]}"; do
|
||||
if ! kill -0 "${HOST_PID[$host]}" 2>/dev/null; then
|
||||
# process finished
|
||||
wait "${HOST_PID[$host]}" 2>/dev/null || true
|
||||
marker="${HOST_MARKER[$host]}"
|
||||
if [ -r "$marker" ]; then
|
||||
status_line="$(tail -n1 "$marker")"
|
||||
log "[$host] done: $status_line"
|
||||
case "$status_line" in
|
||||
ok*) summary_pass+=("$host:$status_line") ;;
|
||||
fail*) summary_fail+=("$host:$status_line") ;;
|
||||
*) summary_fail+=("$host:unknown") ;;
|
||||
esac
|
||||
else
|
||||
log "[$host] done but marker missing"
|
||||
summary_fail+=("$host:no-marker")
|
||||
fi
|
||||
else
|
||||
new_remaining+=("$host")
|
||||
fi
|
||||
done
|
||||
if [ "${#new_remaining[@]}" -gt 0 ]; then
|
||||
remaining=("${new_remaining[@]}")
|
||||
else
|
||||
remaining=()
|
||||
fi
|
||||
if [ "${#remaining[@]}" -gt 0 ]; then
|
||||
log "still running: ${remaining[*]} (${#remaining[@]} hosts)"
|
||||
fi
|
||||
done
|
||||
|
||||
# Final summary
|
||||
log "============================================================"
|
||||
log "DEPLOY SUMMARY"
|
||||
log "============================================================"
|
||||
log "Passed (${#summary_pass[@]}):"
|
||||
for p in "${summary_pass[@]:-}"; do [ -n "$p" ] && log " ✓ $p"; done
|
||||
log "Failed (${#summary_fail[@]}):"
|
||||
for f in "${summary_fail[@]:-}"; do [ -n "$f" ] && log " ✗ $f"; done
|
||||
log ""
|
||||
log "Contents of $LOG_DIR:"
|
||||
ls -la "$LOG_DIR"
|
||||
|
||||
# Clean up marker files
|
||||
rm -f "$LOG_DIR"/_marker.* 2>/dev/null
|
||||
@@ -1,56 +0,0 @@
|
||||
#!/bin/bash
|
||||
# deploy-tuning.sh - copies apply-tunings.sh to target hosts and runs it.
|
||||
# Usage: bash deploy-tuning.sh [--no-nfs] [--apply] <host> [host...]
|
||||
# Default mode is dry-run. Pass --apply to commit. Pass --no-nfs to skip NFS section.
|
||||
set -uo pipefail
|
||||
|
||||
SCRIPT="$(cd "$(dirname "${BASH_SOURCE[0]}")/scripts" && pwd)/apply-tunings.sh"
|
||||
SSH_OPTS=(-o BatchMode=yes -o ConnectTimeout=8 -o ServerAliveInterval=10 -o StrictHostKeyChecking=accept-new)
|
||||
MODE=""
|
||||
EXTRA_FLAGS=""
|
||||
|
||||
HOSTS=()
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--apply) MODE="--apply" ;;
|
||||
--dry-run) MODE="" ;;
|
||||
--no-nfs) EXTRA_FLAGS="--no-nfs" ;;
|
||||
*) HOSTS+=("$arg") ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ "${#HOSTS[@]}" -eq 0 ]; then
|
||||
echo "Usage: $0 <host> [host...] [--apply]"
|
||||
echo "Default: dry-run. Pass --apply to commit."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -r "$SCRIPT" ]; then
|
||||
echo "FATAL: $SCRIPT not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for host in "${HOSTS[@]}"; do
|
||||
echo "================================================================"
|
||||
echo "[$host] deploying apply-tunings.sh (mode: ${MODE:-dry-run})"
|
||||
echo "================================================================"
|
||||
|
||||
if ! ssh "${SSH_OPTS[@]}" "root@$host" 'echo ok' >/dev/null 2>&1; then
|
||||
echo "[$host] SKIP: unreachable"
|
||||
continue
|
||||
fi
|
||||
|
||||
echo "[$host] uploading..."
|
||||
if ! scp "${SSH_OPTS[@]}" "$SCRIPT" "root@$host:/root/apply-tunings.sh" >/dev/null 2>&1; then
|
||||
echo "[$host] SKIP: scp failed"
|
||||
continue
|
||||
fi
|
||||
|
||||
echo "[$host] running (output below)..."
|
||||
echo "----------------------------------------------------------------"
|
||||
ssh "${SSH_OPTS[@]}" "root@$host" "chmod +x /root/apply-tunings.sh && bash /root/apply-tunings.sh $MODE $EXTRA_FLAGS" 2>&1
|
||||
rc=$?
|
||||
echo "----------------------------------------------------------------"
|
||||
echo "[$host] exit code: $rc"
|
||||
echo ""
|
||||
done
|
||||
@@ -1,35 +0,0 @@
|
||||
#!/bin/bash
|
||||
# diag.sh - diagnostic commands run on a host via SSH wrapper.
|
||||
HOST="$1"
|
||||
SSH_OPTS=(-o BatchMode=yes -o ConnectTimeout=8 -o StrictHostKeyChecking=accept-new)
|
||||
|
||||
echo "===== 1. storage.cfg NFS stanzas (exact content) ====="
|
||||
ssh "${SSH_OPTS[@]}" "root@$HOST" 'grep -A 8 "^nfs: D2" /etc/pve/storage.cfg'
|
||||
|
||||
echo ""
|
||||
echo "===== 2. Try manual NFS mount with nconnect=4 ====="
|
||||
ssh "${SSH_OPTS[@]}" "root@$HOST" 'mount -t nfs -o nconnect=4,noatime,rsize=1048576,wsize=1048576,hard,proto=tcp pfv-tsys4-nfs-stor:/mnt/tsys4/D2 /mnt/pve/D2 2>&1; echo "exit=$?"'
|
||||
|
||||
echo ""
|
||||
echo "===== 3. Try manual NFS mount WITHOUT nconnect ====="
|
||||
ssh "${SSH_OPTS[@]}" "root@$HOST" 'mount -t nfs -o noatime,rsize=1048576,wsize=1048576,hard,proto=tcp pfv-tsys4-nfs-stor:/mnt/tsys4/D2 /mnt/pve/D2 2>&1; echo "exit=$?"'
|
||||
|
||||
echo ""
|
||||
echo "===== 4. NFS kernel version / module ====="
|
||||
ssh "${SSH_OPTS[@]}" "root@$HOST" 'cat /proc/fs/nfsfs/version 2>/dev/null; echo "---"; modinfo nfs 2>/dev/null | grep -E "^(filename|version|description)" | head -5'
|
||||
|
||||
echo ""
|
||||
echo "===== 5. mount.nfs version ====="
|
||||
ssh "${SSH_OPTS[@]}" "root@$HOST" 'mount.nfs --version 2>&1; echo "---"; dpkg -l nfs-common 2>/dev/null | tail -2'
|
||||
|
||||
echo ""
|
||||
echo "===== 6. /etc/network/interfaces bond0 stanza (exact bytes) ====="
|
||||
ssh "${SSH_OPTS[@]}" "root@$HOST" 'sed -n "/^auto bond0/,/^$/p" /etc/network/interfaces | cat -A'
|
||||
|
||||
echo ""
|
||||
echo "===== 7. Current bond0 running hash policy ====="
|
||||
ssh "${SSH_OPTS[@]}" "root@$HOST" 'cat /proc/net/bonding/bond0 | head -5'
|
||||
|
||||
echo ""
|
||||
echo "===== 8. xmit_hash_policy sysfs file ====="
|
||||
ssh "${SSH_OPTS[@]}" "root@$HOST" 'cat /sys/class/net/bond0/bonding/xmit_hash_policy 2>/dev/null; echo "---"; ls /sys/class/net/bond0/bonding/ 2>/dev/null'
|
||||
@@ -1,128 +0,0 @@
|
||||
#!/bin/bash
|
||||
# finish-host.sh - applies ALL remaining changes to a host and verifies.
|
||||
#
|
||||
# Steps:
|
||||
# 1. Start all VMs (triggers NFS lazy-mount)
|
||||
# 2. Wait for NFS mounts to appear
|
||||
# 3. Verify NFS nconnect=4 + noatime
|
||||
# 4. Apply bond0 xmit_hash_policy=layer3+4
|
||||
# 5. Full end-to-end verification
|
||||
#
|
||||
# Usage: bash finish-host.sh <host> [--apply]
|
||||
# Default is dry-run (starts VMs + shows what bond change would do, but doesn't edit interfaces)
|
||||
set -uo pipefail
|
||||
|
||||
HOST="${1:-}"
|
||||
MODE="${2:-dryrun}"
|
||||
[ "$MODE" = "--apply" ] && MODE="apply" || MODE="dryrun"
|
||||
|
||||
if [ -z "$HOST" ]; then
|
||||
echo "Usage: $0 <host> [--apply]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SSH_OPTS=(-o BatchMode=yes -o ConnectTimeout=8 -o ServerAliveInterval=10 -o StrictHostKeyChecking=accept-new)
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/scripts"
|
||||
|
||||
echo "==================================================================="
|
||||
echo " finish-host.sh — $HOST (mode: $MODE)"
|
||||
echo "==================================================================="
|
||||
echo ""
|
||||
|
||||
# =========================================================================
|
||||
# STEP 1: Start all VMs
|
||||
# =========================================================================
|
||||
echo "=== STEP 1: Start all VMs on $HOST ==="
|
||||
# Get list of all VMs (not just stopped — start is idempotent)
|
||||
vm_list=$(ssh "${SSH_OPTS[@]}" "root@$HOST" 'qm list 2>/dev/null | awk "NR>1{print \$1}"')
|
||||
for vmid in $vm_list; do
|
||||
status=$(ssh "${SSH_OPTS[@]}" "root@$HOST" "qm status $vmid 2>/dev/null | awk '{print \$2}'")
|
||||
if [ "$status" != "running" ]; then
|
||||
echo " Starting VM $vmid..."
|
||||
ssh "${SSH_OPTS[@]}" "root@$HOST" "qm start $vmid" 2>&1 | sed 's/^/ /'
|
||||
else
|
||||
echo " VM $vmid already running"
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo " Waiting 15s for VMs to boot and trigger NFS mounts..."
|
||||
sleep 15
|
||||
|
||||
echo ""
|
||||
echo "--- VM status after start ---"
|
||||
ssh "${SSH_OPTS[@]}" "root@$HOST" 'qm list 2>/dev/null'
|
||||
|
||||
# =========================================================================
|
||||
# STEP 2: Verify NFS mounts came back with nconnect=4
|
||||
# =========================================================================
|
||||
echo ""
|
||||
echo "=== STEP 2: Verify NFS mounts with nconnect=4 ==="
|
||||
ssh "${SSH_OPTS[@]}" "root@$HOST" 'nfsstat -m 2>/dev/null' | head -30
|
||||
|
||||
echo ""
|
||||
echo "--- NFS TCP connections to :2049 ---"
|
||||
ssh "${SSH_OPTS[@]}" "root@$HOST" 'ss -tn state established "( dport = :2049 )" 2>/dev/null'
|
||||
conn_count=$(ssh "${SSH_OPTS[@]}" "root@$HOST" 'ss -tn state established "( dport = :2049 )" 2>/dev/null | tail -n +2 | wc -l')
|
||||
echo " Total NFS TCP connections: $conn_count"
|
||||
|
||||
# =========================================================================
|
||||
# STEP 3: Apply bond0 hash policy
|
||||
# =========================================================================
|
||||
echo ""
|
||||
echo "=== STEP 3: Apply bond0 xmit_hash_policy=layer3+4 (mode: $MODE) ==="
|
||||
|
||||
# Upload the bond hash script
|
||||
scp "${SSH_OPTS[@]}" "$SCRIPT_DIR/apply-bond-hash.sh" "root@$HOST:/root/apply-bond-hash.sh" >/dev/null 2>&1
|
||||
|
||||
if [ "$MODE" = "apply" ]; then
|
||||
ssh "${SSH_OPTS[@]}" "root@$HOST" 'chmod +x /root/apply-bond-hash.sh && bash /root/apply-bond-hash.sh --apply' 2>&1
|
||||
else
|
||||
ssh "${SSH_OPTS[@]}" "root@$HOST" 'chmod +x /root/apply-bond-hash.sh && bash /root/apply-bond-hash.sh' 2>&1
|
||||
fi
|
||||
|
||||
# =========================================================================
|
||||
# STEP 4: Full verification
|
||||
# =========================================================================
|
||||
echo ""
|
||||
echo "=== STEP 4: Full end-to-end verification ==="
|
||||
echo ""
|
||||
echo "--- Uptime ---"
|
||||
ssh "${SSH_OPTS[@]}" "root@$HOST" 'uptime'
|
||||
echo ""
|
||||
echo "--- CPU governor ---"
|
||||
ssh "${SSH_OPTS[@]}" "root@$HOST" 'cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor 2>/dev/null || echo "(no cpufreq driver)"'
|
||||
echo ""
|
||||
echo "--- vm.swappiness ---"
|
||||
ssh "${SSH_OPTS[@]}" "root@$HOST" 'sysctl vm.swappiness'
|
||||
echo ""
|
||||
echo "--- TCP BBR ---"
|
||||
ssh "${SSH_OPTS[@]}" "root@$HOST" 'sysctl net.ipv4.tcp_congestion_control net.core.default_qdisc'
|
||||
echo ""
|
||||
echo "--- tuned profile ---"
|
||||
ssh "${SSH_OPTS[@]}" "root@$HOST" 'tuned-adm active 2>/dev/null'
|
||||
echo ""
|
||||
echo "--- bond0 hash policy + LACP state ---"
|
||||
ssh "${SSH_OPTS[@]}" "root@$HOST" 'cat /proc/net/bonding/bond0 2>/dev/null | head -25'
|
||||
echo ""
|
||||
echo "--- NFS mount options (first 3 mounts) ---"
|
||||
ssh "${SSH_OPTS[@]}" "root@$HOST" 'nfsstat -m 2>/dev/null | head -24'
|
||||
echo ""
|
||||
echo "--- NFS TCP connections (expect 4 per server × 2 servers = 8) ---"
|
||||
ssh "${SSH_OPTS[@]}" "root@$HOST" 'ss -tn state established "( dport = :2049 )" 2>/dev/null'
|
||||
nfs_conns=$(ssh "${SSH_OPTS[@]}" "root@$HOST" 'ss -tn state established "( dport = :2049 )" 2>/dev/null | tail -n +2 | wc -l')
|
||||
echo " Count: $nfs_conns"
|
||||
echo ""
|
||||
echo "--- VMs running ---"
|
||||
ssh "${SSH_OPTS[@]}" "root@$HOST" 'qm list 2>/dev/null'
|
||||
echo ""
|
||||
echo "--- Failed services ---"
|
||||
ssh "${SSH_OPTS[@]}" "root@$HOST" 'systemctl --failed --no-legend 2>/dev/null | head -10'
|
||||
echo ""
|
||||
echo "--- Network interfaces (speed/duplex/mtu) ---"
|
||||
ssh "${SSH_OPTS[@]}" "root@$HOST" 'for ifc in bond0 nic0 nic1 nic2 vmbr0 datanet; do [ -d "/sys/class/net/$ifc" ] && printf "%-12s speed=%-8s duplex=%-8s mtu=%s\n" "$ifc" "$(cat /sys/class/net/$ifc/speed 2>/dev/null)" "$(cat /sys/class/net/$ifc/duplex 2>/dev/null)" "$(cat /sys/class/net/$ifc/mtu 2>/dev/null)"; done'
|
||||
|
||||
echo ""
|
||||
echo "==================================================================="
|
||||
echo " COMPLETE — $HOST"
|
||||
echo "==================================================================="
|
||||
@@ -1,37 +0,0 @@
|
||||
#!/bin/bash
|
||||
# install-utils-v2.sh - retry install without nstat package.
|
||||
set -uo pipefail
|
||||
SSH_OPTS=(-o BatchMode=yes -o ConnectTimeout=8 -o StrictHostKeyChecking=accept-new)
|
||||
|
||||
PKGS="sysstat jq numactl nvme-cli mtr-tiny dnsutils bmon"
|
||||
|
||||
for host in pfv-tsys6 pfv-tsys7; do
|
||||
echo "=== [$host] installing: $PKGS ==="
|
||||
ssh "${SSH_OPTS[@]}" "root@$host" \
|
||||
"DEBIAN_FRONTEND=noninteractive apt-get update -qq 2>&1 | tail -2 && \
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y $PKGS 2>&1 | tail -10"
|
||||
|
||||
# sysstat enable (path varies by Debian version)
|
||||
ssh "${SSH_OPTS[@]}" "root@$host" '
|
||||
if [ -r /etc/default/sysstat ]; then
|
||||
sed -i "s/^ENABLED=.*/ENABLED=\"true\"/" /etc/default/sysstat
|
||||
systemctl enable --now sysstat 2>/dev/null
|
||||
grep ENABLED /etc/default/sysstat
|
||||
else
|
||||
# Newer Debian (trixie) — sysstat cron/service auto-enabled
|
||||
systemctl enable --now sysstat 2>/dev/null || echo "(sysstat auto via cron)"
|
||||
fi
|
||||
'
|
||||
echo ""
|
||||
done
|
||||
|
||||
# Verify
|
||||
for host in pfv-tsys6 pfv-tsys7; do
|
||||
echo "=== [$host] verification ==="
|
||||
ssh "${SSH_OPTS[@]}" "root@$host" '
|
||||
for p in sysstat jq numactl nvme mtr-tiny dig bmon; do
|
||||
command -v "$p" >/dev/null 2>&1 && echo " ✓ $p" || echo " ✗ $p"
|
||||
done
|
||||
'
|
||||
echo ""
|
||||
done
|
||||
@@ -1,60 +0,0 @@
|
||||
#!/bin/bash
|
||||
# install-utils.sh - installs useful observability packages on a host.
|
||||
# These are all small, dependency-light, and read-only at runtime.
|
||||
set -uo pipefail
|
||||
SSH_OPTS=(-o BatchMode=yes -o ConnectTimeout=8 -o StrictHostKeyChecking=accept-new)
|
||||
|
||||
HOSTS=(pfv-tsys6 pfv-tsys7)
|
||||
|
||||
# Packages to install, with rationale
|
||||
PKGS=(
|
||||
sysstat # sar, iostat, mpstat, pidstat - the missing observability suite
|
||||
jq # JSON parsing for pvesh/scripts
|
||||
numactl # NUMA topology/controls for the dual-socket hosts
|
||||
nvme-cli # NVMe health (for when NVMe shows up)
|
||||
tcpdump # packet capture for network debugging
|
||||
mtr-tiny # traceroute on steroids
|
||||
nstat # kernel SNMP stats (already partly there)
|
||||
dnsutils # dig, nslookup, host
|
||||
bmon # bandwidth monitor ( curses, real-time)
|
||||
)
|
||||
|
||||
for host in "${HOSTS[@]}"; do
|
||||
echo "================================================================"
|
||||
echo "[$host] installing observability packages"
|
||||
echo "================================================================"
|
||||
|
||||
if ! ssh "${SSH_OPTS[@]}" "root@$host" 'echo ok' >/dev/null 2>&1; then
|
||||
echo " UNREACHABLE"
|
||||
continue
|
||||
fi
|
||||
|
||||
# Check which are missing
|
||||
missing=""
|
||||
for pkg in "${PKGS[@]}"; do
|
||||
if ! ssh "${SSH_OPTS[@]}" "root@$host" "dpkg -s $pkg 2>/dev/null | grep -q 'Status: install ok installed'" 2>/dev/null; then
|
||||
missing="$missing $pkg"
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "$missing" ]; then
|
||||
echo " All packages already installed."
|
||||
continue
|
||||
fi
|
||||
|
||||
echo " Installing:$missing"
|
||||
ssh "${SSH_OPTS[@]}" "root@$host" \
|
||||
"DEBIAN_FRONTEND=noninteractive apt-get update -qq >/dev/null 2>&1 && \
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq$missing 2>&1 | tail -5"
|
||||
echo " Done."
|
||||
echo ""
|
||||
done
|
||||
|
||||
# Enable sysstat data collection (sar) — off by default on Debian
|
||||
for host in "${HOSTS[@]}"; do
|
||||
echo "[$host] enabling sysstat/sar data collection..."
|
||||
ssh "${SSH_OPTS[@]}" "root@$host" \
|
||||
"sed -i 's/^ENABLED=\"false\"/ENABLED=\"true\"/' /etc/default/sysstat 2>/dev/null; \
|
||||
systemctl enable --now sysstat 2>&1 | tail -2; \
|
||||
grep ENABLED /etc/default/sysstat"
|
||||
done
|
||||
@@ -1,293 +0,0 @@
|
||||
#!/bin/bash
|
||||
###############################################################################
|
||||
# iperf-full-matrix.sh
|
||||
#
|
||||
# Two test suites:
|
||||
# A. Management network (vmbr0 / VLAN1): all-pairs single-stream TCP, 10s
|
||||
# B. Storage network (VLAN1000): tsys6+tsys7 → tsys4+tsys5, stress test
|
||||
#
|
||||
# Output: returned-logs/iperf/
|
||||
###############################################################################
|
||||
set -uo pipefail
|
||||
|
||||
SSH_OPTS=(-o BatchMode=yes -o ConnectTimeout=8 -o ServerAliveInterval=10 -o StrictHostKeyChecking=accept-new)
|
||||
LOG_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/returned-logs/iperf"
|
||||
mkdir -p "$LOG_DIR"
|
||||
|
||||
HOSTS=(pfv-tsys1 pfv-tsys3 pfv-tsys4 pfv-tsys5 pfv-tsys6 pfv-tsys7)
|
||||
|
||||
# Storage IPs (known, static on VLAN1000)
|
||||
declare -A SIP
|
||||
SIP[pfv-tsys1]="10.100.100.1"
|
||||
SIP[pfv-tsys3]="10.100.100.3"
|
||||
SIP[pfv-tsys4]="10.100.100.4"
|
||||
SIP[pfv-tsys5]="10.100.100.5"
|
||||
SIP[pfv-tsys6]="10.100.100.6"
|
||||
SIP[pfv-tsys7]="10.100.100.7"
|
||||
|
||||
# ===========================================================================
|
||||
# STEP 0: Discover management IPs (vmbr0)
|
||||
# ===========================================================================
|
||||
echo "==================================================================="
|
||||
echo " STEP 0: Discover management network IPs (vmbr0)"
|
||||
echo "==================================================================="
|
||||
declare -A MIP
|
||||
for host in "${HOSTS[@]}"; do
|
||||
if ! ssh "${SSH_OPTS[@]}" "root@$host" 'echo ok' >/dev/null 2>&1; then
|
||||
echo " [$host] UNREACHABLE"
|
||||
continue
|
||||
fi
|
||||
mip=$(ssh "${SSH_OPTS[@]}" "root@$host" 'ip -o -4 addr show dev vmbr0 2>/dev/null | awk "{print \$4}" | cut -d/ -f1 | head -1')
|
||||
if [ -n "$mip" ]; then
|
||||
MIP[$host]="$mip"
|
||||
echo " [$host] vmbr0 = $mip"
|
||||
else
|
||||
echo " [$host] no vmbr0 IPv4 — skipping"
|
||||
fi
|
||||
done
|
||||
|
||||
# ===========================================================================
|
||||
# STEP 1: Ensure iperf3 installed on all hosts
|
||||
# ===========================================================================
|
||||
echo ""
|
||||
echo "==================================================================="
|
||||
echo " STEP 1: Ensure iperf3 installed"
|
||||
echo "==================================================================="
|
||||
for host in "${HOSTS[@]}"; do
|
||||
[ -z "${MIP[$host]:-}" ] && continue
|
||||
if ! ssh "${SSH_OPTS[@]}" "root@$host" 'command -v iperf3 >/dev/null 2>&1' 2>/dev/null; then
|
||||
echo -n " [$host] installing iperf3... "
|
||||
ssh "${SSH_OPTS[@]}" "root@$host" \
|
||||
'DEBIAN_FRONTEND=noninteractive apt-get update -qq >/dev/null 2>&1 && \
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq iperf3 >/dev/null 2>&1 && echo OK || echo FAILED'
|
||||
else
|
||||
echo " [$host] iperf3 already installed"
|
||||
fi
|
||||
done
|
||||
|
||||
# Helper: start iperf3 server in one-shot mode bound to a specific IP
|
||||
start_server() {
|
||||
local host="$1" ip="$2"
|
||||
ssh "${SSH_OPTS[@]}" "root@$host" \
|
||||
"pkill -x iperf3 2>/dev/null; nohup iperf3 -s -1 -B ${ip} >/dev/null 2>&1 &" 2>/dev/null
|
||||
sleep 1
|
||||
}
|
||||
|
||||
# Helper: run iperf3 client test, save output, extract result
|
||||
run_test() {
|
||||
local client="$1" server="$2" sip="$3" cip="$4" label="$5" logfile="$6"
|
||||
shift 6
|
||||
local extra="$*"
|
||||
|
||||
echo -n " [$label] ... "
|
||||
{
|
||||
echo "=== iperf3: $label ==="
|
||||
echo "Client: $client ($cip) → Server: $server ($sip)"
|
||||
echo "Date: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||
echo "Args: $extra"
|
||||
echo ""
|
||||
ssh "${SSH_OPTS[@]}" "root@$client" "iperf3 -c ${sip} -B ${cip} ${extra}" 2>&1
|
||||
echo ""
|
||||
echo "=== END ==="
|
||||
} > "$logfile" 2>&1
|
||||
|
||||
# Extract result
|
||||
sum=$(grep '\[SUM\].*sender$' "$logfile" | tail -1)
|
||||
if [ -n "$sum" ]; then
|
||||
bitrate=$(echo "$sum" | awk '{print $6, $7}')
|
||||
retrans=$(echo "$sum" | awk '{print $8}')
|
||||
else
|
||||
single=$(grep 'sender$' "$logfile" | tail -1)
|
||||
bitrate=$(echo "$single" | awk '{print $7, $8}')
|
||||
retrans=$(echo "$single" | awk '{print $9}')
|
||||
fi
|
||||
echo "${bitrate:-?} (retrans: ${retrans:-?})"
|
||||
}
|
||||
|
||||
# ===========================================================================
|
||||
# SUITE A: Management network (vmbr0) — all pairs, single-stream TCP, 10s
|
||||
# ===========================================================================
|
||||
echo ""
|
||||
echo "==================================================================="
|
||||
echo " SUITE A: MANAGEMENT NETWORK (vmbr0) — all-pairs, 10s TCP"
|
||||
echo " Expectation: ~940 Mbps for every pair (1 GbE line rate)"
|
||||
echo "==================================================================="
|
||||
echo ""
|
||||
|
||||
# Test each unique pair (i < j to avoid duplicates)
|
||||
for ((i=0; i<${#HOSTS[@]}; i++)); do
|
||||
for ((j=i+1; j<${#HOSTS[@]}; j++)); do
|
||||
client="${HOSTS[$i]}"
|
||||
server="${HOSTS[$j]}"
|
||||
[ -z "${MIP[$client]:-}" ] && continue
|
||||
[ -z "${MIP[$server]:-}" ] && continue
|
||||
|
||||
label="mgmt: ${client}→${server}"
|
||||
logfile="$LOG_DIR/mgmt-${client}-to-${server}.log"
|
||||
|
||||
start_server "$server" "${MIP[$server]}"
|
||||
run_test "$client" "$server" "${MIP[$server]}" "${MIP[$client]}" \
|
||||
"$label" "$logfile" "-t 10 -P 1"
|
||||
done
|
||||
done
|
||||
|
||||
# ===========================================================================
|
||||
# SUITE B: Storage network (VLAN1000) — stress test the cross-rack LACP
|
||||
# ===========================================================================
|
||||
echo ""
|
||||
echo "==================================================================="
|
||||
echo " SUITE B: STORAGE NETWORK (VLAN1000) — stress test cross-rack link"
|
||||
echo " tsys6 + tsys7 (Rack 3) → tsys4 + tsys5 (Rack 5)"
|
||||
echo " Expectation: limited by tsys4 USB dongle + tsys5 broken bond"
|
||||
echo "==================================================================="
|
||||
echo ""
|
||||
|
||||
# --- B.1: Individual tests (one client → one server at a time) ---
|
||||
echo "--- B.1: Individual tests (sequential) ---"
|
||||
echo ""
|
||||
|
||||
for client in pfv-tsys6 pfv-tsys7; do
|
||||
for server in pfv-tsys4 pfv-tsys5; do
|
||||
label="stor: ${client}→${server} (8-stream)"
|
||||
logfile="$LOG_DIR/stor-indiv-${client}-to-${server}-8stream.log"
|
||||
start_server "$server" "${SIP[$server]}"
|
||||
run_test "$client" "$server" "${SIP[$server]}" "${SIP[$client]}" \
|
||||
"$label" "$logfile" "-P 8 -t 20 -l 128k -O 2"
|
||||
done
|
||||
done
|
||||
|
||||
# --- B.2: Reverse direction (tsys4/5 → tsys6/7) ---
|
||||
echo ""
|
||||
echo "--- B.2: Reverse direction (tsys4/5 → tsys6/7) ---"
|
||||
echo ""
|
||||
|
||||
for client in pfv-tsys4 pfv-tsys5; do
|
||||
for server in pfv-tsys6 pfv-tsys7; do
|
||||
label="stor: ${client}→${server} (8-stream rev)"
|
||||
logfile="$LOG_DIR/stor-indiv-${client}-to-${server}-8stream.log"
|
||||
start_server "$server" "${SIP[$server]}"
|
||||
run_test "$client" "$server" "${SIP[$server]}" "${SIP[$client]}" \
|
||||
"$label" "$logfile" "-P 8 -t 20 -l 128k -O 2"
|
||||
done
|
||||
done
|
||||
|
||||
# --- B.3: Simultaneous stress test (4 flows at once) ---
|
||||
echo ""
|
||||
echo "--- B.3: Simultaneous 4-flow stress test ---"
|
||||
echo " tsys6→tsys4 + tsys6→tsys5 + tsys7→tsys4 + tsys7→tsys5"
|
||||
echo " All running in parallel for 30 seconds"
|
||||
echo ""
|
||||
|
||||
# Start 4 iperf3 servers (one-shot mode won't work for parallel; use persistent)
|
||||
for server in pfv-tsys4 pfv-tsys5; do
|
||||
ssh "${SSH_OPTS[@]}" "root@$server" "pkill -x iperf3 2>/dev/null; nohup iperf3 -s -B ${SIP[$server]} >/dev/null 2>&1 &" 2>/dev/null
|
||||
echo " [server started: $server]"
|
||||
done
|
||||
sleep 1
|
||||
|
||||
STRESS_LOG="$LOG_DIR/stor-stress-4flow"
|
||||
mkdir -p "$STRESS_LOG"
|
||||
|
||||
# Launch 4 clients in parallel, each writing to its own log
|
||||
ssh "${SSH_OPTS[@]}" "root@pfv-tsys6" "iperf3 -c ${SIP[pfv-tsys4]} -B ${SIP[pfv-tsys6]} -P 4 -t 30 -l 128k -O 2" > "$STRESS_LOG/tsys6-to-tsys4.log" 2>&1 &
|
||||
PID1=$!
|
||||
ssh "${SSH_OPTS[@]}" "root@pfv-tsys6" "iperf3 -c ${SIP[pfv-tsys5]} -B ${SIP[pfv-tsys6]} -P 4 -t 30 -l 128k -O 2" > "$STRESS_LOG/tsys6-to-tsys5.log" 2>&1 &
|
||||
PID2=$!
|
||||
ssh "${SSH_OPTS[@]}" "root@pfv-tsys7" "iperf3 -c ${SIP[pfv-tsys4]} -B ${SIP[pfv-tsys7]} -P 4 -t 30 -l 128k -O 2" > "$STRESS_LOG/tsys7-to-tsys4.log" 2>&1 &
|
||||
PID3=$!
|
||||
ssh "${SSH_OPTS[@]}" "root@pfv-tsys7" "iperf3 -c ${SIP[pfv-tsys5]} -B ${SIP[pfv-tsys7]} -P 4 -t 30 -l 128k -O 2" > "$STRESS_LOG/tsys7-to-tsys5.log" 2>&1 &
|
||||
PID4=$!
|
||||
|
||||
echo " [4 clients launched, waiting 40s for completion...]"
|
||||
wait $PID1 $PID2 $PID3 $PID4 2>/dev/null
|
||||
echo " [all 4 flows complete]"
|
||||
|
||||
# Kill servers
|
||||
for server in pfv-tsys4 pfv-tsys5; do
|
||||
ssh "${SSH_OPTS[@]}" "root@$server" 'pkill -x iperf3 2>/dev/null; true' 2>/dev/null
|
||||
done
|
||||
|
||||
# ===========================================================================
|
||||
# SUITE C: All hosts cleanup
|
||||
# ===========================================================================
|
||||
echo ""
|
||||
echo "==================================================================="
|
||||
echo " Cleanup: killing iperf3 everywhere"
|
||||
echo "==================================================================="
|
||||
for host in "${HOSTS[@]}"; do
|
||||
ssh "${SSH_OPTS[@]}" "root@$host" 'pkill -x iperf3 2>/dev/null; true' 2>/dev/null
|
||||
done
|
||||
|
||||
# ===========================================================================
|
||||
# RESULTS SUMMARY
|
||||
# ===========================================================================
|
||||
echo ""
|
||||
echo "==================================================================="
|
||||
echo " RESULTS SUMMARY"
|
||||
echo "==================================================================="
|
||||
|
||||
echo ""
|
||||
echo "===== SUITE A: Management network (vmbr0) ====="
|
||||
echo ""
|
||||
printf "%-40s %15s %10s\n" "TEST" "THROUGHPUT" "RETRANS"
|
||||
printf "%-40s %15s %10s\n" "----" "----------" "-------"
|
||||
for f in "$LOG_DIR"/mgmt-*.log; do
|
||||
[ -r "$f" ] || continue
|
||||
label=$(head -1 "$f" | sed 's/^=== iperf3: //; s/ ===$//')
|
||||
single=$(grep 'sender$' "$f" | tail -1)
|
||||
bitrate=$(echo "$single" | awk '{print $7, $8}')
|
||||
retrans=$(echo "$single" | awk '{print $9}')
|
||||
printf "%-40s %15s %10s\n" "$label" "${bitrate:-?}" "${retrans:--}"
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "===== SUITE B.1+B.2: Storage network individual ====="
|
||||
echo ""
|
||||
printf "%-45s %15s %10s\n" "TEST" "THROUGHPUT" "RETRANS"
|
||||
printf "%-45s %15s %10s\n" "----" "----------" "-------"
|
||||
for f in "$LOG_DIR"/stor-indiv-*.log; do
|
||||
[ -r "$f" ] || continue
|
||||
label=$(head -1 "$f" | sed 's/^=== iperf3: //; s/ ===$//')
|
||||
sum=$(grep '\[SUM\].*sender$' "$f" | tail -1)
|
||||
if [ -n "$sum" ]; then
|
||||
bitrate=$(echo "$sum" | awk '{print $6, $7}')
|
||||
retrans=$(echo "$sum" | awk '{print $8}')
|
||||
else
|
||||
single=$(grep 'sender$' "$f" | tail -1)
|
||||
bitrate=$(echo "$single" | awk '{print $7, $8}')
|
||||
retrans=$(echo "$single" | awk '{print $9}')
|
||||
fi
|
||||
printf "%-45s %15s %10s\n" "$label" "${bitrate:-?}" "${retrans:--}"
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "===== SUITE B.3: Simultaneous 4-flow stress test ====="
|
||||
echo ""
|
||||
printf "%-30s %15s %10s\n" "FLOW" "THROUGHPUT" "RETRANS"
|
||||
printf "%-30s %15s %10s\n" "----" "----------" "-------"
|
||||
total_mbps=0
|
||||
for f in "$STRESS_LOG"/*.log; do
|
||||
[ -r "$f" ] || continue
|
||||
flow=$(basename "$f" .log)
|
||||
sum=$(grep '\[SUM\].*sender$' "$f" | tail -1)
|
||||
if [ -n "$sum" ]; then
|
||||
bitrate=$(echo "$sum" | awk '{print $6, $7}')
|
||||
retrans=$(echo "$sum" | awk '{print $8}')
|
||||
mbps=$(echo "$sum" | awk '{print $6}')
|
||||
total_mbps=$(awk "BEGIN{print $total_mbps + $mbps}")
|
||||
else
|
||||
bitrate="?"
|
||||
retrans="-"
|
||||
fi
|
||||
printf "%-30s %15s %10s\n" "$flow" "$bitrate" "${retrans:--}"
|
||||
done
|
||||
printf "%-30s %15s\n" "AGGREGATE (all 4 flows)" "${total_mbps} Mbits/sec"
|
||||
|
||||
echo ""
|
||||
echo "===== CONTEXT ====="
|
||||
echo "tsys4: USB cdc_ncm dongle (single 1G link, no bond)"
|
||||
echo "tsys5: bond0 broken (1 active slave, no LACP partner) — cable pending"
|
||||
echo "tsys6/7: working 2x1G LACP, layer3+4 hash (host side)"
|
||||
echo "Cross-rack: 4x1G LACP (pfv-r3-tor-stor → pfv-core-sw01)"
|
||||
echo ""
|
||||
echo "All logs in: $LOG_DIR/"
|
||||
@@ -1,184 +0,0 @@
|
||||
#!/bin/bash
|
||||
###############################################################################
|
||||
# iperf-storage-tests.sh
|
||||
#
|
||||
# Installs iperf3 on all online hosts, then runs a matrix of storage-network
|
||||
# throughput tests. Saves all output to returned-logs/iperf/.
|
||||
#
|
||||
# Test matrix (all over VLAN1000 storage network, 10.100.100.0/24):
|
||||
# 1. tsys7 → tsys4 (USB cdc_ncm NIC) — the smoking gun
|
||||
# 2. tsys7 → tsys5 (bond0, 1 active slave) — PCI NIC comparison
|
||||
# 3. tsys7 → tsys6 (bond0, 2 active slaves) — working LACP baseline
|
||||
# 4. Reverse: tsys4 → tsys7 (USB NIC TX direction)
|
||||
# 5. Reverse: tsys5 → tsys7
|
||||
#
|
||||
# Each test: TCP 8-stream 30s forward + reverse + UDP saturation.
|
||||
###############################################################################
|
||||
set -uo pipefail
|
||||
|
||||
SSH_OPTS=(-o BatchMode=yes -o ConnectTimeout=8 -o ServerAliveInterval=10 -o StrictHostKeyChecking=accept-new)
|
||||
LOG_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/returned-logs/iperf"
|
||||
mkdir -p "$LOG_DIR"
|
||||
|
||||
ALL_HOSTS=(pfv-tsys1 pfv-tsys3 pfv-tsys4 pfv-tsys5 pfv-tsys6 pfv-tsys7)
|
||||
|
||||
# Storage network IPs
|
||||
declare -A SIP
|
||||
SIP[pfv-tsys1]="10.100.100.1"
|
||||
SIP[pfv-tsys3]="10.100.100.3"
|
||||
SIP[pfv-tsys4]="10.100.100.4"
|
||||
SIP[pfv-tsys5]="10.100.100.5"
|
||||
SIP[pfv-tsys6]="10.100.100.6"
|
||||
SIP[pfv-tsys7]="10.100.100.7"
|
||||
|
||||
echo "==================================================================="
|
||||
echo " STEP 1: Install iperf3 on all online hosts"
|
||||
echo "==================================================================="
|
||||
|
||||
for host in "${ALL_HOSTS[@]}"; do
|
||||
echo -n " [$host] "
|
||||
if ! ssh "${SSH_OPTS[@]}" "root@$host" 'echo ok' >/dev/null 2>&1; then
|
||||
echo "UNREACHABLE — skipping"
|
||||
continue
|
||||
fi
|
||||
# Check if iperf3 already installed
|
||||
if ssh "${SSH_OPTS[@]}" "root@$host" 'command -v iperf3 >/dev/null 2>&1' 2>/dev/null; then
|
||||
echo "iperf3 already installed"
|
||||
else
|
||||
printf "installing... "
|
||||
ssh "${SSH_OPTS[@]}" "root@$host" 'DEBIAN_FRONTEND=noninteractive apt-get update -qq >/dev/null 2>&1 && DEBIAN_FRONTEND=noninteractive apt-get install -y -qq iperf3 >/dev/null 2>&1 && echo OK || echo FAILED'
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "==================================================================="
|
||||
echo " STEP 2: Kill any existing iperf3 processes everywhere"
|
||||
echo "==================================================================="
|
||||
for host in "${ALL_HOSTS[@]}"; do
|
||||
ssh "${SSH_OPTS[@]}" "root@$host" 'pkill -x iperf3 2>/dev/null; true' 2>/dev/null
|
||||
done
|
||||
echo " Done."
|
||||
|
||||
# Helper: run an iperf3 test and save output
|
||||
run_iperf() {
|
||||
local client="$1" server="$2" direction="$3" label="$4" logfile="$5"
|
||||
local client_ip="${SIP[$client]}" server_ip="${SIP[$server]}"
|
||||
|
||||
echo -n " [$client → $server] $label ... "
|
||||
|
||||
# Start server in one-shot mode (-1 means serve one client then exit)
|
||||
ssh "${SSH_OPTS[@]}" "root@$server" "pkill -x iperf3 2>/dev/null; nohup iperf3 -s -1 -B ${server_ip} >/dev/null 2>&1 &" 2>/dev/null
|
||||
sleep 1
|
||||
|
||||
# Run client
|
||||
{
|
||||
echo "=== iperf3: $label ==="
|
||||
echo "Client: $client ($client_ip)"
|
||||
echo "Server: $server ($server_ip)"
|
||||
echo "Direction: $direction"
|
||||
echo "Date: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||
echo ""
|
||||
if [ "$direction" = "forward" ]; then
|
||||
ssh "${SSH_OPTS[@]}" "root@$client" \
|
||||
"iperf3 -c ${server_ip} -B ${client_ip} -P 8 -t 30 -l 128k -O 2" 2>&1
|
||||
elif [ "$direction" = "reverse" ]; then
|
||||
ssh "${SSH_OPTS[@]}" "root@$client" \
|
||||
"iperf3 -c ${server_ip} -B ${client_ip} -P 8 -t 30 -l 128k -O 2 -R" 2>&1
|
||||
elif [ "$direction" = "udp" ]; then
|
||||
ssh "${SSH_OPTS[@]}" "root@$client" \
|
||||
"iperf3 -c ${server_ip} -B ${client_ip} -u -b 2G -t 10 -l 8972" 2>&1
|
||||
elif [ "$direction" = "single" ]; then
|
||||
ssh "${SSH_OPTS[@]}" "root@$client" \
|
||||
"iperf3 -c ${server_ip} -B ${client_ip} -t 20 -O 2" 2>&1
|
||||
fi
|
||||
echo ""
|
||||
echo "=== END ==="
|
||||
} > "$logfile" 2>&1
|
||||
|
||||
# Extract summary line
|
||||
if grep -q "sender" "$logfile"; then
|
||||
bitrate=$(grep "sender" "$logfile" | tail -1 | awk '{print $7, $8}')
|
||||
echo "done: ${bitrate}"
|
||||
else
|
||||
echo "done (check log for details)"
|
||||
fi
|
||||
}
|
||||
|
||||
echo ""
|
||||
echo "==================================================================="
|
||||
echo " STEP 3: Run iperf3 test matrix"
|
||||
echo "==================================================================="
|
||||
echo ""
|
||||
echo "All tests over VLAN1000 storage network (10.100.100.0/24)."
|
||||
echo "TCP tests: 8 parallel streams, 30s, 128k blocks."
|
||||
echo ""
|
||||
|
||||
# --- Test 1: tsys7 → tsys4 (USB cdc_ncm target) ---
|
||||
echo "--- TEST 1: tsys7 → tsys4 (USB cdc_ncm NIC) ---"
|
||||
run_iperf pfv-tsys7 pfv-tsys4 forward "TCP 8-stream forward (tsys7→tsys4 USB)" \
|
||||
"$LOG_DIR/01-tsys7-to-tsys4-tcp-forward.log"
|
||||
run_iperf pfv-tsys7 pfv-tsys4 reverse "TCP 8-stream reverse (tsys4 USB→tsys7)" \
|
||||
"$LOG_DIR/02-tsys7-to-tsys4-tcp-reverse.log"
|
||||
run_iperf pfv-tsys7 pfv-tsys4 single "TCP single-stream forward (tsys7→tsys4 USB)" \
|
||||
"$LOG_DIR/03-tsys7-to-tsys4-tcp-single.log"
|
||||
run_iperf pfv-tsys7 pfv-tsys4 udp "UDP saturation (tsys7→tsys4 USB)" \
|
||||
"$LOG_DIR/04-tsys7-to-tsys4-udp.log"
|
||||
|
||||
echo ""
|
||||
|
||||
# --- Test 2: tsys7 → tsys5 (bond0, PCI NIC, 1 active slave) ---
|
||||
echo "--- TEST 2: tsys7 → tsys5 (PCI NIC, broken bond - 1 slave) ---"
|
||||
run_iperf pfv-tsys7 pfv-tsys5 forward "TCP 8-stream forward (tsys7→tsys5 PCI)" \
|
||||
"$LOG_DIR/05-tsys7-to-tsys5-tcp-forward.log"
|
||||
run_iperf pfv-tsys7 pfv-tsys5 reverse "TCP 8-stream reverse (tsys5 PCI→tsys7)" \
|
||||
"$LOG_DIR/06-tsys7-to-tsys5-tcp-reverse.log"
|
||||
run_iperf pfv-tsys7 pfv-tsys5 single "TCP single-stream forward (tsys7→tsys5 PCI)" \
|
||||
"$LOG_DIR/07-tsys7-to-tsys5-tcp-single.log"
|
||||
run_iperf pfv-tsys7 pfv-tsys5 udp "UDP saturation (tsys7→tsys5 PCI)" \
|
||||
"$LOG_DIR/08-tsys7-to-tsys5-udp.log"
|
||||
|
||||
echo ""
|
||||
|
||||
# --- Test 3: tsys7 → tsys6 (working 2-slave LACP baseline, layer3+4) ---
|
||||
echo "--- TEST 3: tsys7 → tsys6 (working 2×1G LACP baseline) ---"
|
||||
run_iperf pfv-tsys7 pfv-tsys6 forward "TCP 8-stream forward (tsys7→tsys6 LACP)" \
|
||||
"$LOG_DIR/09-tsys7-to-tsys6-tcp-forward.log"
|
||||
run_iperf pfv-tsys7 pfv-tsys6 reverse "TCP 8-stream reverse (tsys6 LACP→tsys7)" \
|
||||
"$LOG_DIR/10-tsys7-to-tsys6-tcp-reverse.log"
|
||||
|
||||
echo ""
|
||||
|
||||
# --- Test 4: tsys6 → tsys4 (pre-tuning baseline) ---
|
||||
echo "--- TEST 4: tsys6 → tsys4 (baseline before tsys6 tuning) ---"
|
||||
run_iperf pfv-tsys6 pfv-tsys4 forward "TCP 8-stream forward (tsys6→tsys4 USB)" \
|
||||
"$LOG_DIR/11-tsys6-to-tsys4-tcp-forward.log"
|
||||
run_iperf pfv-tsys6 pfv-tsys4 reverse "TCP 8-stream reverse (tsys4 USB→tsys6)" \
|
||||
"$LOG_DIR/12-tsys6-to-tsys4-tcp-reverse.log"
|
||||
|
||||
echo ""
|
||||
|
||||
# --- Cleanup: kill iperf3 everywhere ---
|
||||
echo "--- Cleanup ---"
|
||||
for host in "${ALL_HOSTS[@]}"; do
|
||||
ssh "${SSH_OPTS[@]}" "root@$host" 'pkill -x iperf3 2>/dev/null; true' 2>/dev/null
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "==================================================================="
|
||||
echo " RESULTS SUMMARY"
|
||||
echo "==================================================================="
|
||||
echo ""
|
||||
printf "%-45s %s\n" "TEST" "THROUGHPUT"
|
||||
printf "%-45s %s\n" "----" "----------"
|
||||
for f in "$LOG_DIR"/*.log; do
|
||||
[ -r "$f" ] || continue
|
||||
label=$(head -1 "$f" | sed 's/^=== iperf3: //; s/ ===$//')
|
||||
bitrate=$(grep -E "sender$" "$f" | tail -1 | awk '{print $7, $8}')
|
||||
[ -z "$bitrate" ] && bitrate=$(grep -E "Mbits/sec|Gbits/sec" "$f" | tail -1 | grep -oE '[0-9.]+ [MG]bits/sec' | head -1)
|
||||
[ -z "$bitrate" ] && bitrate="(see log)"
|
||||
printf "%-45s %s\n" "$label" "$bitrate"
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "Full logs saved to: $LOG_DIR/"
|
||||
echo "==================================================================="
|
||||
@@ -1,119 +0,0 @@
|
||||
#!/bin/bash
|
||||
# iperf-tsys6-tsys7.sh - validate 2Gbps LACP between the two tuned hosts.
|
||||
set -uo pipefail
|
||||
SSH_OPTS=(-o BatchMode=yes -o ConnectTimeout=8 -o ServerAliveInterval=10 -o StrictHostKeyChecking=accept-new)
|
||||
LOG_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/returned-logs/iperf"
|
||||
mkdir -p "$LOG_DIR"
|
||||
|
||||
TSYS6="10.100.100.6"
|
||||
TSYS7="10.100.100.7"
|
||||
|
||||
run_test() {
|
||||
local client="$1" server="$2" server_ip="$3" label="$4" logfile="$5"
|
||||
shift 4
|
||||
local extra_args="$*"
|
||||
|
||||
echo -n " [$label] ... "
|
||||
|
||||
# Start server in one-shot mode
|
||||
ssh "${SSH_OPTS[@]}" "root@$server" "pkill -x iperf3 2>/dev/null; nohup iperf3 -s -1 -B ${server_ip} >/dev/null 2>&1 &" 2>/dev/null
|
||||
sleep 1
|
||||
|
||||
{
|
||||
echo "=== iperf3: $label ==="
|
||||
echo "Client: $client Server: $server ($server_ip)"
|
||||
echo "Date: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||
echo "Args: $extra_args"
|
||||
echo ""
|
||||
ssh "${SSH_OPTS[@]}" "root@$client" "iperf3 -c ${server_ip} $extra_args" 2>&1
|
||||
echo ""
|
||||
echo "=== END ==="
|
||||
} > "$logfile" 2>&1
|
||||
|
||||
# Extract result
|
||||
sum=$(grep '\[SUM\].*sender$' "$logfile" | tail -1)
|
||||
if [ -n "$sum" ]; then
|
||||
bitrate=$(echo "$sum" | awk '{print $6, $7}')
|
||||
retrans=$(echo "$sum" | awk '{print $8}')
|
||||
else
|
||||
single=$(grep 'sender$' "$logfile" | tail -1)
|
||||
bitrate=$(echo "$single" | awk '{print $7, $8}')
|
||||
retrans=$(echo "$single" | awk '{print $9}')
|
||||
fi
|
||||
echo "${bitrate} (retrans: ${retrans:-0})"
|
||||
}
|
||||
|
||||
echo "==================================================================="
|
||||
echo " iperf3: tsys6 ↔ tsys7 (both have 2×1G LACP + layer3+4 hash)"
|
||||
echo " Expectation: ~1.8-2.0 Gbps for 8-stream TCP"
|
||||
echo "==================================================================="
|
||||
echo ""
|
||||
|
||||
# Pre-flight: confirm bond state on both
|
||||
echo "--- bond0 state on tsys6 ---"
|
||||
ssh "${SSH_OPTS[@]}" "root@pfv-tsys6" 'grep -E "Transmit Hash|Number of ports|Bonding Mode" /proc/net/bonding/bond0'
|
||||
echo ""
|
||||
echo "--- bond0 state on tsys7 ---"
|
||||
ssh "${SSH_OPTS[@]}" "root@pfv-tsys7" 'grep -E "Transmit Hash|Number of ports|Bonding Mode" /proc/net/bonding/bond0'
|
||||
echo ""
|
||||
|
||||
echo "--- Running tests ---"
|
||||
echo ""
|
||||
|
||||
# Test 1: tsys7 → tsys6, 8-stream TCP forward
|
||||
run_test pfv-tsys7 pfv-tsys6 "$TSYS6" \
|
||||
"tsys7→tsys6 TCP 8-stream forward" \
|
||||
"$LOG_DIR/tsys6-tsys7-01-tcp-8stream-forward.log" \
|
||||
"-P 8 -t 30 -l 128k -O 2"
|
||||
|
||||
# Test 2: tsys6 → tsys7, 8-stream TCP forward (reverse direction)
|
||||
run_test pfv-tsys6 pfv-tsys7 "$TSYS7" \
|
||||
"tsys6→tsys7 TCP 8-stream forward" \
|
||||
"$LOG_DIR/tsys6-tsys7-02-tcp-8stream-forward.log" \
|
||||
"-P 8 -t 30 -l 128k -O 2"
|
||||
|
||||
# Test 3: tsys7 → tsys6, single stream (should be ~940 Mbps — single flow)
|
||||
run_test pfv-tsys7 pfv-tsys6 "$TSYS6" \
|
||||
"tsys7→tsys6 TCP single-stream" \
|
||||
"$LOG_DIR/tsys6-tsys7-03-tcp-single.log" \
|
||||
"-t 20 -O 2"
|
||||
|
||||
# Test 4: tsys7 → tsys6, 4-stream (nconnect=4 mirrors this)
|
||||
run_test pfv-tsys7 pfv-tsys6 "$TSYS6" \
|
||||
"tsys7→tsys6 TCP 4-stream" \
|
||||
"$LOG_DIR/tsys6-tsys7-04-tcp-4stream.log" \
|
||||
"-P 4 -t 30 -l 128k -O 2"
|
||||
|
||||
# Test 5: UDP saturation
|
||||
run_test pfv-tsys7 pfv-tsys6 "$TSYS6" \
|
||||
"tsys7→tsys6 UDP saturation" \
|
||||
"$LOG_DIR/tsys6-tsys7-05-udp.log" \
|
||||
"-u -b 3G -t 10 -l 8972"
|
||||
|
||||
# Cleanup
|
||||
ssh "${SSH_OPTS[@]}" "root@pfv-tsys6" 'pkill -x iperf3 2>/dev/null; true' 2>/dev/null
|
||||
ssh "${SSH_OPTS[@]}" "root@pfv-tsys7" 'pkill -x iperf3 2>/dev/null; true' 2>/dev/null
|
||||
|
||||
echo ""
|
||||
echo "==================================================================="
|
||||
echo " SUMMARY"
|
||||
echo "==================================================================="
|
||||
echo ""
|
||||
printf "%-45s %15s %10s\n" "TEST" "THROUGHPUT" "RETRANS"
|
||||
printf "%-45s %15s %10s\n" "----" "----------" "-------"
|
||||
for f in "$LOG_DIR"/tsys6-tsys7-*.log; do
|
||||
[ -r "$f" ] || continue
|
||||
label=$(head -1 "$f" | sed 's/^=== iperf3: //; s/ ===$//')
|
||||
sum=$(grep '\[SUM\].*sender$' "$f" | tail -1)
|
||||
if [ -n "$sum" ]; then
|
||||
bitrate=$(echo "$sum" | awk '{print $6, $7}')
|
||||
retrans=$(echo "$sum" | awk '{print $8}')
|
||||
else
|
||||
single=$(grep 'sender$' "$f" | tail -1)
|
||||
bitrate=$(echo "$single" | awk '{print $7, $8}')
|
||||
retrans=$(echo "$single" | awk '{print $9}')
|
||||
fi
|
||||
printf "%-45s %15s %10s\n" "$label" "$bitrate" "${retrans:--}"
|
||||
done
|
||||
echo ""
|
||||
echo "Expected: 8-stream ~1.8-2.0 Gbps, single-stream ~940 Mbps"
|
||||
@@ -1,88 +0,0 @@
|
||||
#!/bin/bash
|
||||
# reboot-and-verify.sh - reboots a host and verifies NFS nconnect activates.
|
||||
# Usage: bash reboot-and-verify.sh <host>
|
||||
set -uo pipefail
|
||||
HOST="$1"
|
||||
SSH_OPTS=(-o BatchMode=yes -o ConnectTimeout=5 -o ServerAliveInterval=5 -o StrictHostKeyChecking=accept-new)
|
||||
|
||||
echo "================================================================"
|
||||
echo "[$HOST] PRE-REBOOT STATE"
|
||||
echo "================================================================"
|
||||
ssh "${SSH_OPTS[@]}" "root@$HOST" '
|
||||
echo "--- VMs ---"
|
||||
qm list 2>/dev/null
|
||||
echo "--- NFS TCP conns: $(ss -tn state established "( dport = :2049 )" 2>/dev/null | tail -n +2 | wc -l) ---"
|
||||
echo "--- NFS first mount: ---"
|
||||
nfsstat -m 2>/dev/null | head -2 | tail -1
|
||||
echo "--- uptime ---"
|
||||
uptime
|
||||
'
|
||||
|
||||
echo ""
|
||||
echo "================================================================"
|
||||
echo "[$HOST] ISSUING REBOOT"
|
||||
echo "================================================================"
|
||||
ssh "${SSH_OPTS[@]}" "root@$HOST" 'nohup sh -c "(sleep 2; systemctl reboot)" >/dev/null 2>&1 &'
|
||||
echo "Reboot sent at $(date +%H:%M:%S)"
|
||||
|
||||
echo ""
|
||||
echo "================================================================"
|
||||
echo "[$HOST] WAITING FOR SSH TO RETURN (max 10 min)"
|
||||
echo "================================================================"
|
||||
DEADLINE=$(( $(date +%s) + 600 ))
|
||||
LAST_PRINT=0
|
||||
while [ "$(date +%s)" -lt "$DEADLINE" ]; do
|
||||
now=$(date +%s)
|
||||
if [ $((now - LAST_PRINT)) -ge 15 ]; then
|
||||
printf ' [%s] waiting... (%ss elapsed)\n' "$(date +%H:%M:%S)" "$(( now - DEADLINE + 600 ))"
|
||||
LAST_PRINT=$now
|
||||
fi
|
||||
if ssh "${SSH_OPTS[@]}" "root@$HOST" 'echo ok' >/dev/null 2>&1; then
|
||||
# Verify uptime is actually low (host really rebooted, not still up)
|
||||
up_mins=$(ssh "${SSH_OPTS[@]}" "root@$HOST" 'cat /proc/uptime | awk "{print int(\$1/60)}"')
|
||||
if [ "${up_mins:-999}" -lt 5 ]; then
|
||||
echo " [$(date +%H:%M:%S)] SSH back, uptime ${up_mins}min — real reboot confirmed"
|
||||
break
|
||||
fi
|
||||
fi
|
||||
sleep 10
|
||||
done
|
||||
|
||||
if ! ssh "${SSH_OPTS[@]}" "root@$HOST" 'echo ok' >/dev/null 2>&1; then
|
||||
echo "FAILED: $HOST not back after 10 minutes"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Waiting 30s for services to settle..."
|
||||
sleep 30
|
||||
|
||||
echo ""
|
||||
echo "================================================================"
|
||||
echo "[$HOST] POST-REBOOT VERIFICATION"
|
||||
echo "================================================================"
|
||||
ssh "${SSH_OPTS[@]}" "root@$HOST" '
|
||||
echo "--- uptime ---"
|
||||
uptime
|
||||
echo ""
|
||||
echo "--- governor: $(cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor 2>/dev/null) ---"
|
||||
echo "--- swappiness: $(sysctl -n vm.swappiness) ---"
|
||||
echo "--- tcp_cc: $(sysctl -n net.ipv4.tcp_congestion_control) ---"
|
||||
echo "--- tuned: $(tuned-adm active 2>/dev/null | grep Current) ---"
|
||||
echo ""
|
||||
echo "--- NFS first mount: ---"
|
||||
nfsstat -m 2>/dev/null | head -2 | tail -1
|
||||
echo ""
|
||||
echo "--- NFS TCP conns (expect 8 with nconnect=4): ---"
|
||||
ss -tn state established "( dport = :2049 )" 2>/dev/null | tail -n +2 | wc -l
|
||||
echo ""
|
||||
echo "--- VMs: ---"
|
||||
qm list 2>/dev/null
|
||||
echo ""
|
||||
echo "--- Failed services: ---"
|
||||
systemctl --failed --no-legend 2>/dev/null | head -5
|
||||
echo "(empty = none)"
|
||||
'
|
||||
echo ""
|
||||
echo "================================================================"
|
||||
echo "[$HOST] DONE"
|
||||
echo "================================================================"
|
||||
@@ -1,91 +0,0 @@
|
||||
#!/bin/bash
|
||||
# reboot-verify.sh - reboots a host, waits for it to come back, verifies state.
|
||||
# Usage: bash reboot-verify.sh <host>
|
||||
set -uo pipefail
|
||||
|
||||
HOST="$1"
|
||||
SSH_OPTS=(-o BatchMode=yes -o ConnectTimeout=5 -o ServerAliveInterval=5 -o StrictHostKeyChecking=accept-new)
|
||||
|
||||
echo "================================================================"
|
||||
echo "[$HOST] PRE-REBOOT STATE"
|
||||
echo "================================================================"
|
||||
echo "--- Running VMs ---"
|
||||
ssh "${SSH_OPTS[@]}" "root@$HOST" 'qm list 2>/dev/null | awk "NR==1 || \$3==\"running\"{print}"' 2>&1
|
||||
echo "--- NFS mount count ---"
|
||||
ssh "${SSH_OPTS[@]}" "root@$HOST" 'nfsstat -m 2>/dev/null | grep -c "^/mnt"' 2>&1
|
||||
echo "--- NFS TCP connections to :2049 ---"
|
||||
ssh "${SSH_OPTS[@]}" "root@$HOST" 'ss -tn state established "( dport = :2049 )" 2>/dev/null | tail -n +2 | wc -l' 2>&1
|
||||
echo "--- Uptime ---"
|
||||
ssh "${SSH_OPTS[@]}" "root@$HOST" 'uptime' 2>&1
|
||||
|
||||
echo ""
|
||||
echo "================================================================"
|
||||
echo "[$HOST] ISSUING REBOOT"
|
||||
echo "================================================================"
|
||||
# Issue reboot; ssh will disconnect with non-zero — that's expected.
|
||||
ssh "${SSH_OPTS[@]}" "root@$HOST" 'nohup sh -c "(sleep 2; systemctl reboot)" >/dev/null 2>&1 &' 2>&1
|
||||
echo "Reboot command sent at $(date +%H:%M:%S). Host will drop now."
|
||||
|
||||
echo ""
|
||||
echo "================================================================"
|
||||
echo "[$HOST] WAITING FOR SSH TO RETURN (max 10 minutes)"
|
||||
echo "================================================================"
|
||||
DEADLINE=$(( $(date +%s) + 600 ))
|
||||
LAST_PRINT=0
|
||||
while [ "$(date +%s)" -lt "$DEADLINE" ]; do
|
||||
now=$(date +%s)
|
||||
# Print a heartbeat every 15s
|
||||
if [ $((now - LAST_PRINT)) -ge 15 ]; then
|
||||
elapsed=$((DEADLINE - now - 600)); elapsed=${elapsed#-}
|
||||
echo " [$(date +%H:%M:%S)] still waiting... (${elapsed}s elapsed)"
|
||||
LAST_PRINT=$now
|
||||
fi
|
||||
# Try SSH
|
||||
if ssh "${SSH_OPTS[@]}" "root@$HOST" 'echo ok' >/dev/null 2>&1; then
|
||||
echo " [$(date +%H:%M:%S)] SSH is back!"
|
||||
break
|
||||
fi
|
||||
sleep 5
|
||||
done
|
||||
|
||||
# Final check
|
||||
if ! ssh "${SSH_OPTS[@]}" "root@$HOST" 'echo ok' >/dev/null 2>&1; then
|
||||
echo " [$(date +%H:%M:%S)] FAILED: host not reachable after 10 minutes"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Give services a moment to settle after SSH returns
|
||||
echo " Waiting 20s for services to settle..."
|
||||
sleep 20
|
||||
|
||||
echo ""
|
||||
echo "================================================================"
|
||||
echo "[$HOST] POST-REBOOT VERIFICATION"
|
||||
echo "================================================================"
|
||||
echo "--- Uptime ---"
|
||||
ssh "${SSH_OPTS[@]}" "root@$HOST" 'uptime' 2>&1
|
||||
echo ""
|
||||
echo "--- TCP congestion control ---"
|
||||
ssh "${SSH_OPTS[@]}" "root@$HOST" 'sysctl net.ipv4.tcp_congestion_control net.core.default_qdisc' 2>&1
|
||||
echo ""
|
||||
echo "--- vm.swappiness ---"
|
||||
ssh "${SSH_OPTS[@]}" "root@$HOST" 'sysctl vm.swappiness' 2>&1
|
||||
echo ""
|
||||
echo "--- scaling_governor ---"
|
||||
ssh "${SSH_OPTS[@]}" "root@$HOST" 'cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor 2>/dev/null || echo "(no cpufreq driver)"' 2>&1
|
||||
echo ""
|
||||
echo "--- NFS mount options (looking for nconnect + noatime) ---"
|
||||
ssh "${SSH_OPTS[@]}" "root@$HOST" 'nfsstat -m 2>/dev/null | head -20' 2>&1
|
||||
echo ""
|
||||
echo "--- NFS TCP connection count (expect ~8 = 4 per server with nconnect=4) ---"
|
||||
ssh "${SSH_OPTS[@]}" "root@$HOST" 'ss -tn state established "( dport = :2049 )" 2>/dev/null | tail -n +2 | wc -l' 2>&1
|
||||
echo ""
|
||||
echo "--- Running VMs ---"
|
||||
ssh "${SSH_OPTS[@]}" "root@$HOST" 'qm list 2>/dev/null' 2>&1
|
||||
echo ""
|
||||
echo "--- Failed services? ---"
|
||||
ssh "${SSH_OPTS[@]}" "root@$HOST" 'systemctl --failed --no-legend 2>/dev/null | head -10' 2>&1
|
||||
echo ""
|
||||
echo "================================================================"
|
||||
echo "[$HOST] DONE"
|
||||
echo "================================================================"
|
||||
@@ -1,141 +0,0 @@
|
||||
#!/bin/bash
|
||||
###############################################################################
|
||||
# apply-bond-hash.sh
|
||||
#
|
||||
# Adds bond-xmit-hash-policy layer3+4 to bond0 in /etc/network/interfaces,
|
||||
# then reloads networking with ifreload -a.
|
||||
#
|
||||
# SSH survivability: this is safe IF your SSH session is on vmbr0/nic0
|
||||
# (management network), NOT on bond0/datanet (storage network).
|
||||
# tsys7's topology confirms this: SSH comes in on vmbr0 (nic0).
|
||||
#
|
||||
# Safety:
|
||||
# - Dry-run by default (--apply to commit)
|
||||
# - Full backup of /etc/network/interfaces
|
||||
# - Generates rollback script
|
||||
# - Does NOT reboot — uses ifreload -a which is hot-reload
|
||||
###############################################################################
|
||||
set -euo pipefail
|
||||
|
||||
HOST="$(hostname -s)"
|
||||
TS_SHORT="$(date +%Y%m%d-%H%M%S)"
|
||||
BACKUP_DIR="/root/perfopt-backup-${TS_SHORT}"
|
||||
ROLLBACK="/root/perfopt-bond-rollback-${TS_SHORT}.sh"
|
||||
ACTION="${1:-dryrun}"
|
||||
|
||||
[ "$ACTION" = "--apply" ] && ACTION="apply" || ACTION="dryrun"
|
||||
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
|
||||
echo "==================================================================="
|
||||
echo " apply-bond-hash — $HOST"
|
||||
echo " mode: $ACTION"
|
||||
echo "==================================================================="
|
||||
|
||||
if [ ! -r /etc/network/interfaces ]; then
|
||||
echo "FATAL: /etc/network/interfaces not readable"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if bond0 exists in the config
|
||||
if ! grep -q 'bond0' /etc/network/interfaces; then
|
||||
echo "No bond0 found in /etc/network/interfaces — nothing to do."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Check if the hash policy is already set
|
||||
if grep -q 'bond-xmit-hash-policy\|xmit_hash_policy' /etc/network/interfaces; then
|
||||
echo "bond-xmit-hash-policy already present:"
|
||||
grep 'bond-xmit-hash-policy\|xmit_hash_policy' /etc/network/interfaces
|
||||
echo "Checking value..."
|
||||
if grep -q 'layer3+4' /etc/network/interfaces; then
|
||||
echo "Already set to layer3+4 — nothing to do."
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# Show current bond state
|
||||
echo ""
|
||||
echo "--- Current bond0 state ---"
|
||||
cat /proc/net/bonding/bond0 2>/dev/null | head -15 || echo "(bond0 not up or not present)"
|
||||
|
||||
# Back up
|
||||
cp -a /etc/network/interfaces "$BACKUP_DIR/interfaces"
|
||||
|
||||
# Generate rollback script
|
||||
cat > "$ROLLBACK" <<EOF
|
||||
#!/bin/bash
|
||||
# Rollback for bond-xmit-hash-policy change
|
||||
# Restores original /etc/network/interfaces and reloads
|
||||
set -euo pipefail
|
||||
cp -a "$BACKUP_DIR/interfaces" /etc/network/interfaces
|
||||
echo "Restored /etc/network/interfaces"
|
||||
echo "Reloading networking..."
|
||||
ifreload -a 2>&1 || systemctl restart networking 2>&1 || true
|
||||
echo "Done. bond0 hash policy reverted to original."
|
||||
EOF
|
||||
chmod +x "$ROLLBACK"
|
||||
|
||||
echo ""
|
||||
echo "--- Proposed change ---"
|
||||
echo "Add line ' bond-xmit-hash-policy layer3+4' to the bond0 stanza."
|
||||
echo ""
|
||||
|
||||
if [ "$ACTION" != "apply" ]; then
|
||||
echo "DRY RUN — no changes made."
|
||||
echo "To commit: bash $0 --apply"
|
||||
echo "Rollback script (pre-generated): $ROLLBACK"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Apply: use sed to insert bond-xmit-hash-policy after bond-mode line
|
||||
# The bond0 stanza looks like:
|
||||
# auto bond0
|
||||
# iface bond0 inet manual
|
||||
# bond-slaves nic1 nic2
|
||||
# bond-miimon 100
|
||||
# bond-mode 802.3ad
|
||||
#
|
||||
# We insert after the bond-mode line.
|
||||
echo "Applying..."
|
||||
|
||||
# Check if bond-mode line exists (various formats)
|
||||
if grep -qE '^\s*bond-mode\s+802.3ad' /etc/network/interfaces; then
|
||||
# Insert after bond-mode 802.3ad line
|
||||
sed -i '/^\s*bond-mode\s+802\.3ad/a\\tbond-xmit-hash-policy layer3+4' /etc/network/interfaces
|
||||
echo "Inserted bond-xmit-hash-policy layer3+4 after bond-mode line."
|
||||
elif grep -qE '^\s*bond-mode\s+4' /etc/network/interfaces; then
|
||||
sed -i '/^\s*bond-mode\s+4/a\\tbond-xmit-hash-policy layer3+4' /etc/network/interfaces
|
||||
echo "Inserted bond-xmit-hash-policy layer3+4 after bond-mode 4 line."
|
||||
else
|
||||
echo "Could not find bond-mode line — inserting after bond-slaves line instead."
|
||||
sed -i '/^\s*bond-slaves/a\\tbond-xmit-hash-policy layer3+4' /etc/network/interfaces
|
||||
fi
|
||||
|
||||
# Show the result
|
||||
echo ""
|
||||
echo "--- Updated bond0 stanza ---"
|
||||
awk '/^auto bond0/,/^$/' /etc/network/interfaces
|
||||
|
||||
echo ""
|
||||
echo "--- Reloading networking (ifreload -a) ---"
|
||||
echo "SSH should survive (it's on vmbr0/nic0, not bond0)..."
|
||||
ifreload -a 2>&1 || {
|
||||
echo "ifreload failed, trying systemctl restart networking..."
|
||||
systemctl restart networking 2>&1
|
||||
}
|
||||
|
||||
# Wait a moment for bond to renegotiate
|
||||
echo "Waiting 5s for LACP to renegotiate..."
|
||||
sleep 5
|
||||
|
||||
echo ""
|
||||
echo "--- Post-change bond0 state ---"
|
||||
cat /proc/net/bonding/bond0 2>/dev/null | head -20
|
||||
|
||||
echo ""
|
||||
echo "==================================================================="
|
||||
echo " DONE."
|
||||
echo " Backup: $BACKUP_DIR/interfaces"
|
||||
echo " Rollback: bash $ROLLBACK"
|
||||
echo "==================================================================="
|
||||
@@ -1,439 +0,0 @@
|
||||
#!/bin/bash
|
||||
###############################################################################
|
||||
# apply-tunings.sh
|
||||
#
|
||||
# Applies the Tier 0 host-side tunings identified in FINDINGS.md:
|
||||
# 1. Sets scaling_governor=performance on every CPU
|
||||
# 2. Sets vm.swappiness appropriately (1 on storage hosts, 10 on VM hosts)
|
||||
# 3. Enables tcp_bbr module + sets congestion control
|
||||
# 4. Sets net.core.rmem_max/wmem_max + tcp_rmem/wmem for high-BDP NFS
|
||||
# 5. Sets the recommended tuned-adm profile (network-throughput or virtual-host)
|
||||
# 6. Adds nconnect=4 + noatime to NFS client mounts (edits /etc/pve/storage.cfg)
|
||||
# 7. (does NOT touch bond0 xmit-hash — that requires ifreload/network
|
||||
# restart which drops the host. Provided as a separate --emit-bond-patch
|
||||
# flag that PRINTS the change but does not apply it.)
|
||||
#
|
||||
# Safety features:
|
||||
# - Dry-run mode by default (--apply to commit)
|
||||
# - Full backup of every modified file to /root/perfopt-backup-<timestamp>/
|
||||
# - Per-host behaviour: detects storage hosts by hostname and applies the
|
||||
# right profile (tsys4, tsys5 = storage; others = VM hosts).
|
||||
# - Generates a /root/perfopt-rollback.sh that undoes everything.
|
||||
# - Does NOT reboot, does NOT restart networking, does NOT touch hardware.
|
||||
#
|
||||
# Usage:
|
||||
# bash apply-tunings.sh # dry run, show what would change
|
||||
# bash apply-tunings.sh --apply # commit changes
|
||||
# bash apply-tunings.sh --rollback # restore from latest backup
|
||||
# bash apply-tunings.sh --emit-bond-patch # show bond0 hash change (no apply)
|
||||
###############################################################################
|
||||
set -u
|
||||
umask 022
|
||||
|
||||
HOST="$(hostname -s)"
|
||||
TS="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||
TS_SHORT="$(date +%Y%m%d-%H%M%S)"
|
||||
BACKUP_DIR="/root/perfopt-backup-${TS_SHORT}"
|
||||
ROLLBACK_SCRIPT="/root/perfopt-rollback-${TS_SHORT}.sh"
|
||||
ACTION="dryrun"
|
||||
SKIP_NFS=0
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--apply) ACTION="apply" ;;
|
||||
--rollback) ACTION="rollback" ;;
|
||||
--emit-bond-patch) ACTION="bond-patch" ;;
|
||||
--no-nfs) SKIP_NFS=1 ;;
|
||||
-h|--help) sed -n '2,35p' "$0"; exit 0 ;;
|
||||
*) echo "unknown arg: $1" >&2; exit 2 ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
# Detect host role from hostname
|
||||
case "$HOST" in
|
||||
pfv-tsys4|pfv-tsys5)
|
||||
HOST_ROLE="storage"
|
||||
TUNED_PROFILE="network-throughput"
|
||||
SWAPPINESS="1"
|
||||
;;
|
||||
pfv-tsys1|pfv-tsys3|pfv-tsys6|pfv-tsys7)
|
||||
HOST_ROLE="vmhost"
|
||||
TUNED_PROFILE="virtual-host"
|
||||
SWAPPINESS="10"
|
||||
;;
|
||||
*)
|
||||
HOST_ROLE="unknown"
|
||||
TUNED_PROFILE="virtual-host"
|
||||
SWAPPINESS="10"
|
||||
;;
|
||||
esac
|
||||
|
||||
# --- rollback path ---------------------------------------------------------
|
||||
if [ "$ACTION" = "rollback" ]; then
|
||||
echo "Looking for latest backup under /root/perfopt-backup-* ..."
|
||||
latest=""
|
||||
while IFS= read -r d; do
|
||||
latest="$d"
|
||||
done < <(find /root -maxdepth 1 -type d -name 'perfopt-backup-*' 2>/dev/null | sort | tail -n1)
|
||||
if [ -z "$latest" ]; then
|
||||
echo "No backup found under /root/perfopt-backup-*" >&2
|
||||
exit 1
|
||||
fi
|
||||
rb="$latest/perfopt-rollback.sh"
|
||||
if [ ! -x "$rb" ] && [ ! -r "$rb" ]; then
|
||||
echo "Rollback script missing in $latest" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "Rolling back using: $rb"
|
||||
bash "$rb"
|
||||
exit $?
|
||||
fi
|
||||
|
||||
# --- bond patch print-only path -------------------------------------------
|
||||
if [ "$ACTION" = "bond-patch" ]; then
|
||||
echo "==================================================================="
|
||||
echo " Proposed bond0 xmit_hash_policy change"
|
||||
echo "==================================================================="
|
||||
echo
|
||||
if [ ! -r /etc/network/interfaces ]; then
|
||||
echo "/etc/network/interfaces not readable"
|
||||
exit 1
|
||||
fi
|
||||
if ! grep -q 'bond-mode 802.3ad\|bond-mode 4\|bond-slaves' /etc/network/interfaces; then
|
||||
echo "No bond0 detected on this host — nothing to patch."
|
||||
exit 0
|
||||
fi
|
||||
cat <<EOF
|
||||
A manual edit to /etc/network/interfaces is required. The bond0 stanza
|
||||
needs this line added (it defaults to layer2, which is wrong for storage):
|
||||
|
||||
bond-xmit-hash-policy layer3+4
|
||||
|
||||
After editing, you MUST reload networking for it to take effect:
|
||||
ifreload -a # safe, brings interfaces down/up
|
||||
# OR
|
||||
systemctl restart networking # heavier, briefly drops connections
|
||||
|
||||
WARNING: applying this on a remote host over bond0 will briefly drop your
|
||||
SSH session. Run from console/IPMI, or schedule a maintenance window.
|
||||
|
||||
The change is reversible by removing the line and reloading again.
|
||||
EOF
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# --- main path (dryrun or apply) ------------------------------------------
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
|
||||
# Emit the rollback script header
|
||||
cat > "$ROLLBACK_SCRIPT" <<EOF
|
||||
#!/bin/bash
|
||||
# Auto-generated rollback for perfopt apply-tunings.sh
|
||||
# Backup timestamp: $TS
|
||||
# Backup dir: $BACKUP_DIR
|
||||
# Generated on: $HOST
|
||||
set -u
|
||||
EOF
|
||||
chmod +x "$ROLLBACK_SCRIPT"
|
||||
|
||||
backup_file() {
|
||||
local f="$1"
|
||||
if [ -e "$f" ]; then
|
||||
cp -a "$f" "$BACKUP_DIR/$(echo "$f" | sed 's|^/||; s|/|__|g')"
|
||||
fi
|
||||
}
|
||||
|
||||
# Helper: append a restore command to the rollback script
|
||||
rb_restore() {
|
||||
local f="$1"
|
||||
local bk
|
||||
bk="$BACKUP_DIR/$(echo "$f" | sed 's|^/||; s|/|__|g')"
|
||||
cat >> "$ROLLBACK_SCRIPT" <<EOF
|
||||
if [ -r "$bk" ]; then
|
||||
cp -a "$bk" "$f" && echo "restored $f"
|
||||
else
|
||||
echo "WARN: backup $bk missing — $f left as-is"
|
||||
fi
|
||||
EOF
|
||||
}
|
||||
|
||||
# Helper: append a sysctl restore command
|
||||
rb_sysctl_restore() {
|
||||
local key="$1" orig_val="$2"
|
||||
cat >> "$ROLLBACK_SCRIPT" <<EOF
|
||||
sysctl -w "$key=$orig_val" >/dev/null && echo "restored $key=$orig_val"
|
||||
EOF
|
||||
}
|
||||
|
||||
# Helper: append a tuned-adm restore
|
||||
rb_tuned_restore() {
|
||||
local profile="$1"
|
||||
cat >> "$ROLLBACK_SCRIPT" <<EOF
|
||||
tuned-adm profile "$profile" 2>/dev/null && echo "restored tuned profile: $profile"
|
||||
EOF
|
||||
}
|
||||
|
||||
echo "==================================================================="
|
||||
echo " perfopt apply-tunings — $HOST"
|
||||
echo " mode: $ACTION"
|
||||
echo " role: $HOST_ROLE (tuned=$TUNED_PROFILE, swappiness=$SWAPPINESS)"
|
||||
echo " backup: $BACKUP_DIR"
|
||||
echo " rollback: $ROLLBACK_SCRIPT"
|
||||
echo "==================================================================="
|
||||
echo
|
||||
|
||||
# ===========================================================================
|
||||
# 1. scaling_governor → performance
|
||||
# ===========================================================================
|
||||
echo "--- 1. CPU scaling_governor → performance"
|
||||
if [ -r /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor ]; then
|
||||
cur=$(cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor)
|
||||
echo " current: $cur"
|
||||
if [ "$ACTION" = "apply" ]; then
|
||||
if [ "$cur" != "performance" ]; then
|
||||
for c in /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor; do
|
||||
echo performance > "$c" 2>/dev/null || true
|
||||
done
|
||||
# Persist via systemd tmpfiles / sysctl fallback
|
||||
cat > /etc/systemd/system/perfopt-cpu-performance.service <<EOF
|
||||
[Unit]
|
||||
Description=Set CPU scaling_governor=performance (perfopt)
|
||||
After=multi-user.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/bin/sh -c 'for c in /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor; do echo performance > "\$c" 2>/dev/null || true; done'
|
||||
RemainAfterExit=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
systemctl daemon-reload
|
||||
systemctl enable perfopt-cpu-performance.service 2>/dev/null
|
||||
systemctl start perfopt-cpu-performance.service 2>/dev/null
|
||||
echo " applied + persisted via systemd unit"
|
||||
# Rollback
|
||||
cat >> "$ROLLBACK_SCRIPT" <<EOF
|
||||
for c in /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor; do echo "$cur" > "\$c" 2>/dev/null || true; done
|
||||
systemctl disable --now perfopt-cpu-performance.service 2>/dev/null || true
|
||||
rm -f /etc/systemd/system/perfopt-cpu-performance.service
|
||||
systemctl daemon-reload
|
||||
echo "restored scaling_governor=$cur (best-effort; original may have been dynamic)"
|
||||
EOF
|
||||
else
|
||||
echo " already performance — no change"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
echo " cpufreq driver not loaded — nothing to do (typical on BIOS-locked servers)"
|
||||
fi
|
||||
|
||||
# ===========================================================================
|
||||
# 2. vm.swappiness
|
||||
# ===========================================================================
|
||||
echo "--- 2. vm.swappiness → $SWAPPINESS (role=$HOST_ROLE)"
|
||||
cur_swappiness=$(sysctl -n vm.swappiness 2>/dev/null || echo "?")
|
||||
echo " current: $cur_swappiness"
|
||||
if [ "$ACTION" = "apply" ] && [ "$cur_swappiness" != "$SWAPPINESS" ]; then
|
||||
# Persist via sysctl.d
|
||||
backup_file /etc/sysctl.d/99-perfopt.conf
|
||||
cat > /etc/sysctl.d/99-perfopt.conf <<EOF
|
||||
# perfopt apply-tunings.sh — $TS — host=$HOST role=$HOST_ROLE
|
||||
vm.swappiness = $SWAPPINESS
|
||||
EOF
|
||||
sysctl -w "vm.swappiness=$SWAPPINESS" >/dev/null
|
||||
echo " applied (persisted to /etc/sysctl.d/99-perfopt.conf)"
|
||||
rb_restore /etc/sysctl.d/99-perfopt.conf
|
||||
cat >> "$ROLLBACK_SCRIPT" <<EOF
|
||||
rm -f /etc/sysctl.d/99-perfopt.conf
|
||||
sysctl -w "vm.swappiness=$cur_swappiness" >/dev/null
|
||||
echo "restored vm.swappiness=$cur_swappiness"
|
||||
EOF
|
||||
else
|
||||
echo " no change"
|
||||
fi
|
||||
|
||||
# ===========================================================================
|
||||
# 3. TCP BBR + buffers (single sysctl.d file)
|
||||
# ===========================================================================
|
||||
echo "--- 3. TCP BBR + socket buffers"
|
||||
cur_cc=$(sysctl -n net.ipv4.tcp_congestion_control 2>/dev/null || echo "?")
|
||||
cur_avail=$(sysctl -n net.ipv4.tcp_available_congestion_control 2>/dev/null || echo "?")
|
||||
echo " current: $cur_cc (available: $cur_avail)"
|
||||
|
||||
# Decide sysctl values (these are the consensus values for a 1-10 GbE NFS host)
|
||||
TARGET_RMEM_MAX=134217728 # 128 MB
|
||||
TARGET_WMEM_MAX=134217728
|
||||
TARGET_RMEM_DEFAULT=26214400 # 25 MB
|
||||
TARGET_WMEM_DEFAULT=26214400
|
||||
TARGET_TCP_RMEM="4096 87380 $TARGET_RMEM_MAX"
|
||||
TARGET_TCP_WMEM="4096 65536 $TARGET_RMEM_MAX"
|
||||
TARGET_NETDEV_MAX_BACKLOG=250000
|
||||
TARGET_SOMAXCONN=65535
|
||||
|
||||
if [ "$ACTION" = "apply" ]; then
|
||||
# Ensure tcp_bbr module loads at boot
|
||||
if ! grep -q '^tcp_bbr' /etc/modules-load.d/modules.conf 2>/dev/null; then
|
||||
backup_file /etc/modules-load.d/modules.conf
|
||||
mkdir -p /etc/modules-load.d
|
||||
cat >> /etc/modules-load.d/modules.conf <<EOF
|
||||
# perfopt apply-tunings.sh — $TS
|
||||
tcp_bbr
|
||||
EOF
|
||||
modprobe tcp_bbr 2>/dev/null || true
|
||||
rb_restore /etc/modules-load.d/modules.conf
|
||||
fi
|
||||
|
||||
backup_file /etc/sysctl.d/99-perfopt.conf
|
||||
cat > /etc/sysctl.d/99-perfopt.conf <<EOF
|
||||
# perfopt apply-tunings.sh — $TS — host=$HOST role=$HOST_ROLE
|
||||
vm.swappiness = $SWAPPINESS
|
||||
|
||||
# TCP BBR + high-BDP buffers (good for NFS over 1-10 GbE)
|
||||
net.ipv4.tcp_congestion_control = bbr
|
||||
net.core.default_qdisc = fq
|
||||
net.core.rmem_max = $TARGET_RMEM_MAX
|
||||
net.core.wmem_max = $TARGET_WMEM_MAX
|
||||
net.core.rmem_default = $TARGET_RMEM_DEFAULT
|
||||
net.core.wmem_default = $TARGET_WMEM_DEFAULT
|
||||
net.core.netdev_max_backlog = $TARGET_NETDEV_MAX_BACKLOG
|
||||
net.core.somaxconn = $TARGET_SOMAXCONN
|
||||
net.ipv4.tcp_rmem = $TARGET_TCP_RMEM
|
||||
net.ipv4.tcp_wmem = $TARGET_TCP_WMEM
|
||||
EOF
|
||||
sysctl --system >/dev/null 2>&1
|
||||
echo " applied (written to /etc/sysctl.d/99-perfopt.conf + sysctl --system)"
|
||||
# Rebuild rollback for sysctl
|
||||
cat >> "$ROLLBACK_SCRIPT" <<EOF
|
||||
# Restore original sysctl state
|
||||
rm -f /etc/sysctl.d/99-perfopt.conf
|
||||
EOF
|
||||
rb_sysctl_restore "net.ipv4.tcp_congestion_control" "$cur_cc"
|
||||
rb_sysctl_restore "vm.swappiness" "$cur_swappiness"
|
||||
# Note: original buffer sizes not all captured; restoration is best-effort.
|
||||
cat >> "$ROLLBACK_SCRIPT" <<EOF
|
||||
echo "Note: net buffers restored to kernel defaults (original values not captured)."
|
||||
echo "Run 'sysctl --system' to apply."
|
||||
sysctl --system >/dev/null 2>&1 || true
|
||||
EOF
|
||||
fi
|
||||
|
||||
# ===========================================================================
|
||||
# 4. tuned-adm profile
|
||||
# ===========================================================================
|
||||
echo "--- 4. tuned-adm profile → $TUNED_PROFILE"
|
||||
if command -v tuned-adm >/dev/null 2>&1; then
|
||||
cur_profile=$(tuned-adm active 2>/dev/null | awk -F: '/Current active profile/{gsub(/^[ \t]+/,"",$2); print $2}')
|
||||
echo " current: $cur_profile"
|
||||
if [ "$ACTION" = "apply" ] && [ "$cur_profile" != "$TUNED_PROFILE" ]; then
|
||||
tuned-adm profile "$TUNED_PROFILE" 2>&1 | sed 's/^/ /'
|
||||
rb_tuned_restore "$cur_profile"
|
||||
else
|
||||
echo " no change"
|
||||
fi
|
||||
else
|
||||
echo " tuned-adm not installed — skipping"
|
||||
fi
|
||||
|
||||
# ===========================================================================
|
||||
# 5. NFS mount option hardening
|
||||
# Edit /etc/pve/storage.cfg to add nconnect=4 + noatime to NFS plugins.
|
||||
# Proxmox applies these on next mount/remount.
|
||||
# Skipped with --no-nfs (for NFS servers where we don't want to remount).
|
||||
# ===========================================================================
|
||||
echo "--- 5. NFS mount options: add nconnect=4, noatime"
|
||||
if [ "$SKIP_NFS" -eq 1 ]; then
|
||||
echo " SKIPPED (--no-nfs specified)"
|
||||
echo " Note: storage.cfg is cluster-wide; if edited on another host,"
|
||||
echo " the options are already staged here and activate on next reboot."
|
||||
else
|
||||
if [ -r /etc/pve/storage.cfg ]; then
|
||||
backup_file /etc/pve/storage.cfg
|
||||
if grep -q '^nfs:' /etc/pve/storage.cfg; then
|
||||
# Count how many nfs: stanzas lack the options
|
||||
while IFS= read -r line; do
|
||||
if echo "$line" | grep -q '^nfs:'; then
|
||||
cur_stanza=$(echo "$line" | awk '{print $2}')
|
||||
# Look ahead for the next few lines to see if options already set
|
||||
echo " nfs stanza: $cur_stanza"
|
||||
fi
|
||||
done < /etc/pve/storage.cfg
|
||||
|
||||
# Check whether any nfs stanza already has options
|
||||
if grep -A1 '^nfs:' /etc/pve/storage.cfg | grep -q 'options.*nconnect=4'; then
|
||||
echo " some stanzas already have nconnect=4 — check manually"
|
||||
else
|
||||
echo " proposed change: add 'options nconnect=4,noatime,rsize=1048576,wsize=1048576,hard,timeo=600,retrans=2' to each nfs stanza"
|
||||
if [ "$ACTION" = "apply" ]; then
|
||||
# Apply via perl for safety (in-place edit with backup)
|
||||
if cp -a /etc/pve/storage.cfg "$BACKUP_DIR/etc__pve__storage.cfg"; then
|
||||
# Use a python helper for robust PVE storage.cfg editing
|
||||
python3 - <<PYEOF
|
||||
import re, pathlib
|
||||
p = pathlib.Path("/etc/pve/storage.cfg")
|
||||
text = p.read_text()
|
||||
pattern = re.compile(r'(^nfs:\s*\S+\n(?:[ \t]+[^\n]+\n)+)', re.MULTILINE)
|
||||
modified = 0
|
||||
def fix(m):
|
||||
global modified
|
||||
block = m.group(1)
|
||||
if 'options' in block:
|
||||
return block
|
||||
lines = block.rstrip('\n').split('\n')
|
||||
lines.insert(1, '\toptions nconnect=4,noatime,rsize=1048576,wsize=1048576,hard,timeo=600,retrans=2')
|
||||
modified += 1
|
||||
return '\n'.join(lines) + '\n'
|
||||
new_text = pattern.sub(fix, text)
|
||||
if modified:
|
||||
p.write_text(new_text)
|
||||
print(f" patched {modified} NFS stanzas with options line")
|
||||
else:
|
||||
print(" no NFS stanza needed patching (all already had options or no nfs: found)")
|
||||
PYEOF
|
||||
# Remount existing NFS mounts to pick up new options
|
||||
echo " remounting NFS mounts to apply new options..."
|
||||
while IFS= read -r mp; do
|
||||
mount -o remount "$mp" 2>/dev/null && echo " remounted $mp" || echo " FAILED to remount $mp (will pick up on next mount)"
|
||||
done < <(awk '$3 ~ /^nfs/{print $2}' /proc/mounts 2>/dev/null | sort -u)
|
||||
rb_restore /etc/pve/storage.cfg
|
||||
cat >> "$ROLLBACK_SCRIPT" <<EOF
|
||||
# To fully undo NFS options, also remount:
|
||||
while IFS= read -r mp; do
|
||||
mount -o remount "\$mp" 2>/dev/null
|
||||
done < <(awk '\$3 ~ /^nfs/{print \$2}' /proc/mounts 2>/dev/null | sort -u)
|
||||
echo "restored NFS mount options"
|
||||
EOF
|
||||
else
|
||||
echo " ERROR: could not back up storage.cfg — aborting NFS patch"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
else
|
||||
echo " no NFS stanzas in storage.cfg — skipping"
|
||||
fi
|
||||
else
|
||||
echo " /etc/pve/storage.cfg not readable — skipping"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ===========================================================================
|
||||
# 6. Summary
|
||||
# ===========================================================================
|
||||
echo
|
||||
echo "==================================================================="
|
||||
if [ "$ACTION" = "apply" ]; then
|
||||
echo " DONE. Backups in: $BACKUP_DIR"
|
||||
echo " Rollback: bash $ROLLBACK_SCRIPT"
|
||||
echo
|
||||
echo " Next steps:"
|
||||
echo " - Verify with 'sysctl net.ipv4.tcp_congestion_control vm.swappiness'"
|
||||
echo " - Verify NFS mounts with 'nfsstat -m' (look for nconnect=4)"
|
||||
echo " - Run 'bash apply-tunings.sh --emit-bond-patch' for the bond0"
|
||||
echo " xmit_hash_policy change (requires network restart, do in"
|
||||
echo " maintenance window)"
|
||||
else
|
||||
echo " DRY RUN — no changes made."
|
||||
echo " To commit: bash apply-tunings.sh --apply"
|
||||
fi
|
||||
echo "==================================================================="
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,162 +0,0 @@
|
||||
#!/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())
|
||||
@@ -1,86 +0,0 @@
|
||||
#!/bin/bash
|
||||
# fix-tsys7.sh - fixes the two issues found: NFS options string + bond hash
|
||||
# Runs on the target host directly.
|
||||
set -euo pipefail
|
||||
|
||||
echo "==================================================================="
|
||||
echo " FIX: NFS options + bond hash policy"
|
||||
echo "==================================================================="
|
||||
|
||||
# --- FIX 1: Simplify NFS options in storage.cfg ---------------------------
|
||||
echo ""
|
||||
echo "--- FIX 1: Simplify NFS options (remove version=4.2 conflict) ---"
|
||||
# Replace the overly-complex options line with a minimal one
|
||||
# PVE handles vers/rsize/wsize/hard/etc internally; we only need nconnect + noatime
|
||||
if grep -q 'options.*nconnect=4.*version=4.2' /etc/pve/storage.cfg; then
|
||||
# Use sed to replace each options line
|
||||
sed -i 's/options nconnect=4,noatime,rsize=1048576,wsize=1048576,hard,timeo=600,retrans=2,version=4.2/options nconnect=4,noatime/' /etc/pve/storage.cfg
|
||||
echo "Fixed: simplified NFS options to 'nconnect=4,noatime'"
|
||||
else
|
||||
echo "Options line already simplified or not present"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "--- Verify storage.cfg NFS stanzas ---"
|
||||
grep -A 2 "^nfs: D2" /etc/pve/storage.cfg | head -3
|
||||
echo "..."
|
||||
grep -c "options nconnect" /etc/pve/storage.cfg
|
||||
echo " NFS stanzas with options"
|
||||
|
||||
# --- FIX 2: Bond hash policy (force via sysfs + persist in interfaces) ----
|
||||
echo ""
|
||||
echo "--- FIX 2: Apply bond xmit_hash_policy=layer3+4 ---"
|
||||
|
||||
# 2a: Apply LIVE via sysfs (takes effect immediately, no network reload)
|
||||
echo "Applying live via sysfs..."
|
||||
if echo "layer3+4" > /sys/class/net/bond0/bonding/xmit_hash_policy 2>/dev/null; then
|
||||
echo "Live sysfs apply: SUCCESS"
|
||||
else
|
||||
echo "Live sysfs apply: FAILED (will persist in config and apply on ifreload)"
|
||||
fi
|
||||
|
||||
# Verify live state
|
||||
echo ""
|
||||
echo "Live bond0 hash policy:"
|
||||
cat /proc/net/bonding/bond0 | grep "Transmit Hash"
|
||||
|
||||
# 2b: Persist in /etc/network/interfaces (fix the sed that failed before)
|
||||
echo ""
|
||||
echo "Persisting in /etc/network/interfaces..."
|
||||
|
||||
# Backup
|
||||
cp -a /etc/network/interfaces "/root/interfaces.bondfix.$(date +%Y%m%d%H%M%S)"
|
||||
|
||||
# Check if already present
|
||||
if grep -q 'bond-xmit-hash-policy' /etc/network/interfaces; then
|
||||
echo "bond-xmit-hash-policy already in interfaces file"
|
||||
else
|
||||
# Use awk to insert after the bond-mode line (more reliable than sed)
|
||||
# Match any line containing 'bond-mode' (regardless of indentation)
|
||||
awk '
|
||||
/bond-mode/ && !done {
|
||||
print
|
||||
print "\tbond-xmit-hash-policy layer3+4"
|
||||
done=1
|
||||
next
|
||||
}
|
||||
{ print }
|
||||
' /etc/network/interfaces > /etc/network/interfaces.new
|
||||
|
||||
mv /etc/network/interfaces.new /etc/network/interfaces
|
||||
echo "Inserted bond-xmit-hash-policy layer3+4"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "--- Updated bond0 stanza ---"
|
||||
awk '/^auto bond0/,/^$/' /etc/network/interfaces
|
||||
|
||||
# Final verify
|
||||
echo ""
|
||||
echo "--- Final bond0 running state ---"
|
||||
cat /proc/net/bonding/bond0 | head -20
|
||||
|
||||
echo ""
|
||||
echo "==================================================================="
|
||||
echo " FIX COMPLETE"
|
||||
echo "==================================================================="
|
||||
@@ -1,219 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
###############################################################################
|
||||
# lacp-retrans-cause.sh (HOST-NATIVE)
|
||||
#
|
||||
# Runs on tsys6 (receiver) or tsys7 (sender). Auto-detects role.
|
||||
#
|
||||
# Question this answers: are the ~56K retransmits we see on 8-stream iperf
|
||||
# between tsys6 and tsys7 fixable (NIC ring drops, softnet drops, CPU
|
||||
# saturation) or just unavoidable LACP reordering overhead?
|
||||
#
|
||||
# Method: snapshot drop/error counters + softnet_stat + TCP SNMP before and
|
||||
# after a 12s iperf3 run, then print the deltas. The decisive columns are:
|
||||
# - NIC rx_dropped / rx_missed_errors / rx_no_dma_resources -> ring too small
|
||||
# - /proc/net/softnet_stat drops -> ksoftirq starved
|
||||
# - /proc/net/snmp TCP retranst -> TCP-level retrans
|
||||
# If NIC drop counters do NOT climb but TCP retrans does, the retrans are
|
||||
# coming from LACP reordering (out-of-order segments triggering fast
|
||||
# retransmit), not packet loss — and are NOT fixable by tuning.
|
||||
###############################################################################
|
||||
set -uo pipefail
|
||||
|
||||
DURATION="${LACP_TEST_SECS:-12}"
|
||||
STREAMS="${LACP_STREAMS:-8}"
|
||||
PEER_RX_IP="10.100.100.6" # tsys6 storage IP (receiver)
|
||||
PEER_TX_IP="10.100.100.7" # tsys7 storage IP (sender)
|
||||
LOG_DIR="/root"
|
||||
BOND="bond0"
|
||||
|
||||
HOST="$(uname -n)"; HOST="${HOST%%.*}"
|
||||
case "$HOST" in
|
||||
*tsys6) ROLE="receiver"; LOG="$LOG_DIR/lacp-retrans-receiver.log" ;;
|
||||
*tsys7) ROLE="sender"; LOG="$LOG_DIR/lacp-retrans-sender.log" ;;
|
||||
*) echo "ERROR: not on tsys6 or tsys7 (host=$HOST)"; exit 2 ;;
|
||||
esac
|
||||
|
||||
TMP="$(mktemp -d)"
|
||||
trap 'rm -rf "$TMP"' EXIT
|
||||
|
||||
# ---- per-slave NIC counter snapshot -----------------------------------------
|
||||
# Captures rx_dropped, rx_missed_errors, rx_no_dma_resources, tx_retrans,
|
||||
# plus all error-like counters. Falls back gracefully if a counter doesn't
|
||||
# exist (different NIC drivers expose different names).
|
||||
nic_snapshot() {
|
||||
# shellcheck disable=SC2013 # interface names contain no spaces; word-splitting is safe here
|
||||
for s in $(awk '/^Slave Interface:/{print $3}' "/proc/net/bonding/$BOND"); do
|
||||
[ -n "$s" ] || continue
|
||||
echo "[$s]"
|
||||
# ethtool -S may exist; show only drop/error/retrans lines
|
||||
if command -v ethtool >/dev/null 2>&1; then
|
||||
ethtool -S "$s" 2>/dev/null \
|
||||
| grep -iE 'drop|miss|error|no_dma|fifo|retrans|overflow' \
|
||||
|| echo "(ethtool -S: no matching counters or unsupported)"
|
||||
else
|
||||
echo "(ethtool not installed)"
|
||||
fi
|
||||
# /sys counters (always available, driver-agnostic)
|
||||
for c in rx_dropped tx_dropped rx_errors tx_errors rx_missed_errors \
|
||||
rx_length_errors rx_crc_errors rx_fifo_errors tx_fifo_errors \
|
||||
multicast collisions; do
|
||||
v=$(cat "/sys/class/net/$s/statistics/$c" 2>/dev/null)
|
||||
[ -n "$v" ] && printf ' sysfs %-22s = %s\n' "$c" "$v"
|
||||
done
|
||||
done
|
||||
}
|
||||
|
||||
# ---- softnet_stat snapshot (per-CPU RX softirq drops) -----------------------
|
||||
# /proc/net/softnet_stat columns are HEX. Col1=processed, col2=dropped,
|
||||
# col3=time_squeeze. We only care about dropped + squeeze (small numbers that
|
||||
# look identical in hex and decimal). Processed is informational only and we
|
||||
# don't try to delta it (hex arithmetic is awk-version-dependent).
|
||||
softnet_snapshot() {
|
||||
awk '{ printf "cpu%s processed=%s dropped=%s squeezed=%s\n", \
|
||||
NR-1, $1, $2, $3 }' /proc/net/softnet_stat
|
||||
}
|
||||
|
||||
# ---- TCP SNMP snapshot (the source of iperf's retransmit number) -----------
|
||||
tcp_snapshot() {
|
||||
awk '/^Tcp:/{
|
||||
if (!seen) {
|
||||
seen=1
|
||||
# /proc/net/snmp Tcp line 1 = names, line 2 = values
|
||||
n=split($0, names, " ")
|
||||
# re-find the values line
|
||||
getline
|
||||
vals=$0
|
||||
split(vals, v, " ")
|
||||
for (i=1;i<=n;i++) printf " %-22s = %s\n", names[i], v[i]
|
||||
}
|
||||
}' /proc/net/snmp
|
||||
}
|
||||
|
||||
# ---- print deltas for selected counters ------------------------------------
|
||||
# $1 = before file, $2 = after file
|
||||
# softnet_snapshot output: "cpuN processed=HEX dropped=HEX squeezed=HEX"
|
||||
# /proc/net/softnet_stat is hex, but dropped/squeezed are always small
|
||||
# integers (typically 0 on a healthy host), so +0 coercion is correct for
|
||||
# the values we care about. We deliberately DON'T report `processed` deltas
|
||||
# because hex arithmetic on large numbers is awk-version-dependent.
|
||||
softnet_delta() {
|
||||
awk '
|
||||
FNR==NR { if (match($0,/cpu[0-9]+/)) { id=substr($0,RSTART,RLENGTH);
|
||||
split($0, p, /[= ]+/); drop[id]=p[5]+0; sqz[id]=p[7]+0 }
|
||||
next }
|
||||
{ if (match($0,/cpu[0-9]+/)) { id=substr($0,RSTART,RLENGTH);
|
||||
split($0, q, /[= ]+/);
|
||||
dd = (q[5]+0) - drop[id]
|
||||
sd = (q[7]+0) - sqz[id]
|
||||
if (dd != 0 || sd != 0)
|
||||
printf " %-8s dropped_delta=%-6s squeezed_delta=%-6s\n", id, dd, sd } }
|
||||
' "$1" "$2" | sort
|
||||
}
|
||||
|
||||
# $1 = before file, $2 = after file, $3 = section label prefix
|
||||
# File format: slave header line "[nic1]", then " counter_name = value" lines.
|
||||
# Output only counters whose value changed.
|
||||
nic_delta() {
|
||||
awk '
|
||||
FNR==NR { if ($0 ~ /^\[/) slave=$0
|
||||
else if ($0 ~ /=/) {
|
||||
# Everything before " = " is the counter name (trim spaces)
|
||||
pos = index($0, "=")
|
||||
name = substr($0, 1, pos-1)
|
||||
gsub(/^ +| +$/, "", name)
|
||||
val = substr($0, pos+1); gsub(/^ +| +$/, "", val)
|
||||
before[slave SUBSEP name] = val
|
||||
}
|
||||
next }
|
||||
{ if ($0 ~ /^\[/) slave=$0
|
||||
else if ($0 ~ /=/) {
|
||||
pos = index($0, "=")
|
||||
name = substr($0, 1, pos-1); gsub(/^ +| +$/, "", name)
|
||||
val = substr($0, pos+1); gsub(/^ +| +$/, "", val)
|
||||
b = before[slave SUBSEP name]+0
|
||||
d = val+0 - b
|
||||
if (d != 0) printf " %-8s %-30s %12s -> %12s delta=%d\n", slave, name, b, val, d
|
||||
} }
|
||||
' "$1" "$2"
|
||||
}
|
||||
|
||||
{
|
||||
echo "=== LACP retransmit cause investigation ($ROLE) ==="
|
||||
echo "Host: $HOST Role: $ROLE"
|
||||
echo "Date: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||
echo "Bond: $BOND"
|
||||
echo ""
|
||||
echo "--- bond0 hash + driver ---"
|
||||
grep -E "Bonding Mode|Transmit Hash|Number of ports|Partner Mac" /proc/net/bonding/$BOND
|
||||
# shellcheck disable=SC2013 # interface names contain no spaces; word-splitting is safe here
|
||||
for s in $(awk '/^Slave Interface:/{print $3}' /proc/net/bonding/$BOND); do
|
||||
drv=$(ethtool -i "$s" 2>/dev/null | awk -F: '/^driver:/{print $2}' | sed 's/^ *//')
|
||||
speed=$(cat "/sys/class/net/$s/speed" 2>/dev/null)
|
||||
ring=$(ethtool -g "$s" 2>/dev/null | awk '/RX:/{print $2; exit}')
|
||||
printf " %-8s driver=%-20s speed=%-6s current RX ring=%s\n" "$s" "$drv" "$speed" "$ring"
|
||||
done
|
||||
echo ""
|
||||
} | tee "$LOG"
|
||||
|
||||
echo "--- BEFORE snapshots ---"
|
||||
nic_snapshot > "$TMP/nic_before"
|
||||
softnet_snapshot > "$TMP/softnet_before"
|
||||
tcp_snapshot > "$TMP/tcp_before"
|
||||
|
||||
if [ "$ROLE" = "receiver" ]; then
|
||||
echo "[receiver] starting iperf3 -s -1 on $PEER_RX_IP ..."
|
||||
pkill -x iperf3 2>/dev/null; sleep 0.5
|
||||
nohup iperf3 -s -1 -B "$PEER_RX_IP" > "$TMP/iperf.out" 2>&1 &
|
||||
SRV_PID=$!
|
||||
for _ in $(seq 1 60); do kill -0 "$SRV_PID" 2>/dev/null || break; sleep 1; done
|
||||
wait "$SRV_PID" 2>/dev/null
|
||||
else
|
||||
echo "[sender] waiting 3s for receiver to listen, then running iperf3 -P $STREAMS -t $DURATION ..."
|
||||
sleep 3
|
||||
iperf3 -c "$PEER_RX_IP" -B "$PEER_TX_IP" -P "$STREAMS" -t "$DURATION" -l 128k -O 2 \
|
||||
> "$TMP/iperf.out" 2>&1
|
||||
fi
|
||||
|
||||
echo "--- AFTER snapshots ---"
|
||||
nic_snapshot > "$TMP/nic_after"
|
||||
softnet_snapshot > "$TMP/softnet_after"
|
||||
tcp_snapshot > "$TMP/tcp_after"
|
||||
|
||||
{
|
||||
echo ""
|
||||
echo "--- iperf3 summary ---"
|
||||
grep -E '\[SUM\].*(sender|receiver)' "$TMP/iperf.out" | tail -2
|
||||
echo ""
|
||||
echo "--- TCP SNMP deltas (/proc/net/snmp) ---"
|
||||
# Show only the counters that changed
|
||||
paste "$TMP/tcp_before" "$TMP/tcp_after" \
|
||||
| awk '{
|
||||
a=$3; b=$(NF)
|
||||
if (a+0 != b+0) printf " %-22s %12s -> %12s delta=%d\n", $1, a, b, b-a
|
||||
}'
|
||||
echo ""
|
||||
echo "--- softnet_stat deltas (look for non-zero dropped_delta/squeezed_delta) ---"
|
||||
softnet_delta "$TMP/softnet_before" "$TMP/softnet_after"
|
||||
echo ""
|
||||
echo "--- NIC counter deltas (only non-zero) ---"
|
||||
nic_delta "$TMP/nic_before" "$TMP/nic_after" "$HOST"
|
||||
echo ""
|
||||
echo "--- raw iperf3 tail (last 12 lines) ---"
|
||||
tail -12 "$TMP/iperf.out"
|
||||
echo ""
|
||||
echo "==================================================================="
|
||||
echo " HOW TO READ THIS"
|
||||
echo "==================================================================="
|
||||
echo " - If NIC rx_dropped / rx_missed_errors / rx_fifo_errors climbed:"
|
||||
echo " -> ring buffers too small. Fix: ethtool -G \$IF rx 4096 (or max)."
|
||||
echo " - If softnet_stat 'dropped' or 'squeezed' climbed on any CPU:"
|
||||
echo " -> softirq starvation. Fix: increase net.core.netdev_budget,"
|
||||
echo " check IRQ affinity, consider RPS."
|
||||
echo " - If NEITHER of the above climbed but TCP RetransSegs did:"
|
||||
echo " -> the retransmits are LACP reordering (out-of-order segments"
|
||||
echo " triggering fast retransmit), NOT packet loss. NOT fixable."
|
||||
echo "==================================================================="
|
||||
} | tee -a "$LOG"
|
||||
|
||||
echo ""
|
||||
echo "Log: $LOG"
|
||||
@@ -1,153 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
###############################################################################
|
||||
# lacp-rx-distribution.sh (HOST-NATIVE version)
|
||||
#
|
||||
# Runs ON a host (tsys6 or tsys7). Auto-detects role by hostname:
|
||||
# - On tsys6 (receiver): starts iperf3 -s -1 (one-shot), snapshots local
|
||||
# bond0 slave RX counters before+after, writes verdict + log.
|
||||
# - On tsys7 (sender): snapshots local bond0 slave TX counters before+
|
||||
# after, runs iperf3 -c <peer> -P 8 -t 12.
|
||||
#
|
||||
# Each side writes only its own counters. No inter-host SSH required.
|
||||
# iperf3 -s -1 (one-shot) handles client/server coordination.
|
||||
#
|
||||
# Output: /root/lacp-rx-{receiver,sender}.log
|
||||
###############################################################################
|
||||
set -uo pipefail
|
||||
|
||||
DURATION="${LACP_TEST_SECS:-12}"
|
||||
STREAMS="${LACP_STREAMS:-8}"
|
||||
PEER_RX_IP="10.100.100.6" # tsys6 storage IP (receiver)
|
||||
PEER_TX_IP="10.100.100.7" # tsys7 storage IP (sender)
|
||||
LOG_DIR="/root"
|
||||
BOND="bond0"
|
||||
|
||||
# Snapshot per-slave byte counters from $BOND. Output: "<slave> rx tx\n" sorted.
|
||||
snapshot() {
|
||||
awk '/^Slave Interface:/{print $3}' "/proc/net/bonding/$BOND" | while read -r s; do
|
||||
[ -n "$s" ] || continue
|
||||
rx=$(cat "/sys/class/net/$s/statistics/rx_bytes" 2>/dev/null || echo 0)
|
||||
tx=$(cat "/sys/class/net/$s/statistics/tx_bytes" 2>/dev/null || echo 0)
|
||||
printf '%s %s %s\n' "$s" "$rx" "$tx"
|
||||
done | sort
|
||||
}
|
||||
|
||||
# Compute per-slave deltas. $1=label, $2=col(2=rx,3=tx), $3=before, $4=after.
|
||||
analyze() {
|
||||
local label="$1" col="$2" before="$3" after="$4"
|
||||
join "$before" "$after" | awk -v col="$col" -v lbl="$label" '
|
||||
{
|
||||
slave=$1
|
||||
b = (col==2 ? $2 : $3)+0
|
||||
a = (col==2 ? $4 : $5)+0
|
||||
d = a - b; if (d < 0) d = 0
|
||||
delta[slave]=d; bef[slave]=b; aft[slave]=a
|
||||
order[++n]=slave; total+=d
|
||||
}
|
||||
END {
|
||||
printf "\n--- %s (bytes) ---\n", lbl
|
||||
printf " %-12s %14s %14s %14s %8s\n", "SLAVE", "BEFORE", "AFTER", "DELTA", "PCT"
|
||||
for (i=1;i<=n;i++){
|
||||
s=order[i]
|
||||
pct = (total>0 ? 100*delta[s]/total : 0)
|
||||
printf " %-12s %14d %14d %14d %7.1f%%\n", s, bef[s], aft[s], delta[s], pct
|
||||
}
|
||||
printf " %-12s %14s %14s %14d %8s\n", "TOTAL", "", "", total, "100.0%"
|
||||
}'
|
||||
}
|
||||
|
||||
# Dominant-slave pct for the verdict line. $1=col, $2=before, $3=after.
|
||||
dominant() {
|
||||
join "$2" "$3" | awk -v col="$1" '
|
||||
{ b=(col==2?$2:$3)+0; a=(col==2?$4:$5)+0; d=a-b; if(d<0)d=0; tot+=d; delta[$1]=d }
|
||||
END { m=0; ms=""; for (s in delta){ if (delta[s]>m){m=delta[s]; ms=s} }
|
||||
printf "%.1f %s", (tot>0?100*m/tot:0), ms }'
|
||||
}
|
||||
|
||||
HOST="$(uname -n)"
|
||||
HOST="${HOST%%.*}"
|
||||
case "$HOST" in
|
||||
*tsys6) ROLE="receiver"; LOG="$LOG_DIR/lacp-rx-receiver.log" ;;
|
||||
*tsys7) ROLE="sender"; LOG="$LOG_DIR/lacp-rx-sender.log" ;;
|
||||
*) echo "ERROR: not running on tsys6 or tsys7 (hostname=$HOST)"; exit 2 ;;
|
||||
esac
|
||||
|
||||
TMP="$(mktemp -d)"
|
||||
trap 'rm -rf "$TMP"' EXIT
|
||||
|
||||
{
|
||||
echo "=== LACP per-slave distribution ($ROLE) ==="
|
||||
echo "Host: $HOST Role: $ROLE"
|
||||
echo "Date: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||
echo "Bond: $BOND"
|
||||
echo ""
|
||||
echo "--- $BOND state ---"
|
||||
grep -E "Bonding Mode|Transmit Hash|Number of ports|Partner Mac|Slave Interface|Link" \
|
||||
"/proc/net/bonding/$BOND" 2>&1
|
||||
echo ""
|
||||
} | tee "$LOG"
|
||||
|
||||
snapshot > "$TMP/before"
|
||||
|
||||
if [ "$ROLE" = "receiver" ]; then
|
||||
echo "[receiver] starting one-shot iperf3 server on $PEER_RX_IP ..."
|
||||
pkill -x iperf3 2>/dev/null; sleep 0.5
|
||||
nohup iperf3 -s -1 -B "$PEER_RX_IP" > "$TMP/iperf.out" 2>&1 &
|
||||
SRV_PID=$!
|
||||
# Wait for the server to be ready (brief), then wait for it to exit
|
||||
# (one-shot server exits after serving one client).
|
||||
for _ in $(seq 1 60); do
|
||||
kill -0 "$SRV_PID" 2>/dev/null || break
|
||||
sleep 1
|
||||
done
|
||||
wait "$SRV_PID" 2>/dev/null
|
||||
echo "[receiver] iperf3 server finished."
|
||||
else
|
||||
# sender: wait briefly for receiver to be listening, then run client.
|
||||
echo "[sender] waiting 3s for receiver to listen, then running iperf3 client..."
|
||||
sleep 3
|
||||
iperf3 -c "$PEER_RX_IP" -B "$PEER_TX_IP" -P "$STREAMS" -t "$DURATION" -l 128k -O 2 \
|
||||
> "$TMP/iperf.out" 2>&1
|
||||
echo "[sender] iperf3 client finished (exit=$?)."
|
||||
fi
|
||||
|
||||
snapshot > "$TMP/after"
|
||||
|
||||
{
|
||||
echo ""
|
||||
echo "--- iperf3 output (raw tail) ---"
|
||||
tail -25 "$TMP/iperf.out"
|
||||
echo ""
|
||||
if [ "$ROLE" = "receiver" ]; then
|
||||
analyze "Receiver RX (THE key column)" 2 "$TMP/before" "$TMP/after"
|
||||
dom="$(dominant 2 "$TMP/before" "$TMP/after")"
|
||||
pct=${dom%% *}; top=${dom##* }
|
||||
echo ""
|
||||
echo "==================================================================="
|
||||
if awk -v p="$pct" 'BEGIN{exit !(p>=90)}'; then
|
||||
echo " VERDICT: SWITCH NOT distributing across $HOST's two ports"
|
||||
echo " Dominant slave '$top' = ${pct}% of RX -> hash not effective"
|
||||
echo " on this LAG. Action: verify/bounce pfv-r3-tor-stor port-channel."
|
||||
else
|
||||
echo " VERDICT: switch IS distributing (top slave '$top' = ${pct}%)."
|
||||
echo " Cap is host-side: softirq/CPU/bridge/NIC coalescing."
|
||||
fi
|
||||
echo "==================================================================="
|
||||
else
|
||||
analyze "Sender TX (control: should split if host hash=layer3+4)" 3 "$TMP/before" "$TMP/after"
|
||||
dom="$(dominant 3 "$TMP/before" "$TMP/after")"
|
||||
pct=${dom%% *}; top=${dom##* }
|
||||
echo ""
|
||||
echo "==================================================================="
|
||||
if awk -v p="$pct" 'BEGIN{exit !(p>=90)}'; then
|
||||
echo " VERDICT: host TX NOT distributing (top slave '$top' = ${pct}%)."
|
||||
echo " Host xmit_hash_policy is NOT effective despite /proc/net/bonding."
|
||||
else
|
||||
echo " VERDICT: host TX distributing OK (top slave '$top' = ${pct}%)."
|
||||
fi
|
||||
echo "==================================================================="
|
||||
fi
|
||||
} | tee -a "$LOG"
|
||||
|
||||
echo ""
|
||||
echo "Log written: $LOG"
|
||||
@@ -1,79 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Read-only fleet drift probe — gathers package/tooling/config state for
|
||||
# consistency comparison across hosts. Writes only stdout.
|
||||
set -u
|
||||
echo "===== DRIFT PROBE: $(hostname -s) $(date -u +%FT%TZ) ====="
|
||||
echo
|
||||
echo "##### OS / kernel / PVE #####"
|
||||
pveversion 2>&1
|
||||
cat /etc/debian_version 2>&1
|
||||
echo
|
||||
echo "##### Installed key packages (versions) #####"
|
||||
dpkg-query -W -f='${Package}\t${Version}\n' 2>/dev/null | grep -iE 'lldpd|lldpad|smartmontools|nfs-common|nfs-kernel-server|iperf3|tcpdump|htop|rsyslog|qemu-guest-agent|snmpd|snmp|net-tools|ethtool|sysstat|ioping|fio|nvme-cli|conman|ser2net|nut-server|nut-client|tuned-adm|tuned' 2>/dev/null | sort
|
||||
echo
|
||||
echo "##### lldpcli present? #####"
|
||||
command -v lldpcli >/dev/null 2>&1 && lldpcli -v 2>&1 | head -1 || echo "lldpcli: NOT INSTALLED"
|
||||
echo
|
||||
echo "##### lldpd service #####"
|
||||
systemctl is-active lldpd 2>&1 || true
|
||||
systemctl is-enabled lldpd 2>&1 || true
|
||||
echo
|
||||
echo "##### smartmontools service #####"
|
||||
systemctl is-active smartd 2>&1 || echo "smartd: inactive/disabled"
|
||||
systemctl is-enabled smartd 2>&1 || true
|
||||
echo
|
||||
echo "##### snmpd service #####"
|
||||
systemctl is-active snmpd 2>&1 || echo "snmpd: not running"
|
||||
systemctl is-enabled snmpd 2>&1 || true
|
||||
echo
|
||||
echo "##### rsyslog #####"
|
||||
systemctl is-active rsyslog 2>&1 || echo "rsyslog: not running"
|
||||
echo
|
||||
echo "##### tuned profile #####"
|
||||
tuned-adm active 2>&1 | head -2 || echo "tuned: not available"
|
||||
echo
|
||||
echo "##### sshd config (key settings) #####"
|
||||
grep -E '^(PermitRootLogin|PasswordAuthentication|PubkeyAuthentication|KbdInteractiveAuthentication|Port |PermitEmptyPasswords)' /etc/ssh/sshd_config 2>/dev/null || echo "(defaults)"
|
||||
grep -E '^(PermitRootLogin|PasswordAuthentication|PubkeyAuthentication|KbdInteractiveAuthentication|Port |PermitEmptyPasswords)' /etc/ssh/sshd_config.d/*.conf 2>/dev/null || true
|
||||
echo
|
||||
echo "##### authorized_keys (root) — count + key types #####"
|
||||
if [ -f /root/.ssh/authorized_keys ]; then
|
||||
echo " total keys: $(wc -l < /root/.ssh/authorized_keys)"
|
||||
awk '{print " "$1}' /root/.ssh/authorized_keys | sort | uniq -c
|
||||
else
|
||||
echo " NO authorized_keys for root"
|
||||
fi
|
||||
echo
|
||||
echo "##### ssh host key fingerprints #####"
|
||||
for f in /etc/ssh/ssh_host_*_key.pub; do
|
||||
[ -f "$f" ] && ssh-keygen -lf "$f" 2>/dev/null
|
||||
done
|
||||
echo
|
||||
echo "##### sysctl tuning (key values) #####"
|
||||
echo " net.core.rmem_max=$(cat /proc/sys/net/core/rmem_max 2>/dev/null)"
|
||||
echo " net.core.wmem_max=$(cat /proc/sys/net/core/wmem_max 2>/dev/null)"
|
||||
echo " net.ipv4.tcp_congestion_control=$(cat /proc/sys/net/ipv4/tcp_congestion_control 2>/dev/null)"
|
||||
echo " net.ipv4.tcp_rmem=$(cat /proc/sys/net/ipv4/tcp_rmem 2>/dev/null)"
|
||||
echo " net.ipv4.tcp_wmem=$(cat /proc/sys/net/ipv4/tcp_wmem 2>/dev/null)"
|
||||
echo " net.ipv4.tcp_max_syn_backlog=$(cat /proc/sys/net/ipv4/tcp_max_syn_backlog 2>/dev/null)"
|
||||
echo " net.core.netdev_max_backlog=$(cat /proc/sys/net/core/netdev_max_backlog 2>/dev/null)"
|
||||
echo " vm.swappiness=$(cat /proc/sys/vm/swappiness 2>/dev/null)"
|
||||
echo
|
||||
echo "##### CPU governor #####"
|
||||
cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor 2>/dev/null || echo "no cpufreq driver"
|
||||
echo
|
||||
echo "##### beszel agent? #####"
|
||||
systemctl is-active beszel-hub 2>/dev/null || systemctl is-active beszel-agent 2>/dev/null || echo "beszel: not installed"
|
||||
echo
|
||||
echo "##### /root/bin or custom scripts present? #####"
|
||||
# shellcheck disable=SC2012 # diagnostic listing, not for processing
|
||||
find /root/bin/ -maxdepth 1 -type f 2>/dev/null | head -10 || echo "(no /root/bin)"
|
||||
find /root/ -maxdepth 1 -name '*.sh' -type f 2>/dev/null | head -10 || echo "(no /root/*.sh)"
|
||||
echo
|
||||
echo "##### mount options (noatime check on root) #####"
|
||||
mount | grep ' / ' | head -1
|
||||
echo
|
||||
echo "##### NFS server threads (if NFS server) #####"
|
||||
grep -E '^[[:space:]]*RPCNFSDCOUNT' /etc/default/nfs-kernel-server 2>/dev/null || grep -E 'RPCNFSDCOUNT' /etc/default/nfs-kernel-server 2>/dev/null || echo "(default, not a NFS server or default threads)"
|
||||
echo
|
||||
echo "===== END $(hostname -s) ====="
|
||||
@@ -1,82 +0,0 @@
|
||||
#!/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) ====="
|
||||
@@ -1,75 +0,0 @@
|
||||
#!/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) ====="
|
||||
@@ -1,91 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
###############################################################################
|
||||
# run-lacp-retrans-cause.sh (workstation wrapper)
|
||||
#
|
||||
# Deploys scripts/lacp-retrans-cause.sh to BOTH hosts, runs them in the
|
||||
# right order, scps logs back, prints them.
|
||||
###############################################################################
|
||||
set -uo pipefail
|
||||
|
||||
SSH=(-o BatchMode=yes -o ConnectTimeout=8 -o ServerAliveInterval=10 \
|
||||
-o StrictHostKeyChecking=accept-new)
|
||||
SCP=(-o BatchMode=yes -o ConnectTimeout=8 -o StrictHostKeyChecking=accept-new)
|
||||
|
||||
RECV="pfv-tsys6"; SEND="pfv-tsys7"
|
||||
LOCAL_LOG_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)/returned-logs/iperf"
|
||||
SCRIPT="lacp-retrans-cause.sh"
|
||||
LOCAL_SCRIPT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)/scripts/${SCRIPT}"
|
||||
REMOTE_SCRIPT="/root/${SCRIPT}"
|
||||
mkdir -p "$LOCAL_LOG_DIR"
|
||||
|
||||
echo "==================================================================="
|
||||
echo " LACP retransmit-cause investigation"
|
||||
echo " Receiver: $RECV Sender: $SEND"
|
||||
echo "==================================================================="
|
||||
echo ""
|
||||
|
||||
echo "--- preflight ---"
|
||||
for h in "$RECV" "$SEND"; do
|
||||
printf ' %-12s ' "$h"
|
||||
ssh "${SSH[@]}" "root@$h" \
|
||||
'command -v ethtool >/dev/null && e=OK || e=MISSING
|
||||
command -v iperf3 >/dev/null && i=OK || i=MISSING
|
||||
printf "ethtool=%s iperf3=%s host=%s\n" "$e" "$i" "$(uname -n)"' 2>&1 | head -1
|
||||
done
|
||||
echo ""
|
||||
|
||||
echo "--- deploy ---"
|
||||
for h in "$RECV" "$SEND"; do
|
||||
printf ' %-12s ' "$h"
|
||||
scp "${SCP[@]}" "$LOCAL_SCRIPT" "root@$h:$REMOTE_SCRIPT" >/dev/null 2>&1 \
|
||||
&& ssh "${SSH[@]}" "root@$h" "chmod +x $REMOTE_SCRIPT" \
|
||||
&& echo "deployed" || echo "FAILED"
|
||||
done
|
||||
echo ""
|
||||
|
||||
echo "--- cleanup stale iperf3 ---"
|
||||
for h in "$RECV" "$SEND"; do
|
||||
ssh "${SSH[@]}" "root@$h" 'pkill -x iperf3 2>/dev/null; true' 2>/dev/null
|
||||
done
|
||||
echo ""
|
||||
|
||||
echo "--- launching receiver on $RECV (background) ---"
|
||||
ssh "${SSH[@]}" "root@$RECV" \
|
||||
"nohup bash $REMOTE_SCRIPT > /root/lacp-retrans-receiver.console 2>&1 &" 2>/dev/null
|
||||
sleep 5
|
||||
echo ""
|
||||
|
||||
echo "--- running sender on $SEND (foreground, ~20s) ---"
|
||||
ssh "${SSH[@]}" "root@$SEND" "bash $REMOTE_SCRIPT" 2>&1 | sed 's/^/ [sender] /'
|
||||
echo ""
|
||||
sleep 3
|
||||
|
||||
echo "--- fetching logs ---"
|
||||
for f in lacp-retrans-receiver.log lacp-retrans-receiver.console lacp-retrans-sender.log; do
|
||||
src=""
|
||||
case "$f" in
|
||||
*receiver*) src="$RECV" ;;
|
||||
*sender*) src="$SEND" ;;
|
||||
esac
|
||||
printf ' %-32s <- %s : ' "$f" "$src"
|
||||
if scp "${SCP[@]}" "root@$src:/root/$f" "$LOCAL_LOG_DIR/$f" >/dev/null 2>&1; then
|
||||
echo "OK ($(wc -c < "$LOCAL_LOG_DIR/$f" 2>/dev/null) bytes)"
|
||||
else
|
||||
echo "MISSING"
|
||||
fi
|
||||
done
|
||||
echo ""
|
||||
|
||||
echo "==================================================================="
|
||||
echo " RECEIVER LOG ($RECV)"
|
||||
echo "==================================================================="
|
||||
cat "$LOCAL_LOG_DIR/lacp-retrans-receiver.log" 2>/dev/null || echo "(missing)"
|
||||
echo ""
|
||||
echo "==================================================================="
|
||||
echo " SENDER LOG ($SEND)"
|
||||
echo "==================================================================="
|
||||
cat "$LOCAL_LOG_DIR/lacp-retrans-sender.log" 2>/dev/null || echo "(missing)"
|
||||
echo ""
|
||||
echo "==================================================================="
|
||||
echo " Local copies in: $LOCAL_LOG_DIR/"
|
||||
echo "==================================================================="
|
||||
@@ -1,112 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
###############################################################################
|
||||
# run-lacp-rx-distribution.sh (workstation wrapper)
|
||||
#
|
||||
# One-shot orchestrator: deploys scripts/lacp-rx-distribution.sh to BOTH
|
||||
# tsys6 (receiver) and tsys7 (sender), runs them in the right order, then
|
||||
# scps both logs back to returned-logs/iperf/.
|
||||
#
|
||||
# Run from the workstation:
|
||||
# bash scripts/run-lacp-rx-distribution.sh
|
||||
#
|
||||
# Idempotent: safe to re-run. iperf3 is killed on both hosts first.
|
||||
###############################################################################
|
||||
set -uo pipefail
|
||||
|
||||
SSH=(-o BatchMode=yes -o ConnectTimeout=8 -o ServerAliveInterval=10 \
|
||||
-o StrictHostKeyChecking=accept-new)
|
||||
SCP=(-o BatchMode=yes -o ConnectTimeout=8 -o StrictHostKeyChecking=accept-new)
|
||||
|
||||
RECV="pfv-tsys6" # receiver (we care most about its RX split)
|
||||
SEND="pfv-tsys7" # sender (control: its TX split)
|
||||
LOCAL_LOG_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)/returned-logs/iperf"
|
||||
SCRIPT="lacp-rx-distribution.sh"
|
||||
LOCAL_SCRIPT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)/scripts/${SCRIPT}"
|
||||
REMOTE_SCRIPT="/root/${SCRIPT}"
|
||||
|
||||
mkdir -p "$LOCAL_LOG_DIR"
|
||||
|
||||
echo "==================================================================="
|
||||
echo " LACP per-slave RX/TX distribution test"
|
||||
echo " Receiver: $RECV (10.100.100.6) Sender: $SEND (10.100.100.7)"
|
||||
echo "==================================================================="
|
||||
echo ""
|
||||
|
||||
# 0. Preflight: confirm SSH and iperf3 on both hosts
|
||||
echo "--- preflight (ssh + iperf3 + bond0) ---"
|
||||
for h in "$RECV" "$SEND"; do
|
||||
printf ' %-12s ' "$h"
|
||||
ssh "${SSH[@]}" "root@$h" \
|
||||
'command -v iperf3 >/dev/null && ip=$(command -v iperf3) || ip=MISSING
|
||||
[ -r /proc/net/bonding/bond0 ] && b=OK || b=NO-BOND0
|
||||
printf "iperf3=%s bond0=%s host=%s\n" "$ip" "$b" "$(uname -n)"' \
|
||||
2>&1 | head -1
|
||||
done
|
||||
echo ""
|
||||
|
||||
# 1. Copy the script to both hosts + chmod
|
||||
echo "--- deploy $SCRIPT to both hosts ---"
|
||||
for h in "$RECV" "$SEND"; do
|
||||
printf ' %-12s ' "$h"
|
||||
scp "${SCP[@]}" "$LOCAL_SCRIPT" "root@$h:$REMOTE_SCRIPT" >/dev/null 2>&1 \
|
||||
&& ssh "${SSH[@]}" "root@$h" "chmod +x $REMOTE_SCRIPT" \
|
||||
&& echo "deployed + chmod +x" \
|
||||
|| echo "DEPLOY FAILED"
|
||||
done
|
||||
echo ""
|
||||
|
||||
# 2. Kill any stale iperf3 on both hosts
|
||||
echo "--- cleanup stale iperf3 ---"
|
||||
for h in "$RECV" "$SEND"; do
|
||||
ssh "${SSH[@]}" "root@$h" 'pkill -x iperf3 2>/dev/null; true' 2>/dev/null
|
||||
done
|
||||
echo ""
|
||||
|
||||
# 3. Start RECEIVER in background (one-shot server, writes /root/lacp-rx-receiver.log)
|
||||
echo "--- starting receiver on $RECV (background) ---"
|
||||
ssh "${SSH[@]}" "root@$RECV" \
|
||||
"nohup bash $REMOTE_SCRIPT > /root/lacp-rx-receiver.console 2>&1 &" 2>/dev/null
|
||||
echo " receiver launched; waiting 5s for it to start iperf3 -s -1 ..."
|
||||
sleep 5
|
||||
echo ""
|
||||
|
||||
# 4. Run SENDER (foreground; ~15s with the default 12s test + 3s pre-sleep)
|
||||
echo "--- running sender on $SEND (foreground, ~20s) ---"
|
||||
ssh "${SSH[@]}" "root@$SEND" \
|
||||
"bash $REMOTE_SCRIPT" 2>&1 | sed 's/^/ [sender] /'
|
||||
echo ""
|
||||
|
||||
# 5. Give receiver a moment to finish writing its log
|
||||
sleep 3
|
||||
|
||||
# 6. Fetch logs back
|
||||
echo "--- fetching logs ---"
|
||||
for f in lacp-rx-receiver.log lacp-rx-receiver.console lacp-rx-sender.log; do
|
||||
src=""
|
||||
case "$f" in
|
||||
lacp-rx-receiver*) src="$RECV" ;;
|
||||
lacp-rx-sender*) src="$SEND" ;;
|
||||
esac
|
||||
printf ' %-28s <- %s : ' "$f" "$src"
|
||||
if scp "${SCP[@]}" "root@$src:/root/$f" "$LOCAL_LOG_DIR/$f" >/dev/null 2>&1; then
|
||||
echo "OK ($(wc -c < "$LOCAL_LOG_DIR/$f" 2>/dev/null) bytes)"
|
||||
else
|
||||
echo "MISSING"
|
||||
fi
|
||||
done
|
||||
echo ""
|
||||
|
||||
# 7. Show the receiver log (the decisive one)
|
||||
echo "==================================================================="
|
||||
echo " RECEIVER LOG ($RECV — the decisive side)"
|
||||
echo "==================================================================="
|
||||
cat "$LOCAL_LOG_DIR/lacp-rx-receiver.log" 2>/dev/null || echo "(no log)"
|
||||
echo ""
|
||||
echo "==================================================================="
|
||||
echo " SENDER LOG ($SEND — control)"
|
||||
echo "==================================================================="
|
||||
cat "$LOCAL_LOG_DIR/lacp-rx-sender.log" 2>/dev/null || echo "(no log)"
|
||||
echo ""
|
||||
echo "==================================================================="
|
||||
echo " Local copies in: $LOCAL_LOG_DIR/"
|
||||
echo "==================================================================="
|
||||
@@ -1,253 +0,0 @@
|
||||
#!/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()
|
||||
|
||||
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:
|
||||
globals()["COMMUNITY"] = community
|
||||
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,65 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Lint wrapper - permanent wrapper to lint every shell script in this project.
|
||||
#
|
||||
# Uses the koalaman/shellcheck:stable docker image so nothing is installed
|
||||
# on the host. Run from anywhere; lints scripts/ and any .sh under switches/.
|
||||
#
|
||||
# Usage:
|
||||
# ./shellcheck.sh # tty output, all scripts
|
||||
# ./shellcheck.sh --fix-info # treat style notes as non-blocking (default)
|
||||
# ./shellcheck.sh --strict # exit non-zero on ANY finding (notes too)
|
||||
# ./shellcheck.sh scripts/assess.sh # lint a single file
|
||||
set -u
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")" && pwd)"
|
||||
IMAGE="koalaman/shellcheck:stable"
|
||||
STRICT=0
|
||||
TARGETS=()
|
||||
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--strict) STRICT=1 ;;
|
||||
--fix-info) STRICT=0 ;;
|
||||
-h|--help)
|
||||
sed -n '2,12p' "$0"; exit 0 ;;
|
||||
*) TARGETS+=("$arg") ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Default targets: everything in scripts/, plus any .cmds is NOT shell - skip.
|
||||
if [ "${#TARGETS[@]}" -eq 0 ]; then
|
||||
while IFS= read -r -d '' f; do
|
||||
TARGETS+=("$f")
|
||||
done < <(find "$ROOT/scripts" -type f \( -name '*.sh' -o -name 'collect-*' -o -name 'assess*' \) -print0 2>/dev/null)
|
||||
fi
|
||||
|
||||
if [ "${#TARGETS[@]}" -eq 0 ]; then
|
||||
echo "no shell scripts found to lint" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Linting ${#TARGETS[@]} file(s) with $IMAGE:"
|
||||
for t in "${TARGETS[@]}"; do echo " - $t"; done
|
||||
echo
|
||||
|
||||
# Make paths relative to ROOT so docker volume maps cleanly
|
||||
REL_TARGETS=()
|
||||
for t in "${TARGETS[@]}"; do
|
||||
rel="${t#"$ROOT"/}"
|
||||
[ "$rel" = "$t" ] && rel="$t"
|
||||
REL_TARGETS+=("$rel")
|
||||
done
|
||||
|
||||
SC_ARGS=(--format=tty)
|
||||
[ "$STRICT" -eq 0 ] && SC_ARGS+=(--severity=warning)
|
||||
|
||||
docker run --rm -v "$ROOT:/mnt" -w /mnt "$IMAGE" \
|
||||
"${SC_ARGS[@]}" "${REL_TARGETS[@]}"
|
||||
RC=$?
|
||||
|
||||
if [ "$STRICT" -eq 1 ]; then
|
||||
exit $RC
|
||||
fi
|
||||
# Non-strict: only fail on parse errors / errors, not style notes.
|
||||
# (a shellcheck exit code of 1 means "findings"; re-run with severity to distinguish.)
|
||||
exit 0
|
||||
@@ -1,169 +0,0 @@
|
||||
#!/bin/bash
|
||||
# validate-fixes.sh - READ-ONLY validation of all applied tunings.
|
||||
# Does NOT reboot, shutdown VMs, or modify anything.
|
||||
set -uo pipefail
|
||||
SSH_OPTS=(-o BatchMode=yes -o ConnectTimeout=8 -o StrictHostKeyChecking=accept-new)
|
||||
|
||||
HOSTS=(pfv-tsys1 pfv-tsys3 pfv-tsys6 pfv-tsys7 pfv-tsys9)
|
||||
|
||||
echo "==================================================================="
|
||||
echo " READ-ONLY VALIDATION — $(date)"
|
||||
echo "==================================================================="
|
||||
echo ""
|
||||
|
||||
for HOST in "${HOSTS[@]}"; do
|
||||
echo "================================================================"
|
||||
echo "[$HOST]"
|
||||
echo "================================================================"
|
||||
|
||||
if ! ssh "${SSH_OPTS[@]}" "root@$HOST" 'echo ok' >/dev/null 2>&1; then
|
||||
echo " UNREACHABLE"
|
||||
echo ""
|
||||
continue
|
||||
fi
|
||||
|
||||
ssh "${SSH_OPTS[@]}" "root@$HOST" '
|
||||
pass=0; fail=0
|
||||
check() {
|
||||
local label="$1" actual="$2" expected="$3"
|
||||
if [ "$actual" = "$expected" ]; then
|
||||
printf " [OK] %-30s %s\n" "$label" "$actual"
|
||||
pass=$((pass+1))
|
||||
else
|
||||
printf " [FAIL] %-30s got=%s want=%s\n" "$label" "$actual" "$expected"
|
||||
fail=$((fail+1))
|
||||
fi
|
||||
}
|
||||
|
||||
# 1. CPU governor
|
||||
gov=$(cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor 2>/dev/null || echo "n/a")
|
||||
if [ "$gov" = "n/a" ]; then
|
||||
printf " [SKIP] %-30s %s\n" "CPU governor" "(no cpufreq driver — OK for server BIOS)"
|
||||
else
|
||||
check "CPU governor" "$gov" "performance"
|
||||
fi
|
||||
|
||||
# 2. vm.swappiness
|
||||
swap=$(sysctl -n vm.swappiness 2>/dev/null)
|
||||
case "'"$(hostname -s)"'" in
|
||||
pfv-tsys4|pfv-tsys5) want_swap="1" ;;
|
||||
*) want_swap="10" ;;
|
||||
esac
|
||||
check "vm.swappiness" "$swap" "$want_swap"
|
||||
|
||||
# 3. TCP congestion control
|
||||
cc=$(sysctl -n net.ipv4.tcp_congestion_control 2>/dev/null)
|
||||
check "tcp_congestion_control" "$cc" "bbr"
|
||||
|
||||
# 4. default_qdisc (paired with BBR)
|
||||
qd=$(sysctl -n net.core.default_qdisc 2>/dev/null)
|
||||
check "net.core.default_qdisc" "$qd" "fq"
|
||||
|
||||
# 5. tuned profile
|
||||
if command -v tuned-adm >/dev/null 2>&1; then
|
||||
tuned=$(tuned-adm active 2>/dev/null | awk -F: "/Current active/{gsub(/^[ \t]+/,\"\",\$2); print \$2}")
|
||||
case "'"$(hostname -s)"'" in
|
||||
pfv-tsys4|pfv-tsys5) want_tuned="throughput-performance" ;;
|
||||
*) want_tuned="virtual-host" ;;
|
||||
esac
|
||||
check "tuned-adm profile" "$tuned" "$want_tuned"
|
||||
else
|
||||
printf " [FAIL] %-30s not installed\n" "tuned-adm"
|
||||
fail=$((fail+1))
|
||||
fi
|
||||
|
||||
# 6. bond0 hash policy (if bond exists)
|
||||
if [ -r /proc/net/bonding/bond0 ]; then
|
||||
hash=$(grep "Transmit Hash" /proc/net/bonding/bond0 2>/dev/null | awk "{print \$4}")
|
||||
check "bond0 xmit_hash_policy" "$hash" "layer3+4"
|
||||
ports=$(grep "Number of ports" /proc/net/bonding/bond0 2>/dev/null | awk "{print \$4}")
|
||||
printf " [INFO] %-30s %s ports active\n" "bond0 LACP ports" "$ports"
|
||||
else
|
||||
printf " [SKIP] %-30s %s\n" "bond0 hash" "(no bond0 — single NIC host)"
|
||||
fi
|
||||
|
||||
# 7. NFS mount options (nconnect + noatime)
|
||||
nfs_first=$(nfsstat -m 2>/dev/null | head -3 | tail -1)
|
||||
if echo "$nfs_first" | grep -q "nconnect=4"; then
|
||||
printf " [OK] %-30s nconnect=4 active\n" "NFS nconnect"
|
||||
pass=$((pass+1))
|
||||
elif echo "$nfs_first" | grep -q "relatime"; then
|
||||
printf " [FAIL] %-30s still relatime (needs reboot/mount)\n" "NFS nconnect"
|
||||
fail=$((fail+1))
|
||||
elif [ -z "$nfs_first" ]; then
|
||||
printf " [WARN] %-30s no NFS mounts (lazy — start a VM)\n" "NFS nconnect"
|
||||
else
|
||||
printf " [FAIL] %-30s unexpected: %s\n" "NFS nconnect" "$nfs_first"
|
||||
fail=$((fail+1))
|
||||
fi
|
||||
|
||||
if echo "$nfs_first" | grep -q "noatime"; then
|
||||
printf " [OK] %-30s noatime active\n" "NFS noatime"
|
||||
pass=$((pass+1))
|
||||
elif [ -n "$nfs_first" ]; then
|
||||
printf " [FAIL] %-30s not noatime\n" "NFS noatime"
|
||||
fail=$((fail+1))
|
||||
fi
|
||||
|
||||
# 8. NFS TCP connection count
|
||||
nfs_conns=$(ss -tn state established "( dport = :2049 )" 2>/dev/null | tail -n +2 | wc -l)
|
||||
if [ "$nfs_conns" -ge 8 ]; then
|
||||
printf " [OK] %-30s %s connections\n" "NFS TCP conns" "$nfs_conns"
|
||||
pass=$((pass+1))
|
||||
elif [ "$nfs_conns" -gt 0 ]; then
|
||||
printf " [WARN] %-30s %s (expect 8 with nconnect=4)\n" "NFS TCP conns" "$nfs_conns"
|
||||
else
|
||||
printf " [WARN] %-30s 0 (lazy mounts — start a VM)\n" "NFS TCP conns"
|
||||
fi
|
||||
|
||||
# 9. sysctl persistence
|
||||
if [ -r /etc/sysctl.d/99-perfopt.conf ]; then
|
||||
printf " [OK] %-30s /etc/sysctl.d/99-perfopt.conf\n" "sysctl persistence"
|
||||
pass=$((pass+1))
|
||||
else
|
||||
printf " [FAIL] %-30s missing\n" "sysctl persistence"
|
||||
fail=$((fail+1))
|
||||
fi
|
||||
|
||||
# 10. Observability packages
|
||||
for cmd in sar jq numactl nvme mtr bmon; do
|
||||
if ! command -v "$cmd" >/dev/null 2>&1; then
|
||||
printf " [FAIL] %-30s not installed\n" "obs: $cmd"
|
||||
fail=$((fail+1))
|
||||
fi
|
||||
done
|
||||
if command -v sar >/dev/null 2>&1 && command -v jq >/dev/null 2>&1 && \
|
||||
command -v numactl >/dev/null 2>&1 && command -v nvme >/dev/null 2>&1 && \
|
||||
command -v mtr >/dev/null 2>&1 && command -v bmon >/dev/null 2>&1; then
|
||||
printf " [OK] %-30s all installed\n" "observability packages"
|
||||
pass=$((pass+1))
|
||||
fi
|
||||
|
||||
# 11. Failed services
|
||||
failed_count=$(systemctl --failed --no-legend 2>/dev/null | wc -l)
|
||||
if [ "$failed_count" = "0" ]; then
|
||||
printf " [OK] %-30s none\n" "failed services"
|
||||
pass=$((pass+1))
|
||||
else
|
||||
printf " [FAIL] %-30s %s failed:\n" "failed services" "$failed_count"
|
||||
systemctl --failed --no-legend 2>/dev/null | sed "s/^/ /"
|
||||
fail=$((fail+1))
|
||||
fi
|
||||
|
||||
# 12. VM status (read-only — just report)
|
||||
running_vms=$(qm list 2>/dev/null | awk "NR>1 && \$3==\"running\"" | wc -l)
|
||||
stopped_vms=$(qm list 2>/dev/null | awk "NR>1 && \$3!=\"running\"" | wc -l)
|
||||
printf " [INFO] %-30s %s running, %s stopped\n" "VM status" "$running_vms" "$stopped_vms"
|
||||
|
||||
# 13. uptime
|
||||
printf " [INFO] %-30s %s\n" "uptime" "$(uptime | sed "s/.*up //" | sed "s/,.*//")"
|
||||
|
||||
echo ""
|
||||
echo " RESULT: $pass passed, $fail failed"
|
||||
'
|
||||
echo ""
|
||||
done
|
||||
|
||||
echo "==================================================================="
|
||||
echo " SUMMARY"
|
||||
echo "==================================================================="
|
||||
@@ -1,130 +0,0 @@
|
||||
#!/bin/bash
|
||||
###############################################################################
|
||||
# validate-vms.sh - Safe-shutdown and restart all VMs on a host to validate
|
||||
# that performance tunings didn't break anything.
|
||||
#
|
||||
# For each VM:
|
||||
# 1. qm shutdown <vmid> --timeout 120 (ACPI safe shutdown)
|
||||
# 2. Wait for stopped state
|
||||
# 3. qm start <vmid>
|
||||
# 4. Wait for running state
|
||||
# 5. Check qm agent responds (if agent enabled)
|
||||
#
|
||||
# Usage: bash validate-vms.sh <host> [host...]
|
||||
###############################################################################
|
||||
set -uo pipefail
|
||||
SSH_OPTS=(-o BatchMode=yes -o ConnectTimeout=8 -o ServerAliveInterval=10 -o StrictHostKeyChecking=accept-new)
|
||||
|
||||
for HOST in "$@"; do
|
||||
echo "================================================================"
|
||||
echo "[$HOST] VM SAFE-SHUTDOWN/RESTART VALIDATION"
|
||||
echo "================================================================"
|
||||
echo ""
|
||||
|
||||
if ! ssh "${SSH_OPTS[@]}" "root@$HOST" 'echo ok' >/dev/null 2>&1; then
|
||||
echo " UNREACHABLE — skipping"
|
||||
continue
|
||||
fi
|
||||
|
||||
# Get list of running VMs
|
||||
VM_LIST=$(ssh "${SSH_OPTS[@]}" "root@$HOST" 'qm list 2>/dev/null | awk "NR>1 && \$3==\"running\"{print \$1}"')
|
||||
if [ -z "$VM_LIST" ]; then
|
||||
echo " No running VMs — nothing to validate"
|
||||
continue
|
||||
fi
|
||||
|
||||
VM_COUNT=$(echo "$VM_LIST" | wc -w)
|
||||
echo " Found $VM_COUNT running VM(s): $(echo "$VM_LIST" | tr '\n' ' ')"
|
||||
echo ""
|
||||
|
||||
# --- Phase 1: Safe shutdown all VMs ---
|
||||
echo "--- PHASE 1: Safe shutdown all VMs (120s timeout each) ---"
|
||||
for vmid in $VM_LIST; do
|
||||
name=$(ssh "${SSH_OPTS[@]}" "root@$HOST" "qm config $vmid 2>/dev/null | awk -F: '/^name:/{gsub(/^ /,\"\");print \$2}'")
|
||||
echo -n " [$vmid $name] shutting down... "
|
||||
ssh "${SSH_OPTS[@]}" "root@$HOST" "qm shutdown $vmid --timeout 120 --forceStop 1" 2>&1 | head -1
|
||||
done
|
||||
|
||||
# Wait for all to stop (max 180s total)
|
||||
echo ""
|
||||
echo -n " Waiting for all VMs to stop"
|
||||
WAIT_DEADLINE=$(( $(date +%s) + 180 ))
|
||||
while [ "$(date +%s)" -lt "$WAIT_DEADLINE" ]; do
|
||||
echo -n "."
|
||||
all_stopped=1
|
||||
for vmid in $VM_LIST; do
|
||||
status=$(ssh "${SSH_OPTS[@]}" "root@$HOST" "qm status $vmid 2>/dev/null | awk '{print \$2}'")
|
||||
if [ "$status" = "running" ]; then
|
||||
all_stopped=0
|
||||
break
|
||||
fi
|
||||
done
|
||||
[ "$all_stopped" = "1" ] && break
|
||||
sleep 5
|
||||
done
|
||||
echo " done"
|
||||
echo ""
|
||||
|
||||
# Show stopped state
|
||||
echo "--- VM status after shutdown ---"
|
||||
ssh "${SSH_OPTS[@]}" "root@$HOST" 'qm list'
|
||||
echo ""
|
||||
|
||||
# --- Phase 2: Start all VMs ---
|
||||
echo "--- PHASE 2: Start all VMs ---"
|
||||
for vmid in $VM_LIST; do
|
||||
name=$(ssh "${SSH_OPTS[@]}" "root@$HOST" "qm config $vmid 2>/dev/null | awk -F: '/^name:/{gsub(/^ /,\"\");print \$2}'")
|
||||
echo -n " [$vmid $name] starting... "
|
||||
if start_output=$(ssh "${SSH_OPTS[@]}" "root@$HOST" "qm start $vmid" 2>&1); then
|
||||
echo "OK"
|
||||
else
|
||||
echo "FAILED: $start_output"
|
||||
fi
|
||||
done
|
||||
|
||||
# Wait 20s for VMs to fully start
|
||||
echo ""
|
||||
echo " Waiting 20s for VMs to boot..."
|
||||
sleep 20
|
||||
|
||||
# --- Phase 3: Verify all VMs running ---
|
||||
echo ""
|
||||
echo "--- PHASE 3: Verification ---"
|
||||
ssh "${SSH_OPTS[@]}" "root@$HOST" 'qm list'
|
||||
echo ""
|
||||
|
||||
# Check NFS mounts still healthy
|
||||
echo "--- NFS mount health ---"
|
||||
ssh "${SSH_OPTS[@]}" "root@$HOST" '
|
||||
mount_count=$(nfsstat -m 2>/dev/null | grep -c "^/mnt")
|
||||
conn_count=$(ss -tn state established "( dport = :2049 )" 2>/dev/null | tail -n +2 | wc -l)
|
||||
echo " NFS mounts: $mount_count"
|
||||
echo " NFS TCP connections: $conn_count"
|
||||
if [ "$mount_count" -gt 0 ]; then
|
||||
echo " First mount options:"
|
||||
nfsstat -m 2>/dev/null | head -2 | tail -1 | sed "s/^/ /"
|
||||
fi
|
||||
'
|
||||
echo ""
|
||||
|
||||
# Check guest agent responsiveness (if agent enabled)
|
||||
echo "--- Guest agent check (VMs with agent:1) ---"
|
||||
for vmid in $VM_LIST; do
|
||||
agent=$(ssh "${SSH_OPTS[@]}" "root@$HOST" "qm config $vmid 2>/dev/null | grep -c '^agent: 1'")
|
||||
if [ "$agent" = "1" ]; then
|
||||
name=$(ssh "${SSH_OPTS[@]}" "root@$HOST" "qm config $vmid 2>/dev/null | awk -F: '/^name:/{gsub(/^ /,\"\");print \$2}'")
|
||||
echo -n " [$vmid $name] agent ping... "
|
||||
if ssh "${SSH_OPTS[@]}" "root@$HOST" "timeout 10 qm agent $vmid ping" >/dev/null 2>&1; then
|
||||
echo "OK"
|
||||
else
|
||||
echo "no response (VM may still be booting)"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "================================================================"
|
||||
echo "[$HOST] VALIDATION COMPLETE"
|
||||
echo "================================================================"
|
||||
echo ""
|
||||
done
|
||||
@@ -1,36 +0,0 @@
|
||||
#!/bin/bash
|
||||
# verify-tuning.sh - verifies apply-tunings.sh results on target hosts.
|
||||
set -uo pipefail
|
||||
SSH_OPTS=(-o BatchMode=yes -o ConnectTimeout=8 -o StrictHostKeyChecking=accept-new)
|
||||
|
||||
for host in "$@"; do
|
||||
echo "================================================================"
|
||||
echo "[$host] verification"
|
||||
echo "================================================================"
|
||||
|
||||
echo "--- TCP congestion control ---"
|
||||
ssh "${SSH_OPTS[@]}" "root@$host" 'sysctl net.ipv4.tcp_congestion_control net.core.default_qdisc 2>/dev/null'
|
||||
|
||||
echo "--- vm.swappiness ---"
|
||||
ssh "${SSH_OPTS[@]}" "root@$host" 'sysctl vm.swappiness 2>/dev/null'
|
||||
|
||||
echo "--- scaling_governor (cpu0) ---"
|
||||
ssh "${SSH_OPTS[@]}" "root@$host" 'cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor 2>/dev/null'
|
||||
|
||||
echo "--- NFS mount options (first 2 mounts) ---"
|
||||
ssh "${SSH_OPTS[@]}" "root@$host" 'nfsstat -m 2>/dev/null | head -30'
|
||||
|
||||
echo "--- storage.cfg: any options lines? ---"
|
||||
ssh "${SSH_OPTS[@]}" "root@$host" 'grep -c "options" /etc/pve/storage.cfg 2>/dev/null || echo 0'
|
||||
|
||||
echo "--- storage.cfg: NFS stanzas (first 3) ---"
|
||||
ssh "${SSH_OPTS[@]}" "root@$host" 'grep -A 6 "^nfs:" /etc/pve/storage.cfg 2>/dev/null | head -25'
|
||||
|
||||
echo "--- nconnect TCP connections to NFS servers ---"
|
||||
ssh "${SSH_OPTS[@]}" "root@$host" 'ss -tn state established "( dport = :nfs or sport = :nfs )" 2>/dev/null | head -20; echo "count:"; ss -tn state established "( dport = :nfs or sport = :nfs )" 2>/dev/null | tail -n +2 | wc -l'
|
||||
|
||||
echo "--- VMs still running? ---"
|
||||
ssh "${SSH_OPTS[@]}" "root@$host" 'qm list 2>/dev/null | head -15'
|
||||
|
||||
echo ""
|
||||
done
|
||||
@@ -1,58 +0,0 @@
|
||||
#!/bin/bash
|
||||
# wait-for-host.sh - polls SSH until host is back, then runs verification.
|
||||
# Usage: bash wait-for-host.sh <host>
|
||||
set -uo pipefail
|
||||
HOST="$1"
|
||||
SSH_OPTS=(-o BatchMode=yes -o ConnectTimeout=5 -o ServerAliveInterval=5 -o StrictHostKeyChecking=accept-new)
|
||||
|
||||
DEADLINE=$(( $(date +%s) + 600 ))
|
||||
echo "Polling $HOST for SSH return (max 10 min)..."
|
||||
while [ "$(date +%s)" -lt "$DEADLINE" ]; do
|
||||
if ssh "${SSH_OPTS[@]}" "root@$HOST" 'echo ok' >/dev/null 2>&1; then
|
||||
echo "[$(date +%H:%M:%S)] SSH is back!"
|
||||
break
|
||||
fi
|
||||
sleep 10
|
||||
echo " [$(date +%H:%M:%S)] still down..."
|
||||
done
|
||||
|
||||
if ! ssh "${SSH_OPTS[@]}" "root@$HOST" 'echo ok' >/dev/null 2>&1; then
|
||||
echo "FAILED: $HOST not back after 10 minutes"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Waiting 30s for services to settle..."
|
||||
sleep 30
|
||||
|
||||
echo ""
|
||||
echo "================================================================"
|
||||
echo "[$HOST] POST-REBOOT VERIFICATION"
|
||||
echo "================================================================"
|
||||
echo "--- Uptime (should be < 5 min) ---"
|
||||
ssh "${SSH_OPTS[@]}" "root@$HOST" 'uptime'
|
||||
echo ""
|
||||
echo "--- TCP congestion control (expect bbr) ---"
|
||||
ssh "${SSH_OPTS[@]}" "root@$HOST" 'sysctl net.ipv4.tcp_congestion_control net.core.default_qdisc'
|
||||
echo ""
|
||||
echo "--- vm.swappiness ---"
|
||||
ssh "${SSH_OPTS[@]}" "root@$HOST" 'sysctl vm.swappiness'
|
||||
echo ""
|
||||
echo "--- scaling_governor ---"
|
||||
ssh "${SSH_OPTS[@]}" "root@$HOST" 'cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor 2>/dev/null || echo "(no cpufreq driver)"'
|
||||
echo ""
|
||||
echo "--- NFS mount options (looking for nconnect=4 + noatime) ---"
|
||||
ssh "${SSH_OPTS[@]}" "root@$HOST" 'nfsstat -m 2>/dev/null | head -16'
|
||||
echo ""
|
||||
echo "--- NFS TCP connection count to :2049 (expect ~8 = 4 per server × 2 servers) ---"
|
||||
ssh "${SSH_OPTS[@]}" "root@$HOST" 'ss -tn state established "( dport = :2049 )" 2>/dev/null'
|
||||
ssh "${SSH_OPTS[@]}" "root@$HOST" 'ss -tn state established "( dport = :2049 )" 2>/dev/null | tail -n +2 | wc -l'
|
||||
echo ""
|
||||
echo "--- Running VMs ---"
|
||||
ssh "${SSH_OPTS[@]}" "root@$HOST" 'qm list 2>/dev/null'
|
||||
echo ""
|
||||
echo "--- Failed services ---"
|
||||
ssh "${SSH_OPTS[@]}" "root@$HOST" 'systemctl --failed --no-legend 2>/dev/null | head -10'
|
||||
echo ""
|
||||
echo "================================================================"
|
||||
echo "[$HOST] DONE"
|
||||
echo "================================================================"
|
||||
@@ -1,111 +0,0 @@
|
||||
# Powerman PDU Management
|
||||
|
||||
Centralized power management for the Cyclades AlterPath PM10i PDU via
|
||||
[Powerman](https://github.com/chaos/powerman), running on pfv-tsys1.
|
||||
|
||||
## Hardware
|
||||
|
||||
| Component | Details |
|
||||
|-----------|---------|
|
||||
| **PDU** | Cyclades AlterPath PM10i (10 controllable AC outlets) |
|
||||
| **Firmware** | v1.9.0 (Aug 4, 2006) |
|
||||
| **Connection** | USB-to-DB9 adapter (Prolific pl2303, serial BJAAb144J07) |
|
||||
| **Host** | pfv-tsys1 (OptiPlex 9020, Proxmox) |
|
||||
| **Serial** | 9600 baud, 8N1, raw mode |
|
||||
| **Credentials** | Factory defaults: `admin` / `pm8` (in cyclades-pm10.dev) |
|
||||
| **Network access** | powermand listens on `127.0.0.1:10101` (local) + `100.121.189.98:10101` (Tailscale) |
|
||||
|
||||
## Device mapping
|
||||
|
||||
```
|
||||
USB adapter (067b:23a3, serial BJAAb144J07)
|
||||
└─ pl2303 driver → /dev/ttyUSB1
|
||||
└─ udev symlink → /dev/cyclades-pm10 (stable across reboots)
|
||||
└─ powermand reads/writes serial → Cyclades PM10i
|
||||
└─ 10 outlets (factory default names: 1-10)
|
||||
```
|
||||
|
||||
The udev rule (`/etc/udev/rules.d/99-cyclades-pdu.rules`) pins the adapter
|
||||
by its USB serial number, so the symlink survives replugs and reboots.
|
||||
|
||||
## Scripts
|
||||
|
||||
All scripts run on the target host (pfv-tsys1) via `tests/remote.sh`:
|
||||
|
||||
```bash
|
||||
# Setup (idempotent — safe to re-run):
|
||||
PROX_HOST=pfv-tsys1 bash tests/remote.sh prox-file powerman/setup.sh
|
||||
|
||||
# Validate PDU control (cycles outlet 10 off → on):
|
||||
PROX_HOST=pfv-tsys1 bash tests/remote.sh prox-file powerman/test-pdu.sh
|
||||
|
||||
# Status check:
|
||||
PROX_HOST=pfv-tsys1 bash tests/remote.sh prox-file powerman/status.sh
|
||||
```
|
||||
|
||||
### Customizing for other hosts/PDUs
|
||||
|
||||
The setup script accepts environment overrides:
|
||||
|
||||
```bash
|
||||
PDU_SERIAL=XXXX PDU_VENDOR=067b PDU_OUTLETS=20 PDU_TYPE=pm20 \
|
||||
PROX_HOST=other-host bash tests/remote.sh prox-file powerman/setup.sh
|
||||
```
|
||||
|
||||
## Usage (daily operations)
|
||||
|
||||
From pfv-tsys1 (or any host with network access to port 10101):
|
||||
|
||||
```bash
|
||||
# List all outlets
|
||||
powerman -l
|
||||
|
||||
# Query status (all outlets)
|
||||
powerman -q
|
||||
|
||||
# Turn outlet off
|
||||
powerman -0 outlet-10
|
||||
|
||||
# Turn outlet on
|
||||
powerman -1 outlet-10
|
||||
|
||||
# Cycle outlet (off → 4s delay → on)
|
||||
powerman -c outlet-10
|
||||
|
||||
# Query a specific outlet
|
||||
powerman -q outlet-10
|
||||
```
|
||||
|
||||
### Remote access from other hosts
|
||||
|
||||
powermand listens on `0.0.0.0:10101`. From another tailnet host:
|
||||
|
||||
```bash
|
||||
powerman --server-host pfv-tsys1 --server-port 10101 -q
|
||||
```
|
||||
|
||||
Or set `POWERMAN_SERVER=pfv-tsys1:10101` in the environment.
|
||||
|
||||
## Configuration files on pfv-tsys1
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `/etc/udev/rules.d/99-cyclades-pdu.rules` | Stable symlink for USB-DB9 adapter |
|
||||
| `/etc/powerman/powerman.conf` | Device definition + 10 outlet nodes |
|
||||
| `/etc/powerman/cyclades-pm10.dev` | Cyclades PM10 protocol spec (shipped with powerman) |
|
||||
|
||||
## Validation results
|
||||
|
||||
2026-07-28: All 8 checks passed.
|
||||
Outlet 10 turned OFF (confirmed), turned ON (confirmed), then cycled.
|
||||
|
||||
## TODO (Friday onsite)
|
||||
|
||||
- [ ] **Rename outlets** in `/etc/powerman/powerman.conf` to match the
|
||||
physical devices plugged into each outlet (e.g., `node "tsys4-psu"
|
||||
"cyclades-pm10" "3"`). Currently all outlets are generically named
|
||||
`outlet-1` through `outlet-10`.
|
||||
- [ ] **Change PDU admin password** from factory default (`pm8`) if
|
||||
security-sensitive. Update `/etc/powerman/cyclades-pm10.dev` login
|
||||
script to match.
|
||||
- [ ] **Verify all 10 outlets** individually once device mapping is known.
|
||||
@@ -1,68 +0,0 @@
|
||||
#!/usr/bin/bash
|
||||
#
|
||||
# powerman/discover.sh — gather USB-DB9 adapter + powerman state on a host
|
||||
#
|
||||
# Usage: PROX_HOST=pfv-tsys1 bash tests/remote.sh prox-file powerman/discover.sh
|
||||
#
|
||||
set -uo pipefail
|
||||
|
||||
echo "============================================"
|
||||
echo " PDU / Powerman Discovery"
|
||||
echo " Host: $(hostname)"
|
||||
echo " Date: $(date)"
|
||||
echo "============================================"
|
||||
|
||||
echo ""
|
||||
echo "=== 1. USB devices ==="
|
||||
lsusb 2>/dev/null || echo "(lsusb not available)"
|
||||
|
||||
echo ""
|
||||
echo "=== 2. USB-Serial adapters (ttyUSB*) ==="
|
||||
ls -la /dev/ttyUSB* 2>/dev/null || echo "(no /dev/ttyUSB* devices)"
|
||||
|
||||
echo ""
|
||||
echo "=== 3. USB-Serial kernel modules ==="
|
||||
lsmod | grep -iE 'usbserial|ftdi|pl2303|cp210|ch34|cdc_acm' 2>/dev/null || echo "(no relevant modules loaded)"
|
||||
|
||||
echo ""
|
||||
echo "=== 4. dmesg for USB serial (last 30 lines) ==="
|
||||
dmesg | grep -iE 'ttyUSB|usbserial|ftdi|pl2303|cp210|ch34|converter' | tail -30 2>/dev/null || echo "(no dmesg matches)"
|
||||
|
||||
echo ""
|
||||
echo "=== 5. All serial devices ==="
|
||||
ls -la /dev/ttyS* /dev/ttyUSB* /dev/ttyACM* 2>/dev/null || echo "(no serial devices found)"
|
||||
|
||||
echo ""
|
||||
echo "=== 6. Powerman installed? ==="
|
||||
dpkg -l powerman 2>/dev/null || echo "(powerman not installed)"
|
||||
which powerman 2>/dev/null || echo "(powerman binary not found)"
|
||||
which powermand 2>/dev/null || echo "(powermand binary not found)"
|
||||
|
||||
echo ""
|
||||
echo "=== 7. Powerman config files ==="
|
||||
ls -la /etc/powerman/ 2>/dev/null || echo "(no /etc/powerman/ directory)"
|
||||
ls -la /etc/powerman/*.dev 2>/dev/null || echo "(no .dev files)"
|
||||
cat /etc/powerman/powerman.conf 2>/dev/null || echo "(no powerman.conf)"
|
||||
|
||||
echo ""
|
||||
echo "=== 8. Available powerman device definitions ==="
|
||||
ls /usr/share/powerman/*.dev 2>/dev/null || ls /etc/powerman/*.dev 2>/dev/null || echo "(no device definitions found)"
|
||||
|
||||
echo ""
|
||||
echo "=== 9. Powermand service status ==="
|
||||
systemctl status powerman 2>/dev/null | head -10 || echo "(powerman service not found)"
|
||||
|
||||
echo ""
|
||||
echo "=== 10. Serial port test (quick probe of /dev/ttyUSB0) ==="
|
||||
if [ -e /dev/ttyUSB0 ]; then
|
||||
stty -F /dev/ttyUSB0 2>/dev/null && echo "(port exists and is configurable)" || echo "(port exists but stty failed)"
|
||||
# Try to read any pending output
|
||||
timeout 2 cat /dev/ttyUSB0 2>/dev/null | head -5 || echo "(no immediate output from port)"
|
||||
else
|
||||
echo "(no /dev/ttyUSB0)"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "============================================"
|
||||
echo " Discovery complete."
|
||||
echo "============================================"
|
||||
@@ -1,65 +0,0 @@
|
||||
#!/usr/bin/bash
|
||||
#
|
||||
# powerman/query-remote.sh — install powerman client locally and query
|
||||
# the Cyclades PDU running on pfv-tsys1 over Tailscale.
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
REMOTE_HOST="${REMOTE_HOST:-pfv-tsys1}"
|
||||
REMOTE_PORT="${REMOTE_PORT:-10101}"
|
||||
|
||||
echo "============================================"
|
||||
echo " Powerman Remote PDU Query"
|
||||
echo " Server: ${REMOTE_HOST}:${REMOTE_PORT} (Tailscale)"
|
||||
echo "============================================"
|
||||
|
||||
# --- 1. Install powerman client if missing ---
|
||||
if ! command -v powerman >/dev/null 2>&1; then
|
||||
echo ""
|
||||
echo "--- Installing powerman client ---"
|
||||
if sudo -n true 2>/dev/null; then
|
||||
sudo apt-get update -qq && sudo apt-get install -y -qq powerman
|
||||
else
|
||||
echo " Passwordless sudo not available. Please run this command in a terminal:"
|
||||
echo ""
|
||||
echo " sudo apt-get update && sudo apt-get install -y powerman"
|
||||
echo ""
|
||||
echo " Then re-run this script."
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo " powerman client already installed."
|
||||
fi
|
||||
|
||||
# --- 2. Verify connectivity ---
|
||||
echo ""
|
||||
echo "--- Connectivity check ---"
|
||||
if timeout 3 bash -c "echo > /dev/tcp/${REMOTE_HOST}/${REMOTE_PORT}" 2>/dev/null; then
|
||||
echo " [OK] ${REMOTE_HOST}:${REMOTE_PORT} reachable"
|
||||
else
|
||||
echo " [FAIL] Cannot reach ${REMOTE_HOST}:${REMOTE_PORT}"
|
||||
echo " Is Tailscale up? Is powermand running on ${REMOTE_HOST}?"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
export POWERMAN_SERVER="${REMOTE_HOST}:${REMOTE_PORT}"
|
||||
|
||||
# --- 3. List outlets ---
|
||||
echo ""
|
||||
echo "--- Outlets ---"
|
||||
powerman -h "${REMOTE_HOST}:${REMOTE_PORT}" -l
|
||||
|
||||
# --- 4. Query status ---
|
||||
echo ""
|
||||
echo "--- Status ---"
|
||||
powerman -h "${REMOTE_HOST}:${REMOTE_PORT}" -q
|
||||
|
||||
echo ""
|
||||
echo "============================================"
|
||||
echo " Done."
|
||||
echo ""
|
||||
echo " To control an outlet from this workstation:"
|
||||
echo " powerman -h ${REMOTE_HOST}:${REMOTE_PORT} -0 outlet-10 # off"
|
||||
echo " powerman -h ${REMOTE_HOST}:${REMOTE_PORT} -1 outlet-10 # on"
|
||||
echo " powerman -h ${REMOTE_HOST}:${REMOTE_PORT} -c outlet-10 # cycle"
|
||||
echo "============================================"
|
||||
@@ -1,181 +0,0 @@
|
||||
#!/usr/bin/bash
|
||||
#
|
||||
# powerman/setup.sh — idempotent powerman setup for Cyclades PM10i PDU
|
||||
#
|
||||
# Creates a stable udev symlink for the USB-DB9 adapter, writes powerman.conf
|
||||
# with 10 outlet nodes, and enables + starts powermand.
|
||||
#
|
||||
# This script is designed to be run ON the target host (pfv-tsys1) as root.
|
||||
# It is idempotent: safe to run multiple times.
|
||||
#
|
||||
# Usage:
|
||||
# PROX_HOST=pfv-tsys1 bash tests/remote.sh prox-file powerman/setup.sh
|
||||
#
|
||||
# Override defaults via environment variables:
|
||||
# PDU_SERIAL — USB adapter serial (default: BJAAb144J07)
|
||||
# PDU_VENDOR — USB vendor ID (default: 067b)
|
||||
# PDU_DEV_NAME — udev symlink name (default: cyclades-pm10)
|
||||
# PDU_BAUD — serial baud rate (default: 9600,8n1)
|
||||
# PDU_TYPE — powerman spec type (default: pm10)
|
||||
# PDU_OUTLETS — number of outlets (default: 10)
|
||||
# PDU_LISTEN — powermand listen (default: 0.0.0.0:10101)
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
# --- Config (overridable via env) ---
|
||||
PDU_SERIAL="${PDU_SERIAL:-BJAAb144J07}"
|
||||
PDU_VENDOR="${PDU_VENDOR:-067b}"
|
||||
PDU_DEV_NAME="${PDU_DEV_NAME:-cyclades-pm10}"
|
||||
PDU_BAUD="${PDU_BAUD:-9600,8n1}"
|
||||
PDU_TYPE="${PDU_TYPE:-pm10}"
|
||||
PDU_OUTLETS="${PDU_OUTLETS:-10}"
|
||||
PDU_LISTEN="${PDU_LISTEN:-}" # Auto-detect Tailscale IP if empty
|
||||
|
||||
UDEV_RULE="/etc/udev/rules.d/99-cyclades-pdu.rules"
|
||||
POWERMAN_CONF="/etc/powerman/powerman.conf"
|
||||
DEV_FILE="/etc/powerman/cyclades-pm10.dev"
|
||||
|
||||
# --- Auto-detect Tailscale IP for listen address ---
|
||||
if [ -z "$PDU_LISTEN" ]; then
|
||||
TS_IP=$(tailscale ip -4 2>/dev/null || true)
|
||||
if [ -n "$TS_IP" ]; then
|
||||
PDU_LISTEN="${TS_IP}:10101"
|
||||
echo " Auto-detected Tailscale IP: $TS_IP"
|
||||
else
|
||||
PDU_LISTEN="127.0.0.1:10101"
|
||||
echo " WARNING: No Tailscale IP detected. Defaulting to localhost."
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "============================================"
|
||||
echo " Powerman PDU Setup"
|
||||
echo " Host: $(hostname)"
|
||||
echo " PDU: Cyclades PM${PDU_OUTLETS}i"
|
||||
echo " Adapter serial: $PDU_SERIAL"
|
||||
echo " Device symlink: /dev/$PDU_DEV_NAME"
|
||||
echo " Listen: $PDU_LISTEN (Tailscale only)"
|
||||
echo "============================================"
|
||||
|
||||
# --- 1. Ensure powerman is installed ---
|
||||
echo ""
|
||||
echo "--- [1/5] Checking powerman installation ---"
|
||||
if ! dpkg -l powerman 2>/dev/null | grep -q '^ii'; then
|
||||
echo " Installing powerman from Debian repo..."
|
||||
apt-get update -qq && apt-get install -y -qq powerman
|
||||
else
|
||||
echo " Powerman already installed: $(dpkg -l powerman | awk '/^ii/{print $3}')"
|
||||
fi
|
||||
|
||||
# --- 2. Create udev rule for stable device name ---
|
||||
echo ""
|
||||
echo "--- [2/5] Creating udev rule for USB-DB9 adapter ---"
|
||||
cat > "$UDEV_RULE" <<UDEV
|
||||
# Stable symlink for Cyclades PM10i PDU USB-DB9 adapter
|
||||
# Generated by powerman/setup.sh
|
||||
SUBSYSTEM=="tty", ATTRS{idVendor}=="${PDU_VENDOR}", ATTRS{serial}=="${PDU_SERIAL}", GROUP="dialout", MODE="0660", SYMLINK+="${PDU_DEV_NAME}"
|
||||
UDEV
|
||||
echo " Written: $UDEV_RULE"
|
||||
|
||||
# Trigger udev to create the symlink now
|
||||
udevadm control --reload-rules 2>/dev/null || true
|
||||
udevadm trigger --subsystem-match=tty 2>/dev/null || true
|
||||
sleep 1
|
||||
|
||||
if [ -e "/dev/${PDU_DEV_NAME}" ]; then
|
||||
echo " Device symlink active: /dev/${PDU_DEV_NAME} -> $(readlink -f "/dev/${PDU_DEV_NAME}")"
|
||||
else
|
||||
echo " WARNING: /dev/${PDU_DEV_NAME} not found yet. Adapter may be unplugged."
|
||||
echo " Falling back to /dev/ttyUSB* discovery..."
|
||||
# Try to find any ttyUSB device as fallback
|
||||
for tty in /dev/ttyUSB*; do
|
||||
if [ -e "$tty" ]; then
|
||||
echo " Found: $tty (using as fallback)"
|
||||
PDU_DEV_NAME="$(basename "$tty")"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# --- 2b. Ensure powermand user can access the serial device ---
|
||||
echo ""
|
||||
echo "--- [2b/5] Fixing serial device permissions ---"
|
||||
if id powerman >/dev/null 2>&1; then
|
||||
if id powerman | grep -qv dialout; then
|
||||
usermod -aG dialout powerman
|
||||
echo " Added 'powerman' user to 'dialout' group"
|
||||
else
|
||||
echo " 'powerman' already in 'dialout' group"
|
||||
fi
|
||||
else
|
||||
echo " (no powerman user — service may run as root)"
|
||||
fi
|
||||
|
||||
# --- 3. Write powerman.conf ---
|
||||
echo ""
|
||||
echo "--- [3/5] Writing powerman.conf ---"
|
||||
|
||||
# Build node definitions
|
||||
NODES=""
|
||||
for i in $(seq 1 "$PDU_OUTLETS"); do
|
||||
NODES+="node \"outlet-${i}\" \"${PDU_DEV_NAME}\" \"${i}\"\n"
|
||||
done
|
||||
|
||||
cat > "$POWERMAN_CONF" <<PMCONF
|
||||
# Powerman configuration for Cyclades PM${PDU_OUTLETS}i PDU
|
||||
# Generated by powerman/setup.sh on $(date)
|
||||
# Device: /dev/${PDU_DEV_NAME} (USB-DB9 adapter serial ${PDU_SERIAL})
|
||||
|
||||
# Listen on localhost (for local admin) and Tailscale (for remote access)
|
||||
listen "127.0.0.1:10101"
|
||||
listen "${PDU_LISTEN}"
|
||||
|
||||
# Device specification for Cyclades PM10
|
||||
include "${DEV_FILE}"
|
||||
|
||||
# The PDU device (serial-attached)
|
||||
device "${PDU_DEV_NAME}" "${PDU_TYPE}" "/dev/${PDU_DEV_NAME}" "${PDU_BAUD}"
|
||||
|
||||
# Outlet nodes (rename these to match attached devices when onsite)
|
||||
$(printf '%b' "$NODES")
|
||||
PMCONF
|
||||
|
||||
echo " Written: $POWERMAN_CONF"
|
||||
echo " Nodes defined: outlet-1 through outlet-${PDU_OUTLETS}"
|
||||
|
||||
# --- 4. Restart powermand ---
|
||||
echo ""
|
||||
echo "--- [4/5] Restarting powermand ---"
|
||||
systemctl enable powerman 2>/dev/null || true
|
||||
systemctl restart powerman 2>/dev/null || true
|
||||
sleep 2
|
||||
|
||||
if systemctl is-active --quiet powerman; then
|
||||
echo " powermand is running."
|
||||
else
|
||||
echo " WARNING: powermand failed to start. Check journalctl -u powerman"
|
||||
journalctl -u powerman --no-pager -n 20 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# --- 5. Verify ---
|
||||
echo ""
|
||||
echo "--- [5/5] Verification ---"
|
||||
echo ""
|
||||
echo " powerman -l (list all outlets):"
|
||||
powerman -l 2>&1 || echo "(powerman -l failed)"
|
||||
|
||||
echo ""
|
||||
echo " powerman -q (query status):"
|
||||
powerman -q 2>&1 || echo "(powerman -q failed — PDU may need a moment)"
|
||||
|
||||
echo ""
|
||||
echo "============================================"
|
||||
echo " Setup complete."
|
||||
echo ""
|
||||
echo " Outlet names are generic (outlet-1 ... outlet-${PDU_OUTLETS})."
|
||||
echo " Rename them in ${POWERMAN_CONF} when onsite to match attached devices."
|
||||
echo ""
|
||||
echo " Test: powerman -0 outlet-10 (off)"
|
||||
echo " powerman -1 outlet-10 (on)"
|
||||
echo " powerman -c outlet-10 (cycle)"
|
||||
echo " powerman -q (status)"
|
||||
echo "============================================"
|
||||
@@ -1,33 +0,0 @@
|
||||
#!/usr/bin/bash
|
||||
#
|
||||
# powerman/status.sh — quick PDU status check
|
||||
#
|
||||
# Usage:
|
||||
# PROX_HOST=pfv-tsys1 bash tests/remote.sh prox-file powerman/status.sh
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
echo "============================================"
|
||||
echo " Cyclades PM10i PDU Status"
|
||||
echo " Host: $(hostname) $(date)"
|
||||
echo "============================================"
|
||||
|
||||
echo ""
|
||||
echo "=== Service ==="
|
||||
systemctl is-active powerman 2>/dev/null && echo "(running)" || echo "(stopped)"
|
||||
|
||||
echo ""
|
||||
echo "=== Device ==="
|
||||
ls -la /dev/cyclades-pm10 2>/dev/null || echo "(no /dev/cyclades-pm10 symlink)"
|
||||
|
||||
echo ""
|
||||
echo "=== Outlets ==="
|
||||
powerman -l 2>&1
|
||||
|
||||
echo ""
|
||||
echo "=== Power Status ==="
|
||||
powerman -q 2>&1
|
||||
|
||||
echo ""
|
||||
echo "=== Temperature ==="
|
||||
powerman -T 2>&1 || echo "(temperature not available)"
|
||||
@@ -1,126 +0,0 @@
|
||||
#!/usr/bin/bash
|
||||
#
|
||||
# powerman/test-pdu.sh — validate PDU control by cycling outlet 10 off and on
|
||||
#
|
||||
# Usage:
|
||||
# PROX_HOST=pfv-tsys1 bash tests/remote.sh prox-file powerman/test-pdu.sh
|
||||
#
|
||||
# Override: OUTLET=10 (which outlet to test)
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
OUTLET="${OUTLET:-10}"
|
||||
NODE="outlet-${OUTLET}"
|
||||
PASS=0; FAIL=0
|
||||
ok() { echo " [PASS] $1"; PASS=$((PASS+1)); }
|
||||
fail() { echo " [FAIL] $1"; FAIL=$((FAIL+1)); }
|
||||
|
||||
echo "============================================"
|
||||
echo " PDU Control Validation"
|
||||
echo " Host: $(hostname)"
|
||||
echo " Test: cycle outlet ${OUTLET} (off → wait → on)"
|
||||
echo "============================================"
|
||||
|
||||
# --- 0. Powermand running? ---
|
||||
echo ""
|
||||
echo "--- [0/5] Powermand service ---"
|
||||
if systemctl is-active --quiet powerman; then
|
||||
ok "powermand is running"
|
||||
else
|
||||
fail "powermand is NOT running"
|
||||
echo " Run setup.sh first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- 1. List outlets ---
|
||||
echo ""
|
||||
echo "--- [1/5] List outlets ---"
|
||||
LIST_OUT=$(powerman -l 2>&1)
|
||||
echo "$LIST_OUT"
|
||||
# powerman shows ranges like "outlet-[1-10]" — match either exact or range form
|
||||
if echo "$LIST_OUT" | grep -qE "outlet-(\[1-?10\]|${OUTLET}\b)"; then
|
||||
ok "Outlet '${NODE}' is defined"
|
||||
else
|
||||
fail "Outlet '${NODE}' not found in powerman -l"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- 2. Query current status ---
|
||||
echo ""
|
||||
echo "--- [2/5] Query initial status ---"
|
||||
INITIAL=$(powerman -q 2>&1)
|
||||
echo "$INITIAL"
|
||||
if [ -n "$INITIAL" ]; then
|
||||
ok "Status query works (PDU is responding)"
|
||||
else
|
||||
fail "Could not query status"
|
||||
echo " PDU may be unresponsive. Check serial connection."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- 3. Turn OFF outlet ---
|
||||
echo ""
|
||||
echo "--- [3/5] Turn OFF outlet ${OUTLET} ---"
|
||||
if powerman -0 "$NODE" 2>&1; then
|
||||
ok "Off command sent successfully"
|
||||
else
|
||||
fail "Off command failed"
|
||||
fi
|
||||
|
||||
sleep 3
|
||||
|
||||
# Verify it's off (query just this outlet)
|
||||
STATUS_OFF=$(powerman -q "$NODE" 2>&1)
|
||||
echo "$STATUS_OFF"
|
||||
if echo "$STATUS_OFF" | grep -qi "off\|unk"; then
|
||||
ok "Outlet ${OUTLET} confirmed OFF"
|
||||
else
|
||||
echo " (status may not perfectly reflect — continuing)"
|
||||
fi
|
||||
|
||||
# --- 4. Turn ON outlet ---
|
||||
echo ""
|
||||
echo "--- [4/5] Turn ON outlet ${OUTLET} ---"
|
||||
if powerman -1 "$NODE" 2>&1; then
|
||||
ok "On command sent successfully"
|
||||
else
|
||||
fail "On command failed"
|
||||
fi
|
||||
|
||||
sleep 3
|
||||
|
||||
# Verify it's on (query just this outlet)
|
||||
STATUS_ON=$(powerman -q "$NODE" 2>&1)
|
||||
echo "$STATUS_ON"
|
||||
if echo "$STATUS_ON" | grep -qi "on"; then
|
||||
ok "Outlet ${OUTLET} confirmed ON"
|
||||
else
|
||||
echo " (status may not perfectly reflect — continuing)"
|
||||
fi
|
||||
|
||||
# --- 5. Cycle test (off → delay → on in one command) ---
|
||||
echo ""
|
||||
echo "--- [5/5] Cycle test (powerman -c) ---"
|
||||
if powerman -c "$NODE" 2>&1; then
|
||||
ok "Cycle command completed"
|
||||
else
|
||||
fail "Cycle command failed"
|
||||
echo " (some PDU firmware reports errors during cycle but still works)"
|
||||
fi
|
||||
|
||||
sleep 5
|
||||
|
||||
# Final status
|
||||
echo ""
|
||||
echo "--- Final status ---"
|
||||
powerman -q 2>&1
|
||||
|
||||
echo ""
|
||||
echo "============================================"
|
||||
echo " Results: $PASS passed, $FAIL failed"
|
||||
if [ "$FAIL" -gt 0 ]; then
|
||||
echo " Some checks failed. Review output above."
|
||||
exit 1
|
||||
fi
|
||||
echo " PDU control validated."
|
||||
echo "============================================"
|
||||
@@ -1,9 +0,0 @@
|
||||
[Unit]
|
||||
Description=Check_MK LibreNMS Agent Socket
|
||||
|
||||
[Socket]
|
||||
ListenStream=6556
|
||||
Accept=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=sockets.target
|
||||
@@ -1,7 +0,0 @@
|
||||
[Unit]
|
||||
Description=Check_MK LibreNMS Agent Service
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
ExecStart=/usr/bin/check_mk_agent
|
||||
StandardOutput=socket
|
||||
@@ -1,659 +0,0 @@
|
||||
#!/bin/bash
|
||||
# +------------------------------------------------------------------+
|
||||
# | ____ _ _ __ __ _ __ |
|
||||
# | / ___| |__ ___ ___| | __ | \/ | |/ / |
|
||||
# | | | | '_ \ / _ \/ __| |/ / | |\/| | ' / |
|
||||
# | | |___| | | | __/ (__| < | | | | . \ |
|
||||
# | \____|_| |_|\___|\___|_|\_\___|_| |_|_|\_\ |
|
||||
# | |
|
||||
# | Copyright Mathias Kettner 2014 mk@mathias-kettner.de |
|
||||
# +------------------------------------------------------------------+
|
||||
#
|
||||
# This file is part of Check_MK.
|
||||
# The official homepage is at http://mathias-kettner.de/check_mk.
|
||||
#
|
||||
# check_mk is free software; you can redistribute it and/or modify it
|
||||
# under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation in version 2. check_mk is distributed
|
||||
# in the hope that it will be useful, but WITHOUT ANY WARRANTY; with-
|
||||
# out even the implied warranty of MERCHANTABILITY or FITNESS FOR A
|
||||
# PARTICULAR PURPOSE. See the GNU General Public License for more de-
|
||||
# ails. You should have received a copy of the GNU General Public
|
||||
# License along with GNU Make; see the file COPYING. If not, write
|
||||
# to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
|
||||
# Boston, MA 02110-1301 USA.
|
||||
|
||||
# Remove locale settings to eliminate localized outputs where possible
|
||||
export LC_ALL=C
|
||||
unset LANG
|
||||
|
||||
export MK_LIBDIR="/usr/lib/check_mk_agent"
|
||||
export MK_CONFDIR="/etc/check_mk"
|
||||
export MK_VARDIR="/var/lib/check_mk_agent"
|
||||
|
||||
# Provide information about the remote host. That helps when data
|
||||
# is being sent only once to each remote host.
|
||||
if [ "$REMOTE_HOST" ] ; then
|
||||
export REMOTE=$REMOTE_HOST
|
||||
elif [ "$SSH_CLIENT" ] ; then
|
||||
export REMOTE=${SSH_CLIENT%% *}
|
||||
fi
|
||||
|
||||
# Make sure, locally installed binaries are found
|
||||
PATH=$PATH:/usr/local/bin
|
||||
|
||||
# All executables in PLUGINSDIR will simply be executed and their
|
||||
# ouput appended to the output of the agent. Plugins define their own
|
||||
# sections and must output headers with '<<<' and '>>>'
|
||||
PLUGINSDIR=$MK_LIBDIR/plugins
|
||||
|
||||
# All executables in LOCALDIR will by executabled and their
|
||||
# output inserted into the section <<<local>>>. Please
|
||||
# refer to online documentation for details about local checks.
|
||||
LOCALDIR=$MK_LIBDIR/local
|
||||
|
||||
# All files in SPOOLDIR will simply appended to the agent
|
||||
# output if they are not outdated (see below)
|
||||
SPOOLDIR=$MK_VARDIR/spool
|
||||
|
||||
# close standard input (for security reasons) and stderr
|
||||
if [ "$1" = -d ]
|
||||
then
|
||||
set -xv
|
||||
else
|
||||
exec </dev/null 2>/dev/null
|
||||
fi
|
||||
|
||||
# Runs a command asynchronous by use of a cache file
|
||||
function run_cached () {
|
||||
local section=
|
||||
if [ "$1" = -s ] ; then local section="echo '<<<$2>>>' ; " ; shift ; fi
|
||||
local NAME=$1
|
||||
local MAXAGE=$2
|
||||
shift 2
|
||||
local CMDLINE="$section$@"
|
||||
|
||||
if [ ! -d $MK_VARDIR/cache ]; then mkdir -p $MK_VARDIR/cache ; fi
|
||||
CACHEFILE="$MK_VARDIR/cache/$NAME.cache"
|
||||
|
||||
# Check if the creation of the cache takes suspiciously long and return
|
||||
# nothing if the age (access time) of $CACHEFILE.new is twice the MAXAGE
|
||||
local NOW=$(date +%s)
|
||||
if [ -e "$CACHEFILE.new" ] ; then
|
||||
local CF_ATIME=$(stat -c %X "$CACHEFILE.new")
|
||||
if [ $((NOW - CF_ATIME)) -ge $((MAXAGE * 2)) ] ; then
|
||||
# Kill the process still accessing that file in case
|
||||
# it is still running. This avoids overlapping processes!
|
||||
fuser -k -9 "$CACHEFILE.new" >/dev/null 2>&1
|
||||
rm -f "$CACHEFILE.new"
|
||||
return
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check if cache file exists and is recent enough
|
||||
if [ -s "$CACHEFILE" ] ; then
|
||||
local MTIME=$(stat -c %Y "$CACHEFILE")
|
||||
if [ $((NOW - MTIME)) -le $MAXAGE ] ; then local USE_CACHEFILE=1 ; fi
|
||||
# Output the file in any case, even if it is
|
||||
# outdated. The new file will not yet be available
|
||||
cat "$CACHEFILE"
|
||||
fi
|
||||
|
||||
# Cache file outdated and new job not yet running? Start it
|
||||
if [ -z "$USE_CACHEFILE" -a ! -e "$CACHEFILE.new" ] ; then
|
||||
echo "set -o noclobber ; exec > \"$CACHEFILE.new\" || exit 1 ; $CMDLINE && mv \"$CACHEFILE.new\" \"$CACHEFILE\" || rm -f \"$CACHEFILE\" \"$CACHEFILE.new\"" | nohup bash >/dev/null 2>&1 &
|
||||
fi
|
||||
}
|
||||
|
||||
# Make run_cached available for subshells (plugins, local checks, etc.)
|
||||
export -f run_cached
|
||||
|
||||
echo '<<<check_mk>>>'
|
||||
echo Version: 1.2.6b5
|
||||
echo AgentOS: linux
|
||||
echo AgentDirectory: $MK_CONFDIR
|
||||
echo DataDirectory: $MK_VARDIR
|
||||
echo SpoolDirectory: $SPOOLDIR
|
||||
echo PluginsDirectory: $PLUGINSDIR
|
||||
echo LocalDirectory: $LOCALDIR
|
||||
|
||||
# If we are called via xinetd, try to find only_from configuration
|
||||
if [ -n "$REMOTE_HOST" ]
|
||||
then
|
||||
echo -n 'OnlyFrom: '
|
||||
echo $(sed -n '/^service[[:space:]]*check_mk/,/}/s/^[[:space:]]*only_from[[:space:]]*=[[:space:]]*\(.*\)/\1/p' /etc/xinetd.d/* | head -n1)
|
||||
fi
|
||||
|
||||
# Print out Partitions / Filesystems. (-P gives non-wrapped POSIXed output)
|
||||
# Heads up: NFS-mounts are generally supressed to avoid agent hangs.
|
||||
# If hard NFS mounts are configured or you have too large nfs retry/timeout
|
||||
# settings, accessing those mounts from the agent would leave you with
|
||||
# thousands of agent processes and, ultimately, a dead monitored system.
|
||||
# These should generally be monitored on the NFS server, not on the clients.
|
||||
|
||||
echo '<<<df>>>'
|
||||
# The exclusion list is getting a bit of a problem. -l should hide any remote FS but seems
|
||||
# to be all but working.
|
||||
excludefs="-x smbfs -x cifs -x iso9660 -x udf -x nfsv4 -x nfs -x mvfs -x zfs"
|
||||
df -PTlk $excludefs | sed 1d
|
||||
|
||||
# df inodes information
|
||||
echo '<<<df>>>'
|
||||
echo '[df_inodes_start]'
|
||||
df -PTli $excludefs | sed 1d
|
||||
echo '[df_inodes_end]'
|
||||
|
||||
# Filesystem usage for ZFS
|
||||
if type zfs > /dev/null 2>&1 ; then
|
||||
echo '<<<zfsget>>>'
|
||||
zfs get -Hp name,quota,used,avail,mountpoint,type -t filesystem,volume || \
|
||||
zfs get -Hp name,quota,used,avail,mountpoint,type
|
||||
echo '[df]'
|
||||
df -PTlk -t zfs | sed 1d
|
||||
fi
|
||||
|
||||
# Check NFS mounts by accessing them with stat -f (System
|
||||
# call statfs()). If this lasts more then 2 seconds we
|
||||
# consider it as hanging. We need waitmax.
|
||||
if type waitmax >/dev/null
|
||||
then
|
||||
STAT_VERSION=$(stat --version | head -1 | cut -d" " -f4)
|
||||
STAT_BROKE="5.3.0"
|
||||
|
||||
echo '<<<nfsmounts>>>'
|
||||
sed -n '/ nfs4\? /s/[^ ]* \([^ ]*\) .*/\1/p' < /proc/mounts |
|
||||
sed 's/\\040/ /g' |
|
||||
while read MP
|
||||
do
|
||||
if [ $STAT_VERSION != $STAT_BROKE ]; then
|
||||
waitmax -s 9 2 stat -f -c "$MP ok %b %f %a %s" "$MP" || \
|
||||
echo "$MP hanging 0 0 0 0"
|
||||
else
|
||||
waitmax -s 9 2 stat -f -c "$MP ok %b %f %a %s" "$MP" && \
|
||||
printf '\n'|| echo "$MP hanging 0 0 0 0"
|
||||
fi
|
||||
done
|
||||
|
||||
echo '<<<cifsmounts>>>'
|
||||
sed -n '/ cifs\? /s/[^ ]* \([^ ]*\) .*/\1/p' < /proc/mounts |
|
||||
sed 's/\\040/ /g' |
|
||||
while read MP
|
||||
do
|
||||
if [ $STAT_VERSION != $STAT_BROKE ]; then
|
||||
waitmax -s 9 2 stat -f -c "$MP ok %b %f %a %s" "$MP" || \
|
||||
echo "$MP hanging 0 0 0 0"
|
||||
else
|
||||
waitmax -s 9 2 stat -f -c "$MP ok %b %f %a %s" "$MP" && \
|
||||
printf '\n'|| echo "$MP hanging 0 0 0 0"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# Check mount options. Filesystems may switch to 'ro' in case
|
||||
# of a read error.
|
||||
echo '<<<mounts>>>'
|
||||
grep ^/dev < /proc/mounts
|
||||
|
||||
# processes including username, without kernel processes
|
||||
echo '<<<ps>>>'
|
||||
ps ax -o user,vsz,rss,cputime,pid,command --columns 10000 | sed -e 1d -e 's/ *\([^ ]*\) *\([^ ]*\) *\([^ ]*\) *\([^ ]*\) *\([^ ]*\) */(\1,\2,\3,\4,\5) /'
|
||||
|
||||
# Memory usage
|
||||
echo '<<<mem>>>'
|
||||
egrep -v '^Swap:|^Mem:|total:' < /proc/meminfo
|
||||
|
||||
# Load and number of processes
|
||||
echo '<<<cpu>>>'
|
||||
echo "$(cat /proc/loadavg) $(grep -E '^CPU|^processor' < /proc/cpuinfo | wc -l)"
|
||||
|
||||
# Uptime
|
||||
echo '<<<uptime>>>'
|
||||
cat /proc/uptime
|
||||
|
||||
|
||||
# New variant: Information about speed and state in one section
|
||||
echo '<<<lnx_if:sep(58)>>>'
|
||||
sed 1,2d /proc/net/dev
|
||||
if type ethtool > /dev/null
|
||||
then
|
||||
for eth in $(sed -e 1,2d < /proc/net/dev | cut -d':' -f1 | sort)
|
||||
do
|
||||
echo "[$eth]"
|
||||
ethtool $eth | egrep '(Speed|Duplex|Link detected|Auto-negotiation):'
|
||||
echo -en "\tAddress: " ; cat /sys/class/net/$eth/address ; echo
|
||||
done
|
||||
fi
|
||||
|
||||
|
||||
# Current state of bonding interfaces
|
||||
if [ -e /proc/net/bonding ] ; then
|
||||
echo '<<<lnx_bonding:sep(58)>>>'
|
||||
pushd /proc/net/bonding > /dev/null ; head -v -n 1000 * ; popd
|
||||
fi
|
||||
|
||||
# Same for Open vSwitch bonding
|
||||
if type ovs-appctl > /dev/null ; then
|
||||
echo '<<<ovs_bonding:sep(58)>>>'
|
||||
for bond in $(ovs-appctl bond/list | sed -e 1d | cut -f2) ; do
|
||||
echo "[$bond]"
|
||||
ovs-appctl bond/show $bond
|
||||
done
|
||||
fi
|
||||
|
||||
|
||||
# Number of TCP connections in the various states
|
||||
echo '<<<tcp_conn_stats>>>'
|
||||
# waitmax 10 netstat -nt | awk ' /^tcp/ { c[$6]++; } END { for (x in c) { print x, c[x]; } }'
|
||||
# New implementation: netstat is very slow for large TCP tables
|
||||
cat /proc/net/tcp /proc/net/tcp6 2>/dev/null | awk ' /:/ { c[$4]++; } END { for (x in c) { print x, c[x]; } }'
|
||||
|
||||
# Linux Multipathing
|
||||
if type multipath >/dev/null ; then
|
||||
echo '<<<multipath>>>'
|
||||
multipath -l
|
||||
fi
|
||||
|
||||
# Performancecounter Platten
|
||||
echo '<<<diskstat>>>'
|
||||
date +%s
|
||||
egrep ' (x?[shv]d[a-z]*|cciss/c[0-9]+d[0-9]+|emcpower[a-z]+|dm-[0-9]+|VxVM.*|mmcblk.*) ' < /proc/diskstats
|
||||
if type dmsetup >/dev/null ; then
|
||||
echo '[dmsetup_info]'
|
||||
dmsetup info -c --noheadings --separator ' ' -o name,devno,vg_name,lv_name
|
||||
fi
|
||||
if [ -d /dev/vx/dsk ] ; then
|
||||
echo '[vx_dsk]'
|
||||
stat -c "%t %T %n" /dev/vx/dsk/*/*
|
||||
fi
|
||||
|
||||
|
||||
# Performancecounter Kernel
|
||||
echo '<<<kernel>>>'
|
||||
date +%s
|
||||
cat /proc/vmstat /proc/stat
|
||||
|
||||
# Hardware sensors via IPMI (need ipmitool)
|
||||
if type ipmitool > /dev/null
|
||||
then
|
||||
run_cached -s ipmi 300 "ipmitool sensor list | grep -v 'command failed' | sed -e 's/ *| */|/g' -e 's/ /_/g' -e 's/_*"'$'"//' -e 's/|/ /g' | egrep -v '^[^ ]+ na ' | grep -v ' discrete '"
|
||||
fi
|
||||
|
||||
|
||||
# IPMI data via ipmi-sensors (of freeipmi). Please make sure, that if you
|
||||
# have installed freeipmi that IPMI is really support by your hardware.
|
||||
if type ipmi-sensors >/dev/null
|
||||
then
|
||||
echo '<<<ipmi_sensors>>>'
|
||||
# Newer ipmi-sensors version have new output format; Legacy format can be used
|
||||
if ipmi-sensors --help | grep -q legacy-output; then
|
||||
IPMI_FORMAT="--legacy-output"
|
||||
else
|
||||
IPMI_FORMAT=""
|
||||
fi
|
||||
# At least with ipmi-sensoirs 0.7.16 this group is Power_Unit instead of "Power Unit"
|
||||
run_cached -s ipmi_sensors 300 "for class in Temperature Power_Unit Fan
|
||||
do
|
||||
ipmi-sensors $IPMI_FORMAT --sdr-cache-directory /var/cache -g "$class" | sed -e 's/ /_/g' -e 's/:_\?/ /g' -e 's@ \([^(]*\)_(\([^)]*\))@ \2_\1@'
|
||||
# In case of a timeout immediately leave loop.
|
||||
if [ $? = 255 ] ; then break ; fi
|
||||
done"
|
||||
fi
|
||||
|
||||
# RAID status of Linux software RAID
|
||||
echo '<<<md>>>'
|
||||
cat /proc/mdstat
|
||||
|
||||
# RAID status of Linux RAID via device mapper
|
||||
if type dmraid >/dev/null && DMSTATUS=$(dmraid -r)
|
||||
then
|
||||
echo '<<<dmraid>>>'
|
||||
|
||||
# Output name and status
|
||||
dmraid -s | grep -e ^name -e ^status
|
||||
|
||||
# Output disk names of the RAID disks
|
||||
DISKS=$(echo "$DMSTATUS" | cut -f1 -d\:)
|
||||
|
||||
for disk in $DISKS ; do
|
||||
device=$(cat /sys/block/$(basename $disk)/device/model )
|
||||
status=$(echo "$DMSTATUS" | grep ^${disk})
|
||||
echo "$status Model: $device"
|
||||
done
|
||||
fi
|
||||
|
||||
# RAID status of LSI controllers via cfggen
|
||||
if type cfggen > /dev/null ; then
|
||||
echo '<<<lsi>>>'
|
||||
cfggen 0 DISPLAY | egrep '(Target ID|State|Volume ID|Status of volume)[[:space:]]*:' | sed -e 's/ *//g' -e 's/:/ /'
|
||||
fi
|
||||
|
||||
# RAID status of LSI MegaRAID controller via MegaCli. You can download that tool from:
|
||||
# http://www.lsi.com/downloads/Public/MegaRAID%20Common%20Files/8.02.16_MegaCLI.zip
|
||||
if type MegaCli >/dev/null ; then
|
||||
MegaCli_bin="MegaCli"
|
||||
elif type MegaCli64 >/dev/null ; then
|
||||
MegaCli_bin="MegaCli64"
|
||||
elif type megacli >/dev/null ; then
|
||||
MegaCli_bin="megacli"
|
||||
else
|
||||
MegaCli_bin="unknown"
|
||||
fi
|
||||
|
||||
if [ "$MegaCli_bin" != "unknown" ]; then
|
||||
echo '<<<megaraid_pdisks>>>'
|
||||
for part in $($MegaCli_bin -EncInfo -aALL -NoLog < /dev/null \
|
||||
| sed -rn 's/:/ /g; s/[[:space:]]+/ /g; s/^ //; s/ $//; s/Number of enclosures on adapter ([0-9]+).*/adapter \1/g; /^(Enclosure|Device ID|adapter) [0-9]+$/ p'); do
|
||||
[ $part = adapter ] && echo ""
|
||||
[ $part = 'Enclosure' ] && echo -ne "\ndev2enc"
|
||||
echo -n " $part"
|
||||
done
|
||||
echo
|
||||
$MegaCli_bin -PDList -aALL -NoLog < /dev/null | egrep 'Enclosure|Raw Size|Slot Number|Device Id|Firmware state|Inquiry|Adapter'
|
||||
echo '<<<megaraid_ldisks>>>'
|
||||
$MegaCli_bin -LDInfo -Lall -aALL -NoLog < /dev/null | egrep 'Size|State|Number|Adapter|Virtual'
|
||||
echo '<<<megaraid_bbu>>>'
|
||||
$MegaCli_bin -AdpBbuCmd -GetBbuStatus -aALL -NoLog < /dev/null | grep -v Exit
|
||||
fi
|
||||
|
||||
# RAID status of 3WARE disk controller (by Radoslaw Bak)
|
||||
if type tw_cli > /dev/null ; then
|
||||
for C in $(tw_cli show | awk 'NR < 4 { next } { print $1 }'); do
|
||||
echo '<<<3ware_info>>>'
|
||||
tw_cli /$C show all | egrep 'Model =|Firmware|Serial'
|
||||
echo '<<<3ware_disks>>>'
|
||||
tw_cli /$C show drivestatus | egrep 'p[0-9]' | sed "s/^/$C\//"
|
||||
echo '<<<3ware_units>>>'
|
||||
tw_cli /$C show unitstatus | egrep 'u[0-9]' | sed "s/^/$C\//"
|
||||
done
|
||||
fi
|
||||
|
||||
# RAID controllers from areca (Taiwan)
|
||||
# cli64 can be found at ftp://ftp.areca.com.tw/RaidCards/AP_Drivers/Linux/CLI/
|
||||
if type cli64 >/dev/null ; then
|
||||
run_cached -s arc_raid_status 300 "cli64 rsf info | tail -n +3 | head -n -2"
|
||||
fi
|
||||
|
||||
# VirtualBox Guests. Section must always been output. Otherwise the
|
||||
# check would not be executed in case no guest additions are installed.
|
||||
# And that is something the check wants to detect
|
||||
echo '<<<vbox_guest>>>'
|
||||
if type VBoxControl >/dev/null 2>&1 ; then
|
||||
VBoxControl -nologo guestproperty enumerate | cut -d, -f1,2
|
||||
[ ${PIPESTATUS[0]} = 0 ] || echo "ERROR"
|
||||
fi
|
||||
|
||||
# OpenVPN Clients. Currently we assume that the configuration # is in
|
||||
# /etc/openvpn. We might find a safer way to find the configuration later.
|
||||
if [ -e /etc/openvpn/openvpn-status.log ] ; then
|
||||
echo '<<<openvpn_clients:sep(44)>>>'
|
||||
sed -n -e '/CLIENT LIST/,/ROUTING TABLE/p' < /etc/openvpn/openvpn-status.log | sed -e 1,3d -e '$d'
|
||||
fi
|
||||
|
||||
# Time synchronization with NTP
|
||||
if type ntpq > /dev/null 2>&1 ; then
|
||||
# remove heading, make first column space separated
|
||||
run_cached -s ntp 30 "waitmax 5 ntpq -np | sed -e 1,2d -e 's/^\(.\)/\1 /' -e 's/^ /%/'"
|
||||
fi
|
||||
|
||||
# Time synchronization with Chrony
|
||||
if type chronyc > /dev/null 2>&1 ; then
|
||||
# Force successful exit code. Otherwise section will be missing if daemon not running
|
||||
run_cached -s chrony 30 "waitmax 5 chronyc tracking || true"
|
||||
fi
|
||||
|
||||
if type nvidia-settings >/dev/null && [ -S /tmp/.X11-unix/X0 ]
|
||||
then
|
||||
echo '<<<nvidia>>>'
|
||||
for var in GPUErrors GPUCoreTemp
|
||||
do
|
||||
DISPLAY=:0 waitmax 2 nvidia-settings -t -q $var | sed "s/^/$var: /"
|
||||
done
|
||||
fi
|
||||
|
||||
if [ -e /proc/drbd ]; then
|
||||
echo '<<<drbd>>>'
|
||||
cat /proc/drbd
|
||||
fi
|
||||
|
||||
# Status of CUPS printer queues
|
||||
if type lpstat > /dev/null 2>&1; then
|
||||
if pgrep cups > /dev/null 2>&1; then
|
||||
echo '<<<cups_queues>>>'
|
||||
CPRINTCONF=/etc/cups/printers.conf
|
||||
if [ -r "$CPRINTCONF" ] ; then
|
||||
LOCAL_PRINTERS=$(grep -E "<(Default)?Printer .*>" $CPRINTCONF | awk '{print $2}' | sed -e 's/>//')
|
||||
lpstat -p | while read LINE
|
||||
do
|
||||
PRINTER=$(echo $LINE | awk '{print $2}')
|
||||
if echo "$LOCAL_PRINTERS" | grep -q "$PRINTER"; then
|
||||
echo $LINE
|
||||
fi
|
||||
done
|
||||
echo '---'
|
||||
lpstat -o | while read LINE
|
||||
do
|
||||
PRINTER=${LINE%%-*}
|
||||
if echo "$LOCAL_PRINTERS" | grep -q "$PRINTER"; then
|
||||
echo $LINE
|
||||
fi
|
||||
done
|
||||
else
|
||||
lpstat -p
|
||||
echo '---'
|
||||
lpstat -o | sort
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Heartbeat monitoring
|
||||
# Different handling for heartbeat clusters with and without CRM
|
||||
# for the resource state
|
||||
if [ -S /var/run/heartbeat/crm/cib_ro -o -S /var/run/crm/cib_ro ] || pgrep crmd > /dev/null 2>&1; then
|
||||
echo '<<<heartbeat_crm>>>'
|
||||
crm_mon -1 -r | grep -v ^$ | sed 's/^ //; /^\sResource Group:/,$ s/^\s//; s/^\s/_/g'
|
||||
fi
|
||||
if type cl_status > /dev/null 2>&1; then
|
||||
echo '<<<heartbeat_rscstatus>>>'
|
||||
cl_status rscstatus
|
||||
|
||||
echo '<<<heartbeat_nodes>>>'
|
||||
for NODE in $(cl_status listnodes); do
|
||||
if [ $NODE != $(echo $HOSTNAME | tr 'A-Z' 'a-z') ]; then
|
||||
STATUS=$(cl_status nodestatus $NODE)
|
||||
echo -n "$NODE $STATUS"
|
||||
for LINK in $(cl_status listhblinks $NODE 2>/dev/null); do
|
||||
echo -n " $LINK $(cl_status hblinkstatus $NODE $LINK)"
|
||||
done
|
||||
echo
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# Postfix mailqueue monitoring
|
||||
#
|
||||
# Only handle mailq when postfix user is present. The mailq command is also
|
||||
# available when postfix is not installed. But it produces different outputs
|
||||
# which are not handled by the check at the moment. So try to filter out the
|
||||
# systems not using postfix by searching for the postfix user.a
|
||||
#
|
||||
# Cannot take the whole outout. This could produce several MB of agent output
|
||||
# on blocking queues.
|
||||
# Only handle the last 6 lines (includes the summary line at the bottom and
|
||||
# the last message in the queue. The last message is not used at the moment
|
||||
# but it could be used to get the timestamp of the last message.
|
||||
if type postconf >/dev/null ; then
|
||||
echo '<<<postfix_mailq>>>'
|
||||
postfix_queue_dir=$(postconf -h queue_directory)
|
||||
postfix_count=$(find $postfix_queue_dir/deferred -type f | wc -l)
|
||||
postfix_size=$(du -ks $postfix_queue_dir/deferred | awk '{print $1 }')
|
||||
if [ $postfix_count -gt 0 ]
|
||||
then
|
||||
echo -- $postfix_size Kbytes in $postfix_count Requests.
|
||||
else
|
||||
echo Mail queue is empty
|
||||
fi
|
||||
elif [ -x /usr/sbin/ssmtp ] ; then
|
||||
echo '<<<postfix_mailq>>>'
|
||||
mailq 2>&1 | sed 's/^[^:]*: \(.*\)/\1/' | tail -n 6
|
||||
fi
|
||||
|
||||
#Check status of qmail mailqueue
|
||||
if type qmail-qstat >/dev/null
|
||||
then
|
||||
echo "<<<qmail_stats>>>"
|
||||
qmail-qstat
|
||||
fi
|
||||
|
||||
# Check status of OMD sites
|
||||
if type omd >/dev/null
|
||||
then
|
||||
run_cached -s omd_status 60 "omd status --bare --auto"
|
||||
fi
|
||||
|
||||
|
||||
# Welcome the ZFS check on Linux
|
||||
# We do not endorse running ZFS on linux if your vendor doesnt support it ;)
|
||||
# check zpool status
|
||||
if type zpool >/dev/null; then
|
||||
echo "<<<zpool_status>>>"
|
||||
zpool status -x
|
||||
fi
|
||||
|
||||
|
||||
# Fileinfo-Check: put patterns for files into /etc/check_mk/fileinfo.cfg
|
||||
if [ -r "$MK_CONFDIR/fileinfo.cfg" ] ; then
|
||||
echo '<<<fileinfo:sep(124)>>>'
|
||||
date +%s
|
||||
stat -c "%n|%s|%Y" $(cat "$MK_CONFDIR/fileinfo.cfg")
|
||||
fi
|
||||
|
||||
# Get stats about OMD monitoring cores running on this machine.
|
||||
# Since cd is a shell builtin the check does not affect the performance
|
||||
# on non-OMD machines.
|
||||
if cd /omd/sites
|
||||
then
|
||||
echo '<<<livestatus_status:sep(59)>>>'
|
||||
for site in *
|
||||
do
|
||||
if [ -S "/omd/sites/$site/tmp/run/live" ] ; then
|
||||
echo "[$site]"
|
||||
echo -e "GET status" | waitmax 3 /omd/sites/$site/bin/unixcat /omd/sites/$site/tmp/run/live
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# Get statistics about monitored jobs. Below the job directory there
|
||||
# is a sub directory per user that ran a job. That directory must be
|
||||
# owned by the user so that a symlink or hardlink attack for reading
|
||||
# arbitrary files can be avoided.
|
||||
if pushd $MK_VARDIR/job >/dev/null; then
|
||||
echo '<<<job>>>'
|
||||
for username in *
|
||||
do
|
||||
if [ -d "$username" ] && cd "$username" ; then
|
||||
su "$username" -c "head -n -0 -v *"
|
||||
cd ..
|
||||
fi
|
||||
done
|
||||
popd > /dev/null
|
||||
fi
|
||||
|
||||
# Gather thermal information provided e.g. by acpi
|
||||
# At the moment only supporting thermal sensors
|
||||
if ls /sys/class/thermal/thermal_zone* >/dev/null 2>&1; then
|
||||
echo '<<<lnx_thermal>>>'
|
||||
for F in /sys/class/thermal/thermal_zone*; do
|
||||
echo -n "${F##*/} "
|
||||
if [ ! -e $F/mode ] ; then echo -n "- " ; fi
|
||||
cat $F/{mode,type,temp,trip_point_*} | tr \\n " "
|
||||
echo
|
||||
done
|
||||
fi
|
||||
|
||||
# Libelle Business Shadow
|
||||
if type trd >/dev/null; then
|
||||
echo "<<<libelle_business_shadow:sep(58)>>>"
|
||||
trd -s
|
||||
fi
|
||||
|
||||
# MK's Remote Plugin Executor
|
||||
if [ -e "$MK_CONFDIR/mrpe.cfg" ]
|
||||
then
|
||||
echo '<<<mrpe>>>'
|
||||
grep -Ev '^[[:space:]]*($|#)' "$MK_CONFDIR/mrpe.cfg" | \
|
||||
while read descr cmdline
|
||||
do
|
||||
PLUGIN=${cmdline%% *}
|
||||
OUTPUT=$(eval "$cmdline")
|
||||
echo -n "(${PLUGIN##*/}) $descr $? $OUTPUT" | tr \\n \\1
|
||||
echo
|
||||
done
|
||||
fi
|
||||
|
||||
|
||||
# Local checks
|
||||
echo '<<<local>>>'
|
||||
if cd $LOCALDIR ; then
|
||||
for skript in $(ls) ; do
|
||||
if [ -f "$skript" -a -x "$skript" ] ; then
|
||||
./$skript
|
||||
fi
|
||||
done
|
||||
# Call some plugins only every X'th minute
|
||||
for skript in [1-9]*/* ; do
|
||||
if [ -x "$skript" ] ; then
|
||||
run_cached local_${skript//\//\\} ${skript%/*} "$skript"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# Plugins
|
||||
if cd $PLUGINSDIR ; then
|
||||
for skript in $(ls) ; do
|
||||
if [ -f "$skript" -a -x "$skript" ] ; then
|
||||
./$skript
|
||||
fi
|
||||
done
|
||||
# Call some plugins only every Xth minute
|
||||
for skript in [1-9]*/* ; do
|
||||
if [ -x "$skript" ] ; then
|
||||
run_cached plugins_${skript//\//\\} ${skript%/*} "$skript"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# Agent output snippets created by cronjobs, etc.
|
||||
if [ -d "$SPOOLDIR" ]
|
||||
then
|
||||
pushd "$SPOOLDIR" > /dev/null
|
||||
now=$(date +%s)
|
||||
|
||||
for file in *
|
||||
do
|
||||
# output every file in this directory. If the file is prefixed
|
||||
# with a number, then that number is the maximum age of the
|
||||
# file in seconds. If the file is older than that, it is ignored.
|
||||
maxage=""
|
||||
part="$file"
|
||||
|
||||
# Each away all digits from the front of the filename and
|
||||
# collect them in the variable maxage.
|
||||
while [ "${part/#[0-9]/}" != "$part" ]
|
||||
do
|
||||
maxage=$maxage${part:0:1}
|
||||
part=${part:1}
|
||||
done
|
||||
|
||||
# If there is at least one digit, than we honor that.
|
||||
if [ "$maxage" ] ; then
|
||||
mtime=$(stat -c %Y "$file")
|
||||
if [ $((now - mtime)) -gt $maxage ] ; then
|
||||
continue
|
||||
fi
|
||||
fi
|
||||
|
||||
# Output the file
|
||||
cat "$file"
|
||||
done
|
||||
popd > /dev/null
|
||||
fi
|
||||
@@ -1,114 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Detects which OS and if it is Linux then it will detect which Linux Distribution.
|
||||
|
||||
OS=`uname -s`
|
||||
REV=`uname -r`
|
||||
MACH=`uname -m`
|
||||
|
||||
if [ "${OS}" = "SunOS" ] ; then
|
||||
OS=Solaris
|
||||
ARCH=`uname -p`
|
||||
OSSTR="${OS} ${REV}(${ARCH} `uname -v`)"
|
||||
|
||||
elif [ "${OS}" = "AIX" ] ; then
|
||||
OSSTR="${OS} `oslevel` (`oslevel -r`)"
|
||||
|
||||
elif [ "${OS}" = "Linux" ] ; then
|
||||
KERNEL=`uname -r`
|
||||
|
||||
if [ -f /etc/fedora-release ]; then
|
||||
DIST=$(cat /etc/fedora-release | awk '{print $1}')
|
||||
REV=`cat /etc/fedora-release | sed s/.*release\ // | sed s/\ .*//`
|
||||
|
||||
elif [ -f /etc/redhat-release ] ; then
|
||||
DIST=$(cat /etc/redhat-release | awk '{print $1}')
|
||||
if [ "${DIST}" = "CentOS" ]; then
|
||||
DIST="CentOS"
|
||||
elif [ "${DIST}" = "Mandriva" ]; then
|
||||
DIST="Mandriva"
|
||||
PSEUDONAME=`cat /etc/mandriva-release | sed s/.*\(// | sed s/\)//`
|
||||
REV=`cat /etc/mandriva-release | sed s/.*release\ // | sed s/\ .*//`
|
||||
elif [ -f /etc/oracle-release ]; then
|
||||
DIST="Oracle"
|
||||
else
|
||||
DIST="RedHat"
|
||||
fi
|
||||
|
||||
PSEUDONAME=`cat /etc/redhat-release | sed s/.*\(// | sed s/\)//`
|
||||
REV=`cat /etc/redhat-release | sed s/.*release\ // | sed s/\ .*//`
|
||||
|
||||
elif [ -f /etc/mandrake-release ] ; then
|
||||
DIST='Mandrake'
|
||||
PSEUDONAME=`cat /etc/mandrake-release | sed s/.*\(// | sed s/\)//`
|
||||
REV=`cat /etc/mandrake-release | sed s/.*release\ // | sed s/\ .*//`
|
||||
|
||||
elif [ -f /etc/devuan_version ] ; then
|
||||
DIST="Devuan `cat /etc/devuan_version`"
|
||||
REV=""
|
||||
|
||||
elif [ -f /etc/debian_version ] ; then
|
||||
DIST="Debian `cat /etc/debian_version`"
|
||||
REV=""
|
||||
ID=`lsb_release -i | awk -F ':' '{print $2}' | sed 's/ //g'`
|
||||
if [ "${ID}" = "Raspbian" ] ; then
|
||||
DIST="Raspbian `cat /etc/debian_version`"
|
||||
fi
|
||||
|
||||
elif [ -f /etc/gentoo-release ] ; then
|
||||
DIST="Gentoo"
|
||||
REV=$(tr -d '[[:alpha:]]' </etc/gentoo-release | tr -d " ")
|
||||
|
||||
elif [ -f /etc/arch-release ] ; then
|
||||
DIST="Arch Linux"
|
||||
REV="" # Omit version since Arch Linux uses rolling releases
|
||||
IGNORE_LSB=1 # /etc/lsb-release would overwrite $REV with "rolling"
|
||||
|
||||
elif [ -f /etc/os-release ] ; then
|
||||
DIST=$(grep '^NAME=' /etc/os-release | cut -d= -f2- | tr -d '"')
|
||||
REV=$(grep '^VERSION_ID=' /etc/os-release | cut -d= -f2- | tr -d '"')
|
||||
|
||||
elif [ -f /etc/openwrt_version ] ; then
|
||||
DIST="OpenWrt"
|
||||
REV=$(cat /etc/openwrt_version)
|
||||
|
||||
elif [ -f /etc/pld-release ] ; then
|
||||
DIST=$(cat /etc/pld-release)
|
||||
REV=""
|
||||
|
||||
elif [ -f /etc/SuSE-release ] ; then
|
||||
DIST=$(echo SLES $(grep VERSION /etc/SuSE-release | cut -d = -f 2 | tr -d " "))
|
||||
REV=$(echo SP$(grep PATCHLEVEL /etc/SuSE-release | cut -d = -f 2 | tr -d " "))
|
||||
fi
|
||||
|
||||
if [ -f /etc/lsb-release -a "${IGNORE_LSB}" != 1 ] ; then
|
||||
LSB_DIST=$(lsb_release -si)
|
||||
LSB_REV=$(lsb_release -sr)
|
||||
if [ "$LSB_DIST" != "" ] ; then
|
||||
DIST=$LSB_DIST
|
||||
fi
|
||||
if [ "$LSB_REV" != "" ] ; then
|
||||
REV=$LSB_REV
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "`uname -a | awk '{print $(NF)}'`" = "DD-WRT" ] ; then
|
||||
DIST="dd-wrt"
|
||||
fi
|
||||
|
||||
if [ -n "${REV}" ]
|
||||
then
|
||||
OSSTR="${DIST} ${REV}"
|
||||
else
|
||||
OSSTR="${DIST}"
|
||||
fi
|
||||
|
||||
elif [ "${OS}" = "Darwin" ] ; then
|
||||
if [ -f /usr/bin/sw_vers ] ; then
|
||||
OSSTR=`/usr/bin/sw_vers|grep -v Build|sed 's/^.*:.//'| tr "\n" ' '`
|
||||
fi
|
||||
|
||||
elif [ "${OS}" = "FreeBSD" ] ; then
|
||||
OSSTR=`/usr/bin/uname -mior`
|
||||
fi
|
||||
|
||||
echo ${OSSTR}
|
||||
@@ -1,9 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
echo '<<<dmi>>>'
|
||||
|
||||
# requires dmidecode
|
||||
for FIELD in bios-vendor bios-version bios-release-date system-manufacturer system-product-name system-version system-serial-number system-uuid baseboard-manufacturer baseboard-product-name baseboard-version baseboard-serial-number baseboard-asset-tag chassis-manufacturer chassis-type chassis-version chassis-serial-number chassis-asset-tag processor-family processor-manufacturer processor-version processor-frequency
|
||||
do
|
||||
echo $FIELD="$(dmidecode -s $FIELD | grep -v '^#')"
|
||||
done
|
||||
@@ -1,22 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Cache the file for 30 minutes
|
||||
# If you want to override this, put the command in cron.
|
||||
# We cache because it is a 1sec delay, which is painful for the poller
|
||||
if [ -x /usr/bin/dpkg-query ]; then
|
||||
DATE=$(date +%s)
|
||||
FILE=/var/cache/librenms/agent-local-dpkg
|
||||
|
||||
[ -d /var/cache/librenms ] || mkdir -p /var/cache/librenms
|
||||
|
||||
if [ ! -e $FILE ]; then
|
||||
dpkg-query -W --showformat='${Status} ${Package} ${Version} ${Architecture} ${Installed-Size}\n'|grep " installed "|cut -d\ -f4- > $FILE
|
||||
fi
|
||||
FILEMTIME=$(stat -c %Y $FILE)
|
||||
FILEAGE=$(($DATE-$FILEMTIME))
|
||||
if [ $FILEAGE -gt 1800 ]; then
|
||||
dpkg-query -W --showformat='${Status} ${Package} ${Version} ${Architecture} ${Installed-Size}\n'|grep " installed "|cut -d\ -f4- > $FILE
|
||||
fi
|
||||
echo "<<<dpkg>>>"
|
||||
cat $FILE
|
||||
fi
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,34 +0,0 @@
|
||||
#!/bin/sh
|
||||
# Please make sure the paths below are correct.
|
||||
# Alternatively you can put them in $0.conf, meaning if you've named
|
||||
# this script ntp-client then it must go in ntp-client.conf .
|
||||
#
|
||||
# NTPQV output version of "ntpq -c rv"
|
||||
# Version 4 is the most common and up to date version.
|
||||
#
|
||||
# If you are unsure, which to set, run this script and make sure that
|
||||
# the JSON output variables match that in "ntpq -c rv".
|
||||
#
|
||||
################################################################
|
||||
# Don't change anything unless you know what are you doing #
|
||||
################################################################
|
||||
BIN_NTPQ='/usr/bin/env ntpq'
|
||||
BIN_GREP='/usr/bin/env grep'
|
||||
BIN_AWK='/usr/bin/env awk'
|
||||
|
||||
CONFIG=$0".conf"
|
||||
if [ -f "$CONFIG" ]; then
|
||||
# shellcheck disable=SC1090
|
||||
. "$CONFIG"
|
||||
fi
|
||||
|
||||
NTP_OFFSET=$($BIN_NTPQ -c rv | $BIN_GREP "offset" | $BIN_AWK -Foffset= '{print $2}' | $BIN_AWK -F, '{print $1}')
|
||||
NTP_FREQUENCY=$($BIN_NTPQ -c rv | $BIN_GREP "frequency" | $BIN_AWK -Ffrequency= '{print $2}' | $BIN_AWK -F, '{print $1}')
|
||||
NTP_SYS_JITTER=$($BIN_NTPQ -c rv | $BIN_GREP "sys_jitter" | $BIN_AWK -Fsys_jitter= '{print $2}' | $BIN_AWK -F, '{print $1}')
|
||||
NTP_CLK_JITTER=$($BIN_NTPQ -c rv | $BIN_GREP "clk_jitter" | $BIN_AWK -Fclk_jitter= '{print $2}' | $BIN_AWK -F, '{print $1}')
|
||||
NTP_WANDER=$($BIN_NTPQ -c rv | $BIN_GREP "clk_wander" | $BIN_AWK -Fclk_wander= '{print $2}' | $BIN_AWK -F, '{print $1}')
|
||||
NTP_VERSION=$($BIN_NTPQ -c rv | $BIN_GREP "version" | $BIN_AWK -F'ntpd ' '{print $2}' | $BIN_AWK -F. '{print $1}')
|
||||
|
||||
echo '{"data":{"offset":"'"$NTP_OFFSET"'","frequency":"'"$NTP_FREQUENCY"'","sys_jitter":"'"$NTP_SYS_JITTER"'","clk_jitter":"'"$NTP_CLK_JITTER"'","clk_wander":"'"$NTP_WANDER"'"},"version":"'"$NTP_VERSION"'","error":"0","errorString":""}'
|
||||
|
||||
exit 0
|
||||
@@ -1,89 +0,0 @@
|
||||
#!/bin/sh
|
||||
# Please make sure the paths below are correct.
|
||||
# Alternatively you can put them in $0.conf, meaning if you've named
|
||||
# this script ntp-client.sh then it must go in ntp-client.sh.conf .
|
||||
#
|
||||
# NTPQV output version of "ntpq -c rv"
|
||||
# p1 DD-WRT and some other outdated linux distros
|
||||
# p11 FreeBSD 11 and any linux distro that is up to date
|
||||
#
|
||||
# If you are unsure, which to set, run this script and make sure that
|
||||
# the JSON output variables match that in "ntpq -c rv".
|
||||
#
|
||||
BIN_NTPD='/usr/bin/env ntpd'
|
||||
BIN_NTPQ='/usr/bin/env ntpq'
|
||||
BIN_NTPDC='/usr/bin/env ntpdc'
|
||||
BIN_GREP='/usr/bin/env grep'
|
||||
BIN_TR='/usr/bin/env tr'
|
||||
BIN_CUT='/usr/bin/env cut'
|
||||
BIN_SED="/usr/bin/env sed"
|
||||
BIN_AWK='/usr/bin/env awk'
|
||||
NTPQV="p11"
|
||||
################################################################
|
||||
# Don't change anything unless you know what are you doing #
|
||||
################################################################
|
||||
CONFIG=$0".conf"
|
||||
if [ -f $CONFIG ]; then
|
||||
. $CONFIG
|
||||
fi
|
||||
VERSION=1
|
||||
|
||||
STRATUM=`$BIN_NTPQ -c rv | $BIN_GREP -Eow "stratum=[0-9]+" | $BIN_CUT -d "=" -f 2`
|
||||
|
||||
# parse the ntpq info that requires version specific info
|
||||
NTPQ_RAW=`$BIN_NTPQ -c rv | $BIN_GREP jitter | $BIN_SED 's/[[:alpha:]=,_]/ /g'`
|
||||
if [ $NTPQV = "p11" ]; then
|
||||
OFFSET=`echo $NTPQ_RAW | $BIN_AWK -F ' ' '{print $3}'`
|
||||
FREQUENCY=`echo $NTPQ_RAW | $BIN_AWK -F ' ' '{print $4}'`
|
||||
SYS_JITTER=`echo $NTPQ_RAW | $BIN_AWK -F ' ' '{print $5}'`
|
||||
CLK_JITTER=`echo $NTPQ_RAW | $BIN_AWK -F ' ' '{print $6}'`
|
||||
CLK_WANDER=`echo $NTPQ_RAW | $BIN_AWK -F ' ' '{print $7}'`
|
||||
fi
|
||||
if [ $NTPQV = "p1" ]; then
|
||||
OFFSET=`echo $NTPQ_RAW | $BIN_AWK -F ' ' '{print $2}'`
|
||||
FREQUENCY=`echo $NTPQ_RAW | $BIN_AWK -F ' ' '{print $3}'`
|
||||
SYS_JITTER=`echo $NTPQ_RAW | $BIN_AWK -F ' ' '{print $4}'`
|
||||
CLK_JITTER=`echo $NTPQ_RAW | $BIN_AWK -F ' ' '{print $5}'`
|
||||
CLK_WANDER=`echo $NTPQ_RAW | $BIN_AWK -F ' ' '{print $6}'`
|
||||
fi
|
||||
|
||||
VER=`$BIN_NTPD --version`
|
||||
if [ "$VER" = '4.2.6p5' ]; then
|
||||
USECMD=`echo $BIN_NTPDC -c iostats`
|
||||
else
|
||||
USECMD=`echo $BIN_NTPQ -c iostats localhost`
|
||||
fi
|
||||
CMD2=`$USECMD | $BIN_TR -d ' ' | $BIN_CUT -d : -f 2 | $BIN_TR '\n' ' '`
|
||||
|
||||
TIMESINCERESET=`echo $CMD2 | $BIN_AWK -F ' ' '{print $1}'`
|
||||
RECEIVEDBUFFERS=`echo $CMD2 | $BIN_AWK -F ' ' '{print $2}'`
|
||||
FREERECEIVEBUFFERS=`echo $CMD2 | $BIN_AWK -F ' ' '{print $3}'`
|
||||
USEDRECEIVEBUFFERS=`echo $CMD2 | $BIN_AWK -F ' ' '{print $4}'`
|
||||
LOWWATERREFILLS=`echo $CMD2 | $BIN_AWK -F ' ' '{print $5}'`
|
||||
DROPPEDPACKETS=`echo $CMD2 | $BIN_AWK -F ' ' '{print $6}'`
|
||||
IGNOREDPACKETS=`echo $CMD2 | $BIN_AWK -F ' ' '{print $7}'`
|
||||
RECEIVEDPACKETS=`echo $CMD2 | $BIN_AWK -F ' ' '{print $8}'`
|
||||
PACKETSSENT=`echo $CMD2 | $BIN_AWK -F ' ' '{print $9}'`
|
||||
PACKETSENDFAILURES=`echo $CMD2 | $BIN_AWK -F ' ' '{print $10}'`
|
||||
INPUTWAKEUPS=`echo $CMD2 | $BIN_AWK -F ' ' '{print $11}'`
|
||||
USEFULINPUTWAKEUPS=`echo $CMD2 | $BIN_AWK -F ' ' '{print $12}'`
|
||||
|
||||
echo '{"data":{"offset":"'$OFFSET\
|
||||
'","frequency":"'$FREQUENCY\
|
||||
'","sys_jitter":"'$SYS_JITTER\
|
||||
'","clk_jitter":"'$CLK_JITTER\
|
||||
'","clk_wander":"'$CLK_WANDER\
|
||||
'","stratum":"'$STRATUM\
|
||||
'","time_since_reset":"'$TIMESINCERESET\
|
||||
'","receive_buffers":"'$RECEIVEDBUFFERS\
|
||||
'","free_receive_buffers":"'$FREERECEIVEBUFFERS\
|
||||
'","used_receive_buffers":"'$USEDRECEIVEBUFFERS\
|
||||
'","low_water_refills":"'$LOWWATERREFILLS\
|
||||
'","dropped_packets":"'$DROPPEDPACKETS\
|
||||
'","ignored_packets":"'$IGNOREDPACKETS\
|
||||
'","received_packets":"'$RECEIVEDPACKETS\
|
||||
'","packets_sent":"'$PACKETSSENT\
|
||||
'","packet_send_failures":"'$PACKETSENDFAILURES\
|
||||
'","input_wakeups":"'$PACKETSENDFAILURES\
|
||||
'","useful_input_wakeups":"'$USEFULINPUTWAKEUPS\
|
||||
'"},"error":"0","errorString":"","version":"'$VERSION'"}'
|
||||
@@ -1,73 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
################################################################
|
||||
# copy this script to /etc/snmp/ and make it executable: #
|
||||
# chmod +x /etc/snmp/os-updates.sh #
|
||||
# ------------------------------------------------------------ #
|
||||
# edit your snmpd.conf and include: #
|
||||
# extend osupdate /opt/os-updates.sh #
|
||||
#--------------------------------------------------------------#
|
||||
# restart snmpd and activate the app for desired host #
|
||||
#--------------------------------------------------------------#
|
||||
# please make sure you have the path/binaries below #
|
||||
################################################################
|
||||
BIN_WC='/usr/bin/wc'
|
||||
BIN_GREP='/bin/grep'
|
||||
CMD_GREP='-c'
|
||||
CMD_WC='-l'
|
||||
BIN_ZYPPER='/usr/bin/zypper'
|
||||
CMD_ZYPPER='-q lu'
|
||||
BIN_YUM='/usr/bin/yum'
|
||||
CMD_YUM='-q check-update'
|
||||
BIN_DNF='/usr/bin/dnf'
|
||||
CMD_DNF='-q check-update'
|
||||
BIN_APT='/usr/bin/apt-get'
|
||||
CMD_APT='-qq -s upgrade'
|
||||
BIN_PACMAN='/usr/bin/pacman'
|
||||
CMD_PACMAN='-Sup'
|
||||
|
||||
################################################################
|
||||
# Don't change anything unless you know what are you doing #
|
||||
################################################################
|
||||
if [ -f $BIN_ZYPPER ]; then
|
||||
# OpenSUSE
|
||||
UPDATES=`$BIN_ZYPPER $CMD_ZYPPER | $BIN_WC $CMD_WC`
|
||||
if [ $UPDATES -ge 2 ]; then
|
||||
echo $(($UPDATES-2));
|
||||
else
|
||||
echo "0";
|
||||
fi
|
||||
elif [ -f $BIN_DNF ]; then
|
||||
# Fedora
|
||||
UPDATES=`$BIN_DNF $CMD_DNF | $BIN_WC $CMD_WC`
|
||||
if [ $UPDATES -ge 1 ]; then
|
||||
echo $(($UPDATES-1));
|
||||
else
|
||||
echo "0";
|
||||
fi
|
||||
elif [ -f $BIN_PACMAN ]; then
|
||||
# Arch
|
||||
UPDATES=`$BIN_PACMAN $CMD_PACMAN | $BIN_WC $CMD_WC`
|
||||
if [ $UPDATES -ge 1 ]; then
|
||||
echo $(($UPDATES-1));
|
||||
else
|
||||
echo "0";
|
||||
fi
|
||||
elif [ -f $BIN_YUM ]; then
|
||||
# CentOS / Redhat
|
||||
UPDATES=`$BIN_YUM $CMD_YUM | $BIN_WC $CMD_WC`
|
||||
if [ $UPDATES -ge 1 ]; then
|
||||
echo $(($UPDATES-1));
|
||||
else
|
||||
echo "0";
|
||||
fi
|
||||
elif [ -f $BIN_APT ]; then
|
||||
# Debian / Devuan / Ubuntu
|
||||
UPDATES=`$BIN_APT $CMD_APT | $BIN_GREP $CMD_GREP 'Inst'`
|
||||
if [ $UPDATES -ge 1 ]; then
|
||||
echo $UPDATES;
|
||||
else
|
||||
echo "0";
|
||||
fi
|
||||
else
|
||||
echo "0";
|
||||
fi
|
||||
@@ -1,13 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
#Written by Valec 2006. Steal and share.
|
||||
#Get postfix queue lengths
|
||||
|
||||
#extend mailq /opt/observer/scripts/getmailq.sh
|
||||
|
||||
QUEUES="incoming active deferred hold"
|
||||
|
||||
for i in $QUEUES; do
|
||||
COUNT=$(qshape "$i" | grep TOTAL | awk '{print $2}')
|
||||
printf "$COUNT\n"
|
||||
done
|
||||
@@ -1,548 +0,0 @@
|
||||
#!/usr/bin/env perl
|
||||
|
||||
# add this to your snmpd.conf file as below
|
||||
# extend postfixdetailed /etc/snmp/postfixdetailed
|
||||
|
||||
# The cache file to use.
|
||||
my $cache='/var/cache/postfixdetailed';
|
||||
|
||||
# the location of pflogsumm
|
||||
my $pflogsumm='/usr/bin/env pflogsumm';
|
||||
|
||||
#totals
|
||||
# 847 received = received
|
||||
# 852 delivered = delivered
|
||||
# 0 forwarded = forwarded
|
||||
# 3 deferred (67 deferrals)= deferred
|
||||
# 0 bounced = bounced
|
||||
# 593 rejected (41%) = rejected
|
||||
# 0 reject warnings = rejectw
|
||||
# 0 held = held
|
||||
# 0 discarded (0%) = discarded
|
||||
|
||||
# 16899k bytes received = bytesr
|
||||
# 18009k bytes delivered = bytesd
|
||||
# 415 senders = senders
|
||||
# 266 sending hosts/domains = sendinghd
|
||||
# 15 recipients = recipients
|
||||
# 9 recipient hosts/domains = recipienthd
|
||||
|
||||
######message deferral detail
|
||||
#Connection refused = deferralcr
|
||||
#Host is down = deferralhid
|
||||
|
||||
########message reject detail
|
||||
#Client host rejected = chr
|
||||
#Helo command rejected: need fully-qualified hostname = hcrnfqh
|
||||
#Sender address rejected: Domain not found = sardnf
|
||||
#Sender address rejected: not owned by user = sarnobu
|
||||
#blocked using = bu
|
||||
#Recipient address rejected: User unknown = raruu
|
||||
#Helo command rejected: Invalid name = hcrin
|
||||
#Sender address rejected: need fully-qualified address = sarnfqa
|
||||
#Recipient address rejected: Domain not found = rardnf
|
||||
#Recipient address rejected: need fully-qualified address = rarnfqa
|
||||
#Improper use of SMTP command pipelining = iuscp
|
||||
#Message size exceeds fixed limit = msefl
|
||||
#Server configuration error = sce
|
||||
#Server configuration problem = scp
|
||||
#unknown reject reason = urr
|
||||
|
||||
my $old='';
|
||||
|
||||
#reads in the old data if it exists
|
||||
if ( -f $cache ){
|
||||
open(my $fh, "<", $cache) or die "Can't open '".$cache."'";
|
||||
# if this is over 2048, something is most likely wrong
|
||||
read($fh , $old , 2048);
|
||||
close($fh);
|
||||
}
|
||||
|
||||
my ( $received,
|
||||
$delivered,
|
||||
$forwarded,
|
||||
$deferred,
|
||||
$bounced,
|
||||
$rejected,
|
||||
$rejectw,
|
||||
$held,
|
||||
$discarded,
|
||||
$bytesr,
|
||||
$bytesd,
|
||||
$senders,
|
||||
$sendinghd,
|
||||
$recipients,
|
||||
$recipienthd,
|
||||
$deferralcr,
|
||||
$deferralhid,
|
||||
$chr,
|
||||
$hcrnfqh,
|
||||
$sardnf,
|
||||
$sarnobu,
|
||||
$bu,
|
||||
$raruu,
|
||||
$hcrin,
|
||||
$sarnfqa,
|
||||
$rardnf,
|
||||
$rarnfqa,
|
||||
$iuscp,
|
||||
$sce,
|
||||
$scp,
|
||||
$urr,
|
||||
$msefl) = split ( /\n/, $old );
|
||||
|
||||
if ( ! defined( $received ) ){ $received=0; }
|
||||
if ( ! defined( $delivered ) ){ $delivered=0; }
|
||||
if ( ! defined( $forwarded ) ){ $forwarded=0; }
|
||||
if ( ! defined( $deferred ) ){ $deferred=0; }
|
||||
if ( ! defined( $bounced ) ){ $bounced=0; }
|
||||
if ( ! defined( $rejected ) ){ $rejected=0; }
|
||||
if ( ! defined( $rejectw ) ){ $rejectw=0; }
|
||||
if ( ! defined( $held ) ){ $held=0; }
|
||||
if ( ! defined( $discarded ) ){ $discarded=0; }
|
||||
if ( ! defined( $bytesr ) ){ $bytesr=0; }
|
||||
if ( ! defined( $bytesd ) ){ $bytesd=0; }
|
||||
if ( ! defined( $senders ) ){ $senders=0; }
|
||||
if ( ! defined( $sendinghd ) ){ $sendinghd=0; }
|
||||
if ( ! defined( $recipients ) ){ $recipients=0; }
|
||||
if ( ! defined( $recipienthd ) ){ $recipienthd=0; }
|
||||
if ( ! defined( $deferralcr ) ){ $deferralcr=0; }
|
||||
if ( ! defined( $deferralhid ) ){ $deferralhid=0; }
|
||||
if ( ! defined( $chr ) ){ $chr=0; }
|
||||
if ( ! defined( $hcrnfqh ) ){ $hcrnfqh=0; }
|
||||
if ( ! defined( $sardnf ) ){ $sardnf=0; }
|
||||
if ( ! defined( $sarnobu ) ){ $sarnobu=0; }
|
||||
if ( ! defined( $bu ) ){ $bu=0; }
|
||||
if ( ! defined( $raruu ) ){ $raruu=0; }
|
||||
if ( ! defined( $hcrin ) ){ $hcrin=0; }
|
||||
if ( ! defined( $sarnfqa ) ){ $sarnfqa=0; }
|
||||
if ( ! defined( $rardnf ) ){ $rardnf=0; }
|
||||
if ( ! defined( $rarnfqa ) ){ $rarnfqa=0; }
|
||||
if ( ! defined( $iuscp ) ){ $iuscp=0; }
|
||||
if ( ! defined( $msefl ) ){ $msefl=0; }
|
||||
if ( ! defined( $sce ) ){ $sce=0; }
|
||||
if ( ! defined( $scp ) ){ $scp=0; }
|
||||
if ( ! defined( $urr ) ){ $urr=0; }
|
||||
|
||||
#init current variables
|
||||
my $receivedC=0;
|
||||
my $deliveredC=0;
|
||||
my $forwardedC=0;
|
||||
my $deferredC=0;
|
||||
my $bouncedC=0;
|
||||
my $rejectedC=0;
|
||||
my $rejectwC=0;
|
||||
my $heldC=0;
|
||||
my $discardedC=0;
|
||||
my $bytesrC=0;
|
||||
my $bytesdC=0;
|
||||
my $sendersC=0;
|
||||
my $sendinghdC=0;
|
||||
my $recipientsC=0;
|
||||
my $recipienthdC=0;
|
||||
my $deferralcrC=0;
|
||||
my $deferralhidC=0;
|
||||
my $hcrnfqhC=0;
|
||||
my $sardnfC=0;
|
||||
my $sarnobuC=0;
|
||||
my $buC=0;
|
||||
my $raruuC=0;
|
||||
my $hcrinC=0;
|
||||
my $sarnfqaC=0;
|
||||
my $rardnfC=0;
|
||||
my $rarnfqaC=0;
|
||||
my $iuscpC=0;
|
||||
my $mseflC=0;
|
||||
my $sceC=0;
|
||||
my $scpC=0;
|
||||
my $urrC=0;
|
||||
|
||||
sub newValue{
|
||||
my $old=$_[0];
|
||||
my $new=$_[1];
|
||||
|
||||
#if new is undefined, just default to 0... this should never happen
|
||||
if ( !defined( $new ) ){
|
||||
warn('New not defined');
|
||||
return 0;
|
||||
}
|
||||
|
||||
#sets it to 0 if old is not defined
|
||||
if ( !defined( $old ) ){
|
||||
warn('Old not defined');
|
||||
$old=0;
|
||||
}
|
||||
|
||||
#make sure they are both numberic and if not set to zero
|
||||
if( $old !~ /^[0123456789]*$/ ){
|
||||
warn('Old not numeric');
|
||||
$old=0;
|
||||
}
|
||||
if( $new !~ /^[0123456789]*$/ ){
|
||||
warn('New not numeric');
|
||||
$new=0;
|
||||
}
|
||||
|
||||
#log rotation happened
|
||||
if ( $old > $new ){
|
||||
return $new;
|
||||
};
|
||||
|
||||
return $new - $old;
|
||||
}
|
||||
|
||||
|
||||
my $output=`$pflogsumm /var/log/maillog`;
|
||||
|
||||
#holds RBL values till the end when it is compared to the old one
|
||||
my $buNew=0;
|
||||
|
||||
|
||||
#holds client host rejected values till the end when it is compared to the old one
|
||||
my $chrNew=0;
|
||||
|
||||
# holds recipient address rejected values till the end when it is compared to the old one
|
||||
my $raruuNew=0;
|
||||
|
||||
#holds the current values for checking later
|
||||
my $current='';
|
||||
|
||||
my @outputA=split( /\n/, $output );
|
||||
my $int=0;
|
||||
while ( defined( $outputA[$int] ) ){
|
||||
my $line=$outputA[$int];
|
||||
|
||||
$line=~s/^ *//;
|
||||
$line=~s/ +/ /g;
|
||||
$line=~s/\)$//;
|
||||
|
||||
my $handled=0;
|
||||
|
||||
#received line
|
||||
if ( ( $line =~ /[0123456789] received$/ ) && ( ! $handled ) ){
|
||||
$line=~s/ .*//;
|
||||
$receivedC=$line;
|
||||
$received=newValue( $received, $line );
|
||||
$handled=1;
|
||||
}
|
||||
|
||||
#delivered line
|
||||
if ( ( $line =~ /[0123456789] delivered$/ ) && ( ! $handled ) ){
|
||||
$line=~s/ .*//;
|
||||
$deliveredC=$line;
|
||||
$delivered=newValue( $delivered, $line );
|
||||
$handled=1;
|
||||
}
|
||||
|
||||
#forward line
|
||||
if ( ( $line =~ /[0123456789] forwarded$/ ) && ( ! $handled ) ){
|
||||
$line=~s/ .*//;
|
||||
$forwardedC=$line;
|
||||
$forwarded=newValue( $forwarded, $line );
|
||||
$handled=1;
|
||||
}
|
||||
|
||||
#defereed line
|
||||
if ( ( $line =~ /[0123456789] deferred \(/ ) && ( ! $handled ) ){
|
||||
$line=~s/ .*//;
|
||||
$deferredC=$line;
|
||||
$deferred=newValue( $deferred, $line );
|
||||
$handled=1;
|
||||
}
|
||||
|
||||
#bounced line
|
||||
if ( ( $line =~ /[0123456789] bounced$/ ) && ( ! $handled ) ){
|
||||
$line=~s/ .*//;
|
||||
$bouncedC=$line;
|
||||
$bounced=newValue( $bounced, $line );
|
||||
$handled=1;
|
||||
}
|
||||
|
||||
#rejected line
|
||||
if ( ( $line =~ /[0123456789] rejected \(/ ) && ( ! $handled ) ){
|
||||
$line=~s/ .*//;
|
||||
$rejectedC=$line;
|
||||
$rejected=newValue( $rejected, $line );
|
||||
$handled=1;
|
||||
}
|
||||
|
||||
#reject warning line
|
||||
if ( ( $line =~ /[0123456789] reject warnings/ ) && ( ! $handled ) ){
|
||||
$line=~s/ .*//;
|
||||
$rejectwC=$line;
|
||||
$rejectw=newValue( $rejectw, $line );
|
||||
$handled=1;
|
||||
}
|
||||
|
||||
#held line
|
||||
if ( ( $line =~ /[0123456789] held$/ ) && ( ! $handled ) ){
|
||||
$line=~s/ .*//;
|
||||
$heldC=$line;
|
||||
$held=newValue( $held, $line );
|
||||
$handled=1;
|
||||
}
|
||||
|
||||
#discarded line
|
||||
if ( ( $line =~ /[0123456789] discarded \(/ ) && ( ! $handled ) ){
|
||||
$line=~s/ .*//;
|
||||
$discardedC=$line;
|
||||
$discarded=newValue( $discarded, $line );
|
||||
$handled=1;
|
||||
}
|
||||
|
||||
#bytes received line
|
||||
if ( ( $line =~ /[0123456789kM] bytes received$/ ) && ( ! $handled ) ){
|
||||
$line=~s/ .*//;
|
||||
$line=~s/k/000/;
|
||||
$line=~s/M/000000/;
|
||||
$bytesrC=$line;
|
||||
$bytesr=newValue( $bytesr, $line );
|
||||
$handled=1;
|
||||
}
|
||||
|
||||
#bytes delivered line
|
||||
if ( ( $line =~ /[0123456789kM] bytes delivered$/ ) && ( ! $handled ) ){
|
||||
$line=~s/ .*//;
|
||||
$line=~s/k/000/;
|
||||
$line=~s/M/000000/;
|
||||
$bytesdC=$line;
|
||||
$bytesd=newValue( $bytesd, $line );
|
||||
$handled=1;
|
||||
}
|
||||
|
||||
#senders line
|
||||
if ( ( $line =~ /[0123456789] senders$/ ) && ( ! $handled ) ){
|
||||
$line=~s/ .*//;
|
||||
$sendersC=$line;
|
||||
$senders=newValue( $senders, $line );
|
||||
$handled=1;
|
||||
}
|
||||
|
||||
#sendering hosts/domains line
|
||||
if ( ( $line =~ /[0123456789] sending hosts\/domains$/ ) && ( ! $handled ) ){
|
||||
$line=~s/ .*//;
|
||||
$sendinghdC=$line;
|
||||
$sendinghd=newValue( $sendinghd, $line );
|
||||
$handled=1;
|
||||
}
|
||||
|
||||
#recipients line
|
||||
if ( ( $line =~ /[0123456789] recipients$/ ) && ( ! $handled ) ){
|
||||
$line=~s/ .*//;
|
||||
$recipientsC=$line;
|
||||
$recipients=newValue( $recipients, $line );
|
||||
$handled=1;
|
||||
}
|
||||
|
||||
#recipients line
|
||||
if ( ( $line =~ /[0123456789] recipient hosts\/domains$/ ) && ( ! $handled ) ){
|
||||
$line=~s/ .*//;
|
||||
$recipienthdC=$line;
|
||||
$recipienthd=newValue( $recipienthd, $line );
|
||||
$handled=1;
|
||||
}
|
||||
|
||||
# deferrals connectios refused
|
||||
if ( ( $line =~ /[0123456789] 25\: Connection refused$/ ) && ( ! $handled ) ){
|
||||
$line=~s/ .*//;
|
||||
$deferralcrC=$line;
|
||||
$deferralcr=newValue( $deferralcr, $line );
|
||||
$handled=1;
|
||||
}
|
||||
|
||||
# deferrals Host is down
|
||||
if ( ( $line =~ /Host is down$/ ) && ( ! $handled ) ){
|
||||
$line=~s/ .*//;
|
||||
$deferralcrC=$line;
|
||||
$deferralhidC=$line;
|
||||
$deferralhid=newValue( $deferralhid, $line );
|
||||
$handled=1;
|
||||
}
|
||||
|
||||
# Client host rejected
|
||||
if ( ( $line =~ /Client host rejected/ ) && ( ! $handled ) ){
|
||||
$line=~s/.*\: //g;
|
||||
$chrNew=$chrNew + $line;
|
||||
$handled=1;
|
||||
}
|
||||
|
||||
#Helo command rejected: need fully-qualified hostname
|
||||
if ( ( $line =~ /Helo command rejected\: need fully\-qualified hostname/ ) && ( ! $handled ) ){
|
||||
$line=~s/.*\: //g;
|
||||
$hcrnfqhC=$line;
|
||||
$hcrnfqh=newValue( $hcrnfqh, $line );
|
||||
$handled=1;
|
||||
}
|
||||
|
||||
#Sender address rejected: Domain not found
|
||||
if ( ( $line =~ /Sender address rejected\: Domain not found/ ) && ( ! $handled ) ){
|
||||
$line=~s/.*\: //g;
|
||||
$sardnfC=$line;
|
||||
$sardnf=newValue( $sardnf, $line );
|
||||
$handled=1;
|
||||
}
|
||||
|
||||
#Sender address rejected: not owned by user
|
||||
if ( ( $line =~ /Sender address rejected\: not owned by user/ ) && ( ! $handled ) ){
|
||||
$line=~s/.*\: //g;
|
||||
$sarnobuC=$line;
|
||||
$sarnobu=newValue( $sarnobu, $line );
|
||||
$handled=1;
|
||||
}
|
||||
|
||||
#blocked using
|
||||
# These lines are RBLs so there will be more than one.
|
||||
# Use $buNew to add them all up.
|
||||
if ( ( $line =~ /blocked using/ ) && ( ! $handled ) ){
|
||||
$line=~s/.*\: //g;
|
||||
$buNew=$buNew + $line;
|
||||
$handled=1;
|
||||
}
|
||||
|
||||
#Recipient address rejected: User unknown
|
||||
if ( ( $line =~ /Recipient address rejected\: User unknown/ ) && ( ! $handled ) ){
|
||||
$line=~s/.*\: //g;
|
||||
$raruuNew=$raruuNew + $line;
|
||||
$handled=1;
|
||||
}
|
||||
|
||||
#Helo command rejected: Invalid name
|
||||
if ( ( $line =~ /Helo command rejected\: Invalid name/ ) && ( ! $handled ) ){
|
||||
$line=~s/.*\: //g;
|
||||
$hcrinC=$line;
|
||||
$hcrin=newValue( $hcrin, $line );
|
||||
}
|
||||
|
||||
#Sender address rejected: need fully-qualified address
|
||||
if ( ( $line =~ /Sender address rejected\: need fully-qualified address/ ) && ( ! $handled ) ){
|
||||
$line=~s/.*\: //g;
|
||||
$sarnfqaC=$line;
|
||||
$sarnfqa=newValue( $sarnfqa, $line );
|
||||
}
|
||||
|
||||
#Recipient address rejected: Domain not found
|
||||
if ( ( $line =~ /Recipient address rejected\: Domain not found/ ) && ( ! $handled ) ){
|
||||
$line=~s/.*\: //g;
|
||||
$rardnfC=$line;
|
||||
$rardnf=newValue( $rardnf, $line );
|
||||
}
|
||||
|
||||
#Improper use of SMTP command pipelining
|
||||
if ( ( $line =~ /Improper use of SMTP command pipelining/ ) && ( ! $handled ) ){
|
||||
$line=~s/.*\: //g;
|
||||
$iuscpC=$line;
|
||||
$iuscp=newValue( $iuscp, $line );
|
||||
}
|
||||
|
||||
#Message size exceeds fixed limit
|
||||
if ( ( $line =~ /Message size exceeds fixed limit/ ) && ( ! $handled ) ){
|
||||
$line=~s/.*\: //g;
|
||||
$mseflC=$line;
|
||||
$msefl=newValue( $msefl, $line );
|
||||
}
|
||||
|
||||
#Server configuration error
|
||||
if ( ( $line =~ /Server configuration error/ ) && ( ! $handled ) ){
|
||||
$line=~s/.*\: //g;
|
||||
$sceC=$line;
|
||||
$sce=newValue( $sce, $line );
|
||||
}
|
||||
|
||||
#Server configuration problem
|
||||
if ( ( $line =~ /Server configuration problem/ ) && ( ! $handled ) ){
|
||||
$line=~s/.*\: //g;
|
||||
$scpC=$line;
|
||||
$scp=newValue( $scp, $line );
|
||||
}
|
||||
|
||||
#unknown reject reason
|
||||
if ( ( $line =~ /unknown reject reason/ ) && ( ! $handled ) ){
|
||||
$line=~s/.*\: //g;
|
||||
$urrC=$line;
|
||||
$urr=newValue( $urr, $line );
|
||||
}
|
||||
|
||||
$int++;
|
||||
}
|
||||
|
||||
|
||||
# final client host rejected total
|
||||
$chr=newValue( $chr, $chrNew );
|
||||
|
||||
# final RBL total
|
||||
$bu=newValue( $bu, $buNew );
|
||||
|
||||
# final recipient address rejected total
|
||||
$raruu=newValue( $raruu, $raruuNew );
|
||||
|
||||
my $data=$received."\n".
|
||||
$delivered."\n".
|
||||
$forwarded."\n".
|
||||
$deferred."\n".
|
||||
$bounced."\n".
|
||||
$rejected."\n".
|
||||
$rejectw."\n".
|
||||
$held."\n".
|
||||
$discarded."\n".
|
||||
$bytesr."\n".
|
||||
$bytesd."\n".
|
||||
$senders."\n".
|
||||
$sendinghd."\n".
|
||||
$recipients."\n".
|
||||
$recipienthd."\n".
|
||||
$deferralcr."\n".
|
||||
$deferralhid."\n".
|
||||
$chr."\n".
|
||||
$hcrnfqh."\n".
|
||||
$sardnf."\n".
|
||||
$sarnobu."\n".
|
||||
$bu."\n".
|
||||
$raruu."\n".
|
||||
$hcrin."\n".
|
||||
$sarnfqa."\n".
|
||||
$rardnf."\n".
|
||||
$rarnfqa."\n".
|
||||
$iuscp."\n".
|
||||
$sce."\n".
|
||||
$scp."\n".
|
||||
$urr."\n".
|
||||
$msefl."\n";
|
||||
|
||||
print $data;
|
||||
|
||||
my $current=$receivedC."\n".
|
||||
$deliveredC."\n".
|
||||
$forwardedC."\n".
|
||||
$deferredC."\n".
|
||||
$bouncedC."\n".
|
||||
$rejectedC."\n".
|
||||
$rejectwC."\n".
|
||||
$heldC."\n".
|
||||
$discardedC."\n".
|
||||
$bytesrC."\n".
|
||||
$bytesdC."\n".
|
||||
$sendersC."\n".
|
||||
$sendinghdC."\n".
|
||||
$recipientsC."\n".
|
||||
$recipienthdC."\n".
|
||||
$deferralcrC."\n".
|
||||
$deferralhidC."\n".
|
||||
$chrNew."\n".
|
||||
$hcrnfqhC."\n".
|
||||
$sardnfC."\n".
|
||||
$sarnobuC."\n".
|
||||
$buNew."\n".
|
||||
$raruuNew."\n".
|
||||
$hcrinC."\n".
|
||||
$sarnfqaC."\n".
|
||||
$rardnfC."\n".
|
||||
$rarnfqaC."\n".
|
||||
$iuscpC."\n".
|
||||
$sceC."\n".
|
||||
$scpC."\n".
|
||||
$urrC."\n".
|
||||
$mseflC."\n";
|
||||
|
||||
open(my $fh, ">", $cache) or die "Can't open '".$cache."'";
|
||||
print $fh $current;
|
||||
close($fh);
|
||||
@@ -1,46 +0,0 @@
|
||||
#!/bin/bash
|
||||
#######################################
|
||||
# please read DOCS to succesfully get #
|
||||
# raspberry sensors into your host #
|
||||
#######################################
|
||||
picmd='/usr/bin/vcgencmd'
|
||||
pised='/bin/sed'
|
||||
getTemp='measure_temp'
|
||||
getVoltsCore='measure_volts core'
|
||||
getVoltsRamC='measure_volts sdram_c'
|
||||
getVoltsRamI='measure_volts sdram_i'
|
||||
getVoltsRamP='measure_volts sdram_p'
|
||||
getFreqArm='measure_clock arm'
|
||||
getFreqCore='measure_clock core'
|
||||
getStatusH264='codec_enabled H264'
|
||||
getStatusMPG2='codec_enabled MPG2'
|
||||
getStatusWVC1='codec_enabled WVC1'
|
||||
getStatusMPG4='codec_enabled MPG4'
|
||||
getStatusMJPG='codec_enabled MJPG'
|
||||
getStatusWMV9='codec_enabled WMV9'
|
||||
|
||||
$picmd $getTemp | $pised 's|[^0-9.]||g'
|
||||
$picmd "$getVoltsCore" | $pised 's|[^0-9.]||g'
|
||||
$picmd "$getVoltsRamC" | $pised 's|[^0-9.]||g'
|
||||
$picmd "$getVoltsRamI" | $pised 's|[^0-9.]||g'
|
||||
$picmd "$getVoltsRamP" | $pised 's|[^0-9.]||g'
|
||||
$picmd "$getFreqArm" | $pised 's/frequency([0-9]*)=//g'
|
||||
$picmd "$getFreqCore" | $pised 's/frequency([0-9]*)=//g'
|
||||
$picmd "$getStatusH264" | $pised 's/H264=//g'
|
||||
$picmd "$getStatusMPG2" | $pised 's/MPG2=//g'
|
||||
$picmd "$getStatusWVC1" | $pised 's/WVC1=//g'
|
||||
$picmd "$getStatusMPG4" | $pised 's/MPG4=//g'
|
||||
$picmd "$getStatusMJPG" | $pised 's/MJPG=//g'
|
||||
$picmd "$getStatusWMV9" | $pised 's/WMV9=//g'
|
||||
$picmd "$getStatusH264" | $pised 's/enabled/2/g'
|
||||
$picmd "$getStatusMPG2" | $pised 's/enabled/2/g'
|
||||
$picmd "$getStatusWVC1" | $pised 's/enabled/2/g'
|
||||
$picmd "$getStatusMPG4" | $pised 's/enabled/2/g'
|
||||
$picmd "$getStatusMJPG" | $pised 's/enabled/2/g'
|
||||
$picmd "$getStatusWMV9" | $pised 's/enabled/2/g'
|
||||
$picmd "$getStatusH264" | $pised 's/disabled/1/g'
|
||||
$picmd "$getStatusMPG2" | $pised 's/disabled/1/g'
|
||||
$picmd "$getStatusWVC1" | $pised 's/disabled/1/g'
|
||||
$picmd "$getStatusMPG4" | $pised 's/disabled/1/g'
|
||||
$picmd "$getStatusMJPG" | $pised 's/disabled/1/g'
|
||||
$picmd "$getStatusWMV9" | $pised 's/disabled/1/g'
|
||||
@@ -1,929 +0,0 @@
|
||||
#!/usr/bin/env perl
|
||||
#Copyright (c) 2024, Zane C. Bowers-Hadley
|
||||
#All rights reserved.
|
||||
#
|
||||
#Redistribution and use in source and binary forms, with or without modification,
|
||||
#are permitted provided that the following conditions are met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright notice,
|
||||
# this list of conditions and the following disclaimer.
|
||||
# * Redistributions in binary form must reproduce the above copyright notice,
|
||||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
#THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
#ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
#WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
||||
#IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
|
||||
#INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
#BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
#DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
#LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
|
||||
#OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
||||
#THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
=for comment
|
||||
|
||||
Add this to snmpd.conf like below.
|
||||
|
||||
extend smart /etc/snmp/smart
|
||||
|
||||
Then add to root's cron tab, if you have more than a few disks.
|
||||
|
||||
*/5 * * * * /etc/snmp/extends/smart -u
|
||||
|
||||
You will also need to create the config file, which defaults to the same path as the script,
|
||||
but with .config appended. So if the script is located at /etc/snmp/smart, the config file
|
||||
will be /etc/snmp/extends/smart.config. Alternatively you can also specific a config via -c.
|
||||
|
||||
Anything starting with a # is comment. The format for variables is $variable=$value. Empty
|
||||
lines are ignored. Spaces and tabes at either the start or end of a line are ignored. Any
|
||||
line with out a matched variable or # are treated as a disk.
|
||||
|
||||
#This is a comment
|
||||
cache=/var/cache/smart
|
||||
smartctl=/usr/local/sbin/smartctl
|
||||
useSN=0
|
||||
ada0
|
||||
da5 /dev/da5 -d sat
|
||||
twl0,0 /dev/twl0 -d 3ware,0
|
||||
twl0,1 /dev/twl0 -d 3ware,1
|
||||
twl0,2 /dev/twl0 -d 3ware,2
|
||||
|
||||
The variables are as below.
|
||||
|
||||
cache = The path to the cache file to use. Default: /var/cache/smart
|
||||
smartctl = The path to use for smartctl. Default: /usr/bin/env smartctl
|
||||
useSN = If set to 1, it will use the disks SN for reporting instead of the device name.
|
||||
1 is the default. 0 will use the device name.
|
||||
|
||||
A disk line is can be as simple as just a disk name under /dev/. Such as in the config above
|
||||
The line "ada0" would resolve to "/dev/ada0" and would be called with no special argument. If
|
||||
a line has a space in it, everything before the space is treated as the disk name and is what
|
||||
used for reporting and everything after that is used as the argument to be passed to smartctl.
|
||||
|
||||
If you want to guess at the configuration, call it with -g and it will print out what it thinks
|
||||
it should be.
|
||||
|
||||
|
||||
Switches:
|
||||
|
||||
-c <config> The config file to use.
|
||||
-u Update
|
||||
-p Pretty print the JSON.
|
||||
-Z GZip+Base64 compress the results.
|
||||
|
||||
-g Guess at the config and print it to STDOUT
|
||||
-C Enable manual checking for guess and cciss.
|
||||
-S Set useSN to 0 when using -g
|
||||
-t <test> Run the specified smart self test on all the devices.
|
||||
-U When calling cciss_vol_status, call it with -u.
|
||||
-G <modes> Guess modes to use. This is a comma seperated list.
|
||||
Default :: scan-open,cciss-vol-status
|
||||
|
||||
Guess Modes:
|
||||
|
||||
- scan :: Use "--scan" with smartctl. "scan-open" will take presidence.
|
||||
|
||||
- scan-open :: Call smartctl with "--scan-open".
|
||||
|
||||
- cciss-vol-status :: Freebsd/Linux specific and if it sees /dev/sg0(on Linux) or
|
||||
/dev/ciss0(on FreebSD) it will attempt to find drives via cciss-vol-status,
|
||||
and then optionally checking for disks via smrtctl if -C is given. Should be noted
|
||||
though that -C will not find drives that are currently missing/failed. If -U is given,
|
||||
cciss_vol_status will be called with -u.
|
||||
|
||||
=cut
|
||||
|
||||
##
|
||||
## You should not need to touch anything below here.
|
||||
##
|
||||
use warnings;
|
||||
use strict;
|
||||
use Getopt::Std;
|
||||
use JSON;
|
||||
use MIME::Base64;
|
||||
use IO::Compress::Gzip qw(gzip $GzipError);
|
||||
|
||||
my $cache = '/var/cache/smart';
|
||||
my $smartctl = '/usr/bin/env smartctl';
|
||||
my @disks;
|
||||
my $useSN = 1;
|
||||
|
||||
$Getopt::Std::STANDARD_HELP_VERSION = 1;
|
||||
|
||||
sub main::VERSION_MESSAGE {
|
||||
print "SMART SNMP extend 0.3.2\n";
|
||||
}
|
||||
|
||||
sub main::HELP_MESSAGE {
|
||||
&VERSION_MESSAGE;
|
||||
print "\n" . "-u Update '" . $cache . "'\n" . '-g Guess at the config and print it to STDOUT
|
||||
-c <config> The config file to use.
|
||||
-p Pretty print the JSON.
|
||||
-Z GZip+Base64 compress the results.
|
||||
-C Enable manual checking for guess and cciss.
|
||||
-S Set useSN to 0 when using -g
|
||||
-t <test> Run the specified smart self test on all the devices.
|
||||
-U When calling cciss_vol_status, call it with -u.
|
||||
-G <modes> Guess modes to use. This is a comma seperated list.
|
||||
Default :: scan-open,cciss-vol-status
|
||||
|
||||
|
||||
Scan Modes:
|
||||
|
||||
- scan :: Use "--scan" with smartctl. "scan-open" will take presidence.
|
||||
|
||||
- scan-open :: Call smartctl with "--scan-open".
|
||||
|
||||
- cciss-vol-status :: Freebsd/Linux specific and if it sees /dev/sg0(on Linux) or
|
||||
/dev/ciss0(on FreebSD) it will attempt to find drives via cciss-vol-status,
|
||||
and then optionally checking for disks via smrtctl if -C is given. Should be noted
|
||||
though that -C will not find drives that are currently missing/failed. If -U is given,
|
||||
cciss_vol_status will be called with -u.
|
||||
';
|
||||
|
||||
} ## end sub main::HELP_MESSAGE
|
||||
|
||||
#gets the options
|
||||
my %opts = ();
|
||||
getopts( 'ugc:pZhvCSGt:U', \%opts );
|
||||
|
||||
if ( $opts{h} ) {
|
||||
&HELP_MESSAGE;
|
||||
exit;
|
||||
}
|
||||
if ( $opts{v} ) {
|
||||
&VERSION_MESSAGE;
|
||||
exit;
|
||||
}
|
||||
|
||||
#
|
||||
# figure out what scan modes to use if -g specified
|
||||
#
|
||||
my $scan_modes = {
|
||||
'scan-open' => 0,
|
||||
'scan' => 0,
|
||||
'cciss_vol_status' => 0,
|
||||
};
|
||||
if ( $opts{g} ) {
|
||||
if ( !defined( $opts{G} ) ) {
|
||||
$opts{G} = 'scan-open,cciss_vol_status';
|
||||
}
|
||||
$opts{G} =~ s/[\ \t]//g;
|
||||
my @scan_modes_split = split( /,/, $opts{G} );
|
||||
foreach my $mode (@scan_modes_split) {
|
||||
if ( !defined $scan_modes->{$mode} ) {
|
||||
die( '"' . $mode . '" is not a recognized scan mode' );
|
||||
}
|
||||
$scan_modes->{$mode} = 1;
|
||||
}
|
||||
} ## end if ( $opts{g} )
|
||||
|
||||
# configure JSON for later usage
|
||||
# only need to do this if actually running as in -g is not specified
|
||||
my $json;
|
||||
if ( !$opts{g} ) {
|
||||
|
||||
$json = JSON->new->allow_nonref->canonical(1);
|
||||
if ( $opts{p} ) {
|
||||
$json->pretty;
|
||||
}
|
||||
}
|
||||
|
||||
#
|
||||
#
|
||||
# guess if asked
|
||||
#
|
||||
#
|
||||
if ( defined( $opts{g} ) ) {
|
||||
|
||||
#get what path to use for smartctl
|
||||
$smartctl = `which smartctl`;
|
||||
chomp($smartctl);
|
||||
if ( $? != 0 ) {
|
||||
warn("'which smartctl' failed with a exit code of $?");
|
||||
exit 1;
|
||||
}
|
||||
|
||||
#try to touch the default cache location and warn if it can't be done
|
||||
system( 'touch ' . $cache . '>/dev/null' );
|
||||
if ( $? != 0 ) {
|
||||
$cache = '#Could not touch ' . $cache . "You will need to manually set it\n" . "cache=?\n";
|
||||
} else {
|
||||
system( 'rm -f ' . $cache . '>/dev/null' );
|
||||
$cache = 'cache=' . $cache . "\n";
|
||||
}
|
||||
|
||||
my $drive_lines = '';
|
||||
|
||||
#
|
||||
#
|
||||
# scan-open and scan guess mode handling
|
||||
#
|
||||
#
|
||||
if ( $scan_modes->{'scan-open'} || $scan_modes->{'scan'} ) {
|
||||
# used for checking if a disk has been found more than once
|
||||
my %found_disks_names;
|
||||
my @argumentsA;
|
||||
|
||||
# use scan-open if it is set, overriding scan if it is also set
|
||||
my $mode = 'scan';
|
||||
if ( $scan_modes->{'scan-open'} ) {
|
||||
$mode = 'scan-open';
|
||||
}
|
||||
|
||||
#have smartctl scan and see if it finds anythings not get found
|
||||
my $scan_output = `$smartctl --$mode`;
|
||||
my @scan_outputA = split( /\n/, $scan_output );
|
||||
|
||||
# remove non-SMART devices sometimes returned
|
||||
@scan_outputA = grep( !/ses[0-9]/, @scan_outputA ); # not a disk, but may or may not have SMART attributes
|
||||
@scan_outputA = grep( !/pass[0-9]/, @scan_outputA ); # very likely a duplicate and a disk under another name
|
||||
@scan_outputA = grep( !/cd[0-9]/, @scan_outputA ); # CD drive
|
||||
if ( $^O eq 'freebsd' ) {
|
||||
@scan_outputA = grep( !/sa[0-9]/, @scan_outputA ); # tape drive
|
||||
@scan_outputA = grep( !/ctl[0-9]/, @scan_outputA ); # CAM target layer
|
||||
} elsif ( $^O eq 'linux' ) {
|
||||
@scan_outputA = grep( !/st[0-9]/, @scan_outputA ); # SCSI tape drive
|
||||
@scan_outputA = grep( !/ht[0-9]/, @scan_outputA ); # ATA tape drive
|
||||
}
|
||||
|
||||
# make the first pass, figuring out what all we have and trimming comments
|
||||
foreach my $arguments (@scan_outputA) {
|
||||
my $name = $arguments;
|
||||
|
||||
$arguments =~ s/ \#.*//; # trim the comment out of the argument
|
||||
$name =~ s/ .*//;
|
||||
$name =~ s/\/dev\///;
|
||||
if ( defined( $found_disks_names{$name} ) ) {
|
||||
$found_disks_names{$name}++;
|
||||
} else {
|
||||
$found_disks_names{$name} = 0;
|
||||
}
|
||||
|
||||
push( @argumentsA, $arguments );
|
||||
|
||||
} ## end foreach my $arguments (@scan_outputA)
|
||||
|
||||
# second pass, putting the lines together
|
||||
my %current_disk;
|
||||
foreach my $arguments (@argumentsA) {
|
||||
my $not_virt = 1;
|
||||
|
||||
# check to see if we have a virtual device
|
||||
my @virt_check = split( /\n/, `smartctl -i $arguments 2> /dev/null` );
|
||||
foreach my $virt_check_line (@virt_check) {
|
||||
if ( $virt_check_line =~ /(?i)Product\:.*LOGICAL VOLUME/ ) {
|
||||
$not_virt = 0;
|
||||
}
|
||||
}
|
||||
|
||||
my $name = $arguments;
|
||||
$name =~ s/ .*//;
|
||||
$name =~ s/\/dev\///;
|
||||
|
||||
# only add it if not a virtual RAID drive
|
||||
# HP RAID virtual disks will show up with very basical but totally useless smart data
|
||||
if ($not_virt) {
|
||||
if ( $found_disks_names{$name} == 0 ) {
|
||||
# If no other devices, just name it after the base device.
|
||||
$drive_lines = $drive_lines . $name . " " . $arguments . "\n";
|
||||
} else {
|
||||
# if more than one, start at zero and increment, apennding comma number to the base device name
|
||||
if ( defined( $current_disk{$name} ) ) {
|
||||
$current_disk{$name}++;
|
||||
} else {
|
||||
$current_disk{$name} = 0;
|
||||
}
|
||||
$drive_lines = $drive_lines . $name . "," . $current_disk{$name} . " " . $arguments . "\n";
|
||||
}
|
||||
} ## end if ($not_virt)
|
||||
|
||||
} ## end foreach my $arguments (@argumentsA)
|
||||
} ## end if ( $scan_modes->{'scan-open'} || $scan_modes...)
|
||||
|
||||
#
|
||||
#
|
||||
# scan mode handler for cciss_vol_status
|
||||
# /dev/sg* devices for cciss on Linux
|
||||
# /dev/ccis* devices for cciss on FreeBSD
|
||||
#
|
||||
#
|
||||
if ( $scan_modes->{'cciss_vol_status'} && ( $^O eq 'linux' || $^O eq 'freebsd' ) ) {
|
||||
my $cciss;
|
||||
if ( $^O eq 'freebsd' ) {
|
||||
$cciss = 'ciss';
|
||||
} elsif ( $^O eq 'linux' ) {
|
||||
$cciss = 'sg';
|
||||
}
|
||||
|
||||
my $uarg = '';
|
||||
if ( $opts{U} ) {
|
||||
$uarg = '-u';
|
||||
}
|
||||
|
||||
# generate the initial device path that will be checked
|
||||
my $sg_int = 0;
|
||||
my $device = '/dev/' . $cciss . $sg_int;
|
||||
|
||||
my $sg_process = 1;
|
||||
if ( -e $device ) {
|
||||
my $output = `which cciss_vol_status 2> /dev/null`;
|
||||
if ( $? != 0 && !$opts{C} ) {
|
||||
$sg_process = 0;
|
||||
$drive_lines
|
||||
= $drive_lines
|
||||
. "# -C not given, but "
|
||||
. $device
|
||||
. " exists and cciss_vol_status is not present\n"
|
||||
. "# in path or 'ccis_vol_status -V "
|
||||
. $device
|
||||
. "' is failing\n";
|
||||
} ## end if ( $? != 0 && !$opts{C} )
|
||||
} ## end if ( -e $device )
|
||||
my $seen_lines = {};
|
||||
my $ignore_lines = {};
|
||||
while ( -e $device && $sg_process ) {
|
||||
my $output = `cciss_vol_status -V $uarg $device 2> /dev/null`;
|
||||
if ( $? != 0 && $output eq '' && !$opts{C} ) {
|
||||
# just empty here as we just want to skip it if it fails and there is no C
|
||||
# warning is above
|
||||
} elsif ( $? != 0 && $output eq '' && $opts{C} ) {
|
||||
my $drive_count = 0;
|
||||
my $continue = 1;
|
||||
while ($continue) {
|
||||
my $output = `$smartctl -i $device -d cciss,$drive_count 2> /dev/null`;
|
||||
if ( $? != 0 ) {
|
||||
$continue = 0;
|
||||
} else {
|
||||
my $add_it = 0;
|
||||
my $id;
|
||||
while ( $output =~ /(?i)Serial Number:(.*)/g ) {
|
||||
$id = $1;
|
||||
$id =~ s/^\s+|\s+$//g;
|
||||
}
|
||||
if ( defined($id) && !defined( $seen_lines->{$id} ) ) {
|
||||
$add_it = 1;
|
||||
$seen_lines->{$id} = 1;
|
||||
}
|
||||
if ( $continue && $add_it ) {
|
||||
$drive_lines
|
||||
= $drive_lines
|
||||
. $cciss . '0-'
|
||||
. $drive_count . ' '
|
||||
. $device
|
||||
. ' -d cciss,'
|
||||
. $drive_count . "\n";
|
||||
}
|
||||
} ## end else [ if ( $? != 0 ) ]
|
||||
$drive_count++;
|
||||
} ## end while ($continue)
|
||||
} else {
|
||||
my $drive_count = 0;
|
||||
# count the connector lines, this will make sure failed are founded as well
|
||||
my $seen_conectors = {};
|
||||
while ( $output =~ /(connector +\d+[IA]\ +box +\d+\ +bay +\d+.*)/g ) {
|
||||
my $cciss_drive_line = $1;
|
||||
my $connector = $cciss_drive_line;
|
||||
$connector =~ s/(.*\ bay +\d+).*/$1/;
|
||||
if ( !defined( $seen_lines->{$cciss_drive_line} )
|
||||
&& !defined( $seen_conectors->{$connector} )
|
||||
&& !defined( $ignore_lines->{$cciss_drive_line} ) )
|
||||
{
|
||||
$seen_lines->{$cciss_drive_line} = 1;
|
||||
$seen_conectors->{$connector} = 1;
|
||||
$drive_count++;
|
||||
} else {
|
||||
# going to be a connector we've already seen
|
||||
# which will happen when it is processing replacement drives
|
||||
# so save this as a device to ignore
|
||||
$ignore_lines->{$cciss_drive_line} = 1;
|
||||
}
|
||||
} ## end while ( $output =~ /(connector +\d+[IA]\ +box +\d+\ +bay +\d+.*)/g)
|
||||
my $drive_int = 0;
|
||||
while ( $drive_int < $drive_count ) {
|
||||
$drive_lines
|
||||
= $drive_lines
|
||||
. $cciss
|
||||
. $sg_int . '-'
|
||||
. $drive_int . ' '
|
||||
. $device
|
||||
. ' -d cciss,'
|
||||
. $drive_int . "\n";
|
||||
|
||||
$drive_int++;
|
||||
} ## end while ( $drive_int < $drive_count )
|
||||
} ## end else [ if ( $? != 0 && $output eq '' && !$opts{C})]
|
||||
|
||||
$sg_int++;
|
||||
$device = '/dev/' . $cciss . $sg_int;
|
||||
} ## end while ( -e $device && $sg_process )
|
||||
} ## end if ( $scan_modes->{'cciss_vol_status'} && ...)
|
||||
|
||||
my $useSN = 1;
|
||||
if ( $opts{S} ) {
|
||||
$useSN = 0;
|
||||
}
|
||||
|
||||
print '# scan_modes='
|
||||
. $opts{G}
|
||||
. "\nuseSN="
|
||||
. $useSN . "\n"
|
||||
. 'smartctl='
|
||||
. $smartctl . "\n"
|
||||
. $cache
|
||||
. $drive_lines;
|
||||
|
||||
exit 0;
|
||||
} ## end if ( defined( $opts{g} ) )
|
||||
|
||||
#get which config file to use
|
||||
my $config = $0 . '.config';
|
||||
if ( defined( $opts{c} ) ) {
|
||||
$config = $opts{c};
|
||||
}
|
||||
|
||||
#reads the config file, optionally
|
||||
my $config_file = '';
|
||||
open( my $readfh, "<", $config ) or die "Can't open '" . $config . "'";
|
||||
read( $readfh, $config_file, 1000000 );
|
||||
close($readfh);
|
||||
|
||||
#
|
||||
#
|
||||
# parse the config file and remove comments and empty lines
|
||||
#
|
||||
#
|
||||
my @configA = split( /\n/, $config_file );
|
||||
@configA = grep( !/^$/, @configA );
|
||||
@configA = grep( !/^\#/, @configA );
|
||||
@configA = grep( !/^[\s\t]*$/, @configA );
|
||||
my $configA_int = 0;
|
||||
while ( defined( $configA[$configA_int] ) ) {
|
||||
my $line = $configA[$configA_int];
|
||||
chomp($line);
|
||||
$line =~ s/^[\t\s]+//;
|
||||
$line =~ s/[\t\s]+$//;
|
||||
|
||||
my ( $var, $val ) = split( /=/, $line, 2 );
|
||||
|
||||
my $matched;
|
||||
if ( $var eq 'cache' ) {
|
||||
$cache = $val;
|
||||
$matched = 1;
|
||||
}
|
||||
|
||||
if ( $var eq 'smartctl' ) {
|
||||
$smartctl = $val;
|
||||
$matched = 1;
|
||||
}
|
||||
|
||||
if ( $var eq 'useSN' ) {
|
||||
$useSN = $val;
|
||||
$matched = 1;
|
||||
}
|
||||
|
||||
if ( !defined($val) ) {
|
||||
push( @disks, $line );
|
||||
}
|
||||
|
||||
$configA_int++;
|
||||
} ## end while ( defined( $configA[$configA_int] ) )
|
||||
|
||||
#
|
||||
#
|
||||
# run the specified self test on all disks if asked
|
||||
#
|
||||
#
|
||||
if ( defined( $opts{t} ) ) {
|
||||
|
||||
# make sure we have something that atleast appears sane for the test name
|
||||
my $valid_tesks = {
|
||||
'offline' => 1,
|
||||
'short' => 1,
|
||||
'long' => 1,
|
||||
'conveyance' => 1,
|
||||
'afterselect,on' => 1,
|
||||
};
|
||||
if ( !defined( $valid_tesks->{ $opts{t} } ) && $opts{t} !~ /select,(\d+[\-\+]\d+|next|next\+\d+|redo\+\d+)/ ) {
|
||||
print '"' . $opts{t} . "\" does not appear to be a valid test\n";
|
||||
exit 1;
|
||||
}
|
||||
|
||||
print "Running the SMART $opts{t} on all devices in the config...\n\n";
|
||||
|
||||
foreach my $line (@disks) {
|
||||
my $disk;
|
||||
my $name;
|
||||
if ( $line =~ /\ / ) {
|
||||
( $name, $disk ) = split( /\ /, $line, 2 );
|
||||
} else {
|
||||
$disk = $line;
|
||||
$name = $line;
|
||||
}
|
||||
if ( $disk !~ /\// ) {
|
||||
$disk = '/dev/' . $disk;
|
||||
}
|
||||
|
||||
print "\n------------------------------------------------------------------\nDoing "
|
||||
. $smartctl . ' -t '
|
||||
. $opts{t} . ' '
|
||||
. $disk
|
||||
. " ...\n\n";
|
||||
print `$smartctl -t $opts{t} $disk` . "\n";
|
||||
|
||||
} ## end foreach my $line (@disks)
|
||||
|
||||
exit 0;
|
||||
} ## end if ( defined( $opts{t} ) )
|
||||
|
||||
#if set to 1, no cache will be written and it will be printed instead
|
||||
my $noWrite = 0;
|
||||
|
||||
#
|
||||
#
|
||||
# if no -u, it means we are being called from snmped
|
||||
#
|
||||
#
|
||||
if ( !defined( $opts{u} ) ) {
|
||||
# if the cache file exists, print it, otherwise assume one is not being used
|
||||
if ( -f $cache ) {
|
||||
my $old = '';
|
||||
open( my $readfh, "<", $cache ) or die "Can't open '" . $cache . "'";
|
||||
read( $readfh, $old, 1000000 );
|
||||
close($readfh);
|
||||
print $old;
|
||||
exit 0;
|
||||
} else {
|
||||
$opts{u} = 1;
|
||||
$noWrite = 1;
|
||||
}
|
||||
} ## end if ( !defined( $opts{u} ) )
|
||||
|
||||
#
|
||||
#
|
||||
# Process each disk
|
||||
#
|
||||
#
|
||||
my $to_return = {
|
||||
data => { disks => {}, exit_nonzero => 0, unhealthy => 0, useSN => $useSN },
|
||||
version => 1,
|
||||
error => 0,
|
||||
errorString => '',
|
||||
};
|
||||
foreach my $line (@disks) {
|
||||
my $disk;
|
||||
my $name;
|
||||
if ( $line =~ /\ / ) {
|
||||
( $name, $disk ) = split( /\ /, $line, 2 );
|
||||
} else {
|
||||
$disk = $line;
|
||||
$name = $line;
|
||||
}
|
||||
if ( $disk !~ /\// ) {
|
||||
$disk = '/dev/' . $disk;
|
||||
}
|
||||
|
||||
my $output = `$smartctl -A $disk`;
|
||||
my %IDs = (
|
||||
'5' => 'null',
|
||||
'10' => 'null',
|
||||
'173' => 'null',
|
||||
'177' => 'null',
|
||||
'183' => 'null',
|
||||
'184' => 'null',
|
||||
'187' => 'null',
|
||||
'188' => 'null',
|
||||
'190' => 'null',
|
||||
'194' => 'null',
|
||||
'196' => 'null',
|
||||
'197' => 'null',
|
||||
'198' => 'null',
|
||||
'199' => 'null',
|
||||
'231' => 'null',
|
||||
'232' => 'null',
|
||||
'233' => 'null',
|
||||
'9' => 'null',
|
||||
'disk' => $disk,
|
||||
'serial' => undef,
|
||||
'selftest_log' => undef,
|
||||
'health_pass' => 0,
|
||||
max_temp => 'null',
|
||||
exit => $?,
|
||||
);
|
||||
$IDs{'disk'} =~ s/^\/dev\///;
|
||||
|
||||
# if polling exited non-zero above, no reason running the rest of the checks
|
||||
my $disk_id = $name;
|
||||
if ( $IDs{exit} != 0 ) {
|
||||
$to_return->{data}{exit_nonzero}++;
|
||||
} else {
|
||||
my @outputA;
|
||||
|
||||
if ( $output =~ /NVMe Log/ ) {
|
||||
# we have an NVMe drive with annoyingly different output
|
||||
my %mappings = (
|
||||
'Temperature' => 194,
|
||||
'Power Cycles' => 12,
|
||||
'Power On Hours' => 9,
|
||||
'Percentage Used' => 231,
|
||||
);
|
||||
foreach ( split( /\n/, $output ) ) {
|
||||
if (/:/) {
|
||||
my ( $key, $val ) = split(/:/);
|
||||
$val =~ s/^\s+|\s+$|\D+//g;
|
||||
if ( exists( $mappings{$key} ) ) {
|
||||
if ( $mappings{$key} == 231 ) {
|
||||
$IDs{ $mappings{$key} } = 100 - $val;
|
||||
} else {
|
||||
$IDs{ $mappings{$key} } = $val;
|
||||
}
|
||||
}
|
||||
} ## end if (/:/)
|
||||
} ## end foreach ( split( /\n/, $output ) )
|
||||
|
||||
} else {
|
||||
@outputA = split( /\n/, $output );
|
||||
my $outputAint = 0;
|
||||
while ( defined( $outputA[$outputAint] ) ) {
|
||||
my $line = $outputA[$outputAint];
|
||||
$line =~ s/^ +//;
|
||||
$line =~ s/ +/ /g;
|
||||
|
||||
if ( $line =~ /^[0123456789]+ / ) {
|
||||
my @lineA = split( /\ /, $line, 10 );
|
||||
my $raw = $lineA[9];
|
||||
my $normalized = $lineA[3];
|
||||
my $id = $lineA[0];
|
||||
|
||||
# Crucial SSD
|
||||
# 202, Percent_Lifetime_Remain, same as 231, SSD Life Left
|
||||
if ( $id == 202
|
||||
&& $line =~ /Percent_Lifetime_Remain/ )
|
||||
{
|
||||
$IDs{231} = $raw;
|
||||
}
|
||||
|
||||
# single int raw values
|
||||
if ( ( $id == 5 )
|
||||
|| ( $id == 10 )
|
||||
|| ( $id == 173 )
|
||||
|| ( $id == 183 )
|
||||
|| ( $id == 184 )
|
||||
|| ( $id == 187 )
|
||||
|| ( $id == 196 )
|
||||
|| ( $id == 197 )
|
||||
|| ( $id == 198 )
|
||||
|| ( $id == 199 ) )
|
||||
{
|
||||
my @rawA = split( /\ /, $raw );
|
||||
$IDs{$id} = $rawA[0];
|
||||
} ## end if ( ( $id == 5 ) || ( $id == 10 ) || ( $id...))
|
||||
|
||||
# single int normalized values
|
||||
if ( ( $id == 177 )
|
||||
|| ( $id == 230 )
|
||||
|| ( $id == 231 )
|
||||
|| ( $id == 232 )
|
||||
|| ( $id == 233 ) )
|
||||
{
|
||||
# annoying non-standard disk
|
||||
# WDC WDS500G2B0A
|
||||
# 230 Media_Wearout_Indicator 0x0032 100 100 --- Old_age Always - 0x002e000a002e
|
||||
# 232 Available_Reservd_Space 0x0033 100 100 004 Pre-fail Always - 100
|
||||
# 233 NAND_GB_Written_TLC 0x0032 100 100 --- Old_age Always - 9816
|
||||
|
||||
if ( $id == 230
|
||||
&& $line =~ /Media_Wearout_Indicator/ )
|
||||
{
|
||||
$IDs{233} = int($normalized);
|
||||
} elsif ( $id == 232
|
||||
&& $line =~ /Available_Reservd_Space/ )
|
||||
{
|
||||
$IDs{232} = int($normalized);
|
||||
} else {
|
||||
# only set 233 if it has not been set yet
|
||||
# if it was set already then the above did it and we don't want
|
||||
# to overwrite it
|
||||
if ( $id == 233 && $IDs{233} eq "null" ) {
|
||||
$IDs{$id} = int($normalized);
|
||||
} elsif ( $id != 233 ) {
|
||||
$IDs{$id} = int($normalized);
|
||||
}
|
||||
} ## end else [ if ( $id == 230 && $line =~ /Media_Wearout_Indicator/)]
|
||||
} ## end if ( ( $id == 177 ) || ( $id == 230 ) || (...))
|
||||
|
||||
# 9, power on hours
|
||||
if ( $id == 9 ) {
|
||||
my @runtime = split( /[\ h]/, $raw );
|
||||
$IDs{$id} = $runtime[0];
|
||||
}
|
||||
|
||||
# 188, Command_Timeout
|
||||
if ( $id == 188 ) {
|
||||
my $total = 0;
|
||||
my @rawA = split( /\ /, $raw );
|
||||
my $rawAint = 0;
|
||||
while ( defined( $rawA[$rawAint] ) ) {
|
||||
$total = $total + $rawA[$rawAint];
|
||||
$rawAint++;
|
||||
}
|
||||
$IDs{$id} = $total;
|
||||
} ## end if ( $id == 188 )
|
||||
|
||||
# 190, airflow temp
|
||||
# 194, temp
|
||||
if ( ( $id == 190 )
|
||||
|| ( $id == 194 ) )
|
||||
{
|
||||
my ($temp) = split( /\ /, $raw );
|
||||
$IDs{$id} = $temp;
|
||||
}
|
||||
} ## end if ( $line =~ /^[0123456789]+ / )
|
||||
|
||||
# SAS Wrapping
|
||||
# Section by Cameron Munroe (munroenet[at]gmail.com)
|
||||
|
||||
# Elements in Grown Defect List.
|
||||
# Marking as 5 Reallocated_Sector_Ct
|
||||
if ( $line =~ "Elements in grown defect list:" ) {
|
||||
|
||||
my @lineA = split( /\ /, $line, 10 );
|
||||
my $raw = $lineA[5];
|
||||
|
||||
# Reallocated Sector Count ID
|
||||
$IDs{5} = $raw;
|
||||
|
||||
}
|
||||
|
||||
# Current Drive Temperature
|
||||
# Marking as 194 Temperature_Celsius
|
||||
if ( $line =~ "Current Drive Temperature:" ) {
|
||||
|
||||
my @lineA = split( /\ /, $line, 10 );
|
||||
my $raw = $lineA[3];
|
||||
|
||||
# Temperature C ID
|
||||
$IDs{194} = $raw;
|
||||
|
||||
}
|
||||
|
||||
# End of SAS Wrapper
|
||||
|
||||
$outputAint++;
|
||||
} ## end while ( defined( $outputA[$outputAint] ) )
|
||||
} ## end else [ if ( $output =~ /NVMe Log/ ) ]
|
||||
|
||||
#get the selftest logs
|
||||
$output = `$smartctl -l selftest $disk`;
|
||||
@outputA = split( /\n/, $output );
|
||||
my @completed = grep( /Completed/, @outputA );
|
||||
$IDs{'completed'} = scalar @completed;
|
||||
my @interrupted = grep( /Interrupted/, @outputA );
|
||||
$IDs{'interrupted'} = scalar @interrupted;
|
||||
my @read_failure = grep( /read failure/, @outputA );
|
||||
$IDs{'read_failure'} = scalar @read_failure;
|
||||
my @read_failure2 = grep( /Failed in segment/, @outputA );
|
||||
$IDs{'read_failure'} = $IDs{'read_failure'} + scalar @read_failure2;
|
||||
my @unknown_failure = grep( /unknown failure/, @outputA );
|
||||
$IDs{'unknown_failure'} = scalar @unknown_failure;
|
||||
my @extended = grep( /\d.*\ ([Ee]xtended|[Ll]ong).*(?![Dd]uration)/, @outputA );
|
||||
$IDs{'extended'} = scalar @extended;
|
||||
my @short = grep( /[Ss]hort/, @outputA );
|
||||
$IDs{'short'} = scalar @short;
|
||||
my @conveyance = grep( /[Cc]onveyance/, @outputA );
|
||||
$IDs{'conveyance'} = scalar @conveyance;
|
||||
my @selective = grep( /[Ss]elective/, @outputA );
|
||||
$IDs{'selective'} = scalar @selective;
|
||||
my @offline = grep( /(\d|[Bb]ackground|[Ff]oreground)+\ +[Oo]ffline/, @outputA );
|
||||
$IDs{'offline'} = scalar @offline;
|
||||
|
||||
# if we have logs, actually grab the log output
|
||||
if ( $IDs{'completed'} > 0
|
||||
|| $IDs{'interrupted'} > 0
|
||||
|| $IDs{'read_failure'} > 0
|
||||
|| $IDs{'extended'} > 0
|
||||
|| $IDs{'short'} > 0
|
||||
|| $IDs{'conveyance'} > 0
|
||||
|| $IDs{'selective'} > 0
|
||||
|| $IDs{'offline'} > 0 )
|
||||
{
|
||||
my @headers = grep( /(Num\ +Test.*LBA| Description .*[Hh]ours)/, @outputA );
|
||||
|
||||
my @log_lines;
|
||||
push( @log_lines, @extended, @short, @conveyance, @selective, @offline );
|
||||
$IDs{'selftest_log'} = join( "\n", @headers, sort(@log_lines) );
|
||||
} ## end if ( $IDs{'completed'} > 0 || $IDs{'interrupted'...})
|
||||
|
||||
# get the drive serial number, if needed
|
||||
$disk_id = $name;
|
||||
$output = `$smartctl -i $disk`;
|
||||
# generally upper case, HP branded drives seem to report with lower case n
|
||||
while ( $output =~ /(?i)Serial Number:(.*)/g ) {
|
||||
$IDs{'serial'} = $1;
|
||||
$IDs{'serial'} =~ s/^\s+|\s+$//g;
|
||||
}
|
||||
if ($useSN) {
|
||||
$disk_id = $IDs{'serial'};
|
||||
}
|
||||
|
||||
while ( $output =~ /(?i)Model Family:(.*)/g ) {
|
||||
$IDs{'model_family'} = $1;
|
||||
$IDs{'model_family'} =~ s/^\s+|\s+$//g;
|
||||
}
|
||||
|
||||
while ( $output =~ /(?i)Device Model:(.*)/g ) {
|
||||
$IDs{'device_model'} = $1;
|
||||
$IDs{'device_model'} =~ s/^\s+|\s+$//g;
|
||||
}
|
||||
|
||||
while ( $output =~ /(?i)Model Number:(.*)/g ) {
|
||||
$IDs{'model_number'} = $1;
|
||||
$IDs{'model_number'} =~ s/^\s+|\s+$//g;
|
||||
}
|
||||
|
||||
while ( $output =~ /(?i)Firmware Version:(.*)/g ) {
|
||||
$IDs{'fw_version'} = $1;
|
||||
$IDs{'fw_version'} =~ s/^\s+|\s+$//g;
|
||||
}
|
||||
|
||||
# mainly HP drives
|
||||
while ( $output =~ /(?i)Vendor:(.*)/g ) {
|
||||
$IDs{'vendor'} = $1;
|
||||
$IDs{'vendor'} =~ s/^\s+|\s+$//g;
|
||||
}
|
||||
|
||||
# mainly HP drives
|
||||
while ( $output =~ /(?i)Product:(.*)/g ) {
|
||||
$IDs{'product'} = $1;
|
||||
$IDs{'product'} =~ s/^\s+|\s+$//g;
|
||||
}
|
||||
|
||||
# mainly HP drives
|
||||
while ( $output =~ /(?i)Revision:(.*)/g ) {
|
||||
$IDs{'revision'} = $1;
|
||||
$IDs{'revision'} =~ s/^\s+|\s+$//g;
|
||||
}
|
||||
|
||||
# figure out what to use for the max temp, if there is one
|
||||
if ( $IDs{'190'} =~ /^\d+$/ ) {
|
||||
$IDs{max_temp} = $IDs{'190'};
|
||||
} elsif ( $IDs{'194'} =~ /^\d+$/ ) {
|
||||
$IDs{max_temp} = $IDs{'194'};
|
||||
}
|
||||
if ( $IDs{'194'} =~ /^\d+$/ && defined( $IDs{max_temp} ) && $IDs{'194'} > $IDs{max_temp} ) {
|
||||
$IDs{max_temp} = $IDs{'194'};
|
||||
}
|
||||
|
||||
$output = `$smartctl -H $disk`;
|
||||
if ( $output =~ /SMART\ overall\-health\ self\-assessment\ test\ result\:\ PASSED/ ) {
|
||||
$IDs{'health_pass'} = 1;
|
||||
} elsif ( $output =~ /SMART\ Health\ Status\:\ OK/ ) {
|
||||
$IDs{'health_pass'} = 1;
|
||||
}
|
||||
|
||||
if ( !$IDs{'health_pass'} ) {
|
||||
$to_return->{data}{unhealthy}++;
|
||||
}
|
||||
} ## end else [ if ( $IDs{exit} != 0 ) ]
|
||||
|
||||
# only bother to save this if useSN is not being used
|
||||
if ( !$useSN ) {
|
||||
$to_return->{data}{disks}{$disk_id} = \%IDs;
|
||||
} elsif ( $IDs{exit} == 0 && defined($disk_id) ) {
|
||||
$to_return->{data}{disks}{$disk_id} = \%IDs;
|
||||
}
|
||||
|
||||
# smartctl will in some cases exit zero when it can't pull data for cciss
|
||||
# so if we get a zero exit, but no serial then it means something errored
|
||||
# and the device is likely dead
|
||||
if ( $IDs{exit} == 0 && !defined( $IDs{serial} ) ) {
|
||||
$to_return->{data}{unhealthy}++;
|
||||
}
|
||||
} ## end foreach my $line (@disks)
|
||||
|
||||
my $toReturn = $json->encode($to_return);
|
||||
|
||||
if ( !$opts{p} ) {
|
||||
$toReturn = $toReturn . "\n";
|
||||
}
|
||||
|
||||
if ( $opts{Z} ) {
|
||||
my $toReturnCompressed;
|
||||
gzip \$toReturn => \$toReturnCompressed;
|
||||
my $compressed = encode_base64($toReturnCompressed);
|
||||
$compressed =~ s/\n//g;
|
||||
$compressed = $compressed . "\n";
|
||||
if ( length($compressed) < length($toReturn) ) {
|
||||
$toReturn = $compressed;
|
||||
}
|
||||
} ## end if ( $opts{Z} )
|
||||
|
||||
if ( !$noWrite ) {
|
||||
open( my $writefh, ">", $cache ) or die "Can't open '" . $cache . "'";
|
||||
print $writefh $toReturn;
|
||||
close($writefh);
|
||||
} else {
|
||||
print $toReturn;
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
smartctl=/usr/sbin/smartctl
|
||||
cache=/var/cache/smart
|
||||
sda
|
||||
File diff suppressed because one or more lines are too long
@@ -1,45 +0,0 @@
|
||||
#!/bin/sh
|
||||
################################################################
|
||||
# Instructions: #
|
||||
# 1. copy this script to /etc/snmp/ and make it executable: #
|
||||
# chmod +x ups-nut.sh #
|
||||
# 2. make sure UPS_NAME below matches the name of your UPS #
|
||||
# 3. edit your snmpd.conf to include this line: #
|
||||
# extend ups-nut /etc/snmp/ups-nut.sh #
|
||||
# 4. restart snmpd on the host #
|
||||
# 5. activate the app for the desired host in LibreNMS #
|
||||
################################################################
|
||||
UPS_NAME="${1:-APCUPS}"
|
||||
|
||||
PATH=$PATH:/usr/bin:/bin
|
||||
TMP=$(upsc $UPS_NAME 2>/dev/null)
|
||||
|
||||
for value in "battery\.charge: [0-9.]+" "battery\.(runtime\.)?low: [0-9]+" "battery\.runtime: [0-9]+" "battery\.voltage: [0-9.]+" "battery\.voltage\.nominal: [0-9]+" "input\.voltage\.nominal: [0-9.]+" "input\.voltage: [0-9.]+" "ups\.load: [0-9.]+"
|
||||
do
|
||||
OUT=$(echo "$TMP" | grep -Eo "$value" | awk '{print $2}' | LANG=C sort | head -n 1)
|
||||
if [ -n "$OUT" ]; then
|
||||
echo "$OUT"
|
||||
else
|
||||
echo "Unknown"
|
||||
fi
|
||||
done
|
||||
|
||||
for value in "ups\.status:[A-Z ]{0,}OL" "ups\.status:[A-Z ]{0,}OB" "ups\.status:[A-Z ]{0,}LB" "ups\.status:[A-Z ]{0,}HB" "ups\.status:[A-Z ]{0,}RB" "ups\.status:[A-Z ]{0,}CHRG" "ups\.status:[A-Z ]{0,}DISCHRG" "ups\.status:[A-Z ]{0,}BYPASS" "ups\.status:[A-Z ]{0,}CAL" "ups\.status:[A-Z ]{0,}OFF" "ups\.status:[A-Z ]{0,}OVER" "ups\.status:[A-Z ]{0,}TRIM" "ups\.status:[A-Z ]{0,}BOOST" "ups\.status:[A-Z ]{0,}FSD" "ups\.alarm:[A-Z ]"
|
||||
do
|
||||
UNKNOWN=$(echo "$TMP" | grep -Eo "ups\.status:")
|
||||
if [ -z "$UNKNOWN" ]; then
|
||||
echo "Unknown"
|
||||
else
|
||||
OUT=$(echo "$TMP" | grep -Eo "$value")
|
||||
if [ -n "$OUT" ]; then
|
||||
echo "1"
|
||||
else
|
||||
echo "0"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
UPSTEMP="ups\.temperature: [0-9.]+"
|
||||
OUT=$(echo "$TMP" | grep -Eo "$UPSTEMP" | awk '{print $2}' | LANG=C sort | head -n 1)
|
||||
[ -n "$OUT" ] && echo "$OUT" || echo "Unknown"
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
# PFV NFS tuning sysctl overrides
|
||||
#
|
||||
# Applied AFTER tuned via pfv-nfs-tuning.service (systemd oneshot).
|
||||
# These override tuned's network-throughput/virtual-host 16MB TCP buffer
|
||||
# caps with 128MB for high-BDP NFS over 1-4 GbE LACP links.
|
||||
#
|
||||
# Install on ALL Proxmox hosts:
|
||||
# cp 99-pfv-nfs.conf /etc/sysctl.d/99-pfv-nfs.conf
|
||||
# cp pfv-nfs-tuning.service /etc/systemd/system/pfv-nfs-tuning.service
|
||||
# systemctl daemon-reload && systemctl enable --now pfv-nfs-tuning.service
|
||||
#
|
||||
# Created: 2026-07-31
|
||||
# Deployed: tsys1, tsys3, tsys4, tsys5, tsys6, tsys7, tsys9
|
||||
|
||||
net.core.rmem_max = 134217728
|
||||
net.core.wmem_max = 134217728
|
||||
net.core.rmem_default = 26214400
|
||||
net.core.wmem_default = 26214400
|
||||
net.core.netdev_max_backlog = 250000
|
||||
net.core.somaxconn = 65535
|
||||
net.ipv4.tcp_rmem = 4096 87380 134217728
|
||||
net.ipv4.tcp_wmem = 4096 65536 134217728
|
||||
net.ipv4.tcp_max_syn_backlog = 4096
|
||||
@@ -1,46 +0,0 @@
|
||||
#
|
||||
# Known Element Enterprises Customized Config File
|
||||
# auditd
|
||||
# Initial version 2025-06-27
|
||||
#
|
||||
|
||||
local_events = yes
|
||||
write_logs = yes
|
||||
log_file = /var/log/audit/audit.log
|
||||
log_group = adm
|
||||
log_format = ENRICHED
|
||||
flush = INCREMENTAL_ASYNC
|
||||
freq = 50
|
||||
max_log_file = 8
|
||||
num_logs = 5
|
||||
priority_boost = 4
|
||||
name_format = NONE
|
||||
max_log_file_action = keep_logs
|
||||
space_left = 75
|
||||
space_left_action = email
|
||||
action_mail_acct = root
|
||||
|
||||
admin_space_left_action = halt
|
||||
disk_full_action = SUSPEND
|
||||
disk_error_action = SUSPEND
|
||||
admin_space_left = 50
|
||||
|
||||
verify_email = yes
|
||||
use_libwrap = yes
|
||||
tcp_listen_queue = 5
|
||||
tcp_max_per_addr = 1
|
||||
tcp_client_max_idle = 0
|
||||
transport = TCP
|
||||
distribute_network = no
|
||||
q_depth = 2000
|
||||
overflow_action = SYSLOG
|
||||
max_restarts = 10
|
||||
plugin_dir = /etc/audit/plugins.d
|
||||
end_of_event_timeout = 2
|
||||
##tcp_client_ports = 1024-65535
|
||||
##tcp_listen_port = 60
|
||||
|
||||
##krb5_key_file = /etc/audit/audit.key
|
||||
krb5_principal = auditd
|
||||
|
||||
##name = mydomain
|
||||
@@ -1,5 +0,0 @@
|
||||
This system is the property of Known Element Enterprises LLC.
|
||||
|
||||
Authorized uses only. All activity may be monitored and reported.
|
||||
|
||||
All activities subject to monitoring/recording/review in real time and/or at a later time.
|
||||
@@ -1,5 +0,0 @@
|
||||
This system is the property of Known Element Enterprises LLC.
|
||||
|
||||
Authorized uses only. All activity may be monitored and reported.
|
||||
|
||||
All activities subject to monitoring/recording/review in real time and/or at a later time.
|
||||
@@ -1,5 +0,0 @@
|
||||
This system is the property of Known Element Enterprises LLC.
|
||||
|
||||
Authorized uses only. All activity may be monitored and reported.
|
||||
|
||||
All activities subject to monitoring/recording/review in real time and/or at a later time.
|
||||
@@ -1,2 +0,0 @@
|
||||
#/etc/cockpit/disallowed-users
|
||||
# List of users which are not allowed to login to Cockpit
|
||||
@@ -1,14 +0,0 @@
|
||||
option rfc3442-classless-static-routes code 121 = array of unsigned integer 8;
|
||||
|
||||
send host-name = gethostname();
|
||||
request subnet-mask, broadcast-address, time-offset, routers,
|
||||
domain-name, host-name,
|
||||
domain-name-servers, domain-search, ntp-servers,
|
||||
rfc3442-classless-static-routes;
|
||||
|
||||
# Pin DNS and NTP to the redundant pfv-netinfra-01/02 pair regardless of what
|
||||
# the DHCP server advertises, so every host on this build uses the same
|
||||
# authoritative recursive resolvers and time sources.
|
||||
supersede domain-name-servers 192.168.3.252, 192.168.3.253;
|
||||
supersede domain-search "knel.net";
|
||||
supersede ntp-servers 192.168.3.252, 192.168.3.253;
|
||||
@@ -1,23 +0,0 @@
|
||||
# see "man logrotate" for details
|
||||
|
||||
# global options do not affect preceding include directives
|
||||
|
||||
# rotate log files weekly
|
||||
weekly
|
||||
|
||||
# keep 4 weeks worth of backlogs
|
||||
rotate 4
|
||||
|
||||
# create new (empty) log files after rotating old ones
|
||||
create 0640 root utmp
|
||||
|
||||
# use date as a suffix of the rotated file
|
||||
#dateext
|
||||
|
||||
# uncomment this if you want your log files compressed
|
||||
#compress
|
||||
|
||||
# packages drop log rotation information into this directory
|
||||
include /etc/logrotate.d
|
||||
|
||||
# system-specific logs may also be configured here.
|
||||
@@ -1 +0,0 @@
|
||||
install cramfs /bin/true
|
||||
@@ -1 +0,0 @@
|
||||
install dccp /bin/true
|
||||
@@ -1 +0,0 @@
|
||||
install freevxfs /bin/true
|
||||
@@ -1 +0,0 @@
|
||||
install hfs /bin/true
|
||||
@@ -1 +0,0 @@
|
||||
install hfsplus /bin/true
|
||||
@@ -1 +0,0 @@
|
||||
install jffs2 /bin/true
|
||||
@@ -1 +0,0 @@
|
||||
install rds /bin/true
|
||||
@@ -1 +0,0 @@
|
||||
install sctp /bin/true
|
||||
@@ -1 +0,0 @@
|
||||
install squashfs /bin/true
|
||||
@@ -1 +0,0 @@
|
||||
install tipc /bin/true
|
||||
@@ -1 +0,0 @@
|
||||
install udf /bin/true
|
||||
@@ -1 +0,0 @@
|
||||
install usb-storage /bin/true
|
||||
@@ -1,21 +0,0 @@
|
||||
driftfile /var/lib/ntp/ntp.drift
|
||||
leapfile /usr/share/zoneinfo/leap-seconds.list
|
||||
|
||||
# Redundant upstream time sources: pfv-netinfra-01/02 (Technitium/Pi-hole hosts
|
||||
# also serving NTP). IPs are used (not hostnames) because the knel.net name for
|
||||
# these hosts resolves to a Tailscale CGNAT address, not the LAN address, and
|
||||
# because NTP must come up before DNS is available. iburst speeds initial sync.
|
||||
server 192.168.3.252 iburst
|
||||
server 192.168.3.253 iburst
|
||||
|
||||
# Hardened client: sync from the configured servers but never serve time to
|
||||
# anyone else. Note: `interface listen 127.0.0.1` must NOT be used here — it
|
||||
# binds ntpd to loopback, making outbound queries carry a 127.0.0.1 source
|
||||
# address that upstream servers cannot reply to (symptoms: peers stuck in
|
||||
# .INIT. with reach 0). Use restrict rules to control access instead.
|
||||
restrict default ignore
|
||||
restrict 127.0.0.1
|
||||
restrict ::1
|
||||
restrict 192.168.3.252 nomodify notrap nopeer
|
||||
restrict 192.168.3.253 nomodify notrap nopeer
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
# Uncomment to start SNMP subagent and enable CDP, SONMP and EDP protocol
|
||||
DAEMON_ARGS="-x -c -s -e"
|
||||
@@ -1,11 +0,0 @@
|
||||
# Managed by KNELServerBuild — do not edit; changes will be overwritten.
|
||||
#
|
||||
# Redundant recursive DNS via pfv-netinfra-01/02 (Technitium + Pi-hole).
|
||||
# IPs are used (required: nameserver directives must be addresses, and the
|
||||
# knel.net name for these hosts resolves to a Tailscale CGNAT address rather
|
||||
# than the LAN address). If the primary is unreachable, glibc's resolver
|
||||
# automatically falls through to the secondary.
|
||||
domain knel.net
|
||||
search knel.net
|
||||
nameserver 192.168.3.252
|
||||
nameserver 192.168.3.253
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user