chore: enforce shellcheck across the repo

Establish shellcheck as a mandatory pre-commit quality gate and bring all 93
shell scripts to a clean state.

- tests/shellcheck.sh: wrapper that runs koalaman/shellcheck:stable via Docker
  (no native binary needed), skips vendored + upstream librenms-agent scripts.
- .shellcheckrc: documents intentional codebase-wide disables (dynamic source
  paths SC1090/SC1091, client-side ssh expansion SC2029).
- AGENTS.md: new Git Policy rule mandating clean shellcheck for every shell
  script before commit.

Fixes applied (real bugs + quality): missing quote in netinfra/gather-configs.sh
(caused cascading parse errors), unquoted expansions, declare-and-assign masking,
egrep -> grep -E, $FUNCNAME array indexing, unused variable removal, cd || exit.
Intentional patterns (sourced config, sysfs/ps diagnostics, ssh heredocs that
expand local config) get justified targeted disables.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
This commit is contained in:
2026-07-30 08:56:31 -05:00
parent 54e9927167
commit 0fa0692c37
37 changed files with 226 additions and 89 deletions
+17
View File
@@ -0,0 +1,17 @@
# ShellCheck configuration for PFVCluster
# (used when running `shellcheck` directly; tests/shellcheck.sh applies the
# same disables via -e for consistent results under Docker)
#
# These checks are DISABLED because they flag intentional conventions of this
# codebase, not bugs:
#
# SC1090 / SC1091 — cannot follow dynamically-computed `source` paths. The KNEL
# framework (vendor/) and test harness source helpers via computed include
# dirs, which shellcheck cannot resolve statically.
# SC2029 — ssh orchestration (tests/remote.sh and perf/k8s/dns scripts)
# deliberately builds and expands the remote command on the CLIENT side before
# sending it. That is the whole point of the single-chokepoint remote pattern.
disable=SC1090,SC1091,SC2029
# Treat external-sourced files as bash (matches #!/usr/bin/env bash framework).
external-sources=true
+11
View File
@@ -47,6 +47,17 @@ vendor/ Vendored KNELShellFramework
2. **Atomic commits.** Each commit coherent on its own. 2. **Atomic commits.** Each commit coherent on its own.
3. **Conventional format**: `feat(scope): desc`, `fix(scope): desc`, 3. **Conventional format**: `feat(scope): desc`, `fix(scope): desc`,
`docs: desc`, `refactor(scope): desc`, `test(scope): desc`. `docs: desc`, `refactor(scope): desc`, `test(scope): desc`.
4. **All shell scripts MUST pass `shellcheck` before commit.** No exceptions.
Run it via the wrapper:
```bash
bash tests/shellcheck.sh # whole repo
bash tests/shellcheck.sh ups/*.sh # specific files
```
This invokes `koalaman/shellcheck:stable` through Docker (no native binary
needed). Fix every reported finding — including `info`-level — or add a
targeted `# shellcheck disable=SCxxxx # <reason>` directive with a
justification. A script that emits any diagnostic is a protocol violation.
Non-bash scripts (PHP with `.sh` shebang `#!/usr/bin/php`, etc.) are exempt.
## Automatic Gardening Protocol ## Automatic Gardening Protocol
+1
View File
@@ -1,4 +1,5 @@
#!/usr/bin/bash #!/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 # console/discover.sh — READ-ONLY discovery of console setup on pfv-tsys4
# #
+7 -7
View File
@@ -33,8 +33,6 @@ CONMAN_LOGDIR="${CONMAN_LOGDIR:-/var/log/conman}"
UDEV_RULES="/etc/udev/rules.d/99-console-ports.rules" UDEV_RULES="/etc/udev/rules.d/99-console-ports.rules"
SER2NET_CONF="/etc/ser2net.yaml" SER2NET_CONF="/etc/ser2net.yaml"
CONMAN_CONF="/etc/conman.conf" CONMAN_CONF="/etc/conman.conf"
CONMAN_CONSOLES="/etc/conman/console-consoles.conf"
CONSOLE_DEV_DIR="/dev/console"
echo "============================================" echo "============================================"
echo " Console Config Generator" echo " Console Config Generator"
@@ -120,9 +118,11 @@ for entry in "${ENTRIES[@]}"; do
# Build the full ID_PATH match. The mapping stores a substring like "usb-0:1.5.4.4" # 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" # 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. # We match on the substring to be portable across PCI bus changes.
echo "" >> "$UDEV_RULES" {
echo "# $name (TCP $tcp_port): $comment" >> "$UDEV_RULES" echo ""
echo "SUBSYSTEM==\"tty\", ENV{ID_PATH}==\"*$id_path*\", SYMLINK+=\"console/$name\"" >> "$UDEV_RULES" echo "# $name (TCP $tcp_port): $comment"
echo "SUBSYSTEM==\"tty\", ENV{ID_PATH}==\"*$id_path*\", SYMLINK+=\"console/$name\""
} >> "$UDEV_RULES"
done done
echo " Written: $UDEV_RULES" echo " Written: $UDEV_RULES"
@@ -148,13 +148,13 @@ fi
echo "#" echo "#"
echo "# All ports use telnet(rfc2217) accepter so conman and telnet clients" echo "# All ports use telnet(rfc2217) accepter so conman and telnet clients"
echo "# negotiate proper telnet binary mode — this prevents CR stripping" echo "# negotiate proper telnet binary mode — this prevents CR stripping"
echo "# and stair-stepping on devices that send \\n\\r (LF+CR) line endings." 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 "# Ports bound to Tailscale IP ($TS_IP) for secure remote access."
echo "#" echo "#"
echo "# Direct telnet: telnet $TS_IP 2001" echo "# Direct telnet: telnet $TS_IP 2001"
echo "# Via conman: conman -f <name>" echo "# Via conman: conman -f <name>"
echo "" echo ""
echo "define: &banner \\r\\nPFV console port \\p device \\d [\\B]\\r\\n\\r\\n" printf '%s\n' "define: &banner \\r\\nPFV console port \\p device \\d [\\B]\\r\\n\\r\\n"
echo "" echo ""
for entry in "${ENTRIES[@]}"; do for entry in "${ENTRIES[@]}"; do
+3 -6
View File
@@ -1,4 +1,5 @@
#!/usr/bin/bash #!/usr/bin/bash
# shellcheck disable=SC2010 # diagnostic; ls|grep on /dev listing is intentional
# #
# console/setup.sh — deploy console management on pfv-tsys4 # console/setup.sh — deploy console management on pfv-tsys4
# #
@@ -19,10 +20,6 @@
set -euo pipefail set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
UDEV_RULES="/etc/udev/rules.d/99-console-ports.rules"
SER2NET_CONF="/etc/ser2net.yaml"
CONMAN_CONSOLES="/etc/conman/console-consoles.conf"
CONSOLE_DEV_DIR="/dev/consoles"
echo "============================================" echo "============================================"
echo " Console Management Setup" echo " Console Management Setup"
@@ -100,7 +97,7 @@ if [ ! -d /dev/consoles ] || [ -z "$(ls /dev/consoles/ 2>/dev/null)" ]; then
line="${line%%#*}" line="${line%%#*}"
line="$(echo "$line" | xargs)" line="$(echo "$line" | xargs)"
[ -z "$line" ] && continue [ -z "$line" ] && continue
IFS='|' read -r tcp_port name id_path baud comment <<< "$line" IFS='|' read -r _ name id_path _ _ <<< "$line"
# Find the ttyUSB whose ID_PATH contains the mapping's id_path substring # Find the ttyUSB whose ID_PATH contains the mapping's id_path substring
for tty in /dev/ttyUSB*; do for tty in /dev/ttyUSB*; do
[ -e "$tty" ] || continue [ -e "$tty" ] || continue
@@ -124,7 +121,7 @@ while IFS= read -r line; do
line="${line%%#*}" line="${line%%#*}"
line="$(echo "$line" | xargs)" line="$(echo "$line" | xargs)"
[ -z "$line" ] && continue [ -z "$line" ] && continue
IFS='|' read -r tcp_port name id_path baud comment <<< "$line" IFS='|' read -r _ name id_path _ _ <<< "$line"
if [ -e "/dev/consoles/$name" ]; then if [ -e "/dev/consoles/$name" ]; then
TARGET=$(readlink -f "/dev/consoles/$name") TARGET=$(readlink -f "/dev/consoles/$name")
echo " [OK] /dev/consoles/$name -> $TARGET" echo " [OK] /dev/consoles/$name -> $TARGET"
+1
View File
@@ -1,4 +1,5 @@
#!/usr/bin/bash #!/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 # console/validate-conman.sh — verify conman can actually reach devices via
# ser2net TCP ports and is capturing log output to files. # ser2net TCP ports and is capturing log output to files.
+1 -1
View File
@@ -35,7 +35,7 @@ mkdir -p "$ZONE_DIR"
log "Syncing zones from $PRIMARY_HOST..." log "Syncing zones from $PRIMARY_HOST..."
if rsync -az --delete --temp-dir=/tmp \ if rsync -az --delete --temp-dir=/tmp \
"${PRIMARY_HOST}:$ZONE_DIR/" "$ZONE_DIR/" >> "$LOG_FILE" 2>&1; then "${PRIMARY_HOST}:$ZONE_DIR/" "$ZONE_DIR/" >> "$LOG_FILE" 2>&1; then
zone_count=$(ls "$ZONE_DIR" | wc -l) zone_count=$(find "$ZONE_DIR" -maxdepth 1 -type f | wc -l)
log "Sync complete: $zone_count zones" log "Sync complete: $zone_count zones"
else else
log "ERROR: rsync failed (rc=$?)" log "ERROR: rsync failed (rc=$?)"
+3 -3
View File
@@ -69,9 +69,9 @@ echo " Production zones: $prod_zones"
echo " Primary (01) zones: $pri_zones" echo " Primary (01) zones: $pri_zones"
echo " Secondary (02) zones: $sec_zones" echo " Secondary (02) zones: $sec_zones"
[ "$prod_zones" -gt 0 ] 2>/dev/null && ok "Production has $prod_zones zones" || fail "Production zone count invalid" if [ "$prod_zones" -gt 0 ] 2>/dev/null; then ok "Production has $prod_zones zones"; else fail "Production zone count invalid"; fi
[ "$pri_zones" -gt 0 ] 2>/dev/null && ok "Primary has $pri_zones zones" || fail "Primary zone count invalid" if [ "$pri_zones" -gt 0 ] 2>/dev/null; then ok "Primary has $pri_zones zones"; else fail "Primary zone count invalid"; fi
[ "$sec_zones" -gt 0 ] 2>/dev/null && ok "Secondary has $sec_zones zones" || fail "Secondary zone count invalid" 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 if [ "$pri_zones" = "$prod_zones" ]; then
ok "Primary zone count matches production ($pri_zones)" ok "Primary zone count matches production ($pri_zones)"
+1
View File
@@ -1,4 +1,5 @@
#!/usr/bin/bash #!/usr/bin/bash
# shellcheck disable=SC2034 # sourced config file; variables are consumed by scripts that source this
# k8s/env.sh — shared config for all k8s scripts. Source this. # k8s/env.sh — shared config for all k8s scripts. Source this.
# #
# All cluster communication goes over Tailscale IPs. No LAN IPs, ever. # All cluster communication goes over Tailscale IPs. No LAN IPs, ever.
+2
View File
@@ -35,6 +35,7 @@ echo "============================================"
echo "" echo ""
echo "--- [1/5] Installing bootstrap node: $CNODE1_NAME ($CNODE1_IP) ---" echo "--- [1/5] Installing bootstrap node: $CNODE1_NAME ($CNODE1_IP) ---"
# shellcheck disable=SC2087 # heredoc intentionally expands local config (node IPs, k3s version) before sending to remote
ssh "${SSH_OPTS[@]}" "${SSH_USER}@${CNODE1_IP}" "sudo -n bash -s" <<REMOTE_BOOT ssh "${SSH_OPTS[@]}" "${SSH_USER}@${CNODE1_IP}" "sudo -n bash -s" <<REMOTE_BOOT
set -euo pipefail set -euo pipefail
export INSTALL_K3S_VERSION="$K3S_VERSION" export INSTALL_K3S_VERSION="$K3S_VERSION"
@@ -103,6 +104,7 @@ for node_ip in "$CNODE2_IP" "$CNODE3_IP"; do
echo "" echo ""
echo "--- [4/5] Joining server: $node_name ($node_ip) ---" echo "--- [4/5] Joining server: $node_name ($node_ip) ---"
# shellcheck disable=SC2087 # heredoc intentionally expands local config before sending to remote
ssh "${SSH_OPTS[@]}" "${SSH_USER}@${node_ip}" "sudo -n bash -s" <<REMOTE_JOIN ssh "${SSH_OPTS[@]}" "${SSH_USER}@${node_ip}" "sudo -n bash -s" <<REMOTE_JOIN
set -euo pipefail set -euo pipefail
export INSTALL_K3S_VERSION="$K3S_VERSION" export INSTALL_K3S_VERSION="$K3S_VERSION"
+1
View File
@@ -71,6 +71,7 @@ for node_ip in "$CNODE2_IP" "$CNODE3_IP"; do
echo "" echo ""
echo "--- [3/4] Joining server: $node_name ($node_ip) ---" echo "--- [3/4] Joining server: $node_name ($node_ip) ---"
# shellcheck disable=SC2087 # heredoc intentionally expands local config before sending to remote
ssh "${SSH_OPTS[@]}" "${SSH_USER}@${node_ip}" "sudo -n bash -s" <<REMOTE_JOIN ssh "${SSH_OPTS[@]}" "${SSH_USER}@${node_ip}" "sudo -n bash -s" <<REMOTE_JOIN
set -euo pipefail set -euo pipefail
export INSTALL_K3S_VERSION="$K3S_VERSION" export INSTALL_K3S_VERSION="$K3S_VERSION"
+1 -1
View File
@@ -99,7 +99,7 @@ echo "--- Workload isolation ---"
USER_PODS=$(kubectl get pods -A --field-selector spec.nodeName="${CNODE1_NAME}" -o jsonpath='{.items[*].metadata.name}' 2>/dev/null | wc -w) USER_PODS=$(kubectl get pods -A --field-selector spec.nodeName="${CNODE1_NAME}" -o jsonpath='{.items[*].metadata.name}' 2>/dev/null | wc -w)
# Subtract system pods # Subtract system pods
SYSTEM_PODS=$(kubectl get pods -A --field-selector spec.nodeName="${CNODE1_NAME}" -l k8s-app --no-headers 2>/dev/null | wc -l) SYSTEM_PODS=$(kubectl get pods -A --field-selector spec.nodeName="${CNODE1_NAME}" -l k8s-app --no-headers 2>/dev/null | wc -l)
if [ "$USER_PODS" -le 10 ]; then ok "Only system pods on cnodes (expected)"; else fail "Unexpected pods on $CNODE1_NAME"; fi if [ "$((USER_PODS - SYSTEM_PODS))" -le 0 ]; then ok "Only system pods on cnodes (expected)"; else fail "Unexpected pods on $CNODE1_NAME"; fi
echo "" echo ""
echo "============================================" echo "============================================"
+1
View File
@@ -1,4 +1,5 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# shellcheck disable=SC2012 # diagnostic baseline script; ls -la listings are intentional
# baseline.sh — quick read-only baseline of a target node. # baseline.sh — quick read-only baseline of a target node.
set -u set -u
hdr() { printf '\n=== %s ===\n' "$1"; } hdr() { printf '\n=== %s ===\n' "$1"; }
+2
View File
@@ -37,9 +37,11 @@ for f in setupVars.conf pihole-FTL.conf adlists.list custom.list local.list rege
echo "--- /etc/pihole/$f ---"; $DG exec pihole cat "/etc/pihole/$f" 2>&1 || true echo "--- /etc/pihole/$f ---"; $DG exec pihole cat "/etc/pihole/$f" 2>&1 || true
done done
echo "-- /etc/dnsmasq.d/* --" echo "-- /etc/dnsmasq.d/* --"
# shellcheck disable=SC2016 # $f/$t expand inside the container's sh, not locally — single quotes are intentional
$DG exec pihole sh -c 'for f in /etc/dnsmasq.d/*; do echo "--- $f ---"; cat "$f"; done' 2>&1 || true $DG exec pihole sh -c 'for f in /etc/dnsmasq.d/*; do echo "--- $f ---"; cat "$f"; done' 2>&1 || true
echo "-- pihole version --"; $DG exec pihole pihole -v 2>&1 || true echo "-- pihole version --"; $DG exec pihole pihole -v 2>&1 || true
echo "-- gravity row counts --" echo "-- gravity row counts --"
# shellcheck disable=SC2016 # $t expands inside the container's sh, not locally
$DG exec pihole sh -c 'for t in adlist domainlist client "group" info; do printf "%s=" "$t"; sqlite3 /etc/pihole/gravity.db "SELECT COUNT(*) FROM $t;" 2>/dev/null; done' 2>&1 || true $DG exec pihole sh -c 'for t in adlist domainlist client "group" info; do printf "%s=" "$t"; sqlite3 /etc/pihole/gravity.db "SELECT COUNT(*) FROM $t;" 2>/dev/null; done' 2>&1 || true
echo "-- adlist addresses --" echo "-- adlist addresses --"
$DG exec pihole sqlite3 /etc/pihole/gravity.db "SELECT address,enabled,comment FROM adlist;" 2>&1 || true $DG exec pihole sqlite3 /etc/pihole/gravity.db "SELECT address,enabled,comment FROM adlist;" 2>&1 || true
+1 -1
View File
@@ -44,7 +44,7 @@ if command -v sqlite3 >/dev/null 2>&1; then
echo "-- adlist count --"; sqlite3 -readonly "$G" "SELECT COUNT(*) FROM adlist;" 2>&1 echo "-- adlist count --"; sqlite3 -readonly "$G" "SELECT COUNT(*) FROM adlist;" 2>&1
echo "-- domainlist count by type --"; sqlite3 -readonly "$G" "SELECT type,COUNT(*) FROM domainlist GROUP BY type;" 2>&1 echo "-- domainlist count by type --"; sqlite3 -readonly "$G" "SELECT type,COUNT(*) FROM domainlist GROUP BY type;" 2>&1
echo "-- domainlist (allow=0/allow_exact, deny=1/deny_exact, etc.) first 80 --"; sqlite3 -readonly "$G" "SELECT type,domain,enabled,comment FROM domainlist LIMIT 80;" 2>&1 echo "-- domainlist (allow=0/allow_exact, deny=1/deny_exact, etc.) first 80 --"; sqlite3 -readonly "$G" "SELECT type,domain,enabled,comment FROM domainlist LIMIT 80;" 2>&1
echo -- client --"; sqlite3 -readonly "$G" "SELECT ip,comment FROM client;" 2>&1 echo "-- client --"; sqlite3 -readonly "$G" "SELECT ip,comment FROM client;" 2>&1
echo "-- group --"; sqlite3 -readonly "$G" "SELECT id,name,enabled,comment FROM 'group';" 2>&1 echo "-- group --"; sqlite3 -readonly "$G" "SELECT id,name,enabled,comment FROM 'group';" 2>&1
echo "-- info --"; sqlite3 -readonly "$G" "SELECT * FROM info;" 2>&1 echo "-- info --"; sqlite3 -readonly "$G" "SELECT * FROM info;" 2>&1
else else
-1
View File
@@ -34,7 +34,6 @@ set -euo pipefail
NETBOOT="localuser@pfv-netboot" NETBOOT="localuser@pfv-netboot"
SVC_ROOT="/home/localuser/services" SVC_ROOT="/home/localuser/services"
PIHOLE_PW='REDACTED_PASSWORD' # replicated verbatim from netboot compose
log() { printf '\n\033[1;36m[%s]\033[0m %s\n' "$(date +%H:%M:%S)" "$*" >&2; } log() { printf '\n\033[1;36m[%s]\033[0m %s\n' "$(date +%H:%M:%S)" "$*" >&2; }
warn() { printf '\n\033[1;33m[WARN %s]\033[0m %s\n' "$(date +%H:%M:%S)" "$*" >&2; } warn() { printf '\n\033[1;33m[WARN %s]\033[0m %s\n' "$(date +%H:%M:%S)" "$*" >&2; }
+3 -1
View File
@@ -41,6 +41,7 @@ trap 'rm -rf "$TMP"' EXIT
# plus all error-like counters. Falls back gracefully if a counter doesn't # plus all error-like counters. Falls back gracefully if a counter doesn't
# exist (different NIC drivers expose different names). # exist (different NIC drivers expose different names).
nic_snapshot() { 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 for s in $(awk '/^Slave Interface:/{print $3}' "/proc/net/bonding/$BOND"); do
[ -n "$s" ] || continue [ -n "$s" ] || continue
echo "[$s]" echo "[$s]"
@@ -144,9 +145,10 @@ echo "Bond: $BOND"
echo "" echo ""
echo "--- bond0 hash + driver ---" echo "--- bond0 hash + driver ---"
grep -E "Bonding Mode|Transmit Hash|Number of ports|Partner Mac" /proc/net/bonding/$BOND 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 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/^ *//') drv=$(ethtool -i "$s" 2>/dev/null | awk -F: '/^driver:/{print $2}' | sed 's/^ *//')
speed=$(cat /sys/class/net/$s/speed 2>/dev/null) speed=$(cat "/sys/class/net/$s/speed" 2>/dev/null)
ring=$(ethtool -g "$s" 2>/dev/null | awk '/RX:/{print $2; exit}') 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" printf " %-8s driver=%-20s speed=%-6s current RX ring=%s\n" "$s" "$drv" "$speed" "$ring"
done done
+1
View File
@@ -68,6 +68,7 @@ echo "[$(ts)] switch=$SWITCH device=$DEVICE baud=$BAUD host=$HOST"
# 1. De-conflict: any local ssh to pfv-tsys4 in flight? # 1. De-conflict: any local ssh to pfv-tsys4 in flight?
echo "[$(ts)] checking for in-flight ssh to pfv-tsys4..." echo "[$(ts)] checking for in-flight ssh to pfv-tsys4..."
# shellcheck disable=SC2009 # intentional: need full ps columns filtered by process args
if ps -eo pid,etime,args | grep -E 'ssh.*pfv-tsys4|scp.*pfv-tsys4' | grep -v grep >/tmp/.swcap.ps 2>&1; then if ps -eo pid,etime,args | grep -E 'ssh.*pfv-tsys4|scp.*pfv-tsys4' | grep -v grep >/tmp/.swcap.ps 2>&1; then
cat /tmp/.swcap.ps cat /tmp/.swcap.ps
echo "[$(ts)] ABORT: another ssh/scp to pfv-tsys4 is running (other agent?)." >&2 echo "[$(ts)] ABORT: another ssh/scp to pfv-tsys4 is running (other agent?)." >&2
+1
View File
@@ -5,6 +5,7 @@ set -u
# De-conflict: any ssh to pfv-tsys4 right now? # De-conflict: any ssh to pfv-tsys4 right now?
echo "===== LOCAL ssh activity =====" echo "===== LOCAL ssh activity ====="
# shellcheck disable=SC2009 # intentional: need full ps columns filtered by process args
ps -eo pid,etime,args | grep -E 'ssh.*pfv-tsys' | grep -v grep || echo "(none to pfv-tsys4)" ps -eo pid,etime,args | grep -E 'ssh.*pfv-tsys' | grep -v grep || echo "(none to pfv-tsys4)"
echo echo
+1
View File
@@ -5,6 +5,7 @@
set -u set -u
echo "===== LOCAL ssh/scp activity (other-agent de-confliction) =====" echo "===== LOCAL ssh/scp activity (other-agent de-confliction) ====="
# shellcheck disable=SC2009 # intentional: need full ps columns (etime,args) filtered by process args
ps -eo pid,ppid,etime,user,args | grep -E 'ssh|scp' | grep -v grep || echo "(none)" ps -eo pid,ppid,etime,user,args | grep -E 'ssh|scp' | grep -v grep || echo "(none)"
echo echo
+3 -3
View File
@@ -1,5 +1,5 @@
#!/bin/bash #!/bin/bash
# shellcheck.sh - permanent wrapper to lint every shell script in this project. # Lint wrapper - permanent wrapper to lint every shell script in this project.
# #
# Uses the koalaman/shellcheck:stable docker image so nothing is installed # Uses the koalaman/shellcheck:stable docker image so nothing is installed
# on the host. Run from anywhere; lints scripts/ and any .sh under switches/. # on the host. Run from anywhere; lints scripts/ and any .sh under switches/.
@@ -45,7 +45,7 @@ echo
# Make paths relative to ROOT so docker volume maps cleanly # Make paths relative to ROOT so docker volume maps cleanly
REL_TARGETS=() REL_TARGETS=()
for t in "${TARGETS[@]}"; do for t in "${TARGETS[@]}"; do
rel="${t#$ROOT/}" rel="${t#"$ROOT"/}"
[ "$rel" = "$t" ] && rel="$t" [ "$rel" = "$t" ] && rel="$t"
REL_TARGETS+=("$rel") REL_TARGETS+=("$rel")
done done
@@ -61,5 +61,5 @@ if [ "$STRICT" -eq 1 ]; then
exit $RC exit $RC
fi fi
# Non-strict: only fail on parse errors / errors, not style notes. # Non-strict: only fail on parse errors / errors, not style notes.
# shellcheck exit code 1 means "findings"; re-run with severity to distinguish. # (a shellcheck exit code of 1 means "findings"; re-run with severity to distinguish.)
exit 0 exit 0
+2 -4
View File
@@ -75,8 +75,7 @@ for HOST in "$@"; do
for vmid in $VM_LIST; do for vmid in $VM_LIST; do
name=$(ssh "${SSH_OPTS[@]}" "root@$HOST" "qm config $vmid 2>/dev/null | awk -F: '/^name:/{gsub(/^ /,\"\");print \$2}'") name=$(ssh "${SSH_OPTS[@]}" "root@$HOST" "qm config $vmid 2>/dev/null | awk -F: '/^name:/{gsub(/^ /,\"\");print \$2}'")
echo -n " [$vmid $name] starting... " echo -n " [$vmid $name] starting... "
start_output=$(ssh "${SSH_OPTS[@]}" "root@$HOST" "qm start $vmid" 2>&1) if start_output=$(ssh "${SSH_OPTS[@]}" "root@$HOST" "qm start $vmid" 2>&1); then
if [ $? -eq 0 ]; then
echo "OK" echo "OK"
else else
echo "FAILED: $start_output" echo "FAILED: $start_output"
@@ -115,8 +114,7 @@ for HOST in "$@"; do
if [ "$agent" = "1" ]; then if [ "$agent" = "1" ]; then
name=$(ssh "${SSH_OPTS[@]}" "root@$HOST" "qm config $vmid 2>/dev/null | awk -F: '/^name:/{gsub(/^ /,\"\");print \$2}'") name=$(ssh "${SSH_OPTS[@]}" "root@$HOST" "qm config $vmid 2>/dev/null | awk -F: '/^name:/{gsub(/^ /,\"\");print \$2}'")
echo -n " [$vmid $name] agent ping... " echo -n " [$vmid $name] agent ping... "
result=$(ssh "${SSH_OPTS[@]}" "root@$HOST" "timeout 10 qm agent $vmid ping 2>&1") if ssh "${SSH_OPTS[@]}" "root@$HOST" "timeout 10 qm agent $vmid ping" >/dev/null 2>&1; then
if [ $? -eq 0 ]; then
echo "OK" echo "OK"
else else
echo "no response (VM may still be booting)" echo "no response (VM may still be booting)"
+1 -1
View File
@@ -82,7 +82,7 @@ udevadm trigger --subsystem-match=tty 2>/dev/null || true
sleep 1 sleep 1
if [ -e "/dev/${PDU_DEV_NAME}" ]; then if [ -e "/dev/${PDU_DEV_NAME}" ]; then
echo " Device symlink active: /dev/${PDU_DEV_NAME} -> $(readlink -f /dev/${PDU_DEV_NAME})" echo " Device symlink active: /dev/${PDU_DEV_NAME} -> $(readlink -f "/dev/${PDU_DEV_NAME}")"
else else
echo " WARNING: /dev/${PDU_DEV_NAME} not found yet. Adapter may be unplugged." echo " WARNING: /dev/${PDU_DEV_NAME} not found yet. Adapter may be unplugged."
echo " Falling back to /dev/ttyUSB* discovery..." echo " Falling back to /dev/ttyUSB* discovery..."
+7 -6
View File
@@ -5,17 +5,18 @@
#magic to detect main int #magic to detect main int
echo "Determining management interface..." echo "Determining management interface..."
#export MAIN_INT=$(brctl show $(netstat -rn|grep 0.0.0.0|head -n1|awk '{print $NF}') | awk '{print $NF}'|tail -1|awk -F '.' '{print $1}') #export MAIN_INT=$(brctl show $(netstat -rn|grep 0.0.0.0|head -n1|awk '{print $NF}') | awk '{print $NF}'|tail -1|awk -F '.' '{print $1}')
export MAIN_INT=$(brctl show|grep vmbr0|awk '{print $NF}'|awk -F '.' '{print $1}') MAIN_INT=$(brctl show|grep vmbr0|awk '{print $NF}'|awk -F '.' '{print $1}')
export MAIN_INT
echo "Management interface is: $MAIN_INT" echo "Management interface is: $MAIN_INT"
#fix the issue #fix the issue
echo "Fixing management interface..." echo "Fixing management interface..."
ethtool -K $MAIN_INT tso off ethtool -K "$MAIN_INT" tso off
ethtool -K $MAIN_INT gro off ethtool -K "$MAIN_INT" gro off
ethtool -K $MAIN_INT gso off ethtool -K "$MAIN_INT" gso off
ethtool -K $MAIN_INT tx off ethtool -K "$MAIN_INT" tx off
ethtool -K $MAIN_INT rx off ethtool -K "$MAIN_INT" rx off
#https://forum.proxmox.com/threads/e1000-driver-hang.58284/ #https://forum.proxmox.com/threads/e1000-driver-hang.58284/
#https://serverfault.com/questions/616485/e1000e-reset-adapter-unexpectedly-detected-hardware-unit-hang #https://serverfault.com/questions/616485/e1000e-reset-adapter-unexpectedly-detected-hardware-unit-hang
@@ -0,0 +1,4 @@
#!/usr/bin/env bash
# auth-cloudron-ldap.sh — placeholder module (Cloudron LDAP auth integration).
# Intentionally empty; populated when the auth stack is deployed.
true
@@ -1,4 +1,5 @@
#!/bin/bash #!/bin/bash
# shellcheck disable=SC2103 # legacy R&D build script; cd/cd- sequence is intentional
#Made from instructions at https://www.tunetheweb.com/performance/http2/ #Made from instructions at https://www.tunetheweb.com/performance/http2/
@@ -24,17 +25,17 @@ CURL_FILE="curl-7.60.0.tar.gz"
#Download and install latest version of openssl #Download and install latest version of openssl
wget $OPENSSL_URL_BASE/$OPENSSL_FILE wget $OPENSSL_URL_BASE/$OPENSSL_FILE
tar xzf $OPENSSL_FILE tar xzf $OPENSSL_FILE
cd openssl-1.1.0h cd openssl-1.1.0h || exit
./config enable-weak-ssl-ciphers shared zlib-dynamic -DOPENSSL_TLS_SECURITY_LEVEL=0 --prefix=/usr/local/custom-ssl/openssl-1.1.0h ; make ; make install ./config enable-weak-ssl-ciphers shared zlib-dynamic -DOPENSSL_TLS_SECURITY_LEVEL=0 --prefix=/usr/local/custom-ssl/openssl-1.1.0h ; make ; make install
ln -s /usr/local/custom-ssl/openssl-1.1.0h /usr/local/openssl ln -s /usr/local/custom-ssl/openssl-1.1.0h /usr/local/openssl
cd - cd - || exit
#Download and install nghttp2 (needed for mod_http2). #Download and install nghttp2 (needed for mod_http2).
wget $NGHTTP_URL_BASE/$NGHTTP_FILE wget $NGHTTP_URL_BASE/$NGHTTP_FILE
tar xzf $NGHTTP_FILE tar xzf $NGHTTP_FILE
cd nghttp2-1.31.0 cd nghttp2-1.31.0 || exit
./configure --prefix=/usr/local/custom-ssl/nghttp ; make ; make install ./configure --prefix=/usr/local/custom-ssl/nghttp ; make ; make install
cd - cd - || exit
#Updated ldconfig so curl build #Updated ldconfig so curl build
@@ -48,34 +49,34 @@ ldconfig
#Download and install curl #Download and install curl
wget $CURL_URL_BASE/$CURL_FILE wget $CURL_URL_BASE/$CURL_FILE
tar xzf curl-7.60.0.tar.gz tar xzf curl-7.60.0.tar.gz
cd curl-7.60.0 cd curl-7.60.0 || exit
./configure --prefix=/usr/local/custom-ssl/curl --with-nghttp2=/usr/local/custom-ssl/nghttp/ --with-ssl=/usr/local/custom-ssl/openssl-1.1.0h/ ; make ; make install ./configure --prefix=/usr/local/custom-ssl/curl --with-nghttp2=/usr/local/custom-ssl/nghttp/ --with-ssl=/usr/local/custom-ssl/openssl-1.1.0h/ ; make ; make install
cd - cd - || exit
#Download and install latest apr #Download and install latest apr
wget $APR_URL_BASE/$APR_FILE wget $APR_URL_BASE/$APR_FILE
tar xzf $APR_FILE tar xzf $APR_FILE
cd apr-1.6.3 cd apr-1.6.3 || exit
./configure --prefix=/usr/local/custom-ssl/apr ; make ; make install ./configure --prefix=/usr/local/custom-ssl/apr ; make ; make install
cd - cd - || exit
#Download and install latest apr-util #Download and install latest apr-util
wget $APR_UTIL_URL_BASE/$APR_UTIL_FILE wget $APR_UTIL_URL_BASE/$APR_UTIL_FILE
tar xzf apr-util-1.6.1.tar.gz tar xzf apr-util-1.6.1.tar.gz
cd apr-util-1.6.1 cd apr-util-1.6.1 || exit
./configure --prefix=/usr/local/custom-ssl/apr-util --with-apr=/usr/local/custom-ssl/apr ; make; make install ./configure --prefix=/usr/local/custom-ssl/apr-util --with-apr=/usr/local/custom-ssl/apr ; make; make install
cd - cd - || exit
#Download and install apache #Download and install apache
wget $APACHE_URL_BASE/$APACHE_FILE wget $APACHE_URL_BASE/$APACHE_FILE
tar xzf httpd-2.4.33.tar.gz tar xzf httpd-2.4.33.tar.gz
cd httpd-2.4.33 cd httpd-2.4.33 || exit
cp -r ../apr-1.6.3 srclib/apr cp -r ../apr-1.6.3 srclib/apr
cp -r ../apr-util-1.6.1 srclib/apr-util cp -r ../apr-util-1.6.1 srclib/apr-util
./configure --prefix=/usr/local/custom-ssl/apache --with-ssl=/usr/local/custom-ssl/openssl-1.1.0h/ --with-pcre=/usr/bin/pcre-config --enable-unique-id --enable-ssl --enable-so --with-included-apr --enable-http2 --with-nghttp2=/usr/local/custom-ssl/nghttp/ ./configure --prefix=/usr/local/custom-ssl/apache --with-ssl=/usr/local/custom-ssl/openssl-1.1.0h/ --with-pcre=/usr/bin/pcre-config --enable-unique-id --enable-ssl --enable-so --with-included-apr --enable-http2 --with-nghttp2=/usr/local/custom-ssl/nghttp/
make make
make install make install
ln -s /usr/local/custom-ssl/apache /usr/local/apache ln -s /usr/local/custom-ssl/apache /usr/local/apache
cd - cd - || exit
@@ -79,7 +79,7 @@ if [ "$SUBODEV_CHECK" = 1 ]; then
fi fi
export DEV_WORKSTATION_CHECK export DEV_WORKSTATION_CHECK
DEV_WORKSTATION_CHECK="$(hostname | egrep -c 'subopi-dev|CharlesDevServer' || true)" DEV_WORKSTATION_CHECK="$(hostname | grep -Ec 'subopi-dev|CharlesDevServer' || true)"
if [ "$DEV_WORKSTATION_CHECK" -eq 0 ]; then if [ "$DEV_WORKSTATION_CHECK" -eq 0 ]; then
@@ -9,7 +9,7 @@ export user_check
user_check="$(echo "$curr_user" | grep -c root)" user_check="$(echo "$curr_user" | grep -c root)"
if [ $user_check -ne 1 ]; then if [ "$user_check" -ne 1 ]; then
print_error "Must run as root." print_error "Must run as root."
error_out error_out
fi fi
+3 -3
View File
@@ -1,7 +1,7 @@
# shellcheck shell=bash disable=SC2148 # sourced function file (no shebang by design)
function pi-detect() function pi-detect()
{ {
print_info Now running "$FUNCNAME".... print_info Now running "${FUNCNAME[0]}"....
if [ -f /sys/firmware/devicetree/base/model ] ; then if [ -f /sys/firmware/devicetree/base/model ] ; then
export IS_RASPI="1" export IS_RASPI="1"
fi fi
@@ -9,5 +9,5 @@ fi
if [ ! -f /sys/firmware/devicetree/base/model ] ; then if [ ! -f /sys/firmware/devicetree/base/model ] ; then
export IS_RASPI="0" export IS_RASPI="0"
fi fi
print_info Completed running "$FUNCNAME" print_info Completed running "${FUNCNAME[0]}"
} }
+26 -26
View File
@@ -60,18 +60,18 @@ LOCALUSER_CHECK="$(getent passwd | grep -c localuser || true)"
####################### #######################
function global-oam() { function global-oam() {
print_info "Now running $FUNCNAME...." print_info "Now running ${FUNCNAME[0]}...."
cat "$SCRIPTS_PATH/up2date.sh" >/usr/local/bin/up2date.sh && chmod +x /usr/local/bin/up2date.sh cat "$SCRIPTS_PATH/up2date.sh" >/usr/local/bin/up2date.sh && chmod +x /usr/local/bin/up2date.sh
bash "$MODULES_PATH/OAM/oam-librenms.sh" bash "$MODULES_PATH/OAM/oam-librenms.sh"
print_info "Completed running $FUNCNAME" print_info "Completed running ${FUNCNAME[0]}"
} }
function global-systemServiceConfigurationFiles() { function global-systemServiceConfigurationFiles() {
print_info "Now running $FUNCNAME...." print_info "Now running ${FUNCNAME[0]}...."
cat "$CONFIGFILES_PATH/ZSH/tsys-zshrc" >/etc/zshrc cat "$CONFIGFILES_PATH/ZSH/tsys-zshrc" >/etc/zshrc
cat "$CONFIGFILES_PATH/SMTP/aliases" >/etc/aliases cat "$CONFIGFILES_PATH/SMTP/aliases" >/etc/aliases
@@ -79,11 +79,11 @@ function global-systemServiceConfigurationFiles() {
newaliases newaliases
print_info "Completed running $FUNCNAME" print_info "Completed running ${FUNCNAME[0]}"
} }
function global-installPackages() { function global-installPackages() {
print_info "Now running $FUNCNAME...." print_info "Now running ${FUNCNAME[0]}...."
# Setup webmin repo, used for RBAC/2fa PAM # Setup webmin repo, used for RBAC/2fa PAM
@@ -195,7 +195,7 @@ function global-installPackages() {
VIRT_TYPE="$(virt-what)" VIRT_TYPE="$(virt-what)"
export IS_VIRT_GUEST export IS_VIRT_GUEST
IS_VIRT_GUEST="$(echo "$VIRT_TYPE" | egrep -c 'hyperv|kvm' || true)" IS_VIRT_GUEST="$(echo "$VIRT_TYPE" | grep -Ec 'hyperv|kvm' || true)"
export IS_KVM_GUEST export IS_KVM_GUEST
IS_KVM_GUEST="$(echo "$VIRT_TYPE" | grep -c 'kvm' || true)" IS_KVM_GUEST="$(echo "$VIRT_TYPE" | grep -c 'kvm' || true)"
@@ -227,12 +227,12 @@ function global-installPackages() {
# vault cli # vault cli
print_info "Completed running $FUNCNAME" print_info "Completed running ${FUNCNAME[0]}"
} }
function global-postPackageConfiguration() { function global-postPackageConfiguration() {
print_info "Now running $FUNCNAME" print_info "Now running ${FUNCNAME[0]}"
systemctl --now enable auditd systemctl --now enable auditd
@@ -253,7 +253,7 @@ function global-postPackageConfiguration() {
#This is under test/dev and may fail #This is under test/dev and may fail
echo "hi from root to root" | mail -s "hi directly to root from $(hostname)" root echo "hi from root to root" | mail -s "hi directly to root from $(hostname)" root
chsh -s $(which zsh) root chsh -s "$(which zsh)" root
if [ "$LOCALUSER_CHECK" -gt 0 ]; then if [ "$LOCALUSER_CHECK" -gt 0 ]; then
chsh -s "$(which zsh)" localuser chsh -s "$(which zsh)" localuser
@@ -312,7 +312,7 @@ function global-postPackageConfiguration() {
fi fi
export NTP_SERVER_CHECK export NTP_SERVER_CHECK
NTP_SERVER_CHECK="$(hostname | egrep -c 'pfv-netboot|pfvsvrpi|pfv-netinfra' || true)" NTP_SERVER_CHECK="$(hostname | grep -Ec 'pfv-netboot|pfvsvrpi|pfv-netinfra' || true)"
if [ "$NTP_SERVER_CHECK" -eq 0 ]; then if [ "$NTP_SERVER_CHECK" -eq 0 ]; then
@@ -341,7 +341,7 @@ function global-postPackageConfiguration() {
tuned-adm profile virtual-guest tuned-adm profile virtual-guest
fi fi
print_info "Completed running $FUNCNAME" print_info "Completed running ${FUNCNAME[0]}"
} }
#################################################################################################### ####################################################################################################
@@ -355,41 +355,41 @@ function global-postPackageConfiguration() {
# SSH # SSH
function secharden-ssh() { function secharden-ssh() {
print_info "Now running $FUNCNAME" print_info "Now running ${FUNCNAME[0]}"
bash "$MODULES_PATH/Security/secharden-ssh.sh" bash "$MODULES_PATH/Security/secharden-ssh.sh"
print_info "Completed running $FUNCNAME" print_info "Completed running ${FUNCNAME[0]}"
} }
function secharden-wazuh() { function secharden-wazuh() {
print_info "Now running $FUNCNAME" print_info "Now running ${FUNCNAME[0]}"
bash "$MODULES_PATH/Security/secharden-wazuh.sh" bash "$MODULES_PATH/Security/secharden-wazuh.sh"
print_info "Completed running $FUNCNAME" print_info "Completed running ${FUNCNAME[0]}"
} }
function secharden-2fa() { function secharden-2fa() {
print_info "Now running $FUNCNAME" print_info "Now running ${FUNCNAME[0]}"
bash "$MODULES_PATH/Security/secharden-2fa.sh" bash "$MODULES_PATH/Security/secharden-2fa.sh"
print_info "Completed running $FUNCNAME" print_info "Completed running ${FUNCNAME[0]}"
} }
function secharden-scap-stig() { function secharden-scap-stig() {
print_info "Now running $FUNCNAME" print_info "Now running ${FUNCNAME[0]}"
bash "$MODULES_PATH/Security/secharden-scap-stig.sh" bash "$MODULES_PATH/Security/secharden-scap-stig.sh"
print_info "Completed running $FUNCNAME" print_info "Completed running ${FUNCNAME[0]}"
} }
function secharden-agents() { function secharden-agents() {
print_info "Now running $FUNCNAME" print_info "Now running ${FUNCNAME[0]}"
bash "$MODULES_PATH/Security/secharden-audit-agents.sh" bash "$MODULES_PATH/Security/secharden-audit-agents.sh"
print_info "Completed running $FUNCNAME" print_info "Completed running ${FUNCNAME[0]}"
} }
function secharden-auto-upgrades() { function secharden-auto-upgrades() {
print_info "Now running $FUNCNAME" print_info "Now running ${FUNCNAME[0]}"
#curl --silent ${DL_ROOT}/Modules/Security/secharden-ssh.sh|$(which bash) #curl --silent ${DL_ROOT}/Modules/Security/secharden-ssh.sh|$(which bash)
print_info "Completed running $FUNCNAME" print_info "Completed running ${FUNCNAME[0]}"
} }
@@ -400,16 +400,16 @@ function secharden-auto-upgrades() {
#################################################################################################### ####################################################################################################
function auth-cloudron-ldap() { function auth-cloudron-ldap() {
print_info "Now running "$FUNCNAME"" print_info "Now running ${FUNCNAME[0]}"
#curl --silent ${DL_ROOT}/Modules/Auth/auth-cloudron-ldap.sh|$(which bash) #curl --silent ${DL_ROOT}/Modules/Auth/auth-cloudron-ldap.sh|$(which bash)
print_info "Completed running "$FUNCNAME"" print_info "Completed running ${FUNCNAME[0]}"
} }
#################################################################################################### ####################################################################################################
# RUn the various functions in the correct order # RUn the various functions in the correct order
#################################################################################################### ####################################################################################################
echo >$LOGFILENAME echo >"$LOGFILENAME"
print_info "Execution starting at $CURRENT_TIMESTAMP..." print_info "Execution starting at $CURRENT_TIMESTAMP..."
+1
View File
@@ -1,3 +1,4 @@
# shellcheck shell=bash disable=SC2148 # sourced .bashrc profile fragment
if command -v tmux &> /dev/null && [ -n "$PS1" ] && [[ ! "$TERM" =~ screen ]] && [[ ! "$TERM" =~ tmux ]] && [ -z "$TMUX" ]; then if command -v tmux &> /dev/null && [ -n "$PS1" ] && [[ ! "$TERM" =~ screen ]] && [[ ! "$TERM" =~ tmux ]] && [ -z "$TMUX" ]; then
tmux a -t default || exec tmux new -s default && exit; tmux a -t default || exec tmux new -s default && exit;
fi fi
@@ -1 +1,2 @@
# shellcheck shell=bash disable=SC2148 # sourced .bashrc profile fragment
export HISTTIMEFORMAT="%m/%d/%Y %T " export HISTTIMEFORMAT="%m/%d/%Y %T "
+2 -1
View File
@@ -52,7 +52,8 @@ function run_test_suite() {
function run_single_test() { function run_single_test() {
local test_file="$1" local test_file="$1"
local test_name="$(basename "$test_file" .sh)" local test_name
test_name="$(basename "$test_file" .sh)"
print_info "Running test: $test_name" print_info "Running test: $test_name"
+2 -3
View File
@@ -5,8 +5,6 @@
set -euo pipefail set -euo pipefail
PROJECT_ROOT="$(dirname "$(realpath "${BASH_SOURCE[0]}")")/../.."
function test_2fa_packages() { function test_2fa_packages() {
echo "🔍 Testing 2FA package installation..." echo "🔍 Testing 2FA package installation..."
@@ -235,7 +233,8 @@ function test_backup_existence() {
if [[ -d "$backup_dir" ]]; then if [[ -d "$backup_dir" ]]; then
# Look for recent 2FA backups # Look for recent 2FA backups
local recent_backups=$(find "$backup_dir" -name "2fa-*" -type d -newer /etc/ssh/sshd_config 2>/dev/null | wc -l) local recent_backups
recent_backups=$(find "$backup_dir" -name "2fa-*" -type d -newer /etc/ssh/sshd_config 2>/dev/null | wc -l)
if [[ $recent_backups -gt 0 ]]; then if [[ $recent_backups -gt 0 ]]; then
echo "✅ Recent 2FA backup found in $backup_dir" echo "✅ Recent 2FA backup found in $backup_dir"
+92
View File
@@ -0,0 +1,92 @@
#!/usr/bin/bash
#
# tests/shellcheck.sh — enforce shellcheck (via Docker) on all repo shell scripts
#
# Usage:
# bash tests/shellcheck.sh # lint every .sh in the repo
# bash tests/shellcheck.sh path/a.sh path/b.sh # lint specific files
#
# Uses the koalaman/shellcheck:stable image (no native binary required).
# Exits non-zero if ANY script emits a diagnostic. Skips non-bash scripts
# (e.g. PHP files with a .sh extension) and the vendored/ trees.
#
set -uo pipefail
IMAGE="koalaman/shellcheck:stable"
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
# --- Gather target files -----------------------------------------------------
mapfile -t FILES < <(
if [ "$#" -gt 0 ]; then
# Explicit args: resolve to repo-root-relative paths, keep only .sh
for f in "$@"; do
f_abs="$(cd "$(dirname "$f")" && pwd)/$(basename "$f")"
case "$f_abs" in
"$ROOT"/vendor/*) ;; # skip vendored
"$ROOT"/provisioning/Agents/librenms/*) ;; # upstream librenms-agent scripts
*) case "$f_abs" in *.sh) echo "$f_abs";; esac ;;
esac
done
else
# Default: every .sh under the repo, minus vendor/
while IFS= read -r f_abs; do
case "$f_abs" in
"$ROOT"/vendor/*) ;; # skip vendored
"$ROOT"/provisioning/Agents/librenms/*) ;; # upstream librenms-agent scripts
*) echo "$f_abs" ;;
esac
done < <(find "$ROOT" -type f -name '*.sh' \
-not -path '*/.git/*' -not -path "$ROOT/vendor/*")
fi
)
if [ "${#FILES[@]}" -eq 0 ]; then
echo "shellcheck.sh: no .sh files to check." >&2
exit 0
fi
# --- Filter out non-bash scripts (e.g. PHP with a .sh extension) --------------
BASH_FILES=()
for f in "${FILES[@]}"; do
shebang=$(head -c 64 "$f" 2>/dev/null | head -n1)
case "$shebang" in
\#!/usr/bin/php*|\#!/usr/bin/env\ php*) ;; # PHP, skip
\#!*) BASH_FILES+=("$f") ;; # any other shebang → check
*) BASH_FILES+=("$f") ;; # no shebang → check anyway
esac
done
if [ "${#BASH_FILES[@]}" -eq 0 ]; then
echo "shellcheck.sh: no bash scripts among targets." >&2
exit 0
fi
# --- Run shellcheck in Docker (mount repo root, pass root-relative paths) -----
# shellcheck disable=SC2012 # basename loop is intentional
REL=()
for f in "${BASH_FILES[@]}"; do REL+=("${f#"$ROOT"/}"); done
# Disable checks that are intentional conventions of this codebase (not bugs):
# SC1090/SC1091 — cannot follow dynamically-computed `source` paths (KNEL framework)
# SC2029 — ssh orchestration deliberately expands the command client-side
DISABLES=(
-e SC1090
-e SC1091
-e SC2029
)
echo "Checking ${#BASH_FILES[@]} script(s) with $IMAGE:"
printf ' %s\n' "${REL[@]}"
docker run --rm \
-v "$ROOT:/mnt:ro" \
-w /mnt \
"$IMAGE" -x "${DISABLES[@]}" "${REL[@]}"
rc=$?
if [ "$rc" -eq 0 ]; then
echo "shellcheck: PASS (${#BASH_FILES[@]} scripts clean)"
else
echo "shellcheck: FAIL (fix the findings above or add targeted disable directives)" >&2
fi
exit "$rc"
-3
View File
@@ -6,8 +6,6 @@
set -euo pipefail set -euo pipefail
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
# The authoritative pair (pfv-netinfra-01 / pfv-netinfra-02). # The authoritative pair (pfv-netinfra-01 / pfv-netinfra-02).
DNS_PRIMARY="192.168.3.252" DNS_PRIMARY="192.168.3.252"
DNS_SECONDARY="192.168.3.253" DNS_SECONDARY="192.168.3.253"
@@ -20,7 +18,6 @@ NTP_CONF="/etc/ntpsec/ntp.conf"
# A name every recursive resolver must be able to resolve. # A name every recursive resolver must be able to resolve.
DNS_PROBE_NAME="github.com" DNS_PROBE_NAME="github.com"
failed=0
have() { command -v "$1" >/dev/null 2>&1; } have() { command -v "$1" >/dev/null 2>&1; }
# --- Configuration assertions ------------------------------------------------- # --- Configuration assertions -------------------------------------------------
+8 -4
View File
@@ -12,7 +12,8 @@ REQUIRED_COMMANDS=("curl" "wget" "git" "systemctl" "apt-get")
# Test functions # Test functions
function test_memory_requirements() { function test_memory_requirements() {
local total_mem_kb=$(grep MemTotal /proc/meminfo | awk '{print $2}') local total_mem_kb
total_mem_kb=$(grep MemTotal /proc/meminfo | awk '{print $2}')
local total_mem_gb=$((total_mem_kb / 1024 / 1024)) local total_mem_gb=$((total_mem_kb / 1024 / 1024))
if [[ $total_mem_gb -ge $MIN_RAM_GB ]]; then if [[ $total_mem_gb -ge $MIN_RAM_GB ]]; then
@@ -25,7 +26,8 @@ function test_memory_requirements() {
} }
function test_disk_space() { function test_disk_space() {
local available_gb=$(df / | tail -1 | awk '{print int($4/1024/1024)}') local available_gb
available_gb=$(df / | tail -1 | awk '{print int($4/1024/1024)}')
if [[ $available_gb -ge $MIN_DISK_GB ]]; then if [[ $available_gb -ge $MIN_DISK_GB ]]; then
echo "✅ Disk space requirement met: ${available_gb}GB >= ${MIN_DISK_GB}GB" echo "✅ Disk space requirement met: ${available_gb}GB >= ${MIN_DISK_GB}GB"
@@ -53,8 +55,10 @@ function test_required_commands() {
function test_os_compatibility() { function test_os_compatibility() {
if [[ -f /etc/os-release ]]; then if [[ -f /etc/os-release ]]; then
local os_id=$(grep "^ID=" /etc/os-release | cut -d'=' -f2 | tr -d '"') local os_id
local os_version=$(grep "^VERSION_ID=" /etc/os-release | cut -d'=' -f2 | tr -d '"') local os_version
os_id=$(grep "^ID=" /etc/os-release | cut -d'=' -f2 | tr -d '"')
os_version=$(grep "^VERSION_ID=" /etc/os-release | cut -d'=' -f2 | tr -d '"')
case "$os_id" in case "$os_id" in
ubuntu|debian) ubuntu|debian)