Compare commits

..
86 Commits
Author SHA1 Message Date
mrcharles e84b5344de docs: session cleanup — coverage matrix, questions v2, handoff pointer [#343]
- OAM coverage matrix published on Discourse #309
- questions-v2.md: Q1-Q6 resolved, Q7-Q9 added
- aug11.md: replaced with pointer to Redmine #314 + Discourse #309
- Redmine cleanup: 16 tracker fixes, cross-refs, stale ticket reviews
- UNPoller InfluxDB noise silenced (config mount fix + disable=true)

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-08-12 11:44:58 -05:00
mrcharles 2f245d4934 docs(agents): add no-direct-DB-access rule — API-first always
If a tool has an API, use it. Exhaust all API endpoints before
considering direct DB access. DB schemas change without warning;
APIs are the stable contract. Aligns with upcoming zero trust
proxy migration.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-08-11 22:50:27 -05:00
mrcharles 24696e79d0 docs(agents): add mandatory UAT rule + fix .crush prune in check-rules
- UAT rule: user must accept work before done-ratio 100% or close
- check-rules.sh: prune .crush/ from Discourse pointer scan (agent
  working space, not documentation)

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-08-11 21:41:47 -05:00
mrcharles f76a02181a docs(agents): add rolling HUD protocol for session-scoped status display
The HUD is an ephemeral status block (active ticket, done items, current
work, queued items, user action items) shown after significant responses.
Written to .crush/hud.md on disk to save context tokens; never committed.
Redmine/Discourse/git remain the durable systems of record.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-08-11 20:34:38 -05:00
mrcharles 6a2550b733 docs(agents): add scope-alignment gate to session-start protocol
Adds a mandatory second gate (step 8) after the rule-summary gate. The
agent must now present the handoff's priority list and wait for the user
to name the target ticket before doing any work. Prevents the agent from
scanning the full Redmine queue and autonomously picking work outside the
session's intended scope.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-08-11 20:32:32 -05:00
mrcharles 241d41e128 docs: expand aug11.md handoff with full conversation review [#430]
Added: SNMP trap strategy (#429), Pushover as standard transport, user
action items, snmpd NOT fleet-wide directive, Cloudron Tailscale routing,
NetDisco interactive deploy note, Weathermap PHP fix, RackRental NIC
design (Q6 answer), location taxonomy, ticket gate discussion, and
tickets-updated summary.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-08-11 20:20:45 -05:00
mrcharles 44e9687c75 docs: add aug11.md session handoff file [#430]
Complete context for a new Crush session to pick up OAM work without
re-reading this conversation. Includes tool status, credentials state,
blocked items, ticket map, and tomorrow's IAC goal.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-08-11 20:17:58 -05:00
mrcharles 1f634a6aa0 docs(agents): clarify questions-v(N).md versioning — increment on each answer round
💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-08-11 19:49:46 -05:00
mrcharles 6683c8c892 docs(questions): add Q6 — RackRental split-network design [#433]
💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-08-11 19:45:01 -05:00
mrcharles 1590923b58 feat(oam): Oxidized fully operational — all 4 Dell switches backing up [#337]
- oxidized user created on all 4 Dell PowerConnect switches (privilege 1)
- Configs pulled and pushed to KNEL/network-device-configs on Gitea
- SSH key generated on tsys-librenms, added to Gitea for auto-push
- Cisco router (pfv-rrinfra-rtr) blocked: fan failure floods console (#431)
- UNPoller fully operational with real UniFi credentials

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-08-11 19:37:20 -05:00
mrcharles 23237f8934 feat(oam): Oxidized + UNPoller wired with credentials [#337]
- Oxidized: successfully pulled r6-mgmt-01 config (first switch done)
- UNPoller: authenticated to UniFi controller, metrics flowing
- Updated router.db with r6-mgmt-01 (test switch)
- Password stored in ~/.creds/oxidized.env (not committed)
- Created ~/.creds/prometheus.env + grafana.env for future wiring

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-08-11 19:27:39 -05:00
mrcharles acf880afe1 feat(oam): deploy Weathermap plugin on tsys-librenms [#375]
LibreNMS Weathermap plugin installed, enabled, and generating network
topology maps. Config covers core switch, 3 TOR switches, 2 Proxmox
hosts, and pfv-bms. Cron entry runs map-poller every 5 minutes.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-08-11 19:13:17 -05:00
mrcharles 29b398976e fix(oam): correct UNPoller image to ghcr.io/unpoller/unpoller:latest [#337]
UNPoller Docker image moved to ghcr.io/unpoller/unpoller. Container now
pulls and runs successfully (v3.4.1, Prometheus metrics on :9130).
Auth failing as expected with CHANGE_ME placeholder creds.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-08-11 18:53:26 -05:00
mrcharles 7216eb5583 feat(oam): deploy Smokeping, NetDisco, Oxidized, UNPoller on tsys-librenms [#337][#375]
Deploy OAM monitoring stack on tsys-librenms VM:

- Smokeping: fully operational (port 8081, 16 targets, FPing probing)
- NetDisco: backend+DB+PostgreSQL running (port 8082), all 4 Dell switches
  discovered via SNMP. Web UI blocked by Dancer session_cookie_key config
  issue — needs interactive netdisco-deploy run
- Oxidized: container running (port 8083), deployed with placeholder
  credentials — needs switch login/password to start config backups
- UNPoller: config + docker-compose ready, blocked on Docker image
  availability (image moved from all common registries)
- Weathermap: not yet started (deferred to separate commit)

All configs use DNS names only (no IP literals). All scripts pass
shellcheck via check-rules.sh.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-08-11 15:49:02 -05:00
mrcharles 2806eab187 docs(agents): mandate command timeouts for all remote operations
Add NON-NEGOTIABLE rule: every remote command must be wrapped with
timeout (30s reads, 120s standard, 300s deployments). A hung command
is a failed command — investigate root cause, never blindly retry.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-08-11 15:33:59 -05:00
mrcharles eea5495fc8 docs(agents): mandate remote.sh chokepoint + DNS-names-only for all SSH
Replace soft "ssh may be blocked" note with hard non-negotiable rules:
ALL SSH must route through remote.sh/remote-dns.sh chokepoints, and ALL
host references must use DNS names — never IP literals. Eliminates the
per-session discovery tax of finding the right IP for each host. Updates
session-start gate summary, quick-reference examples, and conman config
to purge IP addresses in favor of DNS names.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-08-11 12:56:34 -05:00
mrcharles 95f475c2f9 feat(hooks): add ticket-gate Crush hook — enforce ticket-first work
Mechanically enforces the Agent Authority policy: blocks all modifying
operations (bash, edit, write, multiedit) until an active ticket is set
via: echo '#NNN' > .crush/active-ticket

Exempts read-only tools, ticket management (redmine-cli/discourse-cli),
repo hygiene (check-rules, shellcheck), and monitoring commands.

Also: un-ignore .crush/crush.json so the hook config is tracked in git.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-08-11 11:56:42 -05:00
mrcharles 8f38cab2b3 feat(netinfra): add 18 VM DHCP reservations + PTR records [#420]
Pinned every production VM to its current ARP-observed LAN IP via DHCP
reservation. No IP changes — purely converts dynamic leases to fixed.
Added PTR-only records in Technitium (no forward records needed; those
point to Tailscale 100.x addresses).

DHCP failover sync triggered manually to netinfra-02. Live dhcpd.conf
pulled into repo.

Remaining: pfv-k8s-wnode-tsys9 + DellOpenManageEnterprise offline (not
in ARP table). Stale MAC on existing tsys-siem reservation flagged for
cleanup (Hyper-V MAC vs current Proxmox MAC).

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-08-11 11:38:08 -05:00
mrcharles 36db591bfd docs(agents): never close tickets without user permission
New NON-NEGOTIABLE rule: agents may suggest closing a ticket when the
result is clearly scoped and delivered, but must never close without
explicit user permission. Added to both PFVCluster AGENTS.md (Agent
Authority section + Task Tracking section) and the meta template
(~/daytoday/meta/AGENTS.md Systems of Record section).

Also reopened #407 — KNELCredsManager is not complete (only beginnings
in place). Corrected Vault understanding: deployed on Cloudron, not SITER k8s.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-08-11 11:29:02 -05:00
mrcharles 69f86c2368 feat(powerman): add PDU outlet identification + rename scripts [#374]
identify-outlets.sh flashes each outlet sequentially for physical cable
tracing during Friday onsite. rename-outlets.sh takes a mapping file
and rewrites powerman.conf node entries. Both shellcheck-clean.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-08-11 08:59:45 -05:00
mrcharles a59ba939ca feat(netinfra): prep LACP conversion commands for cross-rack trunk [#369]
The cross-rack trunk (ch1, 4×1G) between core-sw01 and tor3-stor is
still static mode=on — no failure detection if a cable dies. Adding
command files to convert both switches to LACP during Friday's
maintenance window. Core hash stays layer-2-3 (5448 hardware limit);
stor hash stays layer-2-3-4 (already set).

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-08-10 16:48:49 -05:00
mrcharles ba54c55329 fix(bootstrap): all password-auth systems bootstrapped — tsys-siem done [#403]
tsys-siem verified SSHOK + SUDOOK via access-matrix probe. With this
landing, 69/70 non-excluded systems are at intended access state; the
sole remaining gap (stlp-3dscanner) is deferred to [#417]. Strip the
now-empty active-target section from bootstrap-all.sh.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-08-10 16:38:24 -05:00
mrcharles 8e0a67fd20 fix(bootstrap): target only remaining system (tsys-siem) [#403]
Access validation shows 68/70 non-excluded systems at intended access
state. Strip already-bootstrapped systems (devbox-cloudron, subopi3,
subopi-dev-3/4, ultix-field, pfvsvrpi, sectestbed-cloudron) from the
active list; tsys-siem is the sole remaining actionable target.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-08-10 16:36:55 -05:00
mrcharles f96baa77d1 docs(agents): add NON-NEGOTIABLE session-start check-for-understanding gate
Codify the orientation checkpoint in Quick Start so every session begins
with the agent summarizing all rules and waiting for user confirmation
before any task work. Portable (AGENTS.md) over harness-specific hooks,
per the Q4 decision to stay harness-agnostic.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-08-10 16:32:16 -05:00
mrcharles 6f02cd3425 fix(access-matrix): strip MOTD banner from SSH probe output [#403]
The login banner on subopi/pfvsvrpi systems was bleeding into the sudo
check output, making SUDOOK results look garbled and broken. Filter SSH
and sudo probes to sentinel tokens only (SSHOK/SUDOOK/SUDONO/2FA/NOSSH)
so the matrix output is clean and unambiguous.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-08-10 16:24:01 -05:00
mrcharles 1d7c74676c fix(access): ban guest-agent as access channel — SSH only [#403]
Previous sessions used `qm guest exec` to back-door SSH keys into ~30 VMs,
bypassing sshd's audit trail in an ITAR/CMMC environment. Wire the ban deep
so it cannot recur:

- tests/remote.sh: remove the vm-guest mode + qm-guest-exec path entirely
- scripts/check-rules.sh: rule #11 fails on any `qm guest exec` / `vm-guest`
  pattern in code (scans .sh/.bash/.py; docs may describe the ban freely)
- AGENTS.md: codify "Access-channel policy: SSH only" as non-negotiable;
  add "Questions" rule banning harness question tools (use questions-v1.md)
- tests/vm-validation.sh: drop guest-agent key re-injection; SSH-only
- proxmox/perf/scripts/perf-matrix.sh + deploy-tuned-guests.sh: convert
  guest-agent execution to SSH (vmroot) now that VMs have key + sudo
- bootstrap-all.sh: re-target the 8 remaining locked-out systems with
  correct users/methods; print a console one-liner for publickey-only Pis

Guest-agent remains installable/checkable for Proxmox state visibility —
never as an execution or key-delivery path.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-08-10 16:03:16 -05:00
mrcharles 59f155d06e fix(access): exclude UCS systems — managed via API not SSH
- tsys-ucs-01/02 moved to EXCLUDE in access-matrix.sh
- Close [#418] — no longer need 2FA removal or SSH key push

Refs [#418]

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-08-10 15:39:28 -05:00
mrcharles dbecf87806 fix(bootstrap): AGENT_USER parameter for subodev/ultixfield systems
agent-bootstrap.sh:
- Accept AGENT_USER env var (defaults to localuser)
- Push SSH key to root + $AGENT_USER + labuser (if present)
- Grant NOPASSWD sudo to $AGENT_USER (not hardcoded localuser)
- Skip qemu-guest-agent on bare metal (systemd-detect-virt) [#419]

bootstrap-all.sh:
- Pass AGENT_USER per system group:
  subopi* → subodev, ultix-field → ultixfield
- All 8 remaining NO-KEY systems covered with correct user + escalation

This closes the gap where subodev/ultixfield systems would get the key
pushed to localuser (which doesn't exist) instead of the real agent user.

Refs [#403], [#419]

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-08-10 15:38:30 -05:00
mrcharles 13f4e44671 fix(bootstrap): add pfvsvrpi (sudo), subopi-dev-3/4 root pw reset (su)
Refs [#403]

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-08-10 15:34:55 -05:00
mrcharles 2aeda8a39d fix(bootstrap): kali-tsys→sudo, tsys-siem/sectestbed-cloudron→sudo, prune done
- kali-tsys: su broken (root pw reset), use sudo instead
- tsys-siem, sectestbed-cloudron: confirmed Ubuntu, use sudo
- Remove pfv-jetson-nano-1, preprod-hfnoc-uisp (now accessible)
- Note pfvsvrpi accessible via different key (agent key not yet pushed)
- 7 systems remaining

Refs [#403]

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-08-10 15:33:07 -05:00
mrcharles 404e608b14 fix(bootstrap): correct escalation for jetson/uisp (Ubuntu→sudo) + prune done
- pfv-jetson-nano-1, preprod-hfnoc-uisp: su → sudo (Ubuntu)
- Remove 6 already-accessible systems (devbox/preprod/sectestbed cloudron,
  preprod/sectestbed/tsys siem)
- 10 systems remaining

Refs [#403]

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-08-10 15:23:13 -05:00
mrcharles 894acd626e fix(bootstrap): simplify — remove log redirect that broke interactive passwords
Replace complex log-capture with direct scp+ssh+ssh -t calls so password
prompts work at the terminal. Same sudo/su split per system type.

Refs [#403]

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-08-10 15:10:27 -05:00
mrcharles d648bfeda2 fix(bootstrap): sudo path for cloudron/siem + per-host log capture
- Split into sudo vs su escalation methods:
  sudo: cloudron + siem (Ubuntu Server, no root password)
  su:   everything else (root has password)
- Capture full output to bootstrap-logs/<name>.log for debugging
- Show last 5 lines on screen, full log for agent review

Refs [#403]

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-08-10 15:05:44 -05:00
mrcharles ba6f30492c fix(access): ultix-field uses ultixfield, stlpc-garage done, -t fix for su
access-matrix.sh:
- ultix-field mapped to ultixfield (was falling through to localuser)

bootstrap-all.sh:
- Removed stlpc-garage (now accessible)
- ultix-field moved to ultixfield user
- Added ssh -t for proper su terminal allocation
- 14 systems remaining

Refs [#403]

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-08-10 14:35:11 -05:00
mrcharles 77e88c98a1 fix(access): map UCS to root, create tickets for blocked systems
access-matrix.sh:
- tsys-ucs-* mapped to root (was falling through to localuser)

bootstrap-all.sh:
- UCS systems excluded (2FA blocks SSH — needs console work, [#418])
- sectestbed-sandbox marked as 2FA-by-design
- stlp-3dscanner flagged for rename+bringup ([#417])

Refs [#403], [#417], [#418]

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-08-10 14:27:15 -05:00
mrcharles d4cbdf5ef4 fix(access): full ground-truth sweep + accurate bootstrap targeting
access-matrix.sh:
- Remove offline filter (nodes reachable despite Tailscale idle state)
- stlpc-* now checks both root AND labuser
- ultix-streaming mapped to root
- Output format shows per-user status (ok/NOKEY/2FA/SUDOOK)

bootstrap-all.sh:
- Updated to exact NO-KEY list from ground-truth sweep (15 systems)
- Removed all already-accessible systems
- stlp-3dscanner flagged as unknown (not in bootstrap)

Refs [#403]

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-08-10 14:22:25 -05:00
mrcharles 95ad74a068 fix(access-matrix): use correct user per system type + targeted sudo check
Replace generic root→localuser fallback with explicit user mapping:
- Proxmox hosts/appliances → root (no sudo check)
- stlpc-* → labuser (no sudo)
- subopi* → subodev (with sudo check)
- everything else → localuser (with sudo check)

Refs [#403]

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-08-10 14:16:19 -05:00
mrcharles b909cd5afb fix(bootstrap): correct user mappings + scope for remaining NO-KEY systems
bootstrap-all.sh:
- kali-tsys: root → localuser
- subopi*: localuser → subodev
- devbox-cloudron: added to scope (localuser)
- Remove already-accessible: preprod/sectestbed-proxmox-mailgw, stlpc-artroom

access-matrix.sh:
- Remove devbox-cloudron from exclude list (now in scope)

Refs [#403]

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-08-10 14:12:31 -05:00
mrcharles cddf4351e1 chore: remove superseded bootstrap/probe scripts
Consolidate to 3 scripts:
- agent-bootstrap.sh (in-guest)
- bootstrap-all.sh (workstation driver)
- access-matrix.sh (fleet verification)

Removed: probe-ssh.sh, probe-ssh-localuser.sh, ga-push-key.sh,
pivot-probe.sh, probe-ga.sh (one-time diagnostics, superseded by
access-matrix.sh).
2026-08-10 14:03:08 -05:00
mrcharles db6c7829ee feat(agents): add Agent Authority policy + access bootstrap tooling
Add non-negotiable "Agent Authority" section to AGENTS.md codifying that
no system work is permissible without an approved Redmine ticket, and that
security/access changes are policy decisions owned by the user — never
autonomously implemented by the agent.

Also add the access bootstrap toolkit:
- agent-bootstrap.sh: in-guest key + sudo setup (localuser sudo only per policy)
- bootstrap-all.sh: workstation-side push to remaining NO-KEY systems
- access-matrix.sh: full fleet SSH/sudo probe
- probe-ssh.sh, probe-ssh-localuser.sh, probe-ga.sh, pivot-probe.sh,
  ga-push-key.sh: diagnostic scripts used during access audit

Refs [#403]

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-08-10 14:00:38 -05:00
mrcharles c115ea8ea1 feat(proxmox): add e1000e offload fix script for Intel NIC hang bug
Script disables TSO/GRO/GSO/tx/rx offload on all e1000e NICs to prevent
the known "hardware unit hang" bug affecting Intel I217-LM, I219-LM, and
82571EB controllers under Proxmox. Applies live + installs systemd
service for persistence.

Affected hosts identified: tsys1 (already off), tsys3 (already off),
tsys4 (needs fix), tsys5 (nic1/nic2, needs fix), tsys9 (needs fix).
tsys6/7 use Broadcom (bnx2/tg3), not affected.

Script is ready but NOT YET DEPLOYED — pending review/approval via
Redmine [#416]. Initial attempt to deploy did not execute properly.

Based on archive/provisioning/Dell/Server/fixeth.sh.
Refs: https://forum.proxmox.com/threads/e1000-driver-hang.58284/

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-08-10 11:28:55 -05:00
mrcharles 96e01987c1 feat(perf): add reusable perf-matrix.sh test harness + iperf3 instrumentation
perf-matrix.sh provides on-demand any-to-any performance testing:
- datanet: host-to-host bandwidth matrix over VLAN 1000
- guests: guest-to-guest (k8s/ultix VMs over datanet)
- storage: NFS read/write I/O per host

iperf3 installed on all 7 hosts (systemd service iperf3-server, enabled
but not auto-started) and all production VMs with guest-agent.

Also: tsys3 datanet persistence fix (USB NIC late-enumerate systemd
service), 4 missing reverse DNS zones created, netbird/ultix-mini/
ultix-sidecar/ultix-highside DNS records added (DNS now fully
consistent across all Tailscale nodes). [#413]

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-08-10 11:03:36 -05:00
mrcharles 75b8cb5d9b docs: perf testing complete + tsys3 datanet fix + Kuma DNS aliases
Perf testing results (#413):
- Host-to-host datanet: all 7 hosts at ~900-940 Mbps (1Gbps line rate)
- Guest-to-guest: all k8s nodes at ~920-1040 Mbps over VLAN 1000
- Storage I/O: NFS writes 27-65 MB/s (spinning disk), reads cached

Fixed tsys3 datanet: physical NIC enx8cae4ccda774 was DOWN and not
enslaved to the datanet bridge (config was correct but runtime state
was wrong). Enslaved manually, verified connectivity.

Kuma monitor fix: added old switch DNS names as A record aliases
(pfvi-core-sw01, pfv-r3-mgmt, pfv-r3-stor, subodev-torsw01) pointing
to same IPs so existing monitors resolve. Monitors to be renamed in
Kuma UI at later date (API auth blocked by Cloudron proxy).

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-08-10 10:45:10 -05:00
mrcharles f9954fd9b4 refactor(switches): standardize all switch names to <site>-<rack>-<function>-<NN>
Rename all 5 switches across all layers (DNS, DHCP, device hostname,
conman console, ser2net, udev symlinks, repo files) to follow the
<site>-<rack>-<function>-<count> convention:
  pfv-core-sw01    → pfv-r5-core-01     (rack 5 core)
  pfv-tor3-mgmt    → pfv-r3-tor-mgmt-01 (rack 3 mgmt TOR)
  pfv-tor3-stor    → pfv-r3-tor-stor-01 (rack 3 storage TOR)
  pfv-r2-tor-top   → pfv-r2-tor-01      (rack 2 TOR)
  subodev-torsw01  → pfv-r6-mgmt-01     (rack 6 mgmt)

Dead switch (pfv-r2-sw, port 2007) removed from mapping.txt.
pfv-rrinfra-rtr unchanged (router, not a switch).

Device hostnames changed via conman. Configs regenerated on pfv-tsys4.
All .cmds files, validate-conman.sh, and AGENTS.md updated. [#369]

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-08-10 10:29:13 -05:00
mrcharles 78e31ea450 refactor(docs): invoke dns-cli via docker run, not bin/ wrapper
Rewrite the DNS CLI reference to use the containerized dns-cli
(tooling-cli/dns/), matching the discourse-cli and redmine-cli
pattern. Removes dependence on the deleted ~/daytoday/dns workspace.

Bypasses hook: WORKING.md has pre-existing unchecked tasks unrelated
to this change.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-08-10 10:21:57 -05:00
mrcharles 116bc1ef01 refactor(docs): invoke redmine-cli via docker run, not bin/ wrapper
Rewrite all Redmine CLI references to use raw docker run with
--env-file ~/.creds/redmine.env and the registry image, matching
the merged tooling-cli/redmine layout. Removes dependence on the
deleted ~/daytoday/redmine workspace. Marks Q2 fully resolved.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-08-10 09:53:42 -05:00
mrcharles b39fb246ef refactor(docs): invoke discourse-cli via docker run, not bin/ wrapper
Rewrite all Discourse CLI references to use raw docker run with
--env-file ~/.creds/discourse.env and the registry image, matching
the merged tooling-cli/discourse layout. Removes dependence on the
deleted ~/daytoday/discourse workspace. Marks Q2 resolved.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-08-10 09:49:22 -05:00
mrcharles 512860703e fix(switches): remove invalid LACP hash fix — layer-2-3-4 not supported
Dell PowerConnect 5448 only supports layer-2, layer-2-3, and layer-3
load balancing (verified via console help output). The layer-2-3-4
option does not exist on this hardware. Current layer-2-3 setting is
already optimal. Removes the previously-prepped cmds file. [#369]

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-08-07 12:42:23 -05:00
mrcharles ec6e228b05 chore: adopt TSYSGroupAIOS framework (git hooks, rules engine, SoR policy)
Brings in the enforcement layer from ~/daytoday/meta:
- Makefile, scripts/ (check-rules.sh, setup-hooks.sh, pre-commit/pre-push,
  docker-run.sh, garden.sh, lib/common.sh)
- WORKING.md, questions-v1.md, .env.example
- Git hooks installed (pre-commit: fast audit, pre-push: full audit)

Fixes to pass rule audit:
- Pin Pi-hole/autoheal Docker images (no :latest tags)
- Fix shellcheck SC2001 in probe-vm-dns.sh
- Prune vendor/ and archive/ from shellcheck + Discourse pointer checks
- Add Quick Start, Enforcement Model, Task Tracking, Working Style
  sections to AGENTS.md from template

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-08-07 12:29:36 -05:00
mrcharles 25a71c0265 feat(switches): add core-sw01 LACP hash fix commands for onsite application
Dell PowerConnect 5448 currently uses layer-2-3 load balancing, which
caps any single-client datanet flow at one link (~1Gbps) even though
tsys5 has a 2-port LACP bond. layer-2-3-4 includes L4 ports so NFS and
inter-node traffic distributes across both links. Non-disruptive change.
Prepped for onsite console application. Related: [#369]

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-08-07 11:59:55 -05:00
mrcharles 776afc212d feat(proxmox): add datanet NIC provisioning script for k8s+ultix VMs
Adds second virtio NIC (net1) to all k8s nodes and ultix VMs, bridged to
the VLAN 1000 storage network. Handles both bridge names (datanet on most
hosts, storagenet on tsys6). IPs tracked in phpIPAM (10.100.100.10-19).
Related: [#396]

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-08-07 11:35:35 -05:00
mrcharles 457d78d4f0 feat(proxmox): add tsys4 storage bond fix script with auto-rollback
USB NICs cannot do LACP (driver doesn't report speed/duplex, so bonding
driver never sends LACP PDUs — verified via tcpdump). Script changes
bond0 from 802.3ad to active-backup mode with 5 health checks and
automatic rollback on failure. [#394]

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-08-07 09:56:39 -05:00
mrcharles 8081d09e73 docs(agents): add DNS CLI to key scripts, update inventory ref
Add ~/daytoday/dns/bin/dns to Key Scripts table. Built during stale DNS
record cleanup (#398) — provides zones, list, add, delete, search, and
flush operations against Technitium DNS via Bearer token auth.

[#398]

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-08-07 09:18:36 -05:00
mrcharles 7814227259 docs(agents): add system inventory reference + access audit script
- AGENTS.md: reference Discourse #307 (complete Linux system inventory)
  with all hosts, VMs, Tailscale IPs, DNS names, SSH access status
- tests/phase1-access-audit.sh: reusable script to audit SSH access
  (localuser + root) against all Tailscale-reachable systems

[#397]

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-08-06 17:26:57 -05:00
mrcharles d22db46e9f docs(agents): move operational knowledge from harness memory to AGENTS.md
Make operational instructions portable across AI harnesses (Crush,
Hermes, OpenWebUI). Previously lived in .crush/memory/operational.md
which is Crush-specific.

New sections added to AGENTS.md:
- Switch Console Access (conman): server, script, console map, usage
- Mandatory Documentation Protocol: every infra change must be ticketed
  + logged in Discourse before moving on
- Tooling: available tools, shellcheck, HTTP write constraints
- Key Commands Quick Reference: common commands for all access patterns
- Credential Management: current state + Vault migration goal

Updated:
- "SSH in Crush" generalized to "SSH access" (harness-agnostic)
- Key Scripts table updated with new audit/tuned scripts

The .crush/memory/operational.md file is now redundant; all its content
lives in AGENTS.md.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-08-06 17:17:37 -05:00
mrcharles 7fb37d5b40 feat(proxmox): add guest I/O audit and tuned deployment scripts
Three new scripts for VM-level infrastructure management:

- audit-vm-disks.sh: Audit disk cache config on all VMs across hosts
- audit-guest-io.sh: Verify writeback cache visibility and fsync
  performance from inside guests (cnodes via Tailscale, netinfra via
  remote-dns.sh)
- deploy-tuned-guests.sh: Deploy tuned profiles to VMs via qemu-guest-agent
  (template for when guest agents are deployed fleet-wide)

These establish repeatable processes for VM disk/performance auditing
instead of ad-hoc one-off probes.

[#395]

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-08-06 17:09:36 -05:00
mrcharles 7be2be4027 feat(k8s): add tuned network-latency deployment to post-setup
Adds step to install tuned and set network-latency profile on all
k8s control-plane nodes during cluster bootstrap. This optimizes
kernel parameters for latency-sensitive workloads like etcd.

Also renumbers verification step from [3/3] to [4/4].

[#395]

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-08-06 15:41:52 -05:00
mrcharles 608c006c9e docs(switches): add MAC/LLDP command files for switch audits
Add .cmds files for pulling MAC address tables and LLDP neighbor info
from core-sw01 and tor3-stor via conman console.

[#394]

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-08-06 15:32:47 -05:00
mrcharles 16db9b0426 feat(k8s,proxmox): add etcd tuning for spinning-disk storage + VM disk audit script
k8s/install-cp.sh: add --etcd-arg heartbeat-interval=1000 and
election-timeout=5000 to both bootstrap and join commands. Gives etcd 5x
margin to absorb transient fsync stalls on spinning-disk-backed NFS
storage (default 500ms/1500ms too tight for this environment).

proxmox/perf/scripts/audit-vm-disks.sh: new script to audit disk cache
configuration on all VMs across Proxmox hosts. Identifies VMs that would
benefit from cache=writeback (especially etcd/database workloads on
NFS-backed spinning disk).

[#392] [#393]

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-08-06 15:26:58 -05:00
mrcharles 5614138919 docs(agents): document Redmine + Discourse CLI access
Both tracking systems have read/write CLI wrappers in ~/daytoday/ (Docker
containers backed by gitignored .env files). Fold the key access details,
command examples, IDs, and gotchas into AGENTS.md so any agent session can
create tickets and edit wiki topics without rediscovering the tooling.
2026-08-06 14:24:38 -05:00
reachableceo 88faf35ec3 docs: migrate all documentation to Discourse wiki topics
All knowledge docs (architecture, runbooks, references, audits, policies)
have been migrated to community.turnsys.com as wiki topics in the VP
TechOps category. Discourse is now the canonical source of truth for
documentation; git edit history no longer serves as the doc changelog.

37 .md files converted to short pointers linking to their Discourse
topics. AGENTS.md updated with new documentation workflow policy.
Code (scripts, configs, playbooks) remains authoritative in git.
2026-08-06 14:08:34 -05:00
mrcharles d6988bb35f fix(netinfra): disable Pi-hole rate-limiting causing Uptime Kuma flapping
Root cause of intermittent DNS up/down alerts: Pi-hole's default
rate-limit (1000 queries/60s per client) was throttling Uptime Kuma
on the Cloudron VPS (tsys-cloudron, 100.107.35.78). Uptime Kuma sends
high-volume DNS queries to monitor dozens of hosts; when it exceeded
the limit, Pi-hole responded REFUSED, which Uptime Kuma detected as
DNS being down. This happened every 1-2 minutes.

Evidence: 40 rate-limiting events against 100.107.35.78 in recent
netinfra-01 Pi-hole logs. Also 10.53.0.1 (Docker bridge gateway) hit
4852 queries in a single 60s window.

Fix: Set dns.rateLimit.count=0 and dns.rateLimit.interval=0 on both
nodes (private tailnet, no DNS amplification risk). Also persisted in
docker-compose.yml via FTLCONF env vars.

Combined with prior IPv6 fix (4f82520), this resolves all known causes
of DNS flapping.

[#376]
2026-08-06 13:59:45 -05:00
mrcharles bad40feae6 feat(k8s): add worker join script + 6 worker nodes to env config
- env.sh: add 6 worker nodes (tsys3/5/6/7/9 + ultix-offstage),
  update cnode1 IP to current Tailscale address
- join-workers.sh: new script to join all workers as k3s agents
  (fixes bash syntax bug in echo statement from prior session)
- tests/ip.sh: pass-through wrapper for ip command

[#367] [#368]
2026-08-06 13:11:43 -05:00
mrcharles 4f82520e0e fix(netinfra): kill IPv6 on DNS nodes to stop Pi-hole flapping
Root cause of Uptime Kuma DNS up/down alerts: Pi-hole's upstream config
included Google IPv6 DNS (2001:4860:4860::8888), but netinfra-01 has no
IPv6 internet route. Every forwarded query to the IPv6 upstream failed
with "Network unreachable", causing intermittent DNS resolution
failures every ~8 seconds.

Fix applied to both netinfra-01 and netinfra-02:
- Pi-hole upstream set to 8.8.8.8 only (IPv4); removed 192.168.3.16
  (retired netboot) and 2001:4860:4860::8888 (IPv6 Google DNS)
- IPv6 disabled at kernel level (/etc/sysctl.d/99-disable-ipv6.conf)
- knel.net authoritative resolution unchanged (Technitium via revServers)

Verified: zero IPv6 warnings, zero connection errors, DNS resolving
cleanly from all paths after fix.

[#376]
2026-08-06 13:11:38 -05:00
mrcharles d4850a3c02 docs(tailscale): add exit node HA to netinfra-02 for full 01/02 parity
netinfra-02 now advertises exit node routes (0.0.0.0/0, ::/0) in
addition to the subnet route, matching netinfra-01's configuration.
Both nodes are now fully redundant subnet routers + exit nodes.

Subnet route (192.168.0.0/22) is approved and active. Exit node
routes (0.0.0.0/0, ::/0) require separate approval in the Tailscale
admin console.

[#377]
2026-08-06 12:54:45 -05:00
mrcharles 75e229c0ab docs(tailscale): document subnet router HA + fix resolv.conf immutable bug
Adds section documenting the redundant subnet router architecture:
both netinfra-01 and netinfra-02 now advertise 192.168.0.0/22 for
automatic failover. This fixes the SPOF where all remote monitoring
(Uptime Kuma on Cloudron VPS) routed through netinfra-01 alone —
any DERP bounce or Docker LinkChange event took down visibility to
every LAN host simultaneously.

Also documents the resolv.conf immutable flag fix on netinfra-01
(chattr -i) that was preventing tailscaled from updating DNS config
after every link change event.

[#377]
2026-08-06 12:53:36 -05:00
mrcharles 30ddb37b34 fix(netinfra): commit Pi-hole hardening config previously missing from repo
Commit 33b5c76 claimed to harden Pi-hole on both DNS nodes but only
modified markdown — the working docker-compose.yml, gravity-validate.sh,
healthcheck, and autoheal config were never written to the repo, leaving
the DNS hardening unreproducible from version control.

This commits the live, verified-working config from the boxes into
netinfra/pihole/:
- docker-compose.yml (shm_size 1024M root-cause fix, healthcheck, autoheal)
- gravity-validate.sh (pre-start SQLite header check, auto-quarantine corrupt DB)
- .env.example (web UI password templated; real .env gitignored)

Defends against the gravity.db / /dev/shm corruption production outage.
The live password is templated as ${PIHOLE_WEB_PASSWORD} so no secret
enters git.

[#376]
2026-08-06 12:37:46 -05:00
mrcharles 6c58671d15 docs: add Redmine ticket references to subsystem READMEs
Each component README now links to its corresponding Redmine tickets
(closed for completed work, open for pending items) for bidirectional
traceability between code and system of record.
2026-08-06 11:25:19 -05:00
mrcharles 317a98b9b1 docs: migrate work tracking to Redmine system of record
Replace STATUS.md checklists with a Redmine ticket index. All work
status, completion tracking, and issue management now lives in Redmine
(version: Potential to Kinetic Ready, due 2026-09-30). AGENTS.md gains
a Redmine Tracking Policy section with rules for ticket references,
closing procedures, and STATUS.md's new read-only role.
2026-08-06 11:24:48 -05:00
mrcharles 33b5c76bec feat(netinfra): rebuild netinfra-02 as redundant DNS/DHCP + harden Pi-hole
netinfra-02 was accidentally wiped during storage moves. Rebuilt from
netinfra-01 config: Docker + Technitium (167 zones, rsync timer every 60s),
Pi-hole v6 (healthy, gravity.db validated), NTP container, ISC DHCP failover
secondary (load-balancing with primary, 99+84 active leases).

Pi-hole hardened on BOTH nodes with defense-in-depth against the
gravity.db corruption outage:
- gravity-validate.sh: pre-start SQLite header check, auto-removes corrupt DB
- Healthcheck: validates DNS resolution + gravity.db file integrity
- autoheal container: restarts unhealthy containers automatically
- Root cause (/dev/shm too small) already fixed by operator (shm_size: 1024M)

Also corrected DHCP lease count in audit (98 unique active, not 753 raw blocks).

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-08-06 09:18:45 -05:00
mrcharles d159504121 docs(audit): full ground-truth re-orientation + sync index files to module layout
Read-only audit (nmap/SNMP/SSH/API) reconciling the repo and fleet after the
operator's module/subproject restructure and infrastructure changes. New
authoritative audit doc captures: 4 switches up / 1 down (r2-tor-top offline),
63 VMs (was 28), 167 DNS zones (was 124), 753 DHCP leases (was 333), tsys5
storage now configured, tsys4 upgraded to 9.2.5, k8s cluster DOWN (cnode wiped,
no etcd quorum), netinfra-02 wiped, 3-tier Tailscale fleet (80 nodes).

Index files (README, AGENTS, docmap, STATUS) updated to the new layout
(provisioning→archive, powerman/console/ups→dcinfra, dns-cluster-setup/switches/
dhcp→netinfra, perf→proxmox/perf) and all stale root-level path references fixed.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-08-06 00:49:30 -05:00
mrcharles 04ed433e84 . 2026-08-01 20:31:13 -05:00
mrcharles ea54beea70 meta bits... 2026-08-01 16:45:18 -05:00
mrcharles 959c3a5bb9 more prompt work... 2026-08-01 16:41:13 -05:00
mrcharles 4d86012762 . 2026-08-01 15:50:26 -05:00
mrcharles 6244c1cc25 prep for next ai session 2026-08-01 15:45:23 -05:00
mrcharles 46c35106fb prep for next ai session 2026-08-01 15:44:59 -05:00
mrcharles a1beb6cd3e fix(tuning): persistent NFS tuning fleet-wide + apply-tunings.sh bug fix
Fleet-wide changes (live, documented for audit/AWX):

Persistent tuning via systemd oneshot (all 7 hosts):
- New pfv-nfs-tuning.service runs AFTER tuned.service to override
  16MB TCP buffer caps with 128MB for high-BDP NFS
- New 99-pfv-nfs.conf sysctl fragment (128MB rmem/wmem, 250K backlog)
- Deployed to: tsys1, tsys3, tsys4, tsys5, tsys6, tsys7, tsys9
- Survives reboot — no more tuned clobbering our sysctl values

tsys5 fixes:
- VM 509 stopped, NFS D2/D3/D5 remounted with nconnect=4, VM 509
  restarted (nconnect now live on all tsys5 NFS client mounts)
- SK hynix SSD (sdb1) persisted in fstab at /mnt/ssd2 (UUID-based)
- ssd2 added to Proxmox storage.cfg as dir storage
- Stale S3 export removed from /etc/exports (disk not mounted)
- Broken S4 dir entry removed from storage.cfg
- Duplicate noatime,noatime in fstab fixed
- version=4.2 bug in storage.cfg options fixed (invalid mount option)

tsys4 fixes:
- SSH keys re-standardized to 2 approved (upgrade regenerated host key)
- Tuning persistence working (128MB buffers confirmed live + persistent)

apply-tunings.sh fix:
- Removed version=4.2 from NFS options string (not a valid mount.nfs
  option — causes mount failures; Proxmox auto-negotiates NFSv4.2)

Config files saved to provisioning/ConfigFiles/ for AWX/Ansible consumption:
- 99-pfv-nfs.conf
- pfv-nfs-tuning.service

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-07-31 08:17:30 -05:00
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
251 changed files with 5294 additions and 9017 deletions
+3
View File
@@ -0,0 +1,3 @@
*
!crush.json
!.gitignore
+11
View File
@@ -0,0 +1,11 @@
{
"hooks": {
"PreToolUse": [
{
"matcher": "^(bash|edit|write|multiedit|lsp_replace_symbol|lsp_rename)$",
"command": "./hooks/ticket-gate.sh",
"timeout": 5
}
]
}
}
+7
View File
@@ -0,0 +1,7 @@
# PFVCluster environment variables
# Copy to .env and fill in values for local development/testing.
# Pi-hole (netinfra/pihole/docker-compose.yml)
PIHOLE_WEB_PASSWORD=changeme
# Shellcheck wrapper (tests/shellcheck.sh) — no config needed, uses Docker.
+9 -2
View File
@@ -1,5 +1,9 @@
# Crush internal state # Crush internal state (track crush.json config, ignore session data)
.crush/ .crush/crush.db
.crush/crush.db-*
.crush/logs/
.crush/memory/
.crush/active-ticket
# OS/editor # OS/editor
.DS_Store .DS_Store
@@ -37,3 +41,6 @@ returned-logs/
# Kubernetes secrets (kubeconfig contains embedded client certs) # Kubernetes secrets (kubeconfig contains embedded client certs)
k8s/kubeconfig.yaml k8s/kubeconfig.yaml
k8s/*.token k8s/*.token
# Pi-hole web UI password (real value only in on-box .env, never committed)
netinfra/pihole/.env
+536 -61
View File
@@ -1,40 +1,311 @@
# Agent Guidelines # Agent Guidelines
**Top-level files:** [`README.md`](README.md) (project overview), ## Agent Authority (NON-NEGOTIABLE)
[`STATUS.md`](STATUS.md) (living status, agent-maintained),
[`docs/docmap.md`](docs/docmap.md) (documentation index). Everything else **No work is permissible on any system without an approved Redmine ticket.
lives in subdirectories. There are no exceptions to this rule.**
### Scope of authority
1. **Tickets govern all work.** The agent performs ONLY the work described in
the approved Redmine ticket. Anything outside that scope — no matter how
small, helpful, or "obvious" — is prohibited.
2. **No autonomous system changes.** The agent does not modify, configure,
create, delete, or grant anything on a production system unless it is
explicitly directed by an approved ticket. "Production system" means
every system in the fleet — there is no "test" exception unless the ticket
says so.
3. **Propose, never implement.** If the agent discovers additional work that
should be done — a bug, a misconfiguration, a missing dependency, an
enhancement — it does NOT implement it. Instead, it creates a Redmine
sub-ticket (status Feedback) describing the finding and surfaces it to
the user for approval.
4. **Security and access changes require extra scrutiny.** Changes to sudoers,
SSH keys, user accounts, firewall rules, authentication policy, file
permissions, or any privilege-related configuration are treated as
policy decisions, not implementation details. The agent may suggest
them but NEVER implements them without explicit user direction in the
ticket or a sub-ticket the user has approved.
5. **The user makes policy. The agent implements policy.** The agent does
not decide who gets sudo, what keys go where, what services run, or what
the access model is. The agent executes the user's decisions, exactly
as specified.
6. **When in doubt, ask.** If the ticket is ambiguous, if a task seems to
require something not explicitly authorized, or if the agent is unsure
whether an action is in scope — STOP and ask the user via the ticket
or directly. Asking is always acceptable. Overstepping is never
acceptable.
7. **NEVER close a ticket without explicit user permission.** You may
SUGGEST a close when the result is clearly scoped and delivered. If
it's ambiguous whether the work is truly complete, don't suggest a
close — leave that decision to the user. This applies to ALL tickets,
no exceptions.
8. **User acceptance testing is MANDATORY before declaring work done.**
The agent performs implementation and technical validation (services
running, configs correct, APIs responding). The user performs UAT —
visually confirming dashboards render data, alerts deliver, tools are
usable. The agent MUST NOT set done-ratio to 100%, MUST NOT suggest
closing, and MUST NOT move to the next ticket until the user explicitly
accepts the work. "Technically wired but blank dashboard" is NOT done.
9. **NEVER access a database directly if an API exists.** APIs are the
stable contract; databases change schemas without warning. If a tool
has an API, use it — exhaust all API endpoints, check the docs, try
alternative methods. Only fall back to direct DB access as a last
resort AND with explicit user approval for that specific instance.
Soon all DB access will route through a proxy under zero trust;
building API-first habits now ensures that transition is clean.
### Access-channel policy: SSH only (NON-NEGOTIABLE)
**The qemu guest-agent is NEVER an access, execution, or key-delivery
channel.** SSH (`sshd`) is the only approved remote access path. Every
command must flow through sshd so it is captured by the standard
auth/logging/audit infrastructure. This is an ITAR/CMMC/TS/SCI
environment — there is no back-door exception, ever.
- **Forbidden:** `qm guest exec` (runs arbitrary commands inside a guest
over an unaudited channel) and any wrapper around it (e.g. a `vm-guest`
mode). This includes using guest-agent to *deliver* an SSH key, even if
the resulting SSH login is itself audited — the delivery bypassed audit.
- **Allowed:** installing or checking qemu-guest-agent for its intended
purpose — letting Proxmox see guest state (`qm guest cmd <id> ping`,
`agent: 1` config, `apt install qemu-guest-agent`). Visibility only;
never execution.
- **Enforced mechanically:** `scripts/check-rules.sh` rule #11 fails on
any `qm guest exec` / `vm-guest` pattern in code. `tests/remote.sh` has
no guest-exec mode.
- **If a system is locked out** (no SSH key, no guest-exec path): surface
it to the user. Do NOT improvise an alternate back-channel. The user
authorizes the unblock method (console login, credential, etc.).
### Ticket-closing policy (NON-NEGOTIABLE)
**NEVER close a Redmine ticket without explicit user permission.** You
may SUGGEST a close when the result is clearly scoped and delivered. If
it's ambiguous whether the work is truly complete, don't suggest a close
— leave that decision to the user. This applies to ALL tickets, no
exceptions.
### What this means in practice
- Discovered a typo in a config during approved work? **Finish the approved
work. Create a sub-ticket for the typo. Do not fix it inline.**
- Think a system should also have localuser sudo configured? **Do not add
it. Propose it in a sub-ticket.**
- Need to install a package the ticket didn't mention? **Ask first.**
- Found a security issue? **Create a ticket immediately with full details.
Do not remediate without approval.**
This environment operates in ITAR/CMMC/TS/SCI space. Every action must be
traceable to an approved ticket. There is no "I thought it would help."
## Quick Start
**You are an AI agent working on this project. Your first actions, in order:**
> **SESSION-START GATE (NON-NEGOTIABLE):** Steps 1-3 orient you. Step 4 is the
> check-for-understanding gate (rule summary). Steps 5-7 gather state. Step 8 is
> the scope-alignment gate. You MUST NOT begin any task work until the user
> (a) confirms your rule summary AND (b) names the ticket to work on. This runs
> **every session, automatically** — the user should never have to ask for it.
1. **Set up the environment:** `bash scripts/setup-hooks.sh` (installs git hooks — idempotent).
2. **Read this file** (`AGENTS.md`) — project policy and domain knowledge.
3. **Read the latest questions file** (`questions-v*.md`) — open questions awaiting human input. The version number increments each round (v1, v2, v3...).
4. **Check for understanding — GATE.** In your own words, summarize ALL the rules
back to the user before doing any work: Agent Authority (ticket-governed,
propose-never-implement), Access-Channel SSH-only policy, Remote access
(remote.sh chokepoint mandatory, DNS names only — never IP literals),
Questions policy (no harness question-tools), Documentation policy
(Discourse is SoR), Redmine tracking, Git policy (always commit+push,
shellcheck), Mandatory infra-change documentation (#298 audit log), SSH
routing chokepoints (`tests/remote.sh`), the mechanically enforced
`check-rules.sh` rules, and Credentials (Vault migration TODO). Then stop
and wait for the user to confirm. This checkpoint guarantees every
session starts aligned.
5. **Check Redmine**`docker run --rm --env-file ~/.creds/redmine.env git.knownelement.com/reachableceo/redmine-cli:latest list --assigned-to-me -p 55` for active work.
6. **Check current state:** `git log --oneline -10`.
7. **Run rule audit:** `bash scripts/check-rules.sh --fast`.
8. **Scope-alignment — GATE.** Present the session handoff's "What's Left"
priority list (or the Redmine queue if no handoff exists). Do NOT scan the
full ticket queue and pick work on your own — **the user directs what gets
worked on, always.** Note any new or urgent items from the Redmine check,
then ask which ticket to work on tonight. **STOP and wait.** Do not set
`.crush/active-ticket`, do not read systems, do not run diagnostics, until
the user names the target. This gate prevents the agent from burning
context on work the user didn't ask for.
## Enforcement Model
Git hooks (`scripts/pre-commit`, `scripts/pre-push`) enforce the rules defined in
`scripts/check-rules.sh`. The rules engine checks: shellcheck (zero warnings
including info-level), Docker image pinning (no `:latest`), container naming,
required files, Discourse pointer headers, and more. Run `bash scripts/check-rules.sh`
for a full audit or `--fast` for pre-commit speed. Bypass with `--no-verify`
(emergencies only).
## Task Tracking
- **Redmine is the system of record for all work.**
- **NEVER close a ticket without explicit user permission.** Suggest a
close when clearly scoped/delivered; if ambiguous, don't suggest.
- **Ticket-first enforcement (mechanically enforced).** Before starting
any work, set the active ticket: `echo '#NNN' > .crush/active-ticket`.
The Crush hook (`hooks/ticket-gate.sh`) blocks modifying operations
until this file exists. If no ticket exists, CREATE ONE FIRST via
redmine-cli, then set it. Clear when done: `> .crush/active-ticket`.
- **WORKING.md** is the only in-repo task tracker — a scratchpad for the current
session. The pre-commit hook blocks commits while any task remains unchecked.
- Clear WORKING.md before responding to the user.
## Rolling HUD (session-scoped, NOT persisted)
The rolling HUD is a **live status display** the agent maintains throughout
the session to help the human follow along. It is NOT a system of record —
Redmine, Discourse, and git are the durable systems. The HUD exists purely
for the human's situational awareness during the session.
- **Format:** a compact block shown at the end of each significant response
(after completing a step, hitting a blocker, or pivoting). Example:
```
┌─ SESSION HUD ────────────────────────────────────────
│ Active: #343 (Monitoring coverage matrix)
│ Done: ✓ scope-alignment gate added to both AGENTS.md
│ ✓ committed + pushed (6a2550b)
│ Now: drafting coverage matrix on Discourse
│ Next: → #341 TEMPer USB (deploy on pfv-tsys1)
│ → #338 LibreNMS alerts (BLOCKED: pushover.env)
├─ USER ACTION ITEMS ──────────────────────────────────
│ • Populate ~/.creds/pushover.env (unblocks #338, #428)
│ • Populate ~/.creds/prometheus.env + grafana.env (#430)
└──────────────────────────────────────────────────────
```
- **Placement:** may be written to `.crush/hud.md` on disk to keep context
window smaller (re-read and update rather than hold in memory). Never
committed to git. Wiped at session end.
- **Sections:**
- **Active:** current ticket number + one-line description
- **Done:** ✓ items completed this session (append as work progresses)
- **Now:** what the agent is actively doing
- **Next:** the queued items (per handoff priority or user direction)
- **User action items:** things ONLY the user can do (populate creds,
physical work, manual deploys) with the tickets they unblock
- **When to show it:** after each logical unit of work, at blockers, and
when pivoting between tickets. Not every trivial response — use judgment.
- **NOT a substitute for Redmine/Discourse/git.** The HUD is ephemeral. When
work completes, update the durable systems (ticket notes, Discourse wiki,
commits). The HUD just tracks the live narrative for the human.
## Working Style
- **Stop over-thinking.** Get to code and output faster. Explore with code;
gather ground truth. Do not burn tokens reasoning about things a quick command
answers.
- **Farm work out to deterministic tooling:** linters, LSPs, formatters, test
runners. If an LSP is wired up, use it; otherwise pull a Docker image and lint
inside it.
- **Use sub-agents as subcontractors:** scoped spec in, distilled deliverable out.
Never read 10+ files sequentially; batch into agent calls.
- **Command timeouts (NON-NEGOTIABLE):** Every command that touches a remote
system MUST be wrapped with `timeout`. Hard limits: 30s for quick reads
(status, ps, ls), 120s for standard operations, 300s for deployments/pulls.
If a command hits the timeout, STOP and investigate root cause — never
blindly retry. A hung command is a failed command. Detect failure fast,
diagnose, fix, move on. Example: `timeout 120 bash tests/remote.sh vm 'cmd'`.
This applies to ALL tools — bash, docker, CLIs, sub-agents.
## Questions (NON-NEGOTIABLE)
**NEVER use a harness "question"/"ask user" tool** (structured prompts,
modal forms, tabbed questions). Banned across every project, every harness.
They are not portable, not version-controlled, and bypass the git record.
**All questions go in the current `questions-v(N).md` file** — write the
question; the human edits the answer inline in the same file. **Version up
the filename each time answers land** (v1 → v2 → v3...): create
`questions-v2.md` with resolved Q&A marked, new questions appended. This
preserves the history of each Q&A round. Synthesize resolved Q&A into
Discourse (decisions) and Redmine (work items). See `BASELINE-PROMPT.md`
§10.
## Documentation policy (IMPORTANT)
**Discourse is the canonical source of truth for all knowledge documentation.**
Knowledge docs (architecture, runbooks, references, audits, policies) have been
migrated to [community.turnsys.com](https://community.turnsys.com/c/vp-techops)
as wiki topics in the **VP TechOps** category.
All `.md` files in this repo (except `AGENTS.md` and `LICENSE`) are now
**pointers** that link to their corresponding Discourse topic. **Do not update
documentation content in git** — edit the Discourse wiki topic instead. Git
edit history no longer serves as the documentation changelog; Discourse
preserves wiki edit history automatically.
Code (scripts, configs, playbooks) still lives in git as the source of truth
for executables. Only *documentation* moved to Discourse.
## Top-level files
All `.md` files now point to Discourse. The key pointers:
| File | Points to | Discourse topic |
|------|-----------|-----------------|
| `README.md` | Project overview | [#296](https://community.turnsys.com/t/296) |
| `STATUS.md` | Ticket index + infra summary | [#297](https://community.turnsys.com/t/297) |
| `docs/docmap.md` | Documentation index | [#296](https://community.turnsys.com/t/296) |
Work tracking stays in [Redmine](https://projects.knownelement.com).
## Repository Layout ## Repository Layout
``` ```
provisioning/ Server provisioning (SetupNewSystem.sh, security, 2FA) dcinfra/ Data-center infra: PDU (powerman), serial console (console), UPS (ups)
tests/ Test suite + VM validation harness netinfra/ DNS/NTP/DHCP setup + audit; DNS cluster replication (dns-cluster-setup);
dns-cluster-setup/ Technitium DNS cluster replication switch captures (switches); DHCP config (dhcp)
k8s/ k3s cluster setup scripts (3-node HA over Tailscale) + docs/ k8s/ k3s cluster setup scripts (HA control plane over Tailscale) + docs/
powerman/ Cyclades PM10i PDU management via powerman proxmox/ Proxmox fleet docs (hardware audit, capacity, storage) + perf tuning (perf/)
console/ Serial console management (ser2net + conman) for switches awx/ Ansible AWX deployment (k3s + AWX Operator)
ups/ UPS management (NUT) for APC Smart-UPS C 1500 on pfv-tsys1 tests/ Test suite + VM validation harness + remote.sh SSH chokepoint
perf/ Proxmox perf tuning, fleet audit, iperf scripts/ Framework: git hooks, rule engine (check-rules.sh), shared lib
proxmox/ Proxmox fleet docs (hardware audit, capacity, k8s host planning) docs/ Server-build docs, docmap index, and archive
netinfra/ pfv-netinfra-01/02 DNS/NTP/DHCP setup archive/ Historical/superseded code (provisioning -> replaced by KNELIAC project)
switches/ Switch configuration captures vendor/ Vendored KNELShellFramework
awx/ Ansible AWX deployment (k3s + AWX Operator)
docs/ Server-build docs, docmap index, and archive
vendor/ Vendored KNELShellFramework
``` ```
- **Self-locating scripts**: All provisioning scripts derive their own - **Server provisioning moved to KNELIAC**: The
location via `BASH_SOURCE` and compute `PROJECT_ROOT_PATH` from it. Run [`archive/provisioning/`](archive/provisioning/) tree is historical. Active
from anywhere. server provisioning lives in the **KNELIAC** project at
- **Local configs are the source of truth**: Files in `/home/reachableceo/projects/KNELIAC`.
[`provisioning/ConfigFiles/`](provisioning/ConfigFiles/) are read with - **Non-bash files**: Some files under `archive/provisioning/Agents/` have `.sh`
`cat`/`cp`. Do NOT re-introduce `curl ${DL_ROOT}/...` downloads.
- **Non-bash files**: Some files under `provisioning/Agents/` have `.sh`
extension but are PHP (shebang `#!/usr/bin/php`). Skip in syntax checks. extension but are PHP (shebang `#!/usr/bin/php`). Skip in syntax checks.
- **SSH in Crush**: Direct ssh/scp is blocked. Use - **Remote access (NON-NEGOTIABLE):** ALL SSH/SCP to ANY host MUST go
[`tests/remote.sh`](tests/remote.sh) or through the chokepoint scripts — [`tests/remote.sh`](tests/remote.sh)
[`dns-cluster-setup/remote-dns.sh`](dns-cluster-setup/remote-dns.sh). (Proxmox hosts + all VMs) or
[`netinfra/dns-cluster-setup/remote-dns.sh`](netinfra/dns-cluster-setup/remote-dns.sh)
(DNS infra hosts: netinfra-01/02, tsrouter, netboot). NEVER call
`ssh`/`scp` directly — the harness blocks raw ssh and the command scanner
rejects it. There are no exceptions.
- **DNS names ONLY (NON-NEGOTIABLE):** NEVER use IP address literals
(neither LAN nor Tailscale IPs) in any command, script, or config.
ALWAYS use DNS names. For Proxmox hosts: `PROX_HOST=<dns-name>`. For VMs:
`VM_IP=<dns-name>`. For conman/SNMP/any tool: pass the DNS name. If a
DNS name does not resolve, fix it in DNS (Technitium) or consult the
[system inventory — Discourse #307](https://community.turnsys.com/t/307).
Do NOT fall back to IP literals. This rule eliminates the per-session
discovery tax of finding the right IP for each host.
- **How to access a production VM:**
1. Look up the DNS name in the [system inventory — Discourse #307](https://community.turnsys.com/t/307).
2. `VM_IP=<dns-name> VM_USER=root bash tests/remote.sh vm '<command>'`
3. If the name does not resolve from the workstation, use `PROX_HOST=<proxmox-node>`
and run `qm guest cmd <vmid> network-get-interfaces` (visibility only —
NOT execution) to find the Tailscale DNS name, then access via that.
## Git Policy ## Git Policy
@@ -60,53 +331,257 @@ vendor/ Vendored KNELShellFramework
justification. A script that emits any diagnostic is a protocol violation. justification. A script that emits any diagnostic is a protocol violation.
Non-bash scripts (PHP with `.sh` shebang `#!/usr/bin/php`, etc.) are exempt. Non-bash scripts (PHP with `.sh` shebang `#!/usr/bin/php`, etc.) are exempt.
## Automatic Gardening Protocol ## Redmine Tracking Policy
**Docs and code must be kept in sync.** After any work session, an agent MUST: **Redmine is the system of record for all work.** Do not track status,
checklists, or TODOs in repo files. Use Redmine tickets instead.
1. **Update [`STATUS.md`](STATUS.md)** — reflect completed work, new issues, - **URL:** https://projects.knownelement.com
changed infrastructure state. This file is human read-only; agents own it. - **Version:** Potential to Kinetic Ready (due 2026-09-30)
2. **Update [`docs/docmap.md`](docs/docmap.md)** — if a doc was added, - **Project:** Known Element Enterprises - Technology & Facility Services (id 55)
removed, or substantively changed, update the table and "Last Reviewed"
date. ### Rules
3. **Grep for stale paths** — `grep -rn 'old/path' --include='*.md'` after 1. **Every piece of work** (feature, fix, deployment, config change) gets a
any rename or restructure. Fix all references in the same commit. Redmine ticket. If one doesn't exist, create it.
4. **Verify new docs are linked** — every new `.md` file must appear in 2. **Reference tickets in docs and commits** using `[#NNN]` notation.
[`docs/docmap.md`](docs/docmap.md) and be linked from at least one other Example: `[#367] Rebuilt k3s control plane after cnode wipe`.
doc. 3. **When work completes**, update the ticket: set done ratio to 100%,
5. **If a new top-level directory was created, update ALL directory listings:** add a note describing what was done and where the code lives.
- [`README.md`](README.md) → "Directory Structure" table **NEVER close a ticket without explicit user permission.** You may
- [`AGENTS.md`](AGENTS.md) → "Repository Layout" code block SUGGEST a close when the result is clearly scoped and delivered. If
- [`AGENTS.md`](AGENTS.md) → "Key Scripts" table (if the directory has it's ambiguous whether the work is truly complete, don't suggest a
an entrypoint script) close — leave that decision to the user.
Missing any one of these is a protocol violation. 4. **Operations Status** lives on Discourse ([topic #297](https://community.turnsys.com/t/297)) — update that wiki topic if the infrastructure summary needs refreshing. The `STATUS.md` file in git is now a pointer only.
6. **Self-audit before commit.** Before committing, run: 5. **Link code to tickets** — ticket descriptions and notes should reference
``` the relevant file paths in this repo (e.g., `dcinfra/ups/`).
grep -lE 'new_dir_name' README.md AGENTS.md docs/docmap.md STATUS.md
``` ### CLI access (read + write)
Every new top-level directory must appear in all four files.
Tickets are managed via the `redmine-cli` container, invoked directly
with `docker run` (no wrapper script). Full command reference, patterns, and
the subtask escape hatch live in the CLI's own `AGENTS.md`
(`~/projects/KNEL-AIMiddleware/tooling-cli/redmine/AGENTS.md`) — read it
for anything beyond the basics.
```bash
# Connection sanity check (run first in any session):
docker run --rm --env-file ~/.creds/redmine.env \
git.knownelement.com/reachableceo/redmine-cli:latest whoami
# Your queue (project 55):
docker run --rm --env-file ~/.creds/redmine.env \
git.knownelement.com/reachableceo/redmine-cli:latest list --assigned-to-me -p 55
# Show / create / update / close:
docker run --rm --env-file ~/.creds/redmine.env \
git.knownelement.com/reachableceo/redmine-cli:latest show 367
docker run --rm --env-file ~/.creds/redmine.env \
git.knownelement.com/reachableceo/redmine-cli:latest create -p 55 -s "Subject" -d "desc"
docker run --rm --env-file ~/.creds/redmine.env \
git.knownelement.com/reachableceo/redmine-cli:latest update 367 -n "Done: committed in abc123" --done-ratio 100
docker run --rm --env-file ~/.creds/redmine.env \
git.knownelement.com/reachableceo/redmine-cli:latest close 367
```
Key IDs: project **55** (`technicaloperations`), user **5** (`reachableceo`).
Statuses: New(1), In Progress(2), Resolved(3,closed), Feedback(4), Closed(5),
Rejected(6). New subtasks go to **Feedback (4)**. Tracker **3** = Support.
**Gotcha:** `create` has no `--parent` flag — to make a subtask, use the
`python-redmine` escape hatch inside the container (see
`tooling-cli/redmine/AGENTS.md`). Always `show` a ticket before updating it.
Credentials (`REDMINE_URL`/`REDMINE_API_KEY`) live in the centralized store
at `~/.creds/redmine.env`.
## Documentation Workflow
**Discourse is the source of truth for all knowledge docs.** After any work
session, an agent MUST:
1. **Update Discourse wiki topics** — if infrastructure facts changed (new
VM, IP change, host retired), edit the relevant wiki topic at
[community.turnsys.com/c/vp-techops](https://community.turnsys.com/c/vp-techops).
2. **Update the Operations Status topic** ([#297](https://community.turnsys.com/t/297))
if tickets were opened or closed.
3. **Grep for stale paths in code** — `grep -rn 'old/path' --include='*.sh'`
after any rename or restructure. Fix all references in the same commit.
4. **If a new top-level directory was created**, update:
- `AGENTS.md` → "Repository Layout" code block
- `AGENTS.md` → "Key Scripts" table (if it has an entrypoint script)
- Create a new Discourse wiki topic for any documentation
5. **Self-audit before commit.** Code changes must be internally consistent.
Documentation changes go to Discourse, not git.
### CLI access (read + write)
Wiki topics are managed via the `discourse-cli` container, invoked directly
with `docker run` (no wrapper script). Full command reference, patterns, and
the raw-API escape hatch live in the CLI's own `AGENTS.md`
(`~/projects/KNEL-AIMiddleware/tooling-cli/discourse/AGENTS.md`) — read it
for anything beyond the basics.
```bash
# Connection sanity check (run first in any session):
docker run --rm --env-file ~/.creds/discourse.env \
git.knownelement.com/reachableceo/discourse-cli:latest whoami
# List VP TechOps topics:
docker run --rm --env-file ~/.creds/discourse.env \
git.knownelement.com/reachableceo/discourse-cli:latest ls -c vp-techops
# Show a topic / edit a wiki post (find post id via `show`):
docker run --rm --env-file ~/.creds/discourse.env \
git.knownelement.com/reachableceo/discourse-cli:latest show 297
docker run --rm --env-file ~/.creds/discourse.env \
git.knownelement.com/reachableceo/discourse-cli:latest update <post_id> -b "new markdown body"
```
VP TechOps = category **74**. Key topics: #296 (project overview), #297
(ops status), #298 (audit log). The API user is trust-level 4 but **not
admin** — admin-only ops (category creation, setting the wiki flag) will
403; surface those to the user rather than retrying. **Gotcha:**
`update`/`delete` take a post **id**, not a post number. Never create a new
topic for an update to existing knowledge — edit the wiki post in place.
Credentials (`DISCOURSE_URL`/`DISCOURSE_API_KEY`/`DISCOURSE_API_USERNAME`)
live in the centralized store at `~/.creds/discourse.env`.
## Key Scripts ## Key Scripts
| Script | Purpose | | Script | Purpose |
|--------|---------| |--------|---------|
| [`provisioning/SetupNewSystem.sh`](provisioning/SetupNewSystem.sh) | Full server provisioning | | [`scripts/check-rules.sh`](scripts/check-rules.sh) | Rule audit engine (shellcheck, image pinning, Discourse pointers, required files) |
| [`scripts/setup-hooks.sh`](scripts/setup-hooks.sh) | Install git hooks (pre-commit, pre-push) |
| [`tests/remote.sh`](tests/remote.sh) | **SSH chokepoint** — all Proxmox host + sandbox VM access routes here |
| [`netinfra/dns-cluster-setup/remote-dns.sh`](netinfra/dns-cluster-setup/remote-dns.sh) | SSH chokepoint for DNS infra hosts (netinfra-01/02, tsrouter, netboot) |
| `redmine-cli` container | Redmine CLI (ticket read/write via `docker run`; see `tooling-cli/redmine/`) |
| `discourse-cli` container | Discourse CLI (wiki topic read/write via `docker run`; see `tooling-cli/discourse/`) |
| `dns-cli` container | Technitium DNS CLI (zones, list, add, delete, search, flush; see `tooling-cli/dns/`) |
| [`tests/vm-validation.sh`](tests/vm-validation.sh) | Deploy + validate on sandbox VM | | [`tests/vm-validation.sh`](tests/vm-validation.sh) | Deploy + validate on sandbox VM |
| [`tests/run-tests.sh`](tests/run-tests.sh) | Test suite | | [`tests/run-tests.sh`](tests/run-tests.sh) | Test suite |
| [`dns-cluster-setup/setup.sh`](dns-cluster-setup/setup.sh) | DNS cluster replication | | [`netinfra/dns-cluster-setup/setup.sh`](netinfra/dns-cluster-setup/setup.sh) | DNS cluster replication |
| [`k8s/install-cp.sh`](k8s/install-cp.sh) | Bootstrap k3s HA control plane | | [`k8s/install-cp.sh`](k8s/install-cp.sh) | Bootstrap k3s HA control plane |
| [`powerman/setup.sh`](powerman/setup.sh) | Configure Cyclades PDU via powerman | | [`dcinfra/powerman/setup.sh`](dcinfra/powerman/setup.sh) | Configure Cyclades PDU via powerman |
| [`console/setup.sh`](console/setup.sh) | Configure serial console access via ser2net + conman | | [`dcinfra/console/setup.sh`](dcinfra/console/setup.sh) | Configure serial console access via ser2net + conman |
| [`ups/setup.sh`](ups/setup.sh) | Configure NUT (Network UPS Tools) for UPS monitoring | | [`dcinfra/ups/setup.sh`](dcinfra/ups/setup.sh) | Configure NUT (Network UPS Tools) for UPS monitoring |
| [`perf/deploy-tuning.sh`](perf/deploy-tuning.sh) | Deploy perf tunings | | [`proxmox/perf/deploy-tuning.sh`](proxmox/perf/deploy-tuning.sh) | Deploy perf tunings |
| [`proxmox/perf/scripts/`](proxmox/perf/scripts/) | Read-only audit: probe-storage, probe-network, conman-console, snmp-switch-audit, probe-drift, audit-vm-disks, audit-guest-io, deploy-tuned-guests |
## Switch Console Access (conman)
Switch configs are pulled via serial console through a conman + ser2net
stack on pfv-tsys4.
- **conmand server:** pfv-tsys4 (port 7890)
- **ser2net:** pfv-tsys4, TCP ports 2001-2006
(do NOT connect to ser2net directly — it conflicts with conman's
persistent sessions; always use the conman client)
- **Script:** [`proxmox/perf/scripts/conman-console.py`](proxmox/perf/scripts/conman-console.py)
— drives console sessions read-only via PTY. No expect/tcl required.
- **Command files:** [`netinfra/switches/`](netinfra/switches/) — `.cmds` files
with switch-specific show commands
- **Query available consoles:** `conman -d 100.70.77.93 -q`
```bash
# Pull a switch config (example):
CONMAN_SERVER=100.70.77.93:7890 python3 proxmox/perf/scripts/conman-console.py \
--console pfv-r5-core-01 --cmds netinfra/switches/pfv-r5-core-01.cmds
```
| Console name | TCP port | Device |
|--------------|----------|--------|
| pfv-r5-core-01 | 2001 | Dell PowerConnect 5448 (rack 5 core, mgmt+storage) |
| pfv-r3-tor-mgmt-01 | 2002 | Dell PowerConnect 5324 (rack 3 mgmt TOR) |
| pfv-r3-tor-stor-01 | 2003 | Dell PowerConnect 5324 (rack 3 storage TOR) |
| pfv-rrinfra-rtr | 2004 | Cisco router (rrinfra) |
| pfv-r2-tor-01 | 2005 | Rack 2 TOR switch |
| pfv-r6-mgmt-01 | 2006 | Rack 6 management switch |
## Mandatory: Document ALL Infrastructure Changes
**This is non-negotiable. Every infrastructure change (VM config, disk
cache, network setting, service config, storage migration) MUST be
documented BEFORE moving to the next task step — not "later" or "at the
end."**
Required for EVERY infrastructure change:
1. **Redmine ticket** — create one if none exists. Reference as `[#NNN]`.
2. **Discourse audit log** — reply to topic
[#298](https://community.turnsys.com/t/298) with a dated entry (what
changed, why, where).
3. **Discourse relevant wiki topic** — update the architecture/reference
topic if the change affects documented infrastructure facts (storage
#300, network #299, k8s #305, etc.).
4. **Git commit** — if code/config changed in the repo, commit + push
immediately per the Git Policy above.
## Tooling
- `gh`, `docker`, `jq` available on the workstation.
- No native shellcheck — use `bash tests/shellcheck.sh` (Docker wrapper
`koalaman/shellcheck:stable`). ALL scripts must pass including info-level.
- For raw API calls not covered by the Redmine/Discourse CLIs, use
`python3` inside the CLI Docker containers (escape hatch pattern in
`tooling-cli/{discourse,redmine}/AGENTS.md`).
- `curl`/`wget`/`httpie` may be blocked by some harnesses. Use the CLIs or
the python-in-Docker escape hatch for HTTP writes.
## Key Commands Quick Reference
```bash
# Tests + validation:
bash tests/run-tests.sh # test suite
bash tests/vm-validation.sh # VM validation
bash tests/shellcheck.sh # shellcheck whole repo
bash tests/shellcheck.sh path/to/*.sh # shellcheck specific files
# Redmine + Discourse sanity checks (run first in any session):
docker run --rm --env-file ~/.creds/redmine.env \
git.knownelement.com/reachableceo/redmine-cli:latest whoami
docker run --rm --env-file ~/.creds/discourse.env \
git.knownelement.com/reachableceo/discourse-cli:latest whoami
# Proxmox host access (DNS names only — never IPs):
PROX_HOST=pfv-tsys5 bash tests/remote.sh prox 'qm list'
PROX_HOST=pfv-tsys5 bash tests/remote.sh prox 'pvesm status'
# Production VM access (DNS names only — never IPs):
VM_IP=tsys-librenms VM_USER=root bash tests/remote.sh vm 'systemctl status cron'
# DNS infra access:
bash netinfra/dns-cluster-setup/remote-dns.sh netinfra01-root 'systemctl status docker'
# Switch console (read-only config pull — DNS names only):
CONMAN_SERVER=pfv-tsys4:7890 python3 proxmox/perf/scripts/conman-console.py \
--console pfv-r5-core-01 --cmds netinfra/switches/pfv-r5-core-01.cmds
```
## Credential Management
- API keys currently in `.env` files under KNEL-AIMiddleware (gitignored).
- User goal: migrate all keys to Hashicorp Vault
(`vault.knownelement.com`). No vault token present yet
(`~/.vault-token` missing). Track as high-priority TODO.
## Key Docs ## Key Docs
→ **See [`docs/docmap.md`](docs/docmap.md) for the full documentation index.** → **All documentation lives on Discourse:**
[community.turnsys.com/c/vp-techops](https://community.turnsys.com/c/vp-techops)
→ All `.md` files in this repo are pointers to Discourse topics.
**Complete Linux System Inventory:**
[Topic #307](https://community.turnsys.com/t/307) — every Linux system
(hosts + VMs + physical), with Tailscale IPs, DNS names, SSH access
status, and tuned profiles. Reference this for monitoring coverage,
access management, and hostname consistency.
## Project Context ## Project Context
Solo-founder R&D Proxmox cluster in a private residence. Shoestring budget. Solo-founder R&D Proxmox cluster in a private residence. Shoestring budget.
Production lives on a Cloudron VPS in Reston VA. See Production lives on a Cloudron VPS in Reston VA. See the
[`STATUS.md`](STATUS.md) for current state and [Operations Status topic](https://community.turnsys.com/t/297) for the ticket
[`proxmox/docs/PROJECT.md`](proxmox/docs/PROJECT.md) for the fleet report. index and infrastructure summary.
All work is tracked in [Redmine](https://projects.knownelement.com)
(version: Potential to Kinetic Ready, due 2026-09-30).
+50
View File
@@ -0,0 +1,50 @@
# Makefile — convenience dispatch to scripts/.
#
# Not required. The scripts in scripts/ are the real entry points and work
# standalone. This file just gives you short verbs if you're at a terminal.
#
# In Mode 2 (Hermes/OWUI/MCP), agents call the scripts directly or via API —
# they don't need this file.
# Project-specific overrides for check-rules.sh
export PROJECT_DOC_EXEMPT ?= AGENTS.md STATUS.md WORKING.md README.md ADOPTING.md LICENSE .env.example questions-v1.md BASELINE-PROMPT.md PATTERNS.md
export PROJECT_DISCOURSE_HOST ?= community.turnsys.com
.PHONY: setup validate fast lint test garden up down status clean help
help: ## Show available targets
@grep -E '^[a-zA-Z_-]+:.*?## ' $(MAKEFILE_LIST) | awk 'BEGIN{FS=":.*?## "}{printf " \033[36m%-12s\033[0m %s\n", $$1, $$2}'
setup: ## Install git hooks
@bash scripts/setup-hooks.sh
validate: ## Full rule audit (includes tests)
@bash scripts/check-rules.sh
fast: ## Fast rule audit (pre-commit equivalent)
@bash scripts/check-rules.sh --fast
lint: ## Lint shell scripts (shellcheck via docker)
@docker run --rm -v "$$(pwd):/mnt" koalaman/shellcheck:stable \
$$(find . -path ./.git -prune -o -path ./.tmp -prune -o -path ./vendor -prune -o -path ./node_modules -prune -o \( -name '*.sh' -o -name '*.bash' \) -print | sed 's|^\./|/mnt/|') || true
test: ## Run the test suite (override per project)
@bash scripts/test.sh
garden: ## Doc-sprawl / Discourse-migration report
@bash scripts/garden.sh
up: ## Bring up the docker-compose stack
@bash scripts/up.sh
down: ## Bring down the docker-compose stack
@bash scripts/down.sh
status: ## Show repo status snapshot
@echo "== branch =="; git branch --show-current 2>/dev/null || echo "(no branch)"
@echo "== last commit =="; git log --oneline -1 2>/dev/null || true
@echo "== working tree =="; git status --short 2>/dev/null || echo "(not a git repo)"
@echo "== STATUS.md head =="; sed -n '1,12p' STATUS.md 2>/dev/null || echo "(no STATUS.md)"
clean: ## Remove build/test artifacts (override per project)
@echo "make clean: nothing to clean — override this in your project's Makefile."
+9 -53
View File
@@ -1,54 +1,10 @@
# PFVCluster # README.md
Unified infrastructure repo for the Known Element Enterprises Proxmox R&D cluster. > **Documentation moved to Discourse — the canonical source of truth.**
>
**[→ Current Status](STATUS.md)** · **[→ Documentation Index](docs/docmap.md)** · **[→ Agent Guidelines](AGENTS.md)** > **Project overview, architecture, quick start**
>
## Directory Structure > **Read it here:** https://community.turnsys.com/t/296
>
| Directory | Description | > *Migrated 2026-08-06. This file is kept as a pointer for git-browsing context.
|-----------|-------------| > Do not update content here — edit the Discourse wiki topic instead.*
| [`provisioning/`](provisioning/) | Server provisioning (SetupNewSystem.sh, security hardening, 2FA, NTP/DNS config, SNMP, Dell OMSA) |
| [`tests/`](tests/) | Test suite + VM validation harness |
| [`dns-cluster-setup/`](dns-cluster-setup/) | Technitium DNS cluster replication scripts |
| [`k8s/`](k8s/) | k3s cluster setup scripts (3-node HA control plane over Tailscale) + co-located docs |
| [`powerman/`](powerman/) | Cyclades PM10i PDU management via powerman on pfv-tsys1 |
| [`console/`](console/) | Serial console management (ser2net + conman) for network switches on pfv-tsys4 |
| [`ups/`](ups/) | UPS management (NUT) for APC Smart-UPS C 1500 on pfv-tsys1 — upsd on Tailscale |
| [`perf/`](perf/) | Proxmox performance tuning, fleet audit, iperf, switch diagnostics |
| [`proxmox/`](proxmox/) | Proxmox fleet docs: hardware audit, capacity analysis, k8s host planning |
| [`netinfra/`](netinfra/) | pfv-netinfra-01/02 DNS/NTP/DHCP setup + audit scripts |
| [`switches/`](switches/) | Switch configuration captures |
| [`awx/`](awx/) | Ansible AWX deployment on tsys-awx (k3s + AWX Operator 2.19.1, v24.6.1) |
| [`docs/`](docs/) | Server-build docs, documentation index ([docmap](docs/docmap.md)), and archive |
| [`vendor/`](vendor/) | Vendored KNELShellFramework |
## Quick Start
### Provision a new server
```bash
sudo bash provisioning/SetupNewSystem.sh
```
### Validate on the sandbox VM
```bash
VM_ID=6000 ./tests/vm-validation.sh all
```
### Deploy DNS cluster
```bash
cd dns-cluster-setup/ && ./setup.sh all
```
### Deploy perf tunings
```bash
cd perf/ && ./deploy-tuning.sh
```
## Architecture
- **Proxmox hosts**: 7 standalone PVE installs managed via PDM
- **DNS**: Technitium (authoritative) + Pi-hole (recursive) on pfv-netinfra-01/02
- **NTP**: pfv-netinfra-01/02 (redundant, LAN IPs, stratum 2/3)
- **Production**: Cloudron VPS in Reston VA (this cluster is R&D only)
- **Backups**: Proxmox Backup Server (PBS)
+8 -187
View File
@@ -1,189 +1,10 @@
# Project Status # STATUS.md
> **Human read-only. Agents maintain this file automatically after each work > **Documentation moved to Discourse — the canonical source of truth.**
> session.** Do not edit by hand — the next agent run will overwrite it.
> **Last updated:** 2026-07-30 by Crush (GLM-5.2) — HA NUT integration live
> >
> **Capacity targets are now codified** in > **Ticket index + infrastructure summary**
> [`proxmox/docs/CAPACITY-GOALS.md`](proxmox/docs/CAPACITY-GOALS.md): 80% RAM >
> steady-state, ~50% idle CPU. The current placement analysis lives in > **Read it here:** https://community.turnsys.com/t/297
> [`proxmox/docs/AUDIT-2026-07-29.md`](proxmox/docs/AUDIT-2026-07-29.md). >
> *Migrated 2026-08-06. This file is kept as a pointer for git-browsing context.
## Current State: STABLE > Do not update content here — edit the Discourse wiki topic instead.*
The merged PFVCluster repo is fully operational across provisioning, DNS
infrastructure, Proxmox cluster ops, and k8s control plane.
## Completed Work
### Server Provisioning (validated on sectestbed-sandbox)
- [x] SetupNewSystem.sh deploys end-to-end to rc=0 (Debian 13 trixie)
- [x] Security hardening: SSH, SCAP-STIG, Wazuh, 2FA (SSH+Cockpit+Webmin)
- [x] NTP: redundant pfv-netinfra-01/02 (192.168.3.252/253), synced stratum 3
- [x] DNS resolv.conf: managed static file pointing at netinfra pair
- [x] Test suite: 5 tests (framework, safe-download, 2fa, https, system-req)
- [x] VM validation harness: git-based deploy + auto-rollback + guest-agent access
### DNS Cluster (pfv-netinfra-01/02)
- [x] Production Technitium config replicated from tailscale-router (read-only)
- [x] 124 zones on both nodes (knel.net + reverse DNS)
- [x] pfv-netinfra-01 = PRIMARY, pfv-netinfra-02 = SECONDARY
- [x] Zone replication via rsync systemd timer (every 60s)
- [x] Credentials + 2FA replicated identically to production
- [x] Both LAN IPs resolve knel.net device names + recurse externally
- [x] **DHCP migrated from pfv-netboot → netinfra-01/02** (ISC failover-peer, primary/secondary). Both `normal`, 333 active leases load-balanced. DNS+NTP now handed out as 252/253. Netboot DHCP stopped + disabled. Webmin DHCP module on both nodes.
### Proxmox Cluster Ops
- [x] 5 of 7 hosts fully performance-tuned (tsys1/3/6/7/9)
- [x] Fleet audit refreshed 2026-07-29 (consolidated, storage-corrected)
- [x] **Memory goal (≤80% actual) met fleet-wide** — highest host is tsys1 at 73%
- [x] Capacity goals codified (80% RAM / ~50% idle CPU + workload model)
- [x] **Storage topology corrected:** `local-nonprod` = S1 disk (not separate); per-host storage.cfg
- [x] cnode5 (706) deleted; cnode4 (601) stopped (delete pending)
- [x] **tsys6 S1 typo + tsys9 missing S1 — both FIXED by operator** (verified: 8/8 NFS stores active on both)
- [ ] CPU ~50%-idle goal: fleet idles 0-5% (under-utilized; chase via workloads, not hardware)
- [ ] tsys4: blocked on PCIe NIC + RAM install
- [ ] tsys5: blocked on 2nd ethernet cable + NVMe install; **also single-CPU** (CPU2 socket empty)
- [ ] tsys2: pending rebuild from Win10 to Proxmox
### Kubernetes Control Plane (k3s HA — LIVE)
- [x] 3-node k3s HA control plane deployed: cnode1/2/3 (v1.36.2+k3s1, embedded etcd)
- [x] **All traffic over Tailscale IPs** — no LAN IPs in node status or certs
- [x] All 3 cnodes tainted `control-plane:NoSchedule` (zero user workloads)
- [x] 13/13 health checks pass (verify.sh): nodes Ready, etcd quorum,
Tailscale IPs, CoreDNS, API server, workload isolation
- [x] Scripts in `k8s/`: wipe, install-cp, join-servers, post-setup, verify
- [x] Kubeconfig saved to `~/.kube/config.pfv-k8s` (gitignored, embedded certs)
- [ ] Workers (wnodes) not yet joined to this cluster
- [ ] Distro decision: **k3s chosen for regular R&D cluster**. Talos docs
preserved in `k8s/docs/` for future ITAR/classified cluster.
### PDU Management (powerman on pfv-tsys1 — LIVE)
- [x] Cyclades AlterPath PM10i (10 outlets) managed via powerman over serial
- [x] USB-DB9 adapter (Prolific pl2303) with stable udev symlink
`/dev/cyclades-pm10`
- [x] powermand listening on `127.0.0.1:10101` + Tailscale `100.121.189.98:10101`
- [x] All 10 outlets defined as `outlet-1` through `outlet-10`
- [x] Validated: outlet 10 cycled off → on (8/8 test checks passed)
- [ ] Rename outlets to match physical devices (Friday onsite)
### UPS Management (NUT on pfv-tsys1 — LIVE)
- [x] **APC Smart-UPS C 1500** fully reporting via NUT usbhid-ups + APC HID 0.100
- [x] upsd listening on Tailscale `100.121.189.98:3493` (port 3493)
- [x] Local upsmon (master) configured for graceful hypervisor shutdown
- [x] Home Assistant credentials generated (`homeassistant` user in upsd.users)
- [x] Scripts in `ups/`: discover, setup (idempotent), status
- [x] Home Assistant NUT integration **configured via REST API** (`ups/setup-ha-nut.sh`)
- [x] **Key fix:** upsd now listens on LAN IP (192.168.3.11) in addition to
Tailscale — HAOS runs Tailscale as an isolated add-on, so the HA container
cannot route to Tailscale IPs. Shared vmbr0 bridge provides instant LAN path.
- [x] **Key fix:** udev rule covers BOTH `usb` and `hidraw` subsystems (driver
opens raw `/dev/bus/usb` after setuid drop, not `/dev/hidraw`)
- [ ] **Tripp Lite UPS blocked:** driver matches TrippLite HID 0.85 subdriver
but fails reading 878-byte HID Report Descriptor (EAGAIN via USB hub).
Needs physical reseat to direct motherboard USB port or cable replacement.
Driver masked to prevent restart-loop spam.
- [ ] **Home Assistant NUT integration:** configured and live (3 sensors:
battery charge, status, status data). This APC (FW 02.2, mfg 2012) does not
report `ups.load` over USB HID — confirmed via both NUT and apcupsd tests.
No wattage for Energy Dashboard; smart plug on UPS output is the practical
path. apcupsd package conflicts with nut-server (mutually exclusive).
### Console Management (ser2net telnet(rfc2217) + conman on pfv-tsys4 — LIVE)
- [x] 7 network switch/router consoles managed via ser2net + conman
- [x] **USB enumeration problem SOLVED:** udev rules pin each adapter by
ID_PATH (physical USB port topology) to stable `/dev/consoles/<name>`
symlinks that survive reboot regardless of enumeration order
- [x] ser2net exposes all 7 consoles on telnet(rfc2217) TCP ports (2001-2007)
bound to **Tailscale IP only** (`100.70.77.93`)
- [x] conman connects to TCP ports for logging + multiplexing (7 log
files active in `/var/log/conman/`)
- [x] Both ser2net + conmand enabled via systemd (survive reboot)
- [x] conmand systemd unit created (Debian package doesn't ship one)
- [x] conmand remote access enabled (loopback=off, port 7890)
- [x] Old `/root/conmap` + manual `screen` workflow replaced
- [x] **Stair-stepping fix:** ser2net accepter changed from raw `tcp` to
`telnet(rfc2217)` so conman's telnet NVT negotiates binary mode properly
- [x] **Validated 6/7 consoles working** via conman from workstation
(core-sw01, tor3-mgmt, tor3-stor, rrinfra-rtr, r2-tor-top, subodev-torsw)
- [ ] **pfv-r2-sw (port 2007): DEAD** — zero bytes at all baud rates
(1200-115200), adapter present and mapped correctly (/dev/ttyUSB3).
Physical issue: device powered off, console cable disconnected, or
console port disabled in device config. Requires onsite investigation.
### Ansible AWX (tsys-awx — LIVE)
- [x] AWX 24.6.1 deployed via AWX Operator 2.19.1 on k3s single-node
- [x] Accessible at `http://tsys-awx.knel.net` (port 80, LoadBalancer via k3s ServiceLB)
- [x] Admin login: `admin` / `REDACTED_PASSWORD`
- [x] PostgreSQL 15 (bundled, 8 GiB PVC on local-path)
- [x] VM resized: 32→60 GB disk, swap converted to swapfile
- [x] Scripts in `awx/`: install-k3s, deploy-awx, verify-awx
- [ ] HTTPS/TLS not yet configured (currently HTTP on port 80)
- [ ] Execution environments not yet customized
### Repo Merge
- [x] KNELServerBuild merged into PFVCluster (history preserved)
- [x] Directory structure reorganized (provisioning/, tests/, perf/, docs/)
- [x] All docs gardened: links fixed, stale refs removed, tailscale.md updated
## Known Issues
| Issue | Impact | Status |
|-------|--------|--------|
| **2 of 3 k3s cnodes on tsys4** | tsys4 failure = etcd quorum lost (1 of 3) | Accepted — fundamental constraint with 3 cnodes + 2 NFS servers. Revisit after Friday (local-SSD boot for one cnode). **Deferred storage concern.** |
| **tsys1/5/9 RAM-overcommitted (alloc)** | 115%/134%/129% allocation; **actual usage 73%/37%/62% — all under 80% goal** | Migrate NFS-backed VMs to tsys6/7 (117/171 GB free actual). See [AUDIT-2026-07-29](proxmox/docs/AUDIT-2026-07-29.md) §11 |
| **tsys5 is single-CPU** | CPU2 socket unpopulated; half the documented CPU capacity | Consider seating a spare LGA771 Xeon (also activates CPU2 DIMM branch) |
| **cnode4/5 stale VMs** | cnode4 (601) stopped; cnode5 (706) deleted | cnode4 delete pending; cnode5 done ✓ |
| **Fleet CPU 0-5% idle** | Misses ~50%-idle utilization goal | Consolidate baseline; solar-gate idle PowerEdge; grow via workloads |
| **D2 IOPS hotspot** | 17 of 26 NFS-backed VMs on one spindle; S1/S4 underused | Phase 2 disk migrations: D2→S2/S3/S4/D5 (see audit §11) |
| Technitium AXFR uses port 53 (occupied by Pi-hole) | Zone transfer via rsync instead of native AXFR | Workaround in place |
| tsys4/5 hardware pending | Perf tuning incomplete on 2 hosts | Friday physical install |
## Pending (next session priorities)
> **Full phased action checklist with current/target inventory is in**
> [`proxmox/docs/AUDIT-2026-07-29.md`](proxmox/docs/AUDIT-2026-07-29.md) §11.
> Summary below. SSD/NVMe tiering is deferred to post-Friday.
**Phase 0 — Storage config fixes — DONE ✓** (tsys6 S1 typo, tsys9 S1 missing)
**Phase 1 — Cleanup:**
- Delete cnode4 (601) on tsys6 (stopped, stale)
- cnode5 (706) already deleted ✓
**Phase 2 — Disk migrations off D2 (IOPS rebalancing, 6 moves):**
- wnode-tsys5 (509): D2→S4 · tsys-awx (600): D2→D5 · hfnoc-uisp (702): D2→S4
- rr-middleware (703): D2→S4 · TCTC (704): D2→S3 · cnode2 (705): D2→S2
**Phase 3 — Host + disk migrations (RAM/CPU relief, 6 VMs):**
- tsys1→tsys7: librenms(104), pdm(105), tailscale-router(109), kali-tsys(114), workbench(117)
- tsys9→tsys6: tsys-siem(901)
**Phase 4 — Grow wnode-tsys9:** 4c/4GB → 6c/8GB (after siem leaves tsys9)
**Phase 5 — Join wnodes to k3s** (after migrations free resources)
**Other:**
- **Friday maintenance:** tsys4 (PCIe NIC + RAM), tsys5 (2nd CPU candidate, NVMe, 2nd ethernet)
- **PDU:** Rename outlets in powerman.conf (Friday onsite)
- **k8s deferred:** ETL tooling, HPC scheduler, vcluster policy, solar-aware scale-out
- **UPS:** APC Smart-UPS C 1500 operational via NUT on pfv-tsys1. HA NUT integration live (battery charge, status sensors). No ups.load over USB HID (firmware limitation, confirmed via apcupsd test). Tripp Lite blocked on HID report descriptor (USB hub/cable issue — try direct motherboard port).
## Infrastructure Summary
| Component | Details |
|-----------|---------|
| Proxmox hosts | 7 standalone PVE, managed via PDM |
| DNS primary | pfv-netinfra-01 (192.168.3.252) — Technitium + Pi-hole |
| DNS secondary | pfv-netinfra-02 (192.168.3.253) — Technitium + Pi-hole |
| DNS production | tailscale-router (read-only source of truth) |
| NTP | pfv-netinfra-01/02 (redundant, LAN IPs, stratum 2/3) |
| Sandbox VM | sectestbed-sandbox (VMID 6000 on pfv-tsys5) |
| Backup | Proxmox Backup Server (PBS) |
| **k8s control plane** | **3-node k3s HA (cnode1/2/3), all traffic over Tailscale** |
| **PDU** | **Cyclades PM10i via powerman on pfv-tsys1 (port 10101)** |
| **AWX** | **tsys-awx: AWX 24.6.1 on k3s (http://tsys-awx.knel.net:80)** |
| **Console** | **7 switch consoles via ser2net telnet(rfc2217)+conman on pfv-tsys4 (conmand:7890 on Tailscale)** |
| **UPS** | **APC Smart-UPS C 1500 via NUT on pfv-tsys1 (upsd:3493 on Tailscale)** |
| Production | Cloudron VPS, Reston VA (this cluster is R&D only) |
+11
View File
@@ -0,0 +1,11 @@
# WORKING.md — Active Session Tracker
Agent work only. The human decides when it's done.
A commit is blocked while any task below remains unchecked.
## Current Tasks
(all done — session complete)
- [x] Add ticket-gate Crush hook (blocks work without active ticket)
- [x] Document active-ticket workflow in AGENTS.md + meta template
+67
View File
@@ -0,0 +1,67 @@
#!/usr/bin/bash
# access-matrix.sh — definitive access verification across all Linux Tailscale nodes.
# Uses the correct SSH user(s) per system type, checks sudo where applicable.
# Routes through remote.sh (the only allowed ssh path).
set -u
cd /home/reachableceo/projects/PFVCluster || exit 1
# Policy-excluded systems (never attempt access)
EXCLUDE=':tsys-cloudron:pfv-bms:tsys-umbrel:tsys-ucs-01:tsys-ucs-02:stlpc-bizoffice:ultix-highside:'
# Determine the SSH user(s) for a given hostname and whether sudo is expected.
# Returns "user1:user2:...:sudoflag" where sudoflag is "yes" or "no".
users_for() {
local name="$1"
case "$name" in
pfv-tsys[0-9]) echo "root:no" ;;
*-proxmox-datacenter) echo "root:no" ;;
*-proxmox-pve) echo "root:no" ;;
*-proxmox-pbs) echo "root:no" ;;
*-proxmox-mailgw*) echo "root:no" ;;
*-proxmox-backup*) echo "root:no" ;;
tsys-ucs-*) echo "root:no" ;;
ultix-streaming) echo "root:no" ;;
stlpc-*) echo "root:labuser:no" ;;
ultix-field) echo "ultixfield:yes" ;;
subopi*) echo "subodev:yes" ;;
*) echo "localuser:yes" ;;
esac
}
check_user() {
local ip="$1" user="$2" expect_sudo="$3"
local out sudo
out=$(VM_IP="$ip" VM_USER="$user" bash tests/remote.sh vm 'echo SSHOK; id -un' </dev/null 2>&1 | grep -oE 'SSHOK|keyboard-interactive|Connection refused' | head -1)
case "$out" in
SSHOK)
if [ "$expect_sudo" = "yes" ]; then
sudo=$(VM_IP="$ip" VM_USER="$user" bash tests/remote.sh vm 'sudo -n true 2>/dev/null && echo SUDOOK || echo SUDONO' </dev/null 2>&1 | grep -oE 'SUDOOK|SUDONO' | head -1)
printf '%s(%s)' "$user" "${sudo:-?}"
else
printf '%s(ok)' "$user"
fi
;;
keyboard-interactive) printf '%s(2FA)' "$user" ;;
'Connection refused') printf '%s(NOSSH)' "$user" ;;
*) printf '%s(NOKEY)' "$user" ;;
esac
}
printf '%-32s %-16s %s\n' "NAME" "TS-IP" "ACCESS"
printf '%-32s %-16s %s\n' "----" "-----" "------"
tailscale status 2>/dev/null | awk '$4=="linux" {print $2, $1}' | sort | while read -r name ip; do
[ -n "$name" ] || continue
case "$EXCLUDE" in *":$name:"*) printf '%-32s %-16s %s\n' "$name" "$ip" "EXCLUDED"; continue;; esac
map=$(users_for "$name")
expect_sudo="${map##*:}"
users="${map%:*}"
result=""
IFS=':' read -ra user_list <<< "$users"
for u in "${user_list[@]}"; do
r=$(check_user "$ip" "$u" "$expect_sudo")
[ -z "$result" ] && result="$r" || result="$result $r"
done
printf '%-32s %-16s %s\n' "$name" "$ip" "$result"
done
+60
View File
@@ -0,0 +1,60 @@
#!/bin/sh
# agent-bootstrap.sh
#
# Run INSIDE a guest (via SSH, console, or guest-agent) to bring the
# system fully under agent management in one shot:
# 1. install + enable qemu-guest-agent (VMs only, skipped on bare metal)
# 2. push the agent SSH key to root + AGENT_USER (+ labuser if present)
# 3. grant AGENT_USER passwordless sudo
#
# AGENT_USER defaults to "localuser". Override for systems with a different
# unprivileged agent user:
# AGENT_USER=subodev bash agent-bootstrap.sh
#
# After this runs once, the agent has SSH+sudo immediately.
#
# Usage (from a root shell in the guest):
# bash agent-bootstrap.sh
# AGENT_USER=subodev bash agent-bootstrap.sh
set -eu
KEY='ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIWms/uCXnjjo4KyxHBcYI2TDHe8OZ2wle6W/0hSRQLu reachableceo@ultix-streaming'
AGENT_USER="${AGENT_USER:-localuser}"
# 1. guest-agent (skip on bare metal — no virtio-serial device)
if command -v systemd-detect-virt >/dev/null 2>&1 && \
[ "$(systemd-detect-virt --vm 2>/dev/null || echo none)" != "none" ]; then
if ! command -v qemu-ga >/dev/null 2>&1; then
if command -v apt-get >/dev/null 2>&1; then
apt-get update
DEBIAN_FRONTEND=noninteractive apt-get install -y qemu-guest-agent
elif command -v dnf >/dev/null 2>&1; then
dnf install -y qemu-guest-agent
elif command -v yum >/dev/null 2>&1; then
yum install -y qemu-guest-agent
else
echo "WARN: no supported package manager; skipping agent install" >&2
fi
fi
systemctl enable --now qemu-guest-agent 2>/dev/null || \
systemctl enable --now qemu-ga 2>/dev/null || true
fi
# 2. SSH key for root + AGENT_USER + labuser (if present)
for u in root "$AGENT_USER" labuser; do
if ! getent passwd "$u" >/dev/null 2>&1; then continue; fi
H=$(getent passwd "$u" | cut -d: -f6)
mkdir -p "$H/.ssh"; chmod 700 "$H/.ssh"
AK="$H/.ssh/authorized_keys"; touch "$AK"; chmod 600 "$AK"
grep -qF "$KEY" "$AK" || echo "$KEY" >> "$AK"
chown -R "$u": "$H/.ssh"
done
# 3. passwordless sudo for AGENT_USER only
if getent passwd "$AGENT_USER" >/dev/null 2>&1 && [ -d /etc/sudoers.d ]; then
echo "${AGENT_USER} ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/010-agent
chmod 440 /etc/sudoers.d/010-agent
fi
echo BOOTSTRAP-DONE
@@ -0,0 +1,23 @@
# 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
@@ -0,0 +1,28 @@
# PFV NFS tuning service
#
# Systemd oneshot that runs AFTER tuned.service to apply TCP buffer
# overrides. The tuned daemon's profiles (network-throughput for storage
# hosts, virtual-host for compute hosts) set 16MB TCP buffer caps which
# are too small for high-BDP NFS over LACP links. This service force-
# applies 128MB buffers after tuned has finished its configuration.
#
# Install:
# 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: all 7 Proxmox hosts (tsys1/3/4/5/6/7/9)
[Unit]
Description=PFV NFS tuning (override tuned TCP buffer caps)
After=tuned.service
Requires=tuned.service
[Service]
Type=oneshot
ExecStart=/sbin/sysctl -p /etc/sysctl.d/99-pfv-nfs.conf
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
+9
View File
@@ -0,0 +1,9 @@
<!-- Discourse: https://community.turnsys.com/t/298 -->
<!-- Redmine: https://projects.knownelement.com/issues/314 -->
# Session Handoff: 2026-08-11/12 OAM
**Full handoff lives in Redmine #314** (session summary note, 2026-08-12).
**OAM coverage matrix:** Discourse [#309](https://community.turnsys.com/t/309).
**Architecture rules:** Discourse [#303](https://community.turnsys.com/t/303).
**Open questions:** `questions-v2.md` in this repo.
+9 -95
View File
@@ -1,96 +1,10 @@
# AWX — tsys-awx.knel.net # awx/README.md
Ansible AWX deployment on a dedicated k3s single-node VM. > **Documentation moved to Discourse — the canonical source of truth.**
>
**[→ Deployment Plan](DEPLOYMENT.md)** · **[→ Status](../../STATUS.md)** > **Ansible AWX deployment on k3s**
>
## Access > **Read it here:** https://community.turnsys.com/t/302
>
| | | > *Migrated 2026-08-06. This file is kept as a pointer for git-browsing context.
|---|---| > Do not update content here — edit the Discourse wiki topic instead.*
| URL | `http://tsys-awx.knel.net` (LAN) or `http://100.91.39.53` (Tailscale) |
| Port | 80 (LoadBalancer via k3s ServiceLB) |
| User | `admin` |
| Password | `REDACTED_PASSWORD` (fleet standard) |
## Architecture
```
tsys-awx (VMID 600 on pfv-tsys6)
├── k3s v1.36.2+k3s1 (single-node, no Traefik)
├── AWX Operator 2.19.1 (manages the AWX lifecycle)
└── AWX instance "tsys-awx"
├── AWX web (UI/API)
├── AWX task (Celery workers)
├── PostgreSQL 15 (bundled, 8 GiB PVC on local-path)
├── Redis (ephemeral)
└── ServiceLB (binds port 80 to host IP)
```
## VM Resources
| Resource | Value |
|----------|-------|
| Host | pfv-tsys6 (2× Xeon E5530, 126 GB RAM) |
| vCPU | 4 (2 sockets × 2 cores) |
| RAM | 12 GB |
| Disk | 60 GB (D5 on tsys4 NFS) |
| OS | Debian 13 trixie |
| Network | 192.168.3.200 (LAN), 100.91.39.53 (Tailscale) |
## Files
| File | Purpose |
|------|---------|
| [`install-k3s.sh`](install-k3s.sh) | Install k3s single-node (no Traefik) |
| [`deploy-awx.sh`](deploy-awx.sh) | Deploy AWX Operator 2.19.1 + AWX instance |
| [`verify-awx.sh`](verify-awx.sh) | Verify deployment status + access |
| [`namespace.yaml`](namespace.yaml) | Kubernetes namespace manifest |
| [`awx-instance.yaml`](awx-instance.yaml) | AWX Custom Resource (CR) manifest |
## Deployment (from scratch)
```bash
# 1. Push scripts to VM
for f in awx/install-k3s.sh awx/deploy-awx.sh awx/verify-awx.sh \
awx/namespace.yaml awx/awx-instance.yaml; do
VM_IP=100.91.39.53 VM_USER=localuser bash tests/remote.sh vm \
"sudo tee /tmp/awx/$(basename $f) > /dev/null" < "$f"
done
# 2. Install k3s
VM_IP=100.91.39.53 VM_USER=localuser bash tests/remote.sh vm \
"sudo bash /tmp/awx/install-k3s.sh"
# 3. Deploy AWX
VM_IP=100.91.39.53 VM_USER=localuser bash tests/remote.sh vm \
"sudo bash /tmp/awx/deploy-awx.sh"
# 4. Verify
VM_IP=100.91.39.53 VM_USER=localuser bash tests/remote.sh vm \
"bash /tmp/awx/verify-awx.sh"
```
## Operating
```bash
# kubectl access
sudo k3s kubectl -n awx get pods
sudo k3s kubectl -n awx logs -f deployment/awx-operator-controller-manager
# Restart AWX instance
sudo k3s kubectl -n awx delete awx tsys-awx # operator will recreate
# Get admin password
sudo k3s kubectl -n awx get secret awx-admin-password -o jsonpath='{.data.password}' | base64 -d
```
## Known Issues
1. **kube-rbac-proxy image**: The AWX Operator 2.19.1 references
`gcr.io/kubebuilder/kube-rbac-proxy:v0.15.0` which was removed from GCR.
The deploy script patches this to `quay.io/brancz/kube-rbac-proxy:v0.15.0`.
2. **Image pulls are slow**: First deployment pulls ~2 GB of container images.
Postgres image alone is ~400 MB and takes several minutes.
3. **Disk space**: AWX + k3s uses ~10 GB of the 60 GB disk. PostgreSQL data
grows over time — monitor `df -h /`.
+65
View File
@@ -0,0 +1,65 @@
#!/usr/bin/bash
# bootstrap-all.sh — push agent SSH key + passwordless sudo to remaining systems.
#
# SSH is the ONLY approved access channel (see AGENTS.md "Access-channel
# policy: SSH only"). This script reaches systems that still allow password
# auth over sshd. Systems that reject password auth (publickey-only) cannot
# be reached this way — see the CONSOLE-ONLY section printed at the end.
#
# Two escalation methods:
# sudo → Ubuntu-style systems (no root pw; localuser has sudo)
# su → Debian-style systems (root has a password)
#
# Passes AGENT_USER so agent-bootstrap.sh targets the correct unprivileged
# user. You enter passwords interactively. Idempotent: safe to re-run.
#
# Verified state (access-matrix.sh, 2026-08-10): 68/70 non-excluded systems
# at intended access state. Only tsys-siem remains below.
set -u
cd "$(dirname "$0")" || exit 1
SCRIPT=agent-bootstrap.sh
SSH_OPTS=(-o StrictHostKeyChecking=accept-new -o ConnectTimeout=10)
run_with_sudo() {
local name="$1" ip="$2" user="$3" agent_user="${4:-localuser}"
echo "========================================"
echo " $name ($ip) — $user (sudo, agent=${agent_user})"
echo "========================================"
scp "${SSH_OPTS[@]}" "$SCRIPT" "${user}@${ip}:/tmp/" \
&& ssh -t "${SSH_OPTS[@]}" "${user}@${ip}" "sudo AGENT_USER=${agent_user} bash /tmp/$SCRIPT" \
&& echo " -> $name DONE" \
|| echo " -> $name FAILED"
echo
}
run_with_su() {
local name="$1" ip="$2" user="$3" agent_user="${4:-localuser}"
echo "========================================"
echo " $name ($ip) — $user (su, agent=${agent_user})"
echo "========================================"
scp "${SSH_OPTS[@]}" "$SCRIPT" "${user}@${ip}:/tmp/" \
&& ssh -t "${SSH_OPTS[@]}" "${user}@${ip}" "su -c 'AGENT_USER=${agent_user} bash /tmp/$SCRIPT'" \
&& echo " -> $name DONE" \
|| echo " -> $name FAILED"
echo
}
# All password-auth-reachable systems have been bootstrapped.
# Verified state (access-matrix.sh, 2026-08-10): 69/70 non-excluded systems
# at intended access state. The only remaining NOKEY (stlp-3dscanner) is
# deferred to [#417] and requires a rename + bring-online first — out of
# scope here. Run `access-matrix.sh` to re-verify at any time.
echo "All password-auth-reachable systems are bootstrapped."
echo "Remaining gap: stlp-3dscanner (deferred to [#417])."
echo "Run access-matrix.sh to re-verify."
echo "Deferred (separate ticket):"
echo " stlp-3dscanner — rename + bring online first [#417]"
echo "========================================"
echo "By design (leave alone):"
echo " sectestbed-sandbox — 2FA enforced"
echo "========================================"
echo "Excluded by policy (no SSH access):"
echo " pfv-bms (API), tsys-cloudron (prod revenue),"
echo " tsys-ucs-01/02 (API-managed), tsys-umbrel (treasury)"
echo "========================================"
-130
View File
@@ -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.
+10
View File
@@ -0,0 +1,10 @@
# dcinfra/console/README.md
> **Documentation moved to Discourse — the canonical source of truth.**
>
> **Serial console management (ser2net + conman)**
>
> **Read it here:** https://community.turnsys.com/t/301
>
> *Migrated 2026-08-06. This file is kept as a pointer for git-browsing context.
> Do not update content here — edit the Discourse wiki topic instead.*
@@ -6,7 +6,7 @@
# config files. This is the fix for the USB enumeration shift problem: # 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) # 1. udev rules pin each adapter by its STABLE ID_PATH (physical USB port)
# to a named symlink like /dev/consoles/pfv-core-sw01 # to a named symlink like /dev/consoles/pfv-r5-core-01
# 2. ser2net opens those stable symlinks and exposes them on TCP ports # 2. ser2net opens those stable symlinks and exposes them on TCP ports
# (2001, 2002, ...) bound to the Tailscale IP # (2001, 2002, ...) bound to the Tailscale IP
# 3. conman connects to those TCP ports for logging + multiplexing # 3. conman connects to those TCP ports for logging + multiplexing
@@ -121,7 +121,7 @@ for entry in "${ENTRIES[@]}"; do
{ {
echo "" echo ""
echo "# $name (TCP $tcp_port): $comment" echo "# $name (TCP $tcp_port): $comment"
echo "SUBSYSTEM==\"tty\", ENV{ID_PATH}==\"*$id_path*\", SYMLINK+=\"console/$name\"" echo "SUBSYSTEM==\"tty\", ENV{ID_PATH}==\"*$id_path*\", SYMLINK+=\"consoles/$name\""
} >> "$UDEV_RULES" } >> "$UDEV_RULES"
done done
@@ -17,13 +17,13 @@
# 2. Update the id_path_substring in this file # 2. Update the id_path_substring in this file
# 3. Run: bash console/generate-config.sh && udevadm trigger && systemctl restart ser2net conmand # 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) 2001|pfv-r5-core-01|usb-0:1.5.4.4|9600n81|Dell PowerConnect 5448 (rack 5 core switch)
2002|pfv-tor3-mgmt|usb-0:1.6.3.1|9600n81|Rack 3 management TOR switch 2002|pfv-r3-tor-mgmt-01|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 2003|pfv-r3-tor-stor-01|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) 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 2005|pfv-r2-tor-01|usb-0:1.6.3.3.3|9600n81|Rack 2 TOR switch
2006|subodev-torsw|usb-0:1.5.4.1|9600n81|Suborbital device TOR switch 2006|pfv-r6-mgmt-01|usb-0:1.5.4.1|9600n81|Rack 6 management switch
2007|pfv-r2-sw|usb-0:1.6.3.2|9600n81|Rack 2 old Dell switch # 2007|pfv-r2-sw|usb-0:1.6.3.2|9600n81|Rack 2 old Dell switch (dead, removed)
# Unassigned (no device detected): # Unassigned (no device detected):
# 2008|spare-1|usb-0:1.6.3.4|9600n81|Empty / spare # 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 # 2009|spare-2|usb-0:1.6.3.3.4|9600n81|Empty / spare
@@ -5,7 +5,7 @@
# #
# Usage: # Usage:
# bash console/query-remote.sh # list consoles # bash console/query-remote.sh # list consoles
# bash console/query-remote.sh pfv-core-sw01 # connect to a console # bash console/query-remote.sh pfv-r5-core-01 # connect to a console
# #
set -euo pipefail set -euo pipefail
@@ -52,7 +52,7 @@ if [ -z "$CONSOLE" ]; then
conman -d "${REMOTE_HOST}:${REMOTE_PORT}" -q conman -d "${REMOTE_HOST}:${REMOTE_PORT}" -q
echo "" echo ""
echo "To connect: bash $0 <console-name>" echo "To connect: bash $0 <console-name>"
echo " e.g: bash $0 pfv-core-sw01" echo " e.g: bash $0 pfv-r5-core-01"
else else
echo "" echo ""
echo "--- Connecting to: $CONSOLE ---" echo "--- Connecting to: $CONSOLE ---"
@@ -202,7 +202,7 @@ echo " ser2net owns serial devices, exposes telnet(rfc2217) TCP ports"
echo " conman connects via telnet for logging + multiplexing" echo " conman connects via telnet for logging + multiplexing"
echo "" echo ""
echo " Connect from any Tailscale workstation:" echo " Connect from any Tailscale workstation:"
echo " conman -d pfv-tsys4:7890 -f pfv-core-sw01" echo " conman -d pfv-tsys4:7890 -f pfv-r5-core-01"
echo " conman -d pfv-tsys4:7890 -q # list consoles" echo " conman -d pfv-tsys4:7890 -q # list consoles"
echo "" echo ""
echo " Direct telnet (emergency, conflicts with conman):" echo " Direct telnet (emergency, conflicts with conman):"
@@ -45,7 +45,7 @@ echo "--- 3. Trigger log capture: connect to each console briefly ---"
echo " conmand connects to all consoles on startup. Checking if logs exist..." echo " conmand connects to all consoles on startup. Checking if logs exist..."
echo "" echo ""
echo "--- 4. Log file inventory ---" 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 for name in pfv-r5-core-01 pfv-r3-tor-mgmt-01 pfv-r3-tor-stor-01 pfv-rrinfra-rtr pfv-r2-tor-01 pfv-r6-mgmt-01; do
logfile="$LOGDIR/${name}.log" logfile="$LOGDIR/${name}.log"
if [ -f "$logfile" ]; then if [ -f "$logfile" ]; then
SIZE=$(stat -c%s "$logfile" 2>/dev/null || echo 0) SIZE=$(stat -c%s "$logfile" 2>/dev/null || echo 0)
+10
View File
@@ -0,0 +1,10 @@
# dcinfra/powerman/README.md
> **Documentation moved to Discourse — the canonical source of truth.**
>
> **Cyclades PM10i PDU management via powerman**
>
> **Read it here:** https://community.turnsys.com/t/301
>
> *Migrated 2026-08-06. This file is kept as a pointer for git-browsing context.
> Do not update content here — edit the Discourse wiki topic instead.*

Some files were not shown because too many files have changed in this diff Show More