Commit Graph
395 Commits
Author SHA1 Message Date
mrcharles 9c2882ea00 fix: Mermaid diagram syntax + deploy-tuning.sh path + fleet quick fixes
Mermaid diagrams fixed for Gitea rendering:
- Replaced all <br/> → <br> (86 tags across 4 docs)
- Replaced Unicode chars with ASCII (em dash, multiply, middle dot, star)
- Fixed invalid bidirectional dotted arrow in NETWORK-TOPOLOGY.md
  (<--. "..." .--> is not valid Mermaid; replaced with two -.-> arrows)
- Simplified node labels for cleaner rendering

Host fixes applied (live, not in repo — documented here for audit trail):
- SSH keys standardized to exactly 2 approved keys on all 7 hosts
  (removed host-generated RSA keys, added missing VP TechOps key to tsys9)
- Packages installed: iperf3 (tsys9), sysstat (tsys5), nvme-cli (tsys4/5),
  net-tools (tsys1/6/7)
- tsys4 tuning: rmem/wmem 16MB→128MB, netdev_max_backlog 5000→250000,
  tcp_max_syn_backlog 1024→2048, tuned profile→network-throughput
- tsys5: nconnect options added to storage.cfg (3 NFS stanzas patched;
  will apply on next Proxmox NFS remount/reboot)
- noatime applied to root fs on tsys4 + tsys5 (was only tsys5)
- tsys1 lldpd: installed but systemd/dbus issue prevents enabling
  ("Transport endpoint is not connected") — needs investigation/reboot

Bug fix: deploy-tuning.sh computed wrong script path (scripts/ vs
perf/scripts/) — fixed to use dirname/scripts/apply-tunings.sh

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-07-31 00:12:04 -05:00
mrcharles 290245349f docs(drift): fleet drift report with executive briefing format
Dedicated drift report covering package/service/tuning/security
inconsistencies across all 7 Proxmox hosts. Executive briefing at the
top (what needs decisions, quick severity summary), detailed matrices
in appendixes.

Key drift findings:
- lldpd inactive on tsys1 (blind spot in topology)
- tsys9 missing 2 SSH keys vs fleet standard
- iperf3 missing on tsys9, net-tools missing on tsys1/6/7
- sysstat missing on tsys5, nvme-cli missing on tsys4/5
- tsys4 tuning drift: 16MB TCP buffers, low backlog, wrong tuned profile
- rsyslog + snmpd + beszel inactive fleet-wide (Saturday OAM Day items)
- noatime only on tsys5 root fs (all others use relatime)

Added perf/scripts/probe-drift.sh as a portable reusable drift probe.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-07-30 23:27:54 -05:00
mrcharles 0a8865de38 docs: full documentation suite — network, storage, hosts, VMs
Four new standalone reference documents with Mermaid diagrams (renderable
on Gitea), built from live ground-truth data gathered 2026-07-30:

- NETWORK-TOPOLOGY.md: physical topology diagram, complete switch port
  maps (core-sw01 + tor3-stor), VLAN/subnet tables, cross-rack trunk
  analysis with Mermaid, NFS data-flow diagram, per-host NIC reference
- STORAGE-ARCHITECTURE.md: tier definitions, all 14 physical disks
  inventoried with SMART/size/tier, NFS export→mount map with Mermaid,
  per-spindle VM placement (IOPS hotspot analysis), capacity summary
- HOST-REFERENCE.md: one-page-per-host for all 7 nodes with hardware,
  network config, storage, tuning state, role, and running VMs
- VM-INVENTORY.md: VM placement diagram, all 28 running VMs with
  disk/CPU/RAM/config details, optimization audit (cache mode, CPU type,
  qga, disk controller, k8s wnode sizing), stopped VM reference

Updated docmap.md with all four new entries.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-07-30 23:10:38 -05:00
mrcharles cd1c2f7332 docs(status): record operator schedule + remote vs onsite work split
Operator onsite Monday 0700. Until then: software/docs/OAM only (Sunday
off). Switch changes + physical cable work deferred to Monday onsite.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-07-30 23:03:48 -05:00
mrcharles 24b08aa592 docs(audit): document observability stack + Saturday OAM day plan
Operator-defined monitoring architecture (no tool duplication):
- Beszel (Cloudron VPS): CPU/RAM/disk metrics on all Proxmox hosts via Tailscale
- Uptime Kuma (Cloudron VPS): up/down alerting via Pushover
- LibreNMS (VMID 104, on-prem): SNMP polling for switches (non-Tailsale gear)
- Home Assistant (VMID 100, on-prem): power/temp instrumentation + alerting

Saturday 2026-08-01 is designated OAM Day: goal is 100% instrumentation
coverage across all four tools + centralized syslog.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-07-30 23:02:11 -05:00
mrcharles 26de312c42 docs(audit): full ground-truth audit — network, storage, compute, switches
Comprehensive end-to-end audit of the Proxmox cluster covering all three
layers (network, storage, compute) with live data gathered from all 7 hosts
+ 4 switches via SNMP, conman consoles, and LLDP.

Key findings documented in AUDIT-2026-07-30.md:

Network (cross-rack NFS bottleneck root-caused):
- core-sw01 LAG hash = layer-2-3 (no L4 ports) — NFS caps at 1 Gbps per
  host pair regardless of nconnect. tor3-stor already uses layer-2-3-4.
- core-sw01 ch1 member mismatch: g16 is UP but not in LAG; g17 is DOWN.
  Only 3 of 4 cross-rack links active.
- Both switches use static mode (no LACP) for the cross-rack trunk.
- tsys5 bond0 degraded (1 of 2 slaves, nic2 down).

Storage (major topology changes):
- tsys5 new NVMe (CT500P1 500GB) + relocated SK hynix SSD (512GB) — both
  installed but NOT mounted/configured yet.
- tsys4 D3 changed (SK hynix SSD → Seagate HDD, still USB tier-3).
- tsys4 /mnt/albert (Hitachi 2TB internal) + /mnt/backup (WD 5TB USB) added.
- tsys5 S3 unmounted (stale export), S4 gone (broken storage.cfg entry).
- Samsung 860 PRO SSD at 93% full.

Fleet consistency:
- tsys4 behind on PVE 9.1.5/kernel 6.17 (fleet: 9.2.5/7.0.14).
- tsys4/5 bond hash + nconnect + rmem/wmem inconsistent with tsys6/7.
- 10+ VMs using cache=writethrough on NFS (slow synchronous writes).

Gardening: updated STATUS.md, docmap.md, AGENTS.md key scripts table.
Superseded AUDIT-2026-07-29 as the authoritative audit.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-07-30 23:01:43 -05:00
mrcharles d9b32e4eef fix(console): fix udev symlink naming bug + add portable audit tooling
Console fix: generate-config.sh wrote SYMLINK+="console/$name" (singular)
but ser2net.yaml opens /dev/consoles/$name (plural). They never matched,
so after every reboot the console ports failed until setup.sh's manual
fallback re-created the symlinks. Fixed the udev rule to use "consoles/"
to match ser2net and the README.

New portable read-only audit tools (AGPLv3-friendly, config-driven):
- perf/scripts/probe-storage.sh: disk/mount/export/SMART/storage.cfg probe
- perf/scripts/probe-network.sh: NIC/bond/LLDP/NFS/nconnect probe
- perf/scripts/conman-console.py: PTY-based conman console driver (replaces
  the old sw-capture.py that conflicted with ser2net)
- perf/scripts/snmp-switch-audit.py: SNMP-based switch inventory (interfaces,
  LLDP, LAG, VLANs) via pysnmp or net-snmp

Removed stale pre-conman switch tooling (sw-capture-remote.sh, sw-capture.py,
sw-probe.sh, sw-conman-probe.sh) and old .cmds files. Added fresh .cmds
files for the two cross-rack trunk endpoint switches.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-07-30 22:51:27 -05:00
mrcharles e7312ad460 chore: retire pfv-tsys8 and fix stale host-skip comment in deploy-check.sh
pfv-tsys8 is permanently retired. Strike its only doc reference and
correct the stale comment in perf/deploy-check.sh that wrongly listed
pfv-tsys9 as "off the air" (tsys9 is online and in the host array;
tsys8 is the retired host).

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-07-30 22:30:24 -05:00
mrcharles 48cdd81779 docs(ups): document apcupsd test results and Energy Dashboard limitations
The APC Smart-UPS C 1500 (FW 02.2, mfg 2012) does not expose ups.load or
ups.realpower over USB HID. Tested apcupsd as an alternative — it reads the
same HID descriptor and returned COMMLOST. Debian's apcupsd package also
conflicts with nut-server (mutually exclusive).

Documented that the HA NUT integration provides battery/runtime/status sensors
only, not wattage for the Energy Dashboard. Practical path is a smart plug on
the UPS output. Updated daily-ops examples (removed ups.load reference since
the variable doesn't exist on this UPS).

NUT stack fully restored after the apcupsd test cycle.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-07-30 12:02:15 -05:00
mrcharles 4b26aca5ee feat(ups): add Home Assistant NUT integration via REST config-flow API
Drive HA's REST config-flow endpoint to add the NUT integration programmatically,
no manual UI clicks required. The script (setup-ha-nut.sh + ha-nut-setup.py) is
idempotent — skips if a NUT entry already exists.

Key finding: HAOS runs Tailscale as an isolated add-on container, so the HA core
container cannot route to Tailscale IPs. Added a LAN listener (192.168.3.11:3493)
to upsd so HA can reach it over the shared vmbr0 bridge. Both VMs (pfv-bms HA at
192.168.3.12 and pfv-tsys1 at 192.168.3.11) are on the same bridge.

Integration is live — sensors for battery charge (100%), status (Online), and
status data (OL) are reporting.

Secrets (HA token, NUT password) are read from ~/.config/pfvcluster/ and never
committed to the repo.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-07-30 11:42:25 -05:00
mrcharles 5456c783c6 feat(ups): NUT setup for APC Smart-UPS C 1500 on pfv-tsys1
Deploy Network UPS Tools to monitor the server-room UPS units and feed data to
Home Assistant for power-usage tracking.

APC Smart-UPS C 1500 is fully operational: battery charge, runtime, load,
voltage, and status all reporting via usbhid-ups + APC HID 0.100 subdriver.
upsd listens on Tailscale (100.121.189.98:3493) for HA polling; local upsmon
provides graceful hypervisor shutdown on battery-low.

Key deployment fix: the udev rule must cover SUBSYSTEM=="usb" (raw
/dev/bus/usb) in addition to hidraw, because usbhid-ups opens the raw USB
device file after dropping to the nut user via setuid(). Matching only hidraw
causes EACCES on driver start.

Tripp Lite UPS (09ae:3016) is blocked — driver matches TrippLite HID 0.85
subdriver but fails reading the 878-byte HID Report Descriptor (EAGAIN via USB
hub). Needs physical reseat to direct motherboard USB port. Driver masked to
prevent restart-loop spam.

Files: ups/discover.sh, ups/setup.sh (idempotent), ups/status.sh, ups/README.md

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-07-30 09:34:05 -05:00
mrcharles 0fa0692c37 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
2026-07-30 08:56:31 -05:00
mrcharles 54e9927167 docs(status): mark console validation complete with pfv-r2-sw known issue
6/7 consoles validated working via conman from workstation. pfv-r2-sw
(port 2007) produces zero bytes at all baud rates — adapter is present
and correctly mapped but the device is physically unreachable (powered
off, cable disconnected, or console port disabled). Requires onsite
investigation.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-07-30 08:31:46 -05:00
mrcharles 72ab6b4b32 docs(status): add UPS USB tracing action item
APC + TrippLite UPS units not enumerating on tsys1/tsys4. tsys4 shows no HID
device at all; tsys1 may have USB passed through to pfv-bms. Need to trace
cables and confirm physical connection before NUT driver setup.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-07-29 19:53:34 -05:00
mrcharles 25a7234f40 fix(console): switch ser2net accepter to telnet(rfc2217) for proper NVT
The original ser2net config used raw TCP accepters, but conman's
dev="host:port" speaks telnet protocol. This protocol mismatch caused
conman's telnet NVT state machine to strip bare CR characters from
device output — particularly from Dell switches that send \n\r (LF+CR)
line endings — producing stair-stepped and garbled terminal display.

Fix: change ser2net accepter from `tcp,IP,PORT` to
`telnet(rfc2217),tcp,IP,PORT`. With both sides speaking telnet, binary
mode is negotiated and CR/LF translation is handled correctly by the
telnet NVT layer. RFC2217 also enables baud rate negotiation for
future use.

Validated at the byte level: PTY capture confirms all prompts render
cleanly at column 0 with proper CRLF handling across 5+ Enter presses.
All 7 consoles connected, 7 log files actively capturing.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-07-29 19:35:05 -05:00
mrcharles 51db52f862 security: redact plaintext password from all files
Replace all occurrences of the fleet admin password with REDACTED_PASSWORD.
Git history will be rewritten next to purge it from all prior commits.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-07-29 19:28:22 -05:00
mrcharles 050e235f8b feat(awx): deploy AWX 24.6.1 on tsys-awx via k3s + AWX Operator
AWX (Ansible automation controller) deployed on tsys-awx.knel.net (VMID 600
on pfv-tsys6). Accessible at http://tsys-awx.knel.net:80 (admin/REDACTED_PASSWORD).

Deployment details:
- k3s v1.36.2 single-node (no Traefik) on Debian 13 trixie
- AWX Operator 2.19.1 + AWX CR (LoadBalancer service via k3s ServiceLB)
- PostgreSQL 15 bundled (8 GiB PVC on local-path provisioner)
- VM disk resized 32->60 GB; swap partition converted to swapfile
- Fixed kube-rbac-proxy image (gcr.io removed -> quay.io/brancz replacement)
- Fixed AWX CR field names (postgres_data_volume_size ->
  postgres_storage_requirements)

Scripts: awx/install-k3s.sh, awx/deploy-awx.sh, awx/verify-awx.sh
Manifests: awx/awx-instance.yaml, awx/namespace.yaml
Docs: awx/README.md

All pods running: operator 2/2, postgres 1/1, web 3/3, task 4/4.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-07-29 19:12:03 -05:00
mrcharles fa0c77fb58 fix(console): switch conman from ser2net-telnet to direct serial access
The ser2net layer between conman and the serial devices was causing
stair-stepping in terminal output. conman's dev="host:port" uses telnet
protocol, but ser2net's accepter was raw TCP (no telnet mode). The telnet
NVT state machine in conman was stripping bare CR characters from device
output — particularly from Dell switches that use old-style \n\r (LF+CR)
line endings instead of standard \r\n. In telnet, a bare \r not followed
by \n or NUL is non-compliant and gets dropped, leaving bare \n that
causes stair-stepping in the raw-mode terminal.

Fix: conman now opens serial devices directly via the stable udev
symlinks (/dev/consoles/<name>) with seropts, eliminating the telnet
layer entirely. ser2net is stopped and disabled but remains installed
for emergency TCP access (documented workflow: stop conmand, start
ser2net, use telnet, then reverse).

Architecture change:
  Before: device → serial → ser2net (raw TCP) → conman (telnet NVT) → terminal
  After:  device → serial → conman (direct) → terminal

Verified: 7/7 serial devices held by conmand, 7/7 log files capturing,
all \r bytes preserved in terminal output (confirmed via PTY capture).

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-07-29 18:29:31 -05:00
mrcharles 80693e4f8e feat(awx): scaffold AWX deployment scripts and manifests
Create top-level awx/ directory with k3s install script, AWX operator +
instance deployment script, namespace manifest, and AWX custom resource
(LoadBalancer service type, bundled PostgreSQL on local-path).

Target: tsys-awx.knel.net (VMID 600 on pfv-tsys6, 4c/12GB/60GB disk).

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-07-29 17:45:26 -05:00
mrcharles 396349640f feat(netinfra): migrate DHCP from pfv-netboot to netinfra-01/02 with failover
ISC DHCP server with failover-peer (primary/secondary) deployed on
pfv-netinfra-01 (192.168.3.252) and pfv-netinfra-02 (192.168.3.253). Both
servers in "normal" state, load-balancing 333 active leases.

Migration details:
- Copied all 37 host reservations + subnet/pool config from pfv-netboot
- DHCP lease database copied from netboot for zero-disruption cutover
- DNS servers changed from 192.168.3.250 (netboot) to 252/253 (netinfra pair)
- NTP servers (252/253) added to DHCP options (netboot didn't hand out NTP)
- Netmask on both nodes fixed /24 -> /22 to match the network
- Webmin + DHCP module installed on both nodes (port 10000, SSL)
- pfv-netboot DHCP stopped + disabled
- Tested via sectestbed-sandbox (DHCP lease obtained from 252, verified DNS/NTP/gateway)
- Snapshot "pre-dhcp-migration" on sandbox as rollback point

Configs: netinfra/dhcp/dhcpd-{primary,secondary}.conf
Plan + results: netinfra/dhcp-migration.md

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-07-29 17:22:15 -05:00
mrcharles 9c47d93a2f docs(goals): codify local storage policy — ephemeral scratch, not OS drives
Add CAPACITY-GOALS.md §6: local storage is a scratch tier for ephemeral workloads
(container image cache, build artifacts, emptyDir), NOT for VM OS drives (which
stay on NFS where redundancy is handled via 01/02 pairs + cross-disk spreading).
Documents per-host local storage suitability: tsys3/9/2 excellent (NVMe/SSD),
tsys5 usable (HDD bulk), tsys6/7 unusable (USB 2.0 at 30 MB/s). Includes the
future k8s StorageClass mapping (local-fast for scratch, nfs-hdd for persistent).
Adds forward-looking §12 to the audit referencing this policy.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-07-29 16:33:56 -05:00
mrcharles edb34a75ec docs(status): sync action items with audit, fix stale disk refs
Rewrite STATUS Pending section to match the audit's phased action plan (§11)
exactly. Remove stale "wnode-tsys6 D5->S4, wnode-tsys7 D5->S1" line (wnode6
already on S4, wnode7 on S3, S1 is not a target). Fix section reference (§12 ->
§11). Correct actual-usage percentages (73%/37%/62%) and CPU idle range (0-5%).
Add D2 IOPS hotspot as a known issue. Fix sectestbed suite count in audit (x9 ->
x10). Verified all VM counts and host data against fresh ground truth.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-07-29 16:29:17 -05:00
mrcharles 23e5ecc4cb docs(audit): mark tsys6/tsys9 S1 storage.cfg fixes as resolved
Operator corrected the tsys6 S1 export path (S21 -> S1) and added the missing
S1 entry to tsys9. Verified both via pvesm status: all 8 NFS stores active on
each host. Updated audit findings table, §5.1/§5.2, and Phase 0 checklist.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-07-29 16:23:37 -05:00
mrcharles 915111e9dd docs(audit): consolidate to single audit with corrected storage topology
Delete AUDIT-2026-07-28.md (old audit). Rewrite AUDIT-2026-07-29.md as the
sole authoritative audit after reading live /etc/pve/storage.cfg, /etc/exports,
and /etc/fstab on every host.

Key corrections:
- local-nonprod on tsys5 IS the S1 physical disk (dir storage, not separate).
  S1 carries 16 sectestbed VMs, not 0 — it is NOT a migration target.
- storage.cfg is per-host, not cluster-wide — each node defines its own entries.
- BUG: tsys6 S1 export path typo (S21 instead of S1) — S1 broken on tsys6.
- GAP: tsys9 has no S1 defined in storage.cfg.
- Revised disk migration plan avoids S1, targets S2/S3/S4/D5 for IOPS spreading.
- D2 goes 17->5 VMs; S2/S3 absorb the load (7 each); S1 untouched (16 idle VMs).
- Refreshed ground truth: cnode5 deleted, cnode4 stopped (user actions this session).

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-07-29 14:32:43 -05:00
mrcharles 6199368710 docs(audit): add VM placement plan (current/target inventory + action checklist)
Add §13-§15 to AUDIT-2026-07-29: current per-VM host+disk inventory, target
end-state inventory (RAM/CPU relief + HDD IOPS rebalancing), and a phased action
checklist. Headline disk finding: D2 carries 17 of 26 NFS VMs while S1 sits
empty — the plan rebalances to 4-5 VMs per spindle (D2 17->5, S1 0->5) using HDD
exports only. Host moves relieve tsys1 (115%->77% alloc) and tsys9. All disks
stay on HDD (SSD/NVMe tiering deferred to post-Friday).

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-07-29 06:57:51 -05:00
mrcharles 575d79f611 docs(audit): RAM/CPU placement audit + codify capacity goals
Add CAPACITY-GOALS.md (80% RAM / ~50% idle CPU targets + workload placement
model) and AUDIT-2026-07-29.md (live end-to-end analysis of all 7 hosts). Key
findings: memory goal met fleet-wide (highest 72%); CPU idles 0-6% (chase via
workloads, not hardware); tsys5 is single-CPU not dual; cnode4/5 are stale
running VMs. Answers the wnode-sizing question (grow tsys9, not tsys1), confirms
tsys6/7 for RackRental, and gives a DDR3 compatibility decision tree (RDIMM-only
servers reject consumer UDIMM). Refreshed STATUS/docmap and the returned audit
logs, and fixed an empty-array abort in deploy-check.sh.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-07-29 05:32:12 -05:00
mrcharles 428e9ecb1d refactor(docs): co-locate k8s/proxmox docs with their code + fix broken perf paths
Docs moved from docs/k8s -> k8s/docs and docs/proxmox -> proxmox/docs to sit
beside their code. Updated all cross-references (docmap, STATUS, README, AGENTS,
k8s README). Fixed SCRIPT_DIR path resolution in 3 perf scripts broken by the
reorg (deploy-check/and-fix/finish-host pointed at root/scripts instead of
perf/scripts). Registered the new top-level proxmox/ dir in all listings.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-07-29 05:28:07 -05:00
mrcharles b860cb116d docs(audit): revised fleet audit with corrected placements + Friday plan
User has fixed the HA pair storage redundancy issue:
- netinfra-01 moved to S2 (tsys5), netinfra-02 stays on D2 (tsys4)
- ucs-02 moved to S2 (tsys5), ucs-01 stays on D2 (tsys4)
Both pairs now survive any single storage server failure. Marked as
FIXED in Known Issues.

Revised audit includes:
- Corrected VM placements from fresh qm config data
- Host capacity analysis: tsys1 (118% RAM), tsys5 (135% RAM), tsys9
  (131% RAM) are overcommitted; tsys6 (38%) and tsys7 (30%) have room
- Wnode storage rebalancing plan: move wnode-tsys6 → S4, wnode-tsys7 →
  S1, giving every wnode its own physical disk
- Friday maintenance plan: tsys2 Proxmox install as primary deliverable,
  VM migrations to relieve overloaded hosts, USB-passthrough VMs relocate
  from tsys1 to tsys9
- Cnode quorum: documented as fundamental constraint (3 cnodes, 2 NFS
  servers), accepted for now, revisit post-Friday when local-SSD boot
  becomes available

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-07-28 21:09:56 -05:00
mrcharles b6f94483e5 fix(console): enable conmand remote access + add conman client script
conmand was binding to localhost only (server loopback=on default), so
the conman client on workstations couldn't connect. The intended workflow
is: conman client (workstation) → conmand (pfv-tsys4:7890 over Tailscale)
→ ser2net (TCP 2001-2007) → serial device. Without remote conmand access,
users had to telnet directly to ser2net, which conflicts with conmand's
persistent connections (kickolduser kicks the telnet session immediately).

Changes:
- generate-config.sh: add server loopback=off to conman.conf so conmand
  listens on 0.0.0.0:7890 (reachable via Tailscale)
- query-remote.sh: new script for workstations — installs conman client,
  verifies connectivity, lists or connects to consoles
- README.md: clarify access model (conman primary, telnet emergency only
  with conmand stopped). Document the kickolduser conflict.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-07-28 20:07:17 -05:00
mrcharles 28e0b0c7a6 docs(audit): fresh fleet audit + fix stale paths across 13 perf scripts
Fresh Proxmox fleet audit (2026-07-28) with current VM placements, RAM,
CPU, and storage for all 7 reachable hosts. Written to
docs/proxmox/AUDIT-2026-07-28.md — supersedes placement data in
PROJECT.md sections 4-8.

Key audit findings:
- CRITICAL: UCS01/02 and netinfra01/02 HA pairs both still on tsys4
  storage. tsys4 failure = DNS/DHCP/NTP + LDAP/AD fully dark. These
  migrations were the #1 recommendation from the previous audit and
  have not been done.
- CRITICAL: 2 of 3 active k3s cnodes (cnode1 + cnode2) on tsys4 NFS.
  tsys4 failure = etcd quorum lost.
- 59% of running VMs still on tsys4 storage (improved from 68%).
- cnode VMIDs have changed since PROJECT.md was written (cnode1 is now
  VMID 906 on tsys9, cnode2 is VMID 705 on tsys7, etc.)

Gardening fixes:
- Removed duplicate fleet-audit.sh (check.sh + deploy-check.sh already
  exist for this purpose)
- Fixed hardcoded path /home/reachableceo/projects/perfopt in 13 perf/
  scripts to use BASH_SOURCE-derived relative paths (per AGENTS.md
  self-locating scripts convention)
- Updated STATUS.md Known Issues with the two critical findings
- Updated STATUS.md Pending with prioritized pre-k8s action items
- Registered AUDIT-2026-07-28.md in docmap.md

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-07-28 20:07:09 -05:00
mrcharles 0600321968 fix(powerman): use -h flag for remote server in query script
Debian's powerman client uses the -h/--server-host flag, not the
POWERMAN_SERVER env var, to connect to a remote daemon. Update query-remote.sh
to pass -h explicitly.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-07-28 19:48:02 -05:00
mrcharles 5dcddfa4ba feat(powerman): add remote query script for workstation PDU access
Script for any Tailscale-connected workstation to install the powerman
client and query the Cyclades PDU on pfv-tsys1. Sets POWERMAN_SERVER so
all powerman commands route to the remote daemon. Handles missing sudo
gracefully with instructions for manual install.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-07-28 19:47:01 -05:00
mrcharles 322e20e50f fix(powerman): bind to Tailscale + localhost instead of 0.0.0.0
Change powermand listen address from 0.0.0.0:10101 (all interfaces) to
127.0.0.1:10101 (local admin) + Tailscale IP:10101 (remote access). The
setup.sh now auto-detects the Tailscale IP at deploy time.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-07-28 19:45:17 -05:00
mrcharles eb99b00a70 feat(console): manage 7 switch consoles via ser2net+conman on pfv-tsys4
Solve the long-standing USB adapter enumeration shift problem: 9 Prolific
USB-to-DB9 adapters on pfv-tsys4 have no unique serial numbers and get
assigned /dev/ttyUSB0-8 based on enumeration order, which changes on every
reboot and breaks the old /root/conmap + manual screen workflow.

Solution: 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/<name>. ser2net opens
these stable symlinks and exposes them on TCP ports (2001-2007) bound to
the Tailscale interface only. conman connects to those TCP ports for
session logging and multi-user console sharing.

Architecture (layered, no port sharing):
  USB adapter → udev symlink → ser2net (TCP) → conman (logging + mux)

Port assignments (all on Tailscale IP 100.70.77.93):
  2001 = pfv-core-sw01     2002 = pfv-tor3-mgmt    2003 = pfv-tor3-stor
  2004 = pfv-rrinfra-rtr   2005 = pfv-r2-tor-top   2006 = subodev-torsw
  2007 = pfv-r2-sw

Scripts (console/):
- mapping.txt: source of truth (TCP port | name | ID_PATH | baud | comment)
- generate-config.sh: generates udev rules, ser2net.yaml, conman.conf
  entries from mapping.txt. Idempotent (markers in conman.conf for clean
  regeneration). Uses | delimiter (ID_PATH values contain colons).
- setup.sh: full deploy — generate configs, create symlinks (udev trigger
  + manual fallback for already-discovered devices), create conmand
  systemd unit (Debian doesn't ship one), restart services
- discover.sh: read-only USB adapter and service state discovery
- validate-conman.sh: verify conman→ser2net→device data path and log capture

Issues fixed during development:
- /dev/console is a kernel char device (major 5, minor 1) — cannot create
  a directory there. Changed symlink namespace to /dev/consoles/.
- conman 0.3.x has no 'include' directive — CONSOLE entries written
  directly into /etc/conman.conf between idempotent markers.
- Debian conman package has no systemd unit — created
  /etc/systemd/system/conmand.service with After=ser2net ordering.
- conman.conf had no LOGDIR — logs weren't being written to
  /var/log/conman/. Fixed by adding server logdir directive.

Validation: 7 symlinks resolving, 7 TCP ports on Tailscale, conmand with
7 consoles registered, 7 log files actively capturing console output,
both services enabled for reboot survival.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-07-28 19:44:02 -05:00
mrcharles 799d270261 docs: fix gardening variance + strengthen protocol for new directories
Two variances from the gardening protocol were found during self-audit:
the k8s/ and powerman/ directories were added but the root README.md
directory table and the AGENTS.md Key Scripts table were not updated.

Root cause: the gardening protocol enumerated STATUS.md and docmap.md but
did not explicitly call out the root README.md directory table or the
AGENTS.md Key Scripts table, so they were easy to miss when adding a new
top-level directory.

Fixes:
- README.md: add k8s/ and powerman/ to the Directory Structure table
- AGENTS.md: add install-cp.sh and powerman/setup.sh to Key Scripts

Permanent fix (encode in protocol so it cannot recur):
- Add rule 5 to the Automatic Gardening Protocol: when a new top-level
  directory is created, ALL directory listings must be updated
  (README.md table, AGENTS.md layout block, AGENTS.md Key Scripts)
- Add rule 6: a grep-based self-audit command to run before commit,
  verifying the new directory appears in all four canonical files

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-07-28 18:49:39 -05:00
mrcharles 8124483da8 feat(powerman): manage Cyclades PM10i PDU via powerman on pfv-tsys1
Set up centralized PDU management for a Cyclades AlterPath PM10i (10
controllable AC outlets) connected to pfv-tsys1 via a Prolific USB-to-DB9
serial adapter. powermand is now listening on 0.0.0.0:10101, making the
PDU manageable over the network from any host on the tailnet.

Scripts (powerman/):
- discover.sh: gather USB adapter, powerman state, device definitions
- setup.sh: idempotent setup — udev rule (stable symlink by serial number),
  powerman.conf with 10 outlet nodes, fix powermand dialout group, restart
  service. Overridable via env vars for other hosts/PDU types
- test-pdu.sh: validate control by cycling outlet 10 off then on (8/8 pass)
- status.sh: quick PDU status check

Issues fixed during setup:
- Config pointed at /dev/ttyUSB0 but adapter is at /dev/ttyUSB1 (fixed
  with udev symlink /dev/cyclades-pm10 pinned to adapter serial)
- powermand (user:powerman) lacked dialout group membership to open the
  serial device (fixed with usermod + udev GROUP="dialout")

Validation: outlet 10 turned off (confirmed), turned on (confirmed), then
cycled. All 10 outlets currently ON and manageable.

TODO tracked for Friday: rename outlets from generic (outlet-1..10) to
match physical devices, and change PDU admin password from factory default.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-07-28 18:22:47 -05:00
mrcharles 8f1642bf96 feat(k8s): deploy 3-node k3s HA control plane over Tailscale
Bootstrap a regular (non-ITAR) k3s cluster on cnode1/2/3 with embedded
etcd. All cluster communication — node registration, API server, etcd
peering, flannel VXLAN — runs exclusively over Tailscale IPs. Zero LAN
addresses appear in node status or TLS certificates.

Scripts (k8s/):
- env.sh: shared config (Tailscale IPs, SSH opts, k3s version)
- wipe.sh: remove existing k3s from all cnodes
- install-cp.sh: full bootstrap (cnode1 --cluster-init, then cnode2/3 join)
- join-servers.sh: re-join cnode2/3 only (fixes broken join state)
- post-setup.sh: apply NoSchedule taints, fetch kubeconfig, verify
- verify.sh: 13-point health check (nodes, Tailscale IPs, taints, etcd,
  CoreDNS, API server, workload isolation)
- probe-nodes.sh: SSH + Tailscale reachability check

All 3 cnodes are tainted control-plane:NoSchedule so no user workloads
can schedule on the control plane. 13/13 health checks pass.

Docs updated: k8s README TL;DR reflects k3s (not Talos) as the deployed
choice, with Talos preserved for the future ITAR cluster.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-07-28 12:21:33 -05:00
mrcharles 174deea2a9 docs(k8s): add Talos architecture, distro decision, and bootstrap plan
Author the docs/k8s/ directory capturing the pfv-k8s control-plane design:

- README.md: TL;DR of all decisions (distro, runtime, cnode count, admin
  access, identity, tenancy, registry, storage)
- DISTRO-DECISION.md: Talos vs k3s analysis. Recommend Talos because the
  ITAR/classified requirement makes its immutable, API-only, measured-boot
  posture structurally easier to certify than SCAP-hardened Debian. k3s was
  only ever a plan (no cluster deployed), so cutover cost is ~zero.
- ARCHITECTURE.md: target arch with mermaid diagrams covering control
  plane, LAN-only network with Tailscale subnet-router admin, Cilium CNI,
  OIDC to Keycloak, per-tenant vcluster isolation (incl. ITAR tenant),
  Harbor pull-through cache on D3 SSD, bootstrap sequence, and DR.

Gardening: register docs/k8s/ in docmap.md, update STATUS.md with the new
k8s section and the three pending user decisions (cnode count, host spread,
Tailscale pattern).

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-07-28 11:50:56 -05:00
mrcharles 99270c344d docs(agents): enforce always-commit-push policy
Strengthen Git Policy point 1 so agents never hold work for review. The
user reviews rendered markdown on Gitea after push, so pausing to "let
them read first" defeats the workflow. Explicitly overrides any default
conservative commit-and-hold behavior.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-07-28 11:50:51 -05:00
mrcharles 6b6a8ff021 docs: organize docs into project subdirectories
Split docs/ into project-based subdirectories:
- docs/proxmox/     fleet ops, hardware, k8s (PROJECT.md, TODO.md, K8S.md)
- docs/server-build/ provisioning, security, DNS (SECURITY.md, tailscale.md,
                    DEPLOYMENT.md, TSYS-2FA-GUIDE.md, DEVELOPMENT-GUIDELINES.md)
- docs/archive/     historical AI reviews, completed todos, pre-refactor docs

docmap.md rewritten with new paths. All cross-references in AGENTS.md,
README.md, STATUS.md, and dns-cluster-setup/README.md updated.

Code directories unchanged — scripts stay where BASH_SOURCE expects them.

🤖 Generated with [Crush](https://github.com/charmassociates/crush)

Assisted-by: GLM-5 via Crush <crush@charm.land>
2026-07-28 11:34:23 -05:00
mrcharles 4f2bfe18a5 docs: add STATUS.md, docmap.md, encode gardening protocol
Restructure top-level to exactly three .md files:
- AGENTS.md (agent operating instructions)
- README.md (project overview, links to status + docmap)
- STATUS.md (living project status, agent-maintained, human read-only)

Add docs/docmap.md as the single documentation index/map. All docs are
categorized (active, operational guides, historical) with last-reviewed
dates. Includes the agent gardening protocol requiring agents to update
STATUS.md and docmap.md after every work session.

Rewrite AGENTS.md to be lean: points to docmap.md for doc discovery,
encodes the automatic gardening protocol (keep docs/code in sync, grep
for stale paths after renames, update STATUS.md after infrastructure
changes). All references are Gitea-renderable relative links.

Simplify README.md: header links to STATUS.md + docmap.md + AGENTS.md,
doc table replaced with pointer to docmap.md.

🤖 Generated with [Crush](https://github.com/charmassociates/crush)

Assisted-by: GLM-5 via Crush <crush@charm.land>
2026-07-28 11:32:03 -05:00
mrcharles a7fbad60ab docs: end-to-end gardening — links, stale refs, tailscale.md update
Comprehensive documentation gardening across the merged repo:

- tailscale.md: fully rewritten with current ground truth. The netinfra
  pair now runs production Technitium with all knel.net records
  replicated. Both LAN IPs resolve knel.net device names and recurse
  externally. The old "NXDOMAIN / zone is stale" findings are replaced
  with the resolved state and current recommendations.
- AGENTS.md: rewritten with Gitea-compatible clickable relative links
  to all key scripts and docs. Autonomous commit/push policy
  prominently documented. SSH user corrected to localuser.
- README.md: directory table and docs table now use clickable links.
- All .md cross-references converted to Gitea-renderable relative links.
- Stale path references (ProjectCode/, Project-Tests/, ProjectDocs/)
  updated to current names (provisioning/, tests/) across all docs.
- Stale repo name "FetchApply" / "KNELServerBuild" updated to
  "PFVCluster" in actionable docs; historical AI-review docs tagged
  with an HTML comment notice.
- REFACTORING-EXAMPLES.md: tagged as historical (pre-refactor patterns).
- tests/README.md, dns-cluster-setup/README.md, docs/DEPLOYMENT.md,
  docs/SECURITY.md: path references fixed to current structure.

🤖 Generated with [Crush](https://github.com/charmassociates/crush)

Assisted-by: GLM-5 via Crush <crush@charm.land>
2026-07-28 11:28:52 -05:00
mrcharles c2b592a66b docs: unified README and AGENTS.md for merged repo
Replace the KNELServerBuild README with a unified PFVCluster README
covering both provisioning and cluster ops. Update AGENTS.md to document
the merged repo layout, key scripts, and project context. Consolidate
all documentation under docs/.

🤖 Generated with [Crush](https://github.com/charmassociates/crush)

Assisted-by: GLM-5 via Crush <crush@charm.land>
2026-07-28 11:25:16 -05:00
mrcharles 4851517947 refactor: reorganize merged repo into clean directory structure
Reorganize the merged KNELServerBuild + PFVCluster repo:

  provisioning/    server provisioning (was ProjectCode/ +
                   Project-Includes/ + Project-ConfigFiles/)
  tests/           test suite (was Project-Tests/)
  perf/            Proxmox perf scripts (was top-level *.sh + scripts/)
  docs/            all documentation (was ProjectDocs/ + PROJECT.md +
                   K8S.md + TODO.md)
  dns-cluster-setup/  Technitium DNS cluster (unchanged)
  netinfra/        netinfra audit scripts (unchanged)
  switches/        switch configs (unchanged)
  vendor/          vendored KNELShellFramework (unchanged)

Update all internal path references from old directory names
(ProjectCode/, Project-Includes/, Project-Tests/) to the new ones
(provisioning/, tests/) across all scripts.

🤖 Generated with [Crush](https://github.com/charmassociates/crush)

Assisted-by: GLM-5 via Crush <crush@charm.land>
2026-07-28 11:24:39 -05:00
mrcharles 132c0854d1 merge: combine KNELServerBuild into PFVCluster
Merge the KNELServerBuild repository (server provisioning, security
hardening, DNS cluster setup, test suite) into PFVCluster (Proxmox
cluster ops, performance tuning, fleet audit). Both histories are
preserved via --allow-unrelated-histories.

The two repos had no source-file collisions; only AGENTS.md and
.gitignore conflicted (both resolved by merging content from both).

Directory reorganization and doc gardening will follow in subsequent
commits.

🤖 Generated with [Crush](https://github.com/charmassociates/crush)

Assisted-by: GLM-5 via Crush <crush@charm.land>
2026-07-28 11:23:22 -05:00
mrcharles b1088e8487 feat(dns-cluster): replicate Technitium production to netinfra pair
Set up a fully scripted, documented Technitium DNS cluster that
replicates the production instance from tailscale-router to
pfv-netinfra-01 (primary) and pfv-netinfra-02 (secondary).

What it does:
- EXPORT: reads the production Technitium config (auth.config with
  users + 2FA, dns.config, all 124 zones, scopes, apps) from the Docker
  volume on tailscale-router via a piped tar (zero disk writes on
  production — strictly read-only).
- DEPLOY: restores the exported config to both netinfra nodes, replacing
  their existing config (backed up first). Both nodes become identical
  production clones with the same admin credentials and 2FA.
- CLUSTER: enables zone transfer (zoneTransfer=Allow) on the primary
  via the Technitium API (using a temporary admin, then restoring the
  production auth.config). Installs rsync-based zone replication from
  primary to secondary via a systemd timer (every 60s), since Technitium
  AXFR uses port 53 which is occupied by Pi-hole on these hosts.
- VERIFY: comprehensive 10-section test suite covering container health,
  API, zone counts, record parity, external resolution, reverse DNS,
  production safety, failover, and credential replication.

Scripts:
- remote-dns.sh: SSH chokepoint for all DNS host access
- setup.sh: master orchestrator (export → deploy → cluster → verify)
- sync-zones.sh: rsync-based zone replication (installed as systemd timer)
- verify.sh: 10-section verification suite

Safety:
- tailscale-router is NEVER modified (read-only export only)
- Production auth.config is backed up before any temporary admin swap
- Each node's existing config is backed up before replacement
- The export tarball is gitignored (contains production credentials)

🤖 Generated with [Crush](https://github.com/charmassociates/crush)

Assisted-by: GLM-5 via Crush <crush@charm.land>
2026-07-28 08:50:14 -05:00
mrcharles 1951667f8b fix(network): remove interface restriction that broke NTP client sync
The ntp.conf hardening used `interface ignore wildcard` +
`interface listen 127.0.0.1`, which binds ntpd to loopback only. Outbound
NTP queries to the upstream servers then carried a 127.0.0.1 source
address that the servers cannot reply to, so the daemon's peers stayed
stuck in .INIT. with reach 0 — even though the servers are reachable
(verified: ntpdate -q succeeds, ntpd does not).

Replace the interface-based restriction with restrict-based hardening:
`restrict default ignore` blocks unsolicited queries from any host (so
the box never serves time to others), while explicit allow rules for the
two upstream servers and localhost let the client sync normally.

🤖 Generated with [Crush](https://github.com/charmassociates/crush)

Assisted-by: GLM-5 via Crush <crush@charm.land>
2026-07-28 06:03:21 -05:00
mrcharles 9a4961d94b docs(network): analyze Tailscale vs managed DNS conflict
Add an architecture analysis for the tension between Tailscale's
default resolv.conf management (100.100.100.100) and the managed
LAN-resolver resolv.conf (.252/.253). Documents a key finding from
live-network probing: knel.net device records only resolve via the
Tailscale MagicDNS path; querying the LAN DNS servers directly returns
NXDOMAIN because their knel.net zone is stale (SOA serial 2025-06-23).

Lays out four options (Tailscale-owned, LAN-pinned, split DNS,
Tailscale-pushes-LAN-resolvers) with pros/cons, recommends leaving DNS
to Tailscale in the short term (since wazuh/postfix/syslog depend on
knel.net names that only resolve there) and fixing the Technitium/Pi-hole
knel.net zone before pinning the LAN resolvers. Confirms the NTP
(LAN-IP) change is safe regardless. Flags that the managed-resolv.conf
change will be overwritten by Tailscale and would break knel.net
resolution if it ever sticks.

🤖 Generated with [Crush](https://github.com/charmassociates/crush)

Assisted-by: GLM-5 via Crush <crush@charm.land>
2026-07-28 05:31:47 -05:00
mrcharles f010fa9609 feat(network): use pfv-netinfra-01/02 as redundant DNS and NTP
Route every host built by this project through the new
pfv-netinfra-01 (192.168.3.252) / pfv-netinfra-02 (192.168.3.253)
pair for both name resolution and time, with automatic failover.

- NTP: replace the single pfv-netboot.knel.net upstream with both
  netinfra servers (iburst) so time sync survives either one failing.
- DNS: add a managed static /etc/resolv.conf (new ConfigFiles/Resolv/).
  The repo previously had no resolver configuration at all. Both servers
  are listed so glibc falls through to the secondary on failure.
- DHCP: request domain-name-servers/domain-search/ntp-servers and
  supersede them to the netinfra pair, so a DHCP renew can't silently
  revert to whatever the DHCP server advertises.
- SetupNewSystem.sh: deploy resolv.conf (robustly replacing any
  systemd-resolved/NetworkManager symlink) and add pfv-netinfra to the
  NTP-server self-exclusion guard so those boxes don't client off
  themselves.

LAN IPs are used throughout (not the knel.net hostnames) because those
hostnames resolve to Tailscale CGNAT addresses, not the LAN addresses,
and NTP must come up before DNS. Add a validation test asserting the
config is present and both servers actually answer DNS and NTP queries.

🤖 Generated with [Crush](https://github.com/charmassociates/crush)

Assisted-by: GLM-5 via Crush <crush@charm.land>
2026-07-28 05:23:13 -05:00
mrcharles 1044224d02 feat: wire Pi-hole to forward knel.net to local Technitium + document setup
Add a shared Docker network (dnsnet, 10.53.0.0/24) connecting Pi-hole and
Technitium containers so Pi-hole can conditionally forward knel.net and
Tailscale-reverse queries to the local authoritative Technitium instance
(10.53.0.53) instead of netboot's upstream 192.168.3.16. Also adds
end-to-end documentation for both the reference node (pfv-netboot) and
the replicated nodes (pfv-netinfra-01/02).

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-07-28 05:16:01 -05:00