Compare commits
6
Commits
main
..
8e5b9558fe
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e5b9558fe | ||
|
|
66e7843f27 | ||
|
|
c14b48f39e | ||
|
|
18d57ea4fb | ||
|
|
89469ff028 | ||
|
|
48cb6842c6 |
@@ -1,3 +0,0 @@
|
||||
*
|
||||
!crush.json
|
||||
!.gitignore
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"hooks": {
|
||||
"PreToolUse": [
|
||||
{
|
||||
"matcher": "^(bash|edit|write|multiedit|lsp_replace_symbol|lsp_rename)$",
|
||||
"command": "./hooks/ticket-gate.sh",
|
||||
"timeout": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
# 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.
|
||||
+2
-13
@@ -1,9 +1,5 @@
|
||||
# Crush internal state (track crush.json config, ignore session data)
|
||||
.crush/crush.db
|
||||
.crush/crush.db-*
|
||||
.crush/logs/
|
||||
.crush/memory/
|
||||
.crush/active-ticket
|
||||
# Crush internal state
|
||||
.crush/
|
||||
|
||||
# OS/editor
|
||||
.DS_Store
|
||||
@@ -37,10 +33,3 @@ __pycache__/
|
||||
# Sensitive exports / runtime data
|
||||
dns-cluster-setup/.export/
|
||||
returned-logs/
|
||||
|
||||
# Kubernetes secrets (kubeconfig contains embedded client certs)
|
||||
k8s/kubeconfig.yaml
|
||||
k8s/*.token
|
||||
|
||||
# Pi-hole web UI password (real value only in on-box .env, never committed)
|
||||
netinfra/pihole/.env
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
# ShellCheck configuration for PFVCluster
|
||||
# (used when running `shellcheck` directly; tests/shellcheck.sh applies the
|
||||
# same disables via -e for consistent results under Docker)
|
||||
#
|
||||
# These checks are DISABLED because they flag intentional conventions of this
|
||||
# codebase, not bugs:
|
||||
#
|
||||
# SC1090 / SC1091 — cannot follow dynamically-computed `source` paths. The KNEL
|
||||
# framework (vendor/) and test harness source helpers via computed include
|
||||
# dirs, which shellcheck cannot resolve statically.
|
||||
# SC2029 — ssh orchestration (tests/remote.sh and perf/k8s/dns scripts)
|
||||
# deliberately builds and expands the remote command on the CLIENT side before
|
||||
# sending it. That is the whole point of the single-chokepoint remote pattern.
|
||||
disable=SC1090,SC1091,SC2029
|
||||
|
||||
# Treat external-sourced files as bash (matches #!/usr/bin/env bash framework).
|
||||
external-sources=true
|
||||
@@ -1,587 +1,63 @@
|
||||
# Agent Guidelines
|
||||
|
||||
## Agent Authority (NON-NEGOTIABLE)
|
||||
|
||||
**No work is permissible on any system without an approved Redmine ticket.
|
||||
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).
|
||||
This repo combines two formerly-separate projects:
|
||||
- **Server provisioning** (formerly KNELServerBuild): `provisioning/`, `tests/`,
|
||||
`vendor/`, `dns-cluster-setup/`
|
||||
- **Proxmox cluster ops** (formerly PFVCluster/perfopt): `perf/`, `netinfra/`,
|
||||
`switches/`, `returned-logs/`
|
||||
|
||||
## Repository Layout
|
||||
|
||||
```
|
||||
dcinfra/ Data-center infra: PDU (powerman), serial console (console), UPS (ups)
|
||||
netinfra/ DNS/NTP/DHCP setup + audit; DNS cluster replication (dns-cluster-setup);
|
||||
switch captures (switches); DHCP config (dhcp)
|
||||
k8s/ k3s cluster setup scripts (HA control plane over Tailscale) + docs/
|
||||
proxmox/ Proxmox fleet docs (hardware audit, capacity, storage) + perf tuning (perf/)
|
||||
awx/ Ansible AWX deployment (k3s + AWX Operator)
|
||||
tests/ Test suite + VM validation harness + remote.sh SSH chokepoint
|
||||
scripts/ Framework: git hooks, rule engine (check-rules.sh), shared lib
|
||||
docs/ Server-build docs, docmap index, and archive
|
||||
archive/ Historical/superseded code (provisioning -> replaced by KNELIAC project)
|
||||
vendor/ Vendored KNELShellFramework
|
||||
```
|
||||
- **Vendored framework**: `KNELShellFramework` lives at
|
||||
`vendor/git@git.knownelement.com/29418/KNEL/KNELShellFramework/`. Its includes
|
||||
are under `Framework-Includes/` there. Never assume `./Framework-Includes`
|
||||
exists relative to the repo root.
|
||||
- **Self-locating scripts**: All provisioning scripts derive their own location
|
||||
via `BASH_SOURCE` and compute `PROJECT_ROOT_PATH` from it. They must never
|
||||
depend on the current working directory. Run from anywhere.
|
||||
- **Local config files are the source of truth**: Configs in
|
||||
`provisioning/ConfigFiles/` are read with `cat`/`cp`. Do NOT re-introduce
|
||||
`curl ${DL_ROOT}/...` downloads — that CDN is deprecated.
|
||||
- **Non-bash agents**: Some files under `provisioning/Agents/` carry a `.sh`
|
||||
extension but are PHP (e.g. `mysql.sh`, shebang `#!/usr/bin/php`). Syntax
|
||||
checkers must skip these.
|
||||
- **Proxmox hosts** are standalone installs managed via **PDM** (Proxmox
|
||||
Datacenter Manager). SSH keys deployed to root on all hosts.
|
||||
- **SSH in Crush**: Direct ssh/scp is blocked in the Crush bash environment.
|
||||
Use the wrapper scripts: `tests/remote.sh`, `dns-cluster-setup/remote-dns.sh`,
|
||||
or the `deploy-check.sh` / `deploy-tuning.sh` patterns.
|
||||
|
||||
- **Server provisioning moved to KNELIAC**: The
|
||||
[`archive/provisioning/`](archive/provisioning/) tree is historical. Active
|
||||
server provisioning lives in the **KNELIAC** project at
|
||||
`/home/reachableceo/projects/KNELIAC`.
|
||||
- **Non-bash files**: Some files under `archive/provisioning/Agents/` have `.sh`
|
||||
extension but are PHP (shebang `#!/usr/bin/php`). Skip in syntax checks.
|
||||
- **Remote access (NON-NEGOTIABLE):** ALL SSH/SCP to ANY host MUST go
|
||||
through the chokepoint scripts — [`tests/remote.sh`](tests/remote.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 Commit Requirements
|
||||
|
||||
## Git Policy
|
||||
1. **Commit atomically**: each logical change its own commit.
|
||||
2. **Conventional commit format**: `feat(scope): desc`, `fix(scope): desc`,
|
||||
`docs: desc`, `refactor(scope): desc`, `test(scope): desc`, `chore: desc`.
|
||||
3. **Verbose messages**: title (50 chars max), blank line, body explaining
|
||||
WHAT and WHY, footer with attribution.
|
||||
|
||||
1. **ALWAYS commit + push. Never wait. Ever.** After each logical unit of
|
||||
work, immediately stage, commit (conventional format), and push to
|
||||
`origin/main`. Do NOT pause for review. Do NOT ask permission. Do NOT
|
||||
stop to "let the user read it first." The user reviews rendered markdown
|
||||
on Gitea AFTER it is pushed. **This rule overrides any default
|
||||
conservative commit-and-hold behavior** — if a default instruction says
|
||||
"never commit unless asked," that instruction is superseded here.
|
||||
2. **Atomic commits.** Each commit coherent on its own.
|
||||
3. **Conventional format**: `feat(scope): desc`, `fix(scope): desc`,
|
||||
`docs: desc`, `refactor(scope): desc`, `test(scope): desc`.
|
||||
4. **All shell scripts MUST pass `shellcheck` before commit.** No exceptions.
|
||||
Run it via the wrapper:
|
||||
```bash
|
||||
bash tests/shellcheck.sh # whole repo
|
||||
bash tests/shellcheck.sh ups/*.sh # specific files
|
||||
```
|
||||
This invokes `koalaman/shellcheck:stable` through Docker (no native binary
|
||||
needed). Fix every reported finding — including `info`-level — or add a
|
||||
targeted `# shellcheck disable=SCxxxx # <reason>` directive with a
|
||||
justification. A script that emits any diagnostic is a protocol violation.
|
||||
Non-bash scripts (PHP with `.sh` shebang `#!/usr/bin/php`, etc.) are exempt.
|
||||
## Autonomous Git Workflow
|
||||
|
||||
## Redmine Tracking Policy
|
||||
Agents are authorized to commit AND push autonomously. After each logical unit
|
||||
of work: stage, commit, push to `origin/main`. Group changes so each commit is
|
||||
coherent on its own.
|
||||
|
||||
**Redmine is the system of record for all work.** Do not track status,
|
||||
checklists, or TODOs in repo files. Use Redmine tickets instead.
|
||||
|
||||
- **URL:** https://projects.knownelement.com
|
||||
- **Version:** Potential to Kinetic Ready (due 2026-09-30)
|
||||
- **Project:** Known Element Enterprises - Technology & Facility Services (id 55)
|
||||
|
||||
### Rules
|
||||
1. **Every piece of work** (feature, fix, deployment, config change) gets a
|
||||
Redmine ticket. If one doesn't exist, create it.
|
||||
2. **Reference tickets in docs and commits** using `[#NNN]` notation.
|
||||
Example: `[#367] Rebuilt k3s control plane after cnode wipe`.
|
||||
3. **When work completes**, update the ticket: set done ratio to 100%,
|
||||
add a note describing what was done and where the code lives.
|
||||
**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.
|
||||
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.
|
||||
5. **Link code to tickets** — ticket descriptions and notes should reference
|
||||
the relevant file paths in this repo (e.g., `dcinfra/ups/`).
|
||||
|
||||
### CLI access (read + write)
|
||||
|
||||
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 |
|
||||
|--------|---------|
|
||||
| [`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/run-tests.sh`](tests/run-tests.sh) | Test suite |
|
||||
| [`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 |
|
||||
| [`dcinfra/powerman/setup.sh`](dcinfra/powerman/setup.sh) | Configure Cyclades PDU via powerman |
|
||||
| [`dcinfra/console/setup.sh`](dcinfra/console/setup.sh) | Configure serial console access via ser2net + conman |
|
||||
| [`dcinfra/ups/setup.sh`](dcinfra/ups/setup.sh) | Configure NUT (Network UPS Tools) for UPS monitoring |
|
||||
| [`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 |
|
||||
| `provisioning/SetupNewSystem.sh` | Full server provisioning (packages, hardening, 2FA) |
|
||||
| `tests/vm-validation.sh` | End-to-end deploy + validate on sandbox VM |
|
||||
| `tests/run-tests.sh` | Project test suite (unit/security/validation) |
|
||||
| `dns-cluster-setup/setup.sh` | Technitium DNS cluster replication |
|
||||
| `perf/deploy-check.sh` | Deploy read-only data collector to hosts |
|
||||
| `perf/deploy-tuning.sh` | Deploy perf tunings to hosts |
|
||||
| `perf/validate-fixes.sh` | Validate applied tuning changes |
|
||||
| `perf/iperf-full-matrix.sh` | Full iperf throughput suite |
|
||||
|
||||
## Switch Console Access (conman)
|
||||
## Project context
|
||||
|
||||
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
|
||||
|
||||
→ **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
|
||||
|
||||
Solo-founder R&D Proxmox cluster in a private residence. Shoestring budget.
|
||||
Production lives on a Cloudron VPS in Reston VA. See the
|
||||
[Operations Status topic](https://community.turnsys.com/t/297) for the ticket
|
||||
index and infrastructure summary.
|
||||
All work is tracked in [Redmine](https://projects.knownelement.com)
|
||||
(version: Potential to Kinetic Ready, due 2026-09-30).
|
||||
This is a solo-founder R&D Proxmox cluster in a private residence. Shoestring
|
||||
budget. Redundancy is not a concern for the R&D cluster. Backups DO matter
|
||||
(PBS in use). Production lives on a VPS in Reston VA (Cloudron). See
|
||||
`docs/PROJECT.md` for the comprehensive fleet report and `docs/TODO.md` for
|
||||
pending hardware work.
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
# 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."
|
||||
@@ -1,10 +1,70 @@
|
||||
# README.md
|
||||
# PFVCluster
|
||||
|
||||
> **Documentation moved to Discourse — the canonical source of truth.**
|
||||
>
|
||||
> **Project overview, architecture, quick start**
|
||||
>
|
||||
> **Read it here:** https://community.turnsys.com/t/296
|
||||
>
|
||||
> *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.*
|
||||
Unified infrastructure repo for the Known Element Enterprises Proxmox R&D cluster.
|
||||
Combines server provisioning, Proxmox cluster operations, and DNS infrastructure.
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
provisioning/ Server provisioning (SetupNewSystem.sh, security hardening,
|
||||
2FA, NTP/DNS config, SNMP, Dell OMSA)
|
||||
tests/ Test suite + VM validation harness
|
||||
dns-cluster-setup/ Technitium DNS cluster replication scripts
|
||||
perf/ Proxmox performance tuning, fleet audit, iperf, switch diagnostics
|
||||
netinfra/ pfv-netinfra-01/02 DNS/NTP setup + audit scripts
|
||||
switches/ Switch configuration captures
|
||||
docs/ All documentation (PROJECT.md, SECURITY.md, tailscale.md, etc.)
|
||||
vendor/ Vendored KNELShellFramework
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Provision a new server
|
||||
```bash
|
||||
sudo bash provisioning/SetupNewSystem.sh
|
||||
```
|
||||
Installs packages, applies security hardening (SSH, SCAP-STIG, 2FA, Wazuh),
|
||||
configures NTP/DNS/SNMP/syslog/postfix.
|
||||
|
||||
### Validate provisioning on the sandbox VM
|
||||
```bash
|
||||
VM_ID=6000 ./tests/vm-validation.sh all
|
||||
```
|
||||
Snapshots, deploys, runs the test suite, auto-rolls back on failure.
|
||||
|
||||
### Run the test suite
|
||||
```bash
|
||||
./tests/run-tests.sh all
|
||||
```
|
||||
|
||||
### Deploy DNS cluster setup
|
||||
```bash
|
||||
cd dns-cluster-setup/
|
||||
./setup.sh all
|
||||
```
|
||||
|
||||
### Deploy perf tunings to hosts
|
||||
```bash
|
||||
cd perf/
|
||||
./deploy-check.sh # read-only data collection
|
||||
./deploy-tuning.sh # apply sysctl/tuned/NFS tunings
|
||||
```
|
||||
|
||||
## Key Documentation
|
||||
|
||||
| Doc | Contents |
|
||||
|-----|----------|
|
||||
| `docs/PROJECT.md` | Comprehensive fleet report (7 hosts, VM inventory, storage) |
|
||||
| `docs/SECURITY.md` | Security architecture and hardening details |
|
||||
| `docs/tailscale.md` | Tailscale vs managed DNS analysis |
|
||||
| `docs/DEPLOYMENT.md` | Deployment procedures |
|
||||
| `docs/TODO.md` | Pending hardware work (tsys2/4/5) |
|
||||
| `dns-cluster-setup/README.md` | DNS cluster setup guide |
|
||||
|
||||
## 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)
|
||||
- **Production**: Cloudron VPS in Reston VA (this cluster is R&D only)
|
||||
- **Backups**: Proxmox Backup Server (PBS)
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
# STATUS.md
|
||||
|
||||
> **Documentation moved to Discourse — the canonical source of truth.**
|
||||
>
|
||||
> **Ticket index + infrastructure summary**
|
||||
>
|
||||
> **Read it here:** https://community.turnsys.com/t/297
|
||||
>
|
||||
> *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.*
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
# 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
|
||||
@@ -1,67 +0,0 @@
|
||||
#!/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
|
||||
@@ -1,60 +0,0 @@
|
||||
#!/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
|
||||
@@ -1,23 +0,0 @@
|
||||
# PFV NFS tuning sysctl overrides
|
||||
#
|
||||
# Applied AFTER tuned via pfv-nfs-tuning.service (systemd oneshot).
|
||||
# These override tuned's network-throughput/virtual-host 16MB TCP buffer
|
||||
# caps with 128MB for high-BDP NFS over 1-4 GbE LACP links.
|
||||
#
|
||||
# Install on ALL Proxmox hosts:
|
||||
# cp 99-pfv-nfs.conf /etc/sysctl.d/99-pfv-nfs.conf
|
||||
# cp pfv-nfs-tuning.service /etc/systemd/system/pfv-nfs-tuning.service
|
||||
# systemctl daemon-reload && systemctl enable --now pfv-nfs-tuning.service
|
||||
#
|
||||
# Created: 2026-07-31
|
||||
# Deployed: tsys1, tsys3, tsys4, tsys5, tsys6, tsys7, tsys9
|
||||
|
||||
net.core.rmem_max = 134217728
|
||||
net.core.wmem_max = 134217728
|
||||
net.core.rmem_default = 26214400
|
||||
net.core.wmem_default = 26214400
|
||||
net.core.netdev_max_backlog = 250000
|
||||
net.core.somaxconn = 65535
|
||||
net.ipv4.tcp_rmem = 4096 87380 134217728
|
||||
net.ipv4.tcp_wmem = 4096 65536 134217728
|
||||
net.ipv4.tcp_max_syn_backlog = 4096
|
||||
@@ -1,28 +0,0 @@
|
||||
# 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
|
||||
@@ -1,4 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# auth-cloudron-ldap.sh — placeholder module (Cloudron LDAP auth integration).
|
||||
# Intentionally empty; populated when the auth stack is deployed.
|
||||
true
|
||||
@@ -1,2 +0,0 @@
|
||||
# shellcheck shell=bash disable=SC2148 # sourced .bashrc profile fragment
|
||||
export HISTTIMEFORMAT="%m/%d/%Y %T "
|
||||
@@ -1,9 +0,0 @@
|
||||
<!-- 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.
|
||||
@@ -1,10 +0,0 @@
|
||||
# awx/README.md
|
||||
|
||||
> **Documentation moved to Discourse — the canonical source of truth.**
|
||||
>
|
||||
> **Ansible AWX deployment on k3s**
|
||||
>
|
||||
> **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.*
|
||||
@@ -1,46 +0,0 @@
|
||||
---
|
||||
# AWX namespace
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: awx
|
||||
---
|
||||
# Admin password secret — the password is 'REDACTED_PASSWORD' (fleet standard)
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: awx-admin-password
|
||||
namespace: awx
|
||||
type: Opaque
|
||||
stringData:
|
||||
password: REDACTED_PASSWORD
|
||||
---
|
||||
# AWX Custom Resource — single instance, LoadBalancer service
|
||||
apiVersion: awx.ansible.com/v1beta1
|
||||
kind: AWX
|
||||
metadata:
|
||||
name: tsys-awx
|
||||
namespace: awx
|
||||
spec:
|
||||
service_type: LoadBalancer
|
||||
ingress_type: none
|
||||
|
||||
admin_user: admin
|
||||
admin_password_secret: awx-admin-password
|
||||
|
||||
# PostgreSQL — bundled, stored on local disk via PVC (k3s local-path)
|
||||
postgres_storage_class: local-path
|
||||
postgres_storage_requirements:
|
||||
requests:
|
||||
storage: 8Gi
|
||||
postgres_resource_requirements:
|
||||
requests:
|
||||
memory: 1Gi
|
||||
|
||||
# Resource limits — fit within 12 GB host RAM
|
||||
web_resource_requirements:
|
||||
requests:
|
||||
memory: 1Gi
|
||||
task_resource_requirements:
|
||||
requests:
|
||||
memory: 1Gi
|
||||
@@ -1,85 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
###############################################################################
|
||||
# deploy-awx.sh — Deploy AWX Operator + instance on k3s.
|
||||
#
|
||||
# Prerequisites: k3s must be installed and running (install-k3s.sh).
|
||||
# Intended to run ON the target VM (tsys-awx.knel.net) as root or via sudo.
|
||||
#
|
||||
# Usage: sudo bash deploy-awx.sh
|
||||
###############################################################################
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
export KUBECONFIG="${KUBECONFIG:-/etc/rancher/k3s/k3s.yaml}"
|
||||
|
||||
OPERATOR_VERSION="${OPERATOR_VERSION:-2.19.1}"
|
||||
|
||||
echo "=========================================================="
|
||||
echo " AWX Operator deployment — version ${OPERATOR_VERSION}"
|
||||
echo "=========================================================="
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Step 1: Create namespace
|
||||
# ---------------------------------------------------------------------------
|
||||
echo ""
|
||||
echo "=== Step 1: Create namespace ==="
|
||||
kubectl apply -f "${SCRIPT_DIR}/namespace.yaml"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Step 2: Deploy AWX Operator
|
||||
# ---------------------------------------------------------------------------
|
||||
echo ""
|
||||
echo "=== Step 2: Deploy AWX Operator ${OPERATOR_VERSION} ==="
|
||||
|
||||
# Clone the operator to get kustomize manifests
|
||||
OPERATOR_DIR="/tmp/awx-operator-${OPERATOR_VERSION}"
|
||||
rm -rf "${OPERATOR_DIR}"
|
||||
git clone --branch "${OPERATOR_VERSION}" --depth 1 \
|
||||
"https://github.com/ansible/awx-operator.git" "${OPERATOR_DIR}" 2>&1 | tail -3
|
||||
|
||||
# The operator's default namespace is 'awx' — matches our setup
|
||||
# Apply the operator via kustomize (config/default has the full manifest set)
|
||||
kubectl apply -k "${OPERATOR_DIR}/config/default" 2>&1 || {
|
||||
echo "kustomize apply failed, trying raw manifests..."
|
||||
kubectl apply -f "https://raw.githubusercontent.com/ansible/awx-operator/${OPERATOR_VERSION}/deploy/awx-operator.yaml"
|
||||
}
|
||||
|
||||
# Fix kube-rbac-proxy image (gcr.io/kubebuilder/kube-rbac-proxy was removed;
|
||||
# quay.io/brancz/kube-rbac-proxy is the maintained replacement)
|
||||
echo ""
|
||||
echo "=== Patching kube-rbac-proxy image ==="
|
||||
kubectl set image deployment/awx-operator-controller-manager -n awx \
|
||||
kube-rbac-proxy=quay.io/brancz/kube-rbac-proxy:v0.15.0 2>&1 || true
|
||||
|
||||
# Scale down any old replicasets that still reference the broken image
|
||||
for rs in $(kubectl -n awx get rs -l control-plane=controller-manager -o jsonpath='{range .items[*]}{.metadata.name}{"\n"}{end}' 2>/dev/null); do
|
||||
img=$(kubectl -n awx get rs "${rs}" -o jsonpath='{.spec.template.spec.containers[?(@.name=="kube-rbac-proxy")].image}' 2>/dev/null)
|
||||
if [[ "${img}" == *"gcr.io/kubebuilder"* ]]; then
|
||||
echo "Scaling down old RS ${rs} (has broken gcr.io image)"
|
||||
kubectl -n awx scale rs "${rs}" --replicas=0 2>&1
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "Waiting for AWX Operator deployment to be ready..."
|
||||
kubectl -n awx wait --for=condition=Available deployment/awx-operator-controller-manager \
|
||||
--timeout=300s 2>&1 || {
|
||||
echo "Operator not ready yet — checking status..."
|
||||
kubectl -n awx get pods
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Step 3: Deploy AWX instance
|
||||
# ---------------------------------------------------------------------------
|
||||
echo ""
|
||||
echo "=== Step 3: Deploy AWX instance ==="
|
||||
kubectl apply -f "${SCRIPT_DIR}/awx-instance.yaml"
|
||||
|
||||
echo ""
|
||||
echo "AWX instance created. Operator will now reconcile."
|
||||
echo "This typically takes 5-10 minutes for the first deployment."
|
||||
echo ""
|
||||
echo "Monitor progress with:"
|
||||
echo " kubectl -n awx get awx tsys-awx -o jsonpath='{.status.conditions}' | jq ."
|
||||
echo " kubectl -n awx get pods -w"
|
||||
echo " kubectl -n awx logs deployment/awx-operator-controller-manager -f"
|
||||
@@ -1,44 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
###############################################################################
|
||||
# install-k3s.sh — Install k3s single-node on the tsys-awx VM.
|
||||
#
|
||||
# Intended to run ON the target VM (tsys-awx.knel.net) as root or via sudo.
|
||||
# Installs k3s without Traefik (we use NodePort/LoadBalancer directly).
|
||||
#
|
||||
# Usage: sudo bash install-k3s.sh
|
||||
###############################################################################
|
||||
set -euo pipefail
|
||||
|
||||
NODE_NAME="${NODE_NAME:-tsys-awx}"
|
||||
|
||||
echo "=========================================================="
|
||||
echo " k3s single-node install — ${NODE_NAME}"
|
||||
echo "=========================================================="
|
||||
|
||||
if command -v k3s >/dev/null 2>&1 && k3s kubectl get nodes >/dev/null 2>&1; then
|
||||
echo "k3s already installed and running. Skipping."
|
||||
k3s kubectl get nodes
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== Installing k3s (this takes 1-2 minutes) ==="
|
||||
curl -sfL https://get.k3s.io | INSTALL_K3S_EXEC="--disable=traefik --write-kubeconfig-mode=644" sh -
|
||||
|
||||
echo ""
|
||||
echo "=== Waiting for k3s node to be Ready ==="
|
||||
export KUBECONFIG=/etc/rancher/k3s/k3s.yaml
|
||||
for i in $(seq 1 30); do
|
||||
if k3s kubectl get nodes 2>/dev/null | grep -q ' Ready'; then
|
||||
echo "Node is Ready!"
|
||||
k3s kubectl get nodes
|
||||
break
|
||||
fi
|
||||
echo " waiting... (${i}/30)"
|
||||
sleep 5
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "=== k3s install complete ==="
|
||||
echo "kubeconfig: /etc/rancher/k3s/k3s.yaml"
|
||||
echo "kubectl: k3s kubectl (or set KUBECONFIG=/etc/rancher/k3s/k3s.yaml)"
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
# AWX Operator namespace
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: awx
|
||||
@@ -1,80 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
###############################################################################
|
||||
# verify-awx.sh — Verify AWX deployment status and access.
|
||||
#
|
||||
# Intended to run ON the target VM (tsys-awx.knel.net).
|
||||
# Usage: bash verify-awx.sh
|
||||
###############################################################################
|
||||
set -euo pipefail
|
||||
|
||||
export KUBECONFIG="${KUBECONFIG:-/etc/rancher/k3s/k3s.yaml}"
|
||||
|
||||
echo "=========================================================="
|
||||
echo " AWX Deployment Verification — $(date)"
|
||||
echo "=========================================================="
|
||||
|
||||
echo ""
|
||||
echo "=== 1. k3s node ==="
|
||||
kubectl get nodes
|
||||
|
||||
echo ""
|
||||
echo "=== 2. AWX pods ==="
|
||||
kubectl -n awx get pods
|
||||
|
||||
echo ""
|
||||
echo "=== 3. AWX CR status ==="
|
||||
kubectl -n awx get awx tsys-awx -o jsonpath='{range .status.conditions[*]}{.type}: {.message}{"\n"}{end}' 2>/dev/null || echo "AWX CR not found"
|
||||
|
||||
echo ""
|
||||
echo "=== 4. Services ==="
|
||||
kubectl -n awx get svc
|
||||
|
||||
echo ""
|
||||
echo "=== 5. LoadBalancer / NodePort access ==="
|
||||
LB_IP=$(kubectl -n awx get svc tsys-awx-service -o jsonpath='{.status.loadBalancer.ingress[0].ip}' 2>/dev/null || echo "")
|
||||
LB_HOST=$(kubectl -n awx get svc tsys-awx-service -o jsonpath='{.status.loadBalancer.ingress[0].hostname}' 2>/dev/null || echo "")
|
||||
NODE_PORT=$(kubectl -n awx get svc tsys-awx-service -o jsonpath='{.spec.ports[0].nodePort}' 2>/dev/null || echo "")
|
||||
|
||||
if [ -n "${LB_IP}" ]; then
|
||||
echo "LoadBalancer IP: ${LB_IP}"
|
||||
ACCESS_URL="http://${LB_IP}"
|
||||
elif [ -n "${LB_HOST}" ]; then
|
||||
echo "LoadBalancer hostname: ${LB_HOST}"
|
||||
ACCESS_URL="http://${LB_HOST}"
|
||||
elif [ -n "${NODE_PORT}" ]; then
|
||||
echo "NodePort: ${NODE_PORT}"
|
||||
ACCESS_URL="http://$(hostname -I | awk '{print $1}'):${NODE_PORT}"
|
||||
else
|
||||
echo "Service not ready yet"
|
||||
ACCESS_URL=""
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== 6. Admin password ==="
|
||||
ADMIN_PASS=$(kubectl -n awx get secret awx-admin-password -o jsonpath='{.data.password}' 2>/dev/null | base64 -d 2>/dev/null || echo "")
|
||||
if [ -n "${ADMIN_PASS}" ]; then
|
||||
echo "User: admin"
|
||||
echo "Password: ${ADMIN_PASS}"
|
||||
else
|
||||
echo "Admin password secret not found"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== 7. HTTP check ==="
|
||||
if [ -n "${ACCESS_URL}" ]; then
|
||||
echo "Testing ${ACCESS_URL}..."
|
||||
HTTP_CODE=$(curl -s -o /dev/null -w '%{http_code}' --max-time 10 "${ACCESS_URL}" 2>/dev/null || echo "failed")
|
||||
echo "HTTP response: ${HTTP_CODE}"
|
||||
if [ "${HTTP_CODE}" = "200" ] || [ "${HTTP_CODE}" = "302" ] || [ "${HTTP_CODE}" = "301" ]; then
|
||||
echo "✓ AWX is accessible at ${ACCESS_URL}"
|
||||
else
|
||||
echo "✗ AWX not yet responding (HTTP ${HTTP_CODE})"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=========================================================="
|
||||
if [ -n "${ACCESS_URL}" ]; then
|
||||
echo " AWX Access URL: ${ACCESS_URL}"
|
||||
fi
|
||||
echo "=========================================================="
|
||||
@@ -1,65 +0,0 @@
|
||||
#!/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 "========================================"
|
||||
@@ -1,10 +0,0 @@
|
||||
# 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.*
|
||||
@@ -1,103 +0,0 @@
|
||||
#!/usr/bin/bash
|
||||
# shellcheck disable=SC2010,SC2012 # diagnostic script; ls|grep/ls -la on sysfs & log dirs is intentional for human-readable output
|
||||
#
|
||||
# console/discover.sh — READ-ONLY discovery of console setup on pfv-tsys4
|
||||
#
|
||||
# Usage: PROX_HOST=pfv-tsys4 bash tests/remote.sh prox-file console/discover.sh
|
||||
#
|
||||
# This script is strictly read-only. No writes to the system.
|
||||
#
|
||||
set -uo pipefail
|
||||
|
||||
echo "============================================"
|
||||
echo " Console Setup Discovery"
|
||||
echo " Host: $(hostname)"
|
||||
echo " Date: $(date)"
|
||||
echo " READ-ONLY"
|
||||
echo "============================================"
|
||||
|
||||
echo ""
|
||||
echo "=== 1. USB devices ==="
|
||||
lsusb 2>/dev/null || echo "(lsusb not available)"
|
||||
|
||||
echo ""
|
||||
echo "=== 2. All ttyUSB* devices (with major/minor) ==="
|
||||
ls -la /dev/ttyUSB* 2>/dev/null || echo "(no /dev/ttyUSB* devices)"
|
||||
|
||||
echo ""
|
||||
echo "=== 3. USB-serial driver bindings ==="
|
||||
echo "-- pl2303 --"
|
||||
ls -la /sys/bus/usb-serial/drivers/pl2303/ 2>/dev/null | grep -v '^total\|^d\|module\|new_id\|uevent' || echo "(none)"
|
||||
echo "-- cp210x --"
|
||||
ls -la /sys/bus/usb-serial/drivers/cp210x/ 2>/dev/null | grep -v '^total\|^d\|module\|new_id\|uevent' || echo "(none)"
|
||||
echo "-- ftdi_sio --"
|
||||
ls -la /sys/bus/usb-serial/drivers/ftdi_sio/ 2>/dev/null | grep -v '^total\|^d\|module\|new_id\|uevent' || echo "(none)"
|
||||
echo "-- ch341 --"
|
||||
ls -la /sys/bus/usb-serial/drivers/ch341/ 2>/dev/null | grep -v '^total\|^d\|module\|new_id\|uevent' || echo "(none)"
|
||||
|
||||
echo ""
|
||||
echo "=== 4. USB serial adapter details (vendor/model/serial per port) ==="
|
||||
for tty in /dev/ttyUSB*; do
|
||||
[ -e "$tty" ] || continue
|
||||
echo "--- $tty ---"
|
||||
udevadm info -q all -n "$tty" 2>/dev/null | grep -E 'ID_VENDOR_ID|ID_MODEL_ID|ID_SERIAL|ID_USB_DRIVER|ID_PATH=' | sed 's/^/ /'
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "=== 5. Existing /root/conmap ==="
|
||||
if [ -f /root/conmap ]; then
|
||||
cat /root/conmap
|
||||
else
|
||||
echo "(no /root/conmap)"
|
||||
fi
|
||||
ls -la /root/conmap* 2>/dev/null
|
||||
|
||||
echo ""
|
||||
echo "=== 6. Screen sessions (running) ==="
|
||||
screen -ls 2>&1 || echo "(screen not running or not installed)"
|
||||
|
||||
echo ""
|
||||
echo "=== 7. Existing screen wrappers/scripts in /root ==="
|
||||
ls -la /root/ 2>/dev/null | grep -iE 'screen|con|console|tty|usb' || echo "(no obvious console scripts in /root)"
|
||||
|
||||
echo ""
|
||||
echo "=== 8. ser2net ==="
|
||||
which ser2net 2>/dev/null || echo "(ser2net not installed)"
|
||||
dpkg -l ser2net 2>/dev/null | tail -2 || echo "(ser2net not in dpkg)"
|
||||
cat /etc/ser2net/ser2net.yaml 2>/dev/null || cat /etc/ser2net.conf 2>/dev/null || cat /etc/ser2net/ser2net.conf 2>/dev/null || echo "(no ser2net config)"
|
||||
systemctl is-active ser2net 2>/dev/null || echo "(ser2net service not found)"
|
||||
|
||||
echo ""
|
||||
echo "=== 9. conman ==="
|
||||
which conman 2>/dev/null || echo "(conman not installed)"
|
||||
which conmand 2>/dev/null || echo "(conmand not installed)"
|
||||
dpkg -l conman 2>/dev/null | tail -2 || echo "(conman not in dpkg)"
|
||||
echo "--- /etc/conman.conf (console lines only) ---"
|
||||
grep -nE 'CONSOLE|SERVER|LOG|SERIAL|DEV|BAUD|^[^#].*name=' /etc/conman.conf 2>/dev/null | head -60 || echo "(no conman.conf or no console entries)"
|
||||
echo "--- conmand service ---"
|
||||
systemctl is-active conmand 2>/dev/null || echo "(conmand not running)"
|
||||
systemctl is-enabled conmand 2>/dev/null || echo "(conmand not enabled)"
|
||||
|
||||
echo ""
|
||||
echo "=== 10. Existing console logs ==="
|
||||
ls -la /var/log/conman/ 2>/dev/null | head -20 || echo "(no /var/log/conman)"
|
||||
ls -la /var/consoles/ 2>/dev/null | head -20 || echo "(no /var/consoles)"
|
||||
|
||||
echo ""
|
||||
echo "=== 11. udev rules for ttyUSB ==="
|
||||
grep -r ttyUSB /etc/udev/rules.d/ 2>/dev/null || echo "(no udev rules for ttyUSB)"
|
||||
grep -r 'console' /etc/udev/rules.d/ 2>/dev/null | head -10 || true
|
||||
|
||||
echo ""
|
||||
echo "=== 12. expect availability ==="
|
||||
command -v expect && expect -v 2>&1 || echo "expect: NOT installed"
|
||||
command -v socat && socat -V 2>&1 | head -1 || echo "socat: NOT installed"
|
||||
|
||||
echo ""
|
||||
echo "=== 13. Ports in use (2001-2099, 7000-7999, 7820-7899) ==="
|
||||
ss -tlnp 2>/dev/null | grep -E ':200[0-9]|:700[0-9]|:782[0-9]|:789[0-9]' || echo "(no relevant ports listening)"
|
||||
|
||||
echo ""
|
||||
echo "============================================"
|
||||
echo " Discovery complete (read-only)."
|
||||
echo "============================================"
|
||||
@@ -1,254 +0,0 @@
|
||||
#!/usr/bin/bash
|
||||
#
|
||||
# console/generate-config.sh — generate udev rules + ser2net.yaml + conman.conf
|
||||
#
|
||||
# Reads console/mapping.txt (the source of truth) and generates all three
|
||||
# config files. This is the fix for the USB enumeration shift problem:
|
||||
#
|
||||
# 1. udev rules pin each adapter by its STABLE ID_PATH (physical USB port)
|
||||
# to a named symlink like /dev/consoles/pfv-r5-core-01
|
||||
# 2. ser2net opens those stable symlinks and exposes them on TCP ports
|
||||
# (2001, 2002, ...) bound to the Tailscale IP
|
||||
# 3. conman connects to those TCP ports for logging + multiplexing
|
||||
#
|
||||
# Run this script ON the target host. It writes to:
|
||||
# /etc/udev/rules.d/99-console-ports.rules
|
||||
# /etc/ser2net.yaml
|
||||
# /etc/conman/console-consoles.conf (included by /etc/conman.conf)
|
||||
#
|
||||
# Usage:
|
||||
# PROX_HOST=pfv-tsys4 bash tests/remote.sh prox-file console/generate-config.sh
|
||||
#
|
||||
# Environment overrides:
|
||||
# MAPPING_FILE — path to mapping.txt (default: auto-detect next to this script)
|
||||
# TS_IP — Tailscale IP to bind ser2net on (default: auto-detect)
|
||||
# CONMAN_LOGDIR — conman log directory (default: /var/log/conman)
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
MAPPING_FILE="${MAPPING_FILE:-$SCRIPT_DIR/mapping.txt}"
|
||||
CONMAN_LOGDIR="${CONMAN_LOGDIR:-/var/log/conman}"
|
||||
|
||||
UDEV_RULES="/etc/udev/rules.d/99-console-ports.rules"
|
||||
SER2NET_CONF="/etc/ser2net.yaml"
|
||||
CONMAN_CONF="/etc/conman.conf"
|
||||
|
||||
echo "============================================"
|
||||
echo " Console Config Generator"
|
||||
echo " Host: $(hostname) $(date)"
|
||||
echo "============================================"
|
||||
|
||||
# --- Locate mapping file ---
|
||||
# When run via remote.sh prox-file, $0 is bash and $SCRIPT_DIR may be wrong.
|
||||
# Search common locations.
|
||||
if [ ! -f "$MAPPING_FILE" ]; then
|
||||
for candidate in \
|
||||
"/root/console/mapping.txt" \
|
||||
"/tmp/mapping.txt" \
|
||||
"$(dirname "$0")/mapping.txt"; do
|
||||
if [ -f "$candidate" ]; then
|
||||
MAPPING_FILE="$candidate"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if [ ! -f "$MAPPING_FILE" ]; then
|
||||
echo "FATAL: mapping file not found. Tried: $MAPPING_FILE"
|
||||
echo "Copy mapping.txt to the target host first."
|
||||
exit 1
|
||||
fi
|
||||
echo " Mapping file: $MAPPING_FILE"
|
||||
|
||||
# --- Auto-detect Tailscale IP ---
|
||||
if [ -z "${TS_IP:-}" ]; then
|
||||
TS_IP=$(tailscale ip -4 2>/dev/null || true)
|
||||
if [ -z "$TS_IP" ]; then
|
||||
echo "FATAL: could not auto-detect Tailscale IP. Set TS_IP manually."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
echo " Tailscale IP: $TS_IP"
|
||||
echo " ser2net will bind to: $TS_IP"
|
||||
|
||||
# --- Parse mapping file (skip comments and blank lines) ---
|
||||
echo ""
|
||||
echo "--- Parsing mapping file ---"
|
||||
ENTRIES=()
|
||||
while IFS= read -r line; do
|
||||
# Skip comments and blank lines
|
||||
line="${line%%#*}"
|
||||
line="$(echo "$line" | xargs)" # trim whitespace
|
||||
[ -z "$line" ] && continue
|
||||
ENTRIES+=("$line")
|
||||
echo " $line"
|
||||
done < "$MAPPING_FILE"
|
||||
|
||||
if [ "${#ENTRIES[@]}" -eq 0 ]; then
|
||||
echo "FATAL: no entries found in mapping file."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo " ${#ENTRIES[@]} console ports configured."
|
||||
|
||||
# ============================================================
|
||||
# 1. Generate udev rules
|
||||
# ============================================================
|
||||
echo ""
|
||||
echo "--- [1/3] Generating udev rules: $UDEV_RULES ---"
|
||||
|
||||
cat > "$UDEV_RULES" <<'UDEV_HEADER'
|
||||
# Stable symlinks for USB-DB9 console adapters
|
||||
# Generated by console/generate-config.sh
|
||||
# DO NOT EDIT — edit mapping.txt and re-run generate-config.sh
|
||||
#
|
||||
# These rules pin each adapter to a named symlink based on its physical
|
||||
# USB port path (ID_PATH), which is stable across reboots regardless of
|
||||
# enumeration order. This is the fix for the "USB adapters shift on reboot"
|
||||
# problem.
|
||||
#
|
||||
# To find the ID_PATH for a device:
|
||||
# udevadm info -q all -n /dev/ttyUSBN | grep ID_PATH
|
||||
UDEV_HEADER
|
||||
|
||||
for entry in "${ENTRIES[@]}"; do
|
||||
IFS='|' read -r tcp_port name id_path baud comment <<< "$entry"
|
||||
# Build the full ID_PATH match. The mapping stores a substring like "usb-0:1.5.4.4"
|
||||
# The actual ID_PATH is like "pci-0000:00:1a.0-usb-0:1.5.4.4:1.0"
|
||||
# We match on the substring to be portable across PCI bus changes.
|
||||
{
|
||||
echo ""
|
||||
echo "# $name (TCP $tcp_port): $comment"
|
||||
echo "SUBSYSTEM==\"tty\", ENV{ID_PATH}==\"*$id_path*\", SYMLINK+=\"consoles/$name\""
|
||||
} >> "$UDEV_RULES"
|
||||
done
|
||||
|
||||
echo " Written: $UDEV_RULES"
|
||||
echo " Symlinks: /dev/consoles/<name> for each device"
|
||||
|
||||
# ============================================================
|
||||
# 2. Generate ser2net.yaml
|
||||
# ============================================================
|
||||
echo ""
|
||||
echo "--- [2/3] Generating ser2net config: $SER2NET_CONF ---"
|
||||
|
||||
# Backup existing config if not already backed up
|
||||
if [ -f "$SER2NET_CONF" ] && [ ! -f "${SER2NET_CONF}.orig" ]; then
|
||||
cp "$SER2NET_CONF" "${SER2NET_CONF}.orig"
|
||||
echo " Backed up original to ${SER2NET_CONF}.orig"
|
||||
fi
|
||||
|
||||
{
|
||||
echo "%YAML 1.1"
|
||||
echo "---"
|
||||
echo "# ser2net configuration for pfv-tsys4 console ports"
|
||||
echo "# Generated by console/generate-config.sh on $(date)"
|
||||
echo "#"
|
||||
echo "# All ports use telnet(rfc2217) accepter so conman and telnet clients"
|
||||
echo "# negotiate proper telnet binary mode — this prevents CR stripping"
|
||||
printf '%s\n' "# and stair-stepping on devices that send \\n\\r (LF+CR) line endings."
|
||||
echo "# Ports bound to Tailscale IP ($TS_IP) for secure remote access."
|
||||
echo "#"
|
||||
echo "# Direct telnet: telnet $TS_IP 2001"
|
||||
echo "# Via conman: conman -f <name>"
|
||||
echo ""
|
||||
printf '%s\n' "define: &banner \\r\\nPFV console port \\p device \\d [\\B]\\r\\n\\r\\n"
|
||||
echo ""
|
||||
|
||||
for entry in "${ENTRIES[@]}"; do
|
||||
IFS='|' read -r tcp_port name id_path baud comment <<< "$entry"
|
||||
# ser2net connection block — telnet(rfc2217) accepter so conman and
|
||||
# telnet clients negotiate proper telnet binary mode. This prevents
|
||||
# CR stripping that occurs with raw TCP + conman's telnet NVT.
|
||||
echo "connection: &con${tcp_port}"
|
||||
echo " accepter: telnet(rfc2217),tcp,${TS_IP},${tcp_port}"
|
||||
echo " enable: on"
|
||||
echo " options:"
|
||||
echo " banner: *banner"
|
||||
echo " kickolduser: true"
|
||||
echo " telnet-brk-on-sync: true"
|
||||
echo " connector: serialdev,"
|
||||
echo " /dev/consoles/${name},"
|
||||
echo " ${baud},local"
|
||||
echo ""
|
||||
done
|
||||
} > "$SER2NET_CONF"
|
||||
|
||||
echo " Written: $SER2NET_CONF"
|
||||
echo " ${#ENTRIES[@]} TCP ports configured ($TS_IP:2001-20XX)"
|
||||
|
||||
# ============================================================
|
||||
# 3. Write conman console entries directly into conman.conf
|
||||
# ============================================================
|
||||
# conman 0.3.x does NOT support the 'include' directive, so we write
|
||||
# CONSOLE entries directly into /etc/conman.conf between idempotent markers.
|
||||
echo ""
|
||||
echo "--- [3/3] Writing conman consoles into $CONMAN_CONF ---"
|
||||
|
||||
# Ensure logdir exists
|
||||
mkdir -p "$CONMAN_LOGDIR" 2>/dev/null || true
|
||||
|
||||
# Ensure LOGDIR is set in conman.conf (server-level directive for log file paths)
|
||||
if ! grep -qiE '^\s*server\s+logdir\s*=' "$CONMAN_CONF" 2>/dev/null; then
|
||||
# Insert near the top, after the first SERVER directives
|
||||
sed -i "1i\\server logdir = \"$CONMAN_LOGDIR\"" "$CONMAN_CONF"
|
||||
echo " Added server logdir = \"$CONMAN_LOGDIR\" to $CONMAN_CONF"
|
||||
fi
|
||||
|
||||
# Ensure loopback=off so conmand is reachable over Tailscale (not localhost-only)
|
||||
if ! grep -qiE '^\s*server\s+loopback\s*=' "$CONMAN_CONF" 2>/dev/null; then
|
||||
sed -i "/^server logdir/a server loopback=off" "$CONMAN_CONF"
|
||||
echo " Added server loopback=off to $CONMAN_CONF (enables remote access)"
|
||||
fi
|
||||
|
||||
# Remove any previous auto-generated block (between markers)
|
||||
# Then append the new block
|
||||
MARKER_BEGIN="# BEGIN PFV CONSOLE DEFINITIONS (auto-generated — do not edit between markers)"
|
||||
MARKER_END="# END PFV CONSOLE DEFINITIONS"
|
||||
|
||||
# Strip old block if present
|
||||
if grep -q "$MARKER_BEGIN" "$CONMAN_CONF" 2>/dev/null; then
|
||||
sed -i "/$MARKER_BEGIN/,/$MARKER_END/d" "$CONMAN_CONF"
|
||||
echo " Removed previous console definitions."
|
||||
fi
|
||||
|
||||
# Append new block
|
||||
{
|
||||
echo ""
|
||||
echo "$MARKER_BEGIN"
|
||||
echo "# Generated by console/generate-config.sh on $(date)"
|
||||
echo "# Each console connects to a ser2net TCP port via telnet protocol."
|
||||
echo "# ser2net uses telnet(rfc2217) accepter so binary mode is negotiated"
|
||||
echo "# and CR/LF translation is handled correctly by the telnet NVT layer."
|
||||
echo "# Access: conman -f <name>"
|
||||
echo ""
|
||||
for entry in "${ENTRIES[@]}"; do
|
||||
IFS='|' read -r tcp_port name id_path baud comment <<< "$entry"
|
||||
echo "CONSOLE name=\"${name}\" dev=\"${TS_IP}:${tcp_port}\" log=\"${name}.log\" logopts=\"timestamp\""
|
||||
done
|
||||
echo "$MARKER_END"
|
||||
} >> "$CONMAN_CONF"
|
||||
|
||||
CONSOLE_COUNT=$(grep -c "^CONSOLE " "$CONMAN_CONF" 2>/dev/null || echo 0)
|
||||
echo " Written $CONSOLE_COUNT CONSOLE entries to $CONMAN_CONF"
|
||||
|
||||
# ============================================================
|
||||
# Summary
|
||||
# ============================================================
|
||||
echo ""
|
||||
echo "============================================"
|
||||
echo " Configuration generated successfully."
|
||||
echo ""
|
||||
echo " Files written:"
|
||||
echo " $UDEV_RULES ($(wc -l < "$UDEV_RULES") lines)"
|
||||
echo " $SER2NET_CONF ($(wc -l < "$SER2NET_CONF") lines)"
|
||||
echo " $CONMAN_CONF (CONSOLE entries appended between markers)"
|
||||
echo ""
|
||||
echo " Next steps:"
|
||||
echo " 1. Reload udev: udevadm control --reload-rules && udevadm trigger"
|
||||
echo " 2. Restart ser2net: systemctl restart ser2net"
|
||||
echo " 3. Start conman: systemctl enable --now conmand"
|
||||
echo " 4. Or run: bash $(basename "$0" .sh | sed 's/generate-config/setup/') .sh"
|
||||
echo "============================================"
|
||||
@@ -1,29 +0,0 @@
|
||||
# console/mapping.txt — Source of Truth for console port assignments
|
||||
#
|
||||
# Format: <tcp_port>|<name>|<id_path_substring>|<baud>|<comment>
|
||||
#
|
||||
# Delimiter is | (pipe) because ID_PATH values contain colons.
|
||||
#
|
||||
# - tcp_port: TCP port ser2net listens on (also the conman console name suffix)
|
||||
# - name: Device name (used for /dev/console/<name> symlink, conman console name)
|
||||
# - id_path_substring: Stable USB physical path from `udevadm info -q all -n /dev/ttyUSBN | grep ID_PATH`
|
||||
# These are STABLE across reboots as long as adapters aren't moved
|
||||
# to different physical USB ports.
|
||||
# - baud: Serial baud rate (9600n81 = 9600 8N1, no flow control)
|
||||
# - comment: Free-form description
|
||||
#
|
||||
# To RE-MAP after physically moving an adapter:
|
||||
# 1. Run: bash console/discover.sh (find the new ID_PATH for the device)
|
||||
# 2. Update the id_path_substring in this file
|
||||
# 3. Run: bash console/generate-config.sh && udevadm trigger && systemctl restart ser2net conmand
|
||||
#
|
||||
2001|pfv-r5-core-01|usb-0:1.5.4.4|9600n81|Dell PowerConnect 5448 (rack 5 core switch)
|
||||
2002|pfv-r3-tor-mgmt-01|usb-0:1.6.3.1|9600n81|Rack 3 management 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)
|
||||
2005|pfv-r2-tor-01|usb-0:1.6.3.3.3|9600n81|Rack 2 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 (dead, removed)
|
||||
# Unassigned (no device detected):
|
||||
# 2008|spare-1|usb-0:1.6.3.4|9600n81|Empty / spare
|
||||
# 2009|spare-2|usb-0:1.6.3.3.4|9600n81|Empty / spare
|
||||
@@ -1,62 +0,0 @@
|
||||
#!/usr/bin/bash
|
||||
#
|
||||
# console/query-remote.sh — install conman client and connect to a console
|
||||
# on pfv-tsys4 over Tailscale.
|
||||
#
|
||||
# Usage:
|
||||
# bash console/query-remote.sh # list consoles
|
||||
# bash console/query-remote.sh pfv-r5-core-01 # connect to a console
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
REMOTE_HOST="${REMOTE_HOST:-pfv-tsys4}"
|
||||
REMOTE_PORT="${REMOTE_PORT:-7890}"
|
||||
|
||||
echo "============================================"
|
||||
echo " Conman Remote Console Access"
|
||||
echo " Server: ${REMOTE_HOST}:${REMOTE_PORT} (Tailscale)"
|
||||
echo "============================================"
|
||||
|
||||
# --- 1. Install conman client if missing ---
|
||||
if ! command -v conman >/dev/null 2>&1; then
|
||||
echo ""
|
||||
echo "--- Installing conman client ---"
|
||||
if sudo -n true 2>/dev/null; then
|
||||
sudo apt-get update -qq && sudo apt-get install -y -qq conman
|
||||
else
|
||||
echo " Passwordless sudo not available. Please run:"
|
||||
echo " sudo apt-get update && sudo apt-get install -y conman"
|
||||
echo " Then re-run this script."
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo " conman client already installed."
|
||||
fi
|
||||
|
||||
# --- 2. Verify connectivity ---
|
||||
echo ""
|
||||
echo "--- Connectivity check ---"
|
||||
if timeout 3 bash -c "echo > /dev/tcp/${REMOTE_HOST}/${REMOTE_PORT}" 2>/dev/null; then
|
||||
echo " [OK] ${REMOTE_HOST}:${REMOTE_PORT} reachable"
|
||||
else
|
||||
echo " [FAIL] Cannot reach ${REMOTE_HOST}:${REMOTE_PORT}"
|
||||
echo " Is Tailscale up? Is conmand running on ${REMOTE_HOST}?"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- 3. List or connect ---
|
||||
CONSOLE="${1:-}"
|
||||
if [ -z "$CONSOLE" ]; then
|
||||
echo ""
|
||||
echo "--- Available consoles ---"
|
||||
conman -d "${REMOTE_HOST}:${REMOTE_PORT}" -q
|
||||
echo ""
|
||||
echo "To connect: bash $0 <console-name>"
|
||||
echo " e.g: bash $0 pfv-r5-core-01"
|
||||
else
|
||||
echo ""
|
||||
echo "--- Connecting to: $CONSOLE ---"
|
||||
echo " Escape sequence: &. (to disconnect)"
|
||||
echo ""
|
||||
conman -d "${REMOTE_HOST}:${REMOTE_PORT}" -f "$CONSOLE"
|
||||
fi
|
||||
@@ -1,217 +0,0 @@
|
||||
#!/usr/bin/bash
|
||||
# shellcheck disable=SC2010 # diagnostic; ls|grep on /dev listing is intentional
|
||||
#
|
||||
# console/setup.sh — deploy console management on pfv-tsys4
|
||||
#
|
||||
# Orchestrates the full setup:
|
||||
# 1. Ensures ser2net + conman are installed
|
||||
# 2. Copies mapping.txt to the target host (if running remotely)
|
||||
# 3. Runs generate-config.sh to produce udev rules + ser2net.yaml + conman.conf
|
||||
# 4. Reloads udev, creates /dev/consoles/ symlinks
|
||||
# 5. Restarts ser2net (TCP ports on Tailscale IP)
|
||||
# 6. Enables + starts conmand (logging + multiplexing)
|
||||
# 7. Verifies
|
||||
#
|
||||
# This script is IDEMPOTENT — safe to run multiple times.
|
||||
#
|
||||
# Usage:
|
||||
# PROX_HOST=pfv-tsys4 bash tests/remote.sh prox-file console/setup.sh
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
|
||||
echo "============================================"
|
||||
echo " Console Management Setup"
|
||||
echo " Host: $(hostname) $(date)"
|
||||
echo "============================================"
|
||||
|
||||
# --- 1. Install dependencies ---
|
||||
echo ""
|
||||
echo "--- [1/7] Checking dependencies ---"
|
||||
NEED_INSTALL=()
|
||||
dpkg -l ser2net 2>/dev/null | grep -q '^ii' && echo " ser2net: installed" || NEED_INSTALL+=(ser2net)
|
||||
dpkg -l conman 2>/dev/null | grep -q '^ii' && echo " conman: installed" || NEED_INSTALL+=(conman)
|
||||
|
||||
if [ "${#NEED_INSTALL[@]}" -gt 0 ]; then
|
||||
echo " Installing: ${NEED_INSTALL[*]}"
|
||||
apt-get update -qq
|
||||
apt-get install -y -qq "${NEED_INSTALL[@]}"
|
||||
else
|
||||
echo " All dependencies present."
|
||||
fi
|
||||
|
||||
# --- 2. Ensure mapping file is available ---
|
||||
echo ""
|
||||
echo "--- [2/7] Locating mapping file ---"
|
||||
|
||||
MAPPING_FILE=""
|
||||
for candidate in \
|
||||
"$SCRIPT_DIR/mapping.txt" \
|
||||
"$(dirname "$0")/mapping.txt" \
|
||||
"/root/console/mapping.txt" \
|
||||
"/tmp/mapping.txt"; do
|
||||
if [ -f "$candidate" ]; then
|
||||
MAPPING_FILE="$candidate"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "$MAPPING_FILE" ]; then
|
||||
echo "FATAL: mapping.txt not found. Copy it to the target host."
|
||||
exit 1
|
||||
fi
|
||||
echo " Using: $MAPPING_FILE"
|
||||
|
||||
# --- 3. Generate configs ---
|
||||
echo ""
|
||||
echo "--- [3/7] Generating configs ---"
|
||||
export MAPPING_FILE
|
||||
bash "$(dirname "$0")/generate-config.sh" 2>&1 || bash "$SCRIPT_DIR/generate-config.sh" 2>&1 || {
|
||||
echo "FATAL: generate-config.sh failed."
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- 4. Reload udev + create symlinks ---
|
||||
echo ""
|
||||
echo "--- [4/7] Reloading udev rules ---"
|
||||
udevadm control --reload-rules
|
||||
# Try trigger first (works on some systems)
|
||||
for tty in /sys/class/tty/ttyUSB*; do
|
||||
[ -e "$tty" ] && udevadm trigger --action=add "$tty" 2>/dev/null || true
|
||||
done
|
||||
# Also try writing to uevent (forces udev reprocessing)
|
||||
for tty in /sys/class/tty/ttyUSB*; do
|
||||
[ -e "$tty/uevent" ] && echo "add" > "$tty/uevent" 2>/dev/null || true
|
||||
done
|
||||
sleep 2
|
||||
|
||||
# FALLBACK: if udev symlinks don't exist (common when devices are already
|
||||
# discovered — udev trigger doesn't always re-create symlinks for existing
|
||||
# devices), create them manually by matching ID_PATH. The udev rules will
|
||||
# handle future boots/hotplugs automatically.
|
||||
if [ ! -d /dev/consoles ] || [ -z "$(ls /dev/consoles/ 2>/dev/null)" ]; then
|
||||
echo " udev trigger didn't create symlinks. Creating manually..."
|
||||
mkdir -p /dev/consoles
|
||||
while IFS= read -r line; do
|
||||
line="${line%%#*}"
|
||||
line="$(echo "$line" | xargs)"
|
||||
[ -z "$line" ] && continue
|
||||
IFS='|' read -r _ name id_path _ _ <<< "$line"
|
||||
# Find the ttyUSB whose ID_PATH contains the mapping's id_path substring
|
||||
for tty in /dev/ttyUSB*; do
|
||||
[ -e "$tty" ] || continue
|
||||
DEV_IDPATH=$(udevadm info -q property -n "$tty" 2>/dev/null | grep ^ID_PATH= | cut -d= -f2)
|
||||
if echo "$DEV_IDPATH" | grep -q "$id_path"; then
|
||||
ln -sf "$tty" "/dev/consoles/$name"
|
||||
echo " ln -s $tty -> /dev/consoles/$name"
|
||||
break
|
||||
fi
|
||||
done
|
||||
done < "$MAPPING_FILE"
|
||||
fi
|
||||
|
||||
echo " Stable symlinks:"
|
||||
ls -la /dev/consoles/ 2>/dev/null | grep -v '^total\|^d' | sed 's/^/ /' || echo " (none created)"
|
||||
|
||||
# Verify each symlink resolves
|
||||
echo ""
|
||||
echo " Symlink verification:"
|
||||
while IFS= read -r line; do
|
||||
line="${line%%#*}"
|
||||
line="$(echo "$line" | xargs)"
|
||||
[ -z "$line" ] && continue
|
||||
IFS='|' read -r _ name id_path _ _ <<< "$line"
|
||||
if [ -e "/dev/consoles/$name" ]; then
|
||||
TARGET=$(readlink -f "/dev/consoles/$name")
|
||||
echo " [OK] /dev/consoles/$name -> $TARGET"
|
||||
else
|
||||
echo " [MISSING] /dev/consoles/$name (adapter unplugged or ID_PATH changed)"
|
||||
fi
|
||||
done < "$MAPPING_FILE"
|
||||
|
||||
# --- 5. Restart ser2net ---
|
||||
echo ""
|
||||
echo "--- [5/7] Restarting ser2net ---"
|
||||
systemctl enable ser2net
|
||||
systemctl restart ser2net
|
||||
sleep 2
|
||||
|
||||
if systemctl is-active --quiet ser2net; then
|
||||
echo " ser2net is running (telnet rfc2217 accepters)."
|
||||
TS_IP=$(tailscale ip -4 2>/dev/null || echo "127.0.0.1")
|
||||
echo " Listening ports:"
|
||||
ss -tlnp | grep ser2net | grep -oE "${TS_IP}:[0-9]+" | sort -t: -k2 -n | sed 's/^/ /'
|
||||
else
|
||||
echo " WARNING: ser2net failed to start. Checking journal..."
|
||||
journalctl -u ser2net --no-pager -n 20
|
||||
fi
|
||||
|
||||
# --- 6. Enable + start conmand ---
|
||||
echo ""
|
||||
echo "--- [6/7] Starting conmand ---"
|
||||
|
||||
# conman package on Debian may not ship a systemd unit. Create one if missing.
|
||||
if ! systemctl cat conmand >/dev/null 2>&1; then
|
||||
echo " No systemd unit for conmand — creating one..."
|
||||
cat > /etc/systemd/system/conmand.service <<'CONMAND_UNIT'
|
||||
[Unit]
|
||||
Description=ConMan (Console Manager)
|
||||
After=network.target ser2net.service
|
||||
Requires=ser2net.service
|
||||
|
||||
[Service]
|
||||
Type=forking
|
||||
ExecStart=/usr/sbin/conmand -c /etc/conman.conf
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
CONMAND_UNIT
|
||||
systemctl daemon-reload
|
||||
echo " Created /etc/systemd/system/conmand.service"
|
||||
fi
|
||||
|
||||
# Kill any manually-started conmand first
|
||||
pkill -x conmand 2>/dev/null || true
|
||||
sleep 1
|
||||
|
||||
systemctl enable conmand 2>/dev/null || true
|
||||
systemctl restart conmand 2>/dev/null || true
|
||||
sleep 2
|
||||
|
||||
if systemctl is-active --quiet conmand; then
|
||||
echo " conmand is running."
|
||||
echo " Consoles:"
|
||||
conman -q 2>&1 | sed 's/^/ /' || true
|
||||
else
|
||||
echo " WARNING: conmand failed to start. Checking journal..."
|
||||
journalctl -u conmand --no-pager -n 20 2>/dev/null || true
|
||||
# Try manual start as fallback
|
||||
echo " Attempting manual start..."
|
||||
/usr/sbin/conmand -c /etc/conman.conf 2>&1 || true
|
||||
fi
|
||||
|
||||
# --- 7. Summary ---
|
||||
echo ""
|
||||
echo "--- [7/7] Setup complete ---"
|
||||
echo ""
|
||||
echo " ser2net + conman architecture (telnet rfc2217):"
|
||||
echo " ser2net owns serial devices, exposes telnet(rfc2217) TCP ports"
|
||||
echo " conman connects via telnet for logging + multiplexing"
|
||||
echo ""
|
||||
echo " Connect from any Tailscale workstation:"
|
||||
echo " conman -d pfv-tsys4:7890 -f pfv-r5-core-01"
|
||||
echo " conman -d pfv-tsys4:7890 -q # list consoles"
|
||||
echo ""
|
||||
echo " Direct telnet (emergency, conflicts with conman):"
|
||||
echo " ssh pfv-tsys4 'systemctl stop conmand'"
|
||||
echo " telnet pfv-tsys4 2001"
|
||||
echo " ssh pfv-tsys4 'systemctl start conmand'"
|
||||
echo ""
|
||||
echo " To regenerate after changing mapping.txt:"
|
||||
echo " bash generate-config.sh"
|
||||
echo " udevadm trigger"
|
||||
echo " systemctl restart ser2net conmand"
|
||||
echo "============================================"
|
||||
@@ -1,89 +0,0 @@
|
||||
#!/usr/bin/bash
|
||||
# shellcheck disable=SC2012,SC2001 # diagnostic script; ls -la listings and sed line-prefixing are intentional
|
||||
#
|
||||
# console/validate-conman.sh — verify conman can actually reach devices via
|
||||
# ser2net TCP ports and is capturing log output to files.
|
||||
#
|
||||
# This tests the real data path: conman → TCP 200X → ser2net → /dev/consoles/X → device
|
||||
#
|
||||
set -uo pipefail
|
||||
|
||||
TS_IP=$(tailscale ip -4)
|
||||
LOGDIR="/var/log/conman"
|
||||
|
||||
echo "============================================"
|
||||
echo " Conman Data Path + Log Validation"
|
||||
echo " Host: $(hostname) TS IP: $TS_IP"
|
||||
echo "============================================"
|
||||
|
||||
echo ""
|
||||
echo "--- 1. conman.conf log settings ---"
|
||||
grep -E "logdir|LOGDIR|^GLOBAL LOG" /etc/conman.conf 2>/dev/null | grep -v "^#" || echo " (no explicit logdir — defaults to /var/log/conman)"
|
||||
echo " Log dir: $LOGDIR"
|
||||
ls -la "$LOGDIR"/ 2>/dev/null | head -15 || echo " ($LOGDIR does not exist yet)"
|
||||
|
||||
echo ""
|
||||
echo "--- 2. CONSOLE entries: each has a log= directive? ---"
|
||||
# Extract the auto-generated block and check each CONSOLE line has log=
|
||||
sed -n '/BEGIN PFV CONSOLE/,/END PFV CONSOLE/p' /etc/conman.conf | grep "^CONSOLE" | while read -r line; do
|
||||
name=$(echo "$line" | sed -n 's/.*name="\([^"]*\)".*/\1/p')
|
||||
if echo "$line" | grep -q 'log='; then
|
||||
logfile=$(echo "$line" | sed -n 's/.*log="\([^"]*\)".*/\1/p')
|
||||
echo " [OK] $name → log=$logfile"
|
||||
else
|
||||
echo " [FAIL] $name has NO log= directive"
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "--- 3. Trigger log capture: connect to each console briefly ---"
|
||||
# conman -e changes the escape char. We use -j (join, read-only) with a timeout.
|
||||
# Actually, conman doesn't have a built-in "connect for N seconds" — but conmand
|
||||
# connects to each device ON STARTUP and keeps the connection open for logging.
|
||||
# The log files should already be created. Let's check timestamps.
|
||||
|
||||
echo " conmand connects to all consoles on startup. Checking if logs exist..."
|
||||
echo ""
|
||||
echo "--- 4. Log file inventory ---"
|
||||
for name in pfv-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"
|
||||
if [ -f "$logfile" ]; then
|
||||
SIZE=$(stat -c%s "$logfile" 2>/dev/null || echo 0)
|
||||
MTIME=$(stat -c%y "$logfile" 2>/dev/null | cut -d. -f1)
|
||||
echo " [OK] $logfile ($SIZE bytes, modified $MTIME)"
|
||||
else
|
||||
echo " [MISSING] $logfile — conmand may not be writing yet"
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "--- 5. conmand connection status (journal) ---"
|
||||
# conmand logs connection attempts/errors to syslog
|
||||
journalctl -u conmand --no-pager -n 50 2>/dev/null | grep -iE "connect|error|fail|console|refused|timeout" | tail -15 || echo " (no relevant journal entries)"
|
||||
|
||||
echo ""
|
||||
echo "--- 6. Verify ser2net is proxying data (telnet rfc2217) ---"
|
||||
echo " Probing TCP $TS_IP:2007 for data..."
|
||||
RESPONSE=$(timeout 3 bash -c "printf '\r\r' | nc -w 2 $TS_IP 2007 2>/dev/null" | tr -cd '[:print:][:space:]' | head -5)
|
||||
if [ -n "$RESPONSE" ]; then
|
||||
echo " [OK] Data flowing through ser2net TCP 2007:"
|
||||
echo "$RESPONSE" | sed 's/^/ /'
|
||||
else
|
||||
echo " (no immediate response — device may need more interaction)"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "--- 7. Check if conmand has open connections to ser2net ports ---"
|
||||
CONMAND_PID=$(pgrep -x conmand 2>/dev/null || echo "")
|
||||
if [ -n "$CONMAND_PID" ]; then
|
||||
echo " conmand PID: $CONMAND_PID"
|
||||
echo " Open connections to ser2net (expect 7 to 100.x:200X):"
|
||||
ss -tnp 2>/dev/null | grep "pid=$CONMAND_PID" | grep -oE "100\.[0-9.]+:200[0-9]" | sort | sed 's/^/ /'
|
||||
COUNT=$(ss -tnp 2>/dev/null | grep "pid=$CONMAND_PID" | grep -c ":200")
|
||||
echo " Total conmand→ser2net connections: $COUNT (expect 7)"
|
||||
else
|
||||
echo " [FAIL] conmand not running"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "============================================"
|
||||
@@ -1,10 +0,0 @@
|
||||
# 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.*
|
||||
@@ -1,68 +0,0 @@
|
||||
#!/usr/bin/bash
|
||||
#
|
||||
# powerman/discover.sh — gather USB-DB9 adapter + powerman state on a host
|
||||
#
|
||||
# Usage: PROX_HOST=pfv-tsys1 bash tests/remote.sh prox-file powerman/discover.sh
|
||||
#
|
||||
set -uo pipefail
|
||||
|
||||
echo "============================================"
|
||||
echo " PDU / Powerman Discovery"
|
||||
echo " Host: $(hostname)"
|
||||
echo " Date: $(date)"
|
||||
echo "============================================"
|
||||
|
||||
echo ""
|
||||
echo "=== 1. USB devices ==="
|
||||
lsusb 2>/dev/null || echo "(lsusb not available)"
|
||||
|
||||
echo ""
|
||||
echo "=== 2. USB-Serial adapters (ttyUSB*) ==="
|
||||
ls -la /dev/ttyUSB* 2>/dev/null || echo "(no /dev/ttyUSB* devices)"
|
||||
|
||||
echo ""
|
||||
echo "=== 3. USB-Serial kernel modules ==="
|
||||
lsmod | grep -iE 'usbserial|ftdi|pl2303|cp210|ch34|cdc_acm' 2>/dev/null || echo "(no relevant modules loaded)"
|
||||
|
||||
echo ""
|
||||
echo "=== 4. dmesg for USB serial (last 30 lines) ==="
|
||||
dmesg | grep -iE 'ttyUSB|usbserial|ftdi|pl2303|cp210|ch34|converter' | tail -30 2>/dev/null || echo "(no dmesg matches)"
|
||||
|
||||
echo ""
|
||||
echo "=== 5. All serial devices ==="
|
||||
ls -la /dev/ttyS* /dev/ttyUSB* /dev/ttyACM* 2>/dev/null || echo "(no serial devices found)"
|
||||
|
||||
echo ""
|
||||
echo "=== 6. Powerman installed? ==="
|
||||
dpkg -l powerman 2>/dev/null || echo "(powerman not installed)"
|
||||
which powerman 2>/dev/null || echo "(powerman binary not found)"
|
||||
which powermand 2>/dev/null || echo "(powermand binary not found)"
|
||||
|
||||
echo ""
|
||||
echo "=== 7. Powerman config files ==="
|
||||
ls -la /etc/powerman/ 2>/dev/null || echo "(no /etc/powerman/ directory)"
|
||||
ls -la /etc/powerman/*.dev 2>/dev/null || echo "(no .dev files)"
|
||||
cat /etc/powerman/powerman.conf 2>/dev/null || echo "(no powerman.conf)"
|
||||
|
||||
echo ""
|
||||
echo "=== 8. Available powerman device definitions ==="
|
||||
ls /usr/share/powerman/*.dev 2>/dev/null || ls /etc/powerman/*.dev 2>/dev/null || echo "(no device definitions found)"
|
||||
|
||||
echo ""
|
||||
echo "=== 9. Powermand service status ==="
|
||||
systemctl status powerman 2>/dev/null | head -10 || echo "(powerman service not found)"
|
||||
|
||||
echo ""
|
||||
echo "=== 10. Serial port test (quick probe of /dev/ttyUSB0) ==="
|
||||
if [ -e /dev/ttyUSB0 ]; then
|
||||
stty -F /dev/ttyUSB0 2>/dev/null && echo "(port exists and is configurable)" || echo "(port exists but stty failed)"
|
||||
# Try to read any pending output
|
||||
timeout 2 cat /dev/ttyUSB0 2>/dev/null | head -5 || echo "(no immediate output from port)"
|
||||
else
|
||||
echo "(no /dev/ttyUSB0)"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "============================================"
|
||||
echo " Discovery complete."
|
||||
echo "============================================"
|
||||
@@ -1,41 +0,0 @@
|
||||
#!/usr/bin/bash
|
||||
# powerman/identify-outlets.sh — flash each PDU outlet sequentially for physical tracing
|
||||
#
|
||||
# Run this from the workstation. It flashes each outlet one at a time so you
|
||||
# can walk the rack and see which device's LED blinks. Write down the mapping,
|
||||
# then run rename-outlets.sh with that mapping.
|
||||
#
|
||||
# Usage:
|
||||
# bash dcinfra/powerman/identify-outlets.sh
|
||||
#
|
||||
# On Friday: run this, walk the rack, note which outlet → which device.
|
||||
set -uo pipefail
|
||||
PROX_HOST="${PROX_HOST:-pfv-tsys1}"
|
||||
REMOTE_SH="$(cd "$(dirname "$0")/../.." && pwd)/tests/remote.sh"
|
||||
|
||||
echo "PDU Outlet Identification — Flash Sequence"
|
||||
echo "============================================"
|
||||
echo "Each outlet will flash for 5 seconds. Walk the rack and note the device."
|
||||
echo "Press Enter to start..."
|
||||
read -r
|
||||
|
||||
for i in $(seq 1 10); do
|
||||
echo "--- Outlet $i: FLASHING (5s) ---"
|
||||
PROX_HOST="$PROX_HOST" bash "$REMOTE_SH" prox "powerman -f outlet-$i" </dev/null 2>/dev/null
|
||||
sleep 5
|
||||
PROX_HOST="$PROX_HOST" bash "$REMOTE_SH" prox "powerman -u outlet-$i" </dev/null 2>/dev/null
|
||||
echo " Outlet $i → ? (write it down)"
|
||||
echo ""
|
||||
[ "$i" -lt 10 ] && { echo "Press Enter for next outlet..."; read -r; }
|
||||
done
|
||||
|
||||
echo "============================================"
|
||||
echo "Done. Now create your mapping file and run:"
|
||||
echo " bash dcinfra/powerman/rename-outlets.sh"
|
||||
echo ""
|
||||
echo "Format: outlet-number:new-name (one per line)"
|
||||
echo "Example:"
|
||||
echo " 1:pfv-tsys1"
|
||||
echo " 2:pfv-tsys3"
|
||||
echo " ..."
|
||||
echo "============================================"
|
||||
@@ -1,65 +0,0 @@
|
||||
#!/usr/bin/bash
|
||||
#
|
||||
# powerman/query-remote.sh — install powerman client locally and query
|
||||
# the Cyclades PDU running on pfv-tsys1 over Tailscale.
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
REMOTE_HOST="${REMOTE_HOST:-pfv-tsys1}"
|
||||
REMOTE_PORT="${REMOTE_PORT:-10101}"
|
||||
|
||||
echo "============================================"
|
||||
echo " Powerman Remote PDU Query"
|
||||
echo " Server: ${REMOTE_HOST}:${REMOTE_PORT} (Tailscale)"
|
||||
echo "============================================"
|
||||
|
||||
# --- 1. Install powerman client if missing ---
|
||||
if ! command -v powerman >/dev/null 2>&1; then
|
||||
echo ""
|
||||
echo "--- Installing powerman client ---"
|
||||
if sudo -n true 2>/dev/null; then
|
||||
sudo apt-get update -qq && sudo apt-get install -y -qq powerman
|
||||
else
|
||||
echo " Passwordless sudo not available. Please run this command in a terminal:"
|
||||
echo ""
|
||||
echo " sudo apt-get update && sudo apt-get install -y powerman"
|
||||
echo ""
|
||||
echo " Then re-run this script."
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo " powerman client already installed."
|
||||
fi
|
||||
|
||||
# --- 2. Verify connectivity ---
|
||||
echo ""
|
||||
echo "--- Connectivity check ---"
|
||||
if timeout 3 bash -c "echo > /dev/tcp/${REMOTE_HOST}/${REMOTE_PORT}" 2>/dev/null; then
|
||||
echo " [OK] ${REMOTE_HOST}:${REMOTE_PORT} reachable"
|
||||
else
|
||||
echo " [FAIL] Cannot reach ${REMOTE_HOST}:${REMOTE_PORT}"
|
||||
echo " Is Tailscale up? Is powermand running on ${REMOTE_HOST}?"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
export POWERMAN_SERVER="${REMOTE_HOST}:${REMOTE_PORT}"
|
||||
|
||||
# --- 3. List outlets ---
|
||||
echo ""
|
||||
echo "--- Outlets ---"
|
||||
powerman -h "${REMOTE_HOST}:${REMOTE_PORT}" -l
|
||||
|
||||
# --- 4. Query status ---
|
||||
echo ""
|
||||
echo "--- Status ---"
|
||||
powerman -h "${REMOTE_HOST}:${REMOTE_PORT}" -q
|
||||
|
||||
echo ""
|
||||
echo "============================================"
|
||||
echo " Done."
|
||||
echo ""
|
||||
echo " To control an outlet from this workstation:"
|
||||
echo " powerman -h ${REMOTE_HOST}:${REMOTE_PORT} -0 outlet-10 # off"
|
||||
echo " powerman -h ${REMOTE_HOST}:${REMOTE_PORT} -1 outlet-10 # on"
|
||||
echo " powerman -h ${REMOTE_HOST}:${REMOTE_PORT} -c outlet-10 # cycle"
|
||||
echo "============================================"
|
||||
@@ -1,52 +0,0 @@
|
||||
#!/usr/bin/bash
|
||||
# powerman/rename-outlets.sh — rename PDU outlets in powerman.conf
|
||||
#
|
||||
# Takes a mapping file (outlet-number:new-name, one per line) and rewrites
|
||||
# the node entries in /etc/powerman/powerman.conf on pfv-tsys1, then
|
||||
# restarts powermand.
|
||||
#
|
||||
# Usage:
|
||||
# bash dcinfra/powerman/rename-outlets.sh <mapping-file>
|
||||
#
|
||||
# Example mapping file:
|
||||
# 1:pfv-tsys1
|
||||
# 2:pfv-tsys3
|
||||
# 3:pfv-tsys4
|
||||
# ...
|
||||
set -euo pipefail
|
||||
|
||||
PROX_HOST="${PROX_HOST:-pfv-tsys1}"
|
||||
REMOTE_SH="$(cd "$(dirname "$0")/../.." && pwd)/tests/remote.sh"
|
||||
MAP_FILE="${1:-}"
|
||||
|
||||
if [ -z "$MAP_FILE" ] || [ ! -f "$MAP_FILE" ]; then
|
||||
echo "Usage: $0 <mapping-file>"
|
||||
echo " Format: outlet-number:new-name (one per line)"
|
||||
echo " Run identify-outlets.sh first to get the mapping."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Build the new node lines
|
||||
NODE_LINES=""
|
||||
while IFS=: read -r num name; do
|
||||
[ -z "$num" ] && continue
|
||||
NODE_LINES+="node \"$name\" \"cyclades-pm10\" \"$num\""$'\n'
|
||||
done < "$MAP_FILE"
|
||||
|
||||
# Send to tsys1: backup conf, write new node section, restart powermand
|
||||
PROX_HOST="$PROX_HOST" bash "$REMOTE_SH" prox-file - <<REMOTE_SCRIPT
|
||||
set -euo pipefail
|
||||
cp /etc/powerman/powerman.conf /etc/powerman/powerman.conf.bak.\$(date +%Y%m%d-%H%M%S)
|
||||
|
||||
# Strip existing node lines and append new ones
|
||||
grep -v '^node "outlet-' /etc/powerman/powerman.conf > /tmp/powerman.conf.new
|
||||
cat >> /tmp/powerman.conf.new <<'NODES'
|
||||
$(echo -n "$NODE_LINES")
|
||||
NODES
|
||||
mv /tmp/powerman.conf.new /etc/powerman/powerman.conf
|
||||
systemctl restart powerman
|
||||
sleep 1
|
||||
powerman -l
|
||||
REMOTE_SCRIPT
|
||||
|
||||
echo "PDU outlets renamed. Verify with: PROX_HOST=$PROX_HOST bash $REMOTE_SH prox 'powerman -q'"
|
||||
@@ -1,181 +0,0 @@
|
||||
#!/usr/bin/bash
|
||||
#
|
||||
# powerman/setup.sh — idempotent powerman setup for Cyclades PM10i PDU
|
||||
#
|
||||
# Creates a stable udev symlink for the USB-DB9 adapter, writes powerman.conf
|
||||
# with 10 outlet nodes, and enables + starts powermand.
|
||||
#
|
||||
# This script is designed to be run ON the target host (pfv-tsys1) as root.
|
||||
# It is idempotent: safe to run multiple times.
|
||||
#
|
||||
# Usage:
|
||||
# PROX_HOST=pfv-tsys1 bash tests/remote.sh prox-file powerman/setup.sh
|
||||
#
|
||||
# Override defaults via environment variables:
|
||||
# PDU_SERIAL — USB adapter serial (default: BJAAb144J07)
|
||||
# PDU_VENDOR — USB vendor ID (default: 067b)
|
||||
# PDU_DEV_NAME — udev symlink name (default: cyclades-pm10)
|
||||
# PDU_BAUD — serial baud rate (default: 9600,8n1)
|
||||
# PDU_TYPE — powerman spec type (default: pm10)
|
||||
# PDU_OUTLETS — number of outlets (default: 10)
|
||||
# PDU_LISTEN — powermand listen (default: 0.0.0.0:10101)
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
# --- Config (overridable via env) ---
|
||||
PDU_SERIAL="${PDU_SERIAL:-BJAAb144J07}"
|
||||
PDU_VENDOR="${PDU_VENDOR:-067b}"
|
||||
PDU_DEV_NAME="${PDU_DEV_NAME:-cyclades-pm10}"
|
||||
PDU_BAUD="${PDU_BAUD:-9600,8n1}"
|
||||
PDU_TYPE="${PDU_TYPE:-pm10}"
|
||||
PDU_OUTLETS="${PDU_OUTLETS:-10}"
|
||||
PDU_LISTEN="${PDU_LISTEN:-}" # Auto-detect Tailscale IP if empty
|
||||
|
||||
UDEV_RULE="/etc/udev/rules.d/99-cyclades-pdu.rules"
|
||||
POWERMAN_CONF="/etc/powerman/powerman.conf"
|
||||
DEV_FILE="/etc/powerman/cyclades-pm10.dev"
|
||||
|
||||
# --- Auto-detect Tailscale IP for listen address ---
|
||||
if [ -z "$PDU_LISTEN" ]; then
|
||||
TS_IP=$(tailscale ip -4 2>/dev/null || true)
|
||||
if [ -n "$TS_IP" ]; then
|
||||
PDU_LISTEN="${TS_IP}:10101"
|
||||
echo " Auto-detected Tailscale IP: $TS_IP"
|
||||
else
|
||||
PDU_LISTEN="127.0.0.1:10101"
|
||||
echo " WARNING: No Tailscale IP detected. Defaulting to localhost."
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "============================================"
|
||||
echo " Powerman PDU Setup"
|
||||
echo " Host: $(hostname)"
|
||||
echo " PDU: Cyclades PM${PDU_OUTLETS}i"
|
||||
echo " Adapter serial: $PDU_SERIAL"
|
||||
echo " Device symlink: /dev/$PDU_DEV_NAME"
|
||||
echo " Listen: $PDU_LISTEN (Tailscale only)"
|
||||
echo "============================================"
|
||||
|
||||
# --- 1. Ensure powerman is installed ---
|
||||
echo ""
|
||||
echo "--- [1/5] Checking powerman installation ---"
|
||||
if ! dpkg -l powerman 2>/dev/null | grep -q '^ii'; then
|
||||
echo " Installing powerman from Debian repo..."
|
||||
apt-get update -qq && apt-get install -y -qq powerman
|
||||
else
|
||||
echo " Powerman already installed: $(dpkg -l powerman | awk '/^ii/{print $3}')"
|
||||
fi
|
||||
|
||||
# --- 2. Create udev rule for stable device name ---
|
||||
echo ""
|
||||
echo "--- [2/5] Creating udev rule for USB-DB9 adapter ---"
|
||||
cat > "$UDEV_RULE" <<UDEV
|
||||
# Stable symlink for Cyclades PM10i PDU USB-DB9 adapter
|
||||
# Generated by powerman/setup.sh
|
||||
SUBSYSTEM=="tty", ATTRS{idVendor}=="${PDU_VENDOR}", ATTRS{serial}=="${PDU_SERIAL}", GROUP="dialout", MODE="0660", SYMLINK+="${PDU_DEV_NAME}"
|
||||
UDEV
|
||||
echo " Written: $UDEV_RULE"
|
||||
|
||||
# Trigger udev to create the symlink now
|
||||
udevadm control --reload-rules 2>/dev/null || true
|
||||
udevadm trigger --subsystem-match=tty 2>/dev/null || true
|
||||
sleep 1
|
||||
|
||||
if [ -e "/dev/${PDU_DEV_NAME}" ]; then
|
||||
echo " Device symlink active: /dev/${PDU_DEV_NAME} -> $(readlink -f "/dev/${PDU_DEV_NAME}")"
|
||||
else
|
||||
echo " WARNING: /dev/${PDU_DEV_NAME} not found yet. Adapter may be unplugged."
|
||||
echo " Falling back to /dev/ttyUSB* discovery..."
|
||||
# Try to find any ttyUSB device as fallback
|
||||
for tty in /dev/ttyUSB*; do
|
||||
if [ -e "$tty" ]; then
|
||||
echo " Found: $tty (using as fallback)"
|
||||
PDU_DEV_NAME="$(basename "$tty")"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# --- 2b. Ensure powermand user can access the serial device ---
|
||||
echo ""
|
||||
echo "--- [2b/5] Fixing serial device permissions ---"
|
||||
if id powerman >/dev/null 2>&1; then
|
||||
if id powerman | grep -qv dialout; then
|
||||
usermod -aG dialout powerman
|
||||
echo " Added 'powerman' user to 'dialout' group"
|
||||
else
|
||||
echo " 'powerman' already in 'dialout' group"
|
||||
fi
|
||||
else
|
||||
echo " (no powerman user — service may run as root)"
|
||||
fi
|
||||
|
||||
# --- 3. Write powerman.conf ---
|
||||
echo ""
|
||||
echo "--- [3/5] Writing powerman.conf ---"
|
||||
|
||||
# Build node definitions
|
||||
NODES=""
|
||||
for i in $(seq 1 "$PDU_OUTLETS"); do
|
||||
NODES+="node \"outlet-${i}\" \"${PDU_DEV_NAME}\" \"${i}\"\n"
|
||||
done
|
||||
|
||||
cat > "$POWERMAN_CONF" <<PMCONF
|
||||
# Powerman configuration for Cyclades PM${PDU_OUTLETS}i PDU
|
||||
# Generated by powerman/setup.sh on $(date)
|
||||
# Device: /dev/${PDU_DEV_NAME} (USB-DB9 adapter serial ${PDU_SERIAL})
|
||||
|
||||
# Listen on localhost (for local admin) and Tailscale (for remote access)
|
||||
listen "127.0.0.1:10101"
|
||||
listen "${PDU_LISTEN}"
|
||||
|
||||
# Device specification for Cyclades PM10
|
||||
include "${DEV_FILE}"
|
||||
|
||||
# The PDU device (serial-attached)
|
||||
device "${PDU_DEV_NAME}" "${PDU_TYPE}" "/dev/${PDU_DEV_NAME}" "${PDU_BAUD}"
|
||||
|
||||
# Outlet nodes (rename these to match attached devices when onsite)
|
||||
$(printf '%b' "$NODES")
|
||||
PMCONF
|
||||
|
||||
echo " Written: $POWERMAN_CONF"
|
||||
echo " Nodes defined: outlet-1 through outlet-${PDU_OUTLETS}"
|
||||
|
||||
# --- 4. Restart powermand ---
|
||||
echo ""
|
||||
echo "--- [4/5] Restarting powermand ---"
|
||||
systemctl enable powerman 2>/dev/null || true
|
||||
systemctl restart powerman 2>/dev/null || true
|
||||
sleep 2
|
||||
|
||||
if systemctl is-active --quiet powerman; then
|
||||
echo " powermand is running."
|
||||
else
|
||||
echo " WARNING: powermand failed to start. Check journalctl -u powerman"
|
||||
journalctl -u powerman --no-pager -n 20 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# --- 5. Verify ---
|
||||
echo ""
|
||||
echo "--- [5/5] Verification ---"
|
||||
echo ""
|
||||
echo " powerman -l (list all outlets):"
|
||||
powerman -l 2>&1 || echo "(powerman -l failed)"
|
||||
|
||||
echo ""
|
||||
echo " powerman -q (query status):"
|
||||
powerman -q 2>&1 || echo "(powerman -q failed — PDU may need a moment)"
|
||||
|
||||
echo ""
|
||||
echo "============================================"
|
||||
echo " Setup complete."
|
||||
echo ""
|
||||
echo " Outlet names are generic (outlet-1 ... outlet-${PDU_OUTLETS})."
|
||||
echo " Rename them in ${POWERMAN_CONF} when onsite to match attached devices."
|
||||
echo ""
|
||||
echo " Test: powerman -0 outlet-10 (off)"
|
||||
echo " powerman -1 outlet-10 (on)"
|
||||
echo " powerman -c outlet-10 (cycle)"
|
||||
echo " powerman -q (status)"
|
||||
echo "============================================"
|
||||
@@ -1,33 +0,0 @@
|
||||
#!/usr/bin/bash
|
||||
#
|
||||
# powerman/status.sh — quick PDU status check
|
||||
#
|
||||
# Usage:
|
||||
# PROX_HOST=pfv-tsys1 bash tests/remote.sh prox-file powerman/status.sh
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
echo "============================================"
|
||||
echo " Cyclades PM10i PDU Status"
|
||||
echo " Host: $(hostname) $(date)"
|
||||
echo "============================================"
|
||||
|
||||
echo ""
|
||||
echo "=== Service ==="
|
||||
systemctl is-active powerman 2>/dev/null && echo "(running)" || echo "(stopped)"
|
||||
|
||||
echo ""
|
||||
echo "=== Device ==="
|
||||
ls -la /dev/cyclades-pm10 2>/dev/null || echo "(no /dev/cyclades-pm10 symlink)"
|
||||
|
||||
echo ""
|
||||
echo "=== Outlets ==="
|
||||
powerman -l 2>&1
|
||||
|
||||
echo ""
|
||||
echo "=== Power Status ==="
|
||||
powerman -q 2>&1
|
||||
|
||||
echo ""
|
||||
echo "=== Temperature ==="
|
||||
powerman -T 2>&1 || echo "(temperature not available)"
|
||||
@@ -1,126 +0,0 @@
|
||||
#!/usr/bin/bash
|
||||
#
|
||||
# powerman/test-pdu.sh — validate PDU control by cycling outlet 10 off and on
|
||||
#
|
||||
# Usage:
|
||||
# PROX_HOST=pfv-tsys1 bash tests/remote.sh prox-file powerman/test-pdu.sh
|
||||
#
|
||||
# Override: OUTLET=10 (which outlet to test)
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
OUTLET="${OUTLET:-10}"
|
||||
NODE="outlet-${OUTLET}"
|
||||
PASS=0; FAIL=0
|
||||
ok() { echo " [PASS] $1"; PASS=$((PASS+1)); }
|
||||
fail() { echo " [FAIL] $1"; FAIL=$((FAIL+1)); }
|
||||
|
||||
echo "============================================"
|
||||
echo " PDU Control Validation"
|
||||
echo " Host: $(hostname)"
|
||||
echo " Test: cycle outlet ${OUTLET} (off → wait → on)"
|
||||
echo "============================================"
|
||||
|
||||
# --- 0. Powermand running? ---
|
||||
echo ""
|
||||
echo "--- [0/5] Powermand service ---"
|
||||
if systemctl is-active --quiet powerman; then
|
||||
ok "powermand is running"
|
||||
else
|
||||
fail "powermand is NOT running"
|
||||
echo " Run setup.sh first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- 1. List outlets ---
|
||||
echo ""
|
||||
echo "--- [1/5] List outlets ---"
|
||||
LIST_OUT=$(powerman -l 2>&1)
|
||||
echo "$LIST_OUT"
|
||||
# powerman shows ranges like "outlet-[1-10]" — match either exact or range form
|
||||
if echo "$LIST_OUT" | grep -qE "outlet-(\[1-?10\]|${OUTLET}\b)"; then
|
||||
ok "Outlet '${NODE}' is defined"
|
||||
else
|
||||
fail "Outlet '${NODE}' not found in powerman -l"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- 2. Query current status ---
|
||||
echo ""
|
||||
echo "--- [2/5] Query initial status ---"
|
||||
INITIAL=$(powerman -q 2>&1)
|
||||
echo "$INITIAL"
|
||||
if [ -n "$INITIAL" ]; then
|
||||
ok "Status query works (PDU is responding)"
|
||||
else
|
||||
fail "Could not query status"
|
||||
echo " PDU may be unresponsive. Check serial connection."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- 3. Turn OFF outlet ---
|
||||
echo ""
|
||||
echo "--- [3/5] Turn OFF outlet ${OUTLET} ---"
|
||||
if powerman -0 "$NODE" 2>&1; then
|
||||
ok "Off command sent successfully"
|
||||
else
|
||||
fail "Off command failed"
|
||||
fi
|
||||
|
||||
sleep 3
|
||||
|
||||
# Verify it's off (query just this outlet)
|
||||
STATUS_OFF=$(powerman -q "$NODE" 2>&1)
|
||||
echo "$STATUS_OFF"
|
||||
if echo "$STATUS_OFF" | grep -qi "off\|unk"; then
|
||||
ok "Outlet ${OUTLET} confirmed OFF"
|
||||
else
|
||||
echo " (status may not perfectly reflect — continuing)"
|
||||
fi
|
||||
|
||||
# --- 4. Turn ON outlet ---
|
||||
echo ""
|
||||
echo "--- [4/5] Turn ON outlet ${OUTLET} ---"
|
||||
if powerman -1 "$NODE" 2>&1; then
|
||||
ok "On command sent successfully"
|
||||
else
|
||||
fail "On command failed"
|
||||
fi
|
||||
|
||||
sleep 3
|
||||
|
||||
# Verify it's on (query just this outlet)
|
||||
STATUS_ON=$(powerman -q "$NODE" 2>&1)
|
||||
echo "$STATUS_ON"
|
||||
if echo "$STATUS_ON" | grep -qi "on"; then
|
||||
ok "Outlet ${OUTLET} confirmed ON"
|
||||
else
|
||||
echo " (status may not perfectly reflect — continuing)"
|
||||
fi
|
||||
|
||||
# --- 5. Cycle test (off → delay → on in one command) ---
|
||||
echo ""
|
||||
echo "--- [5/5] Cycle test (powerman -c) ---"
|
||||
if powerman -c "$NODE" 2>&1; then
|
||||
ok "Cycle command completed"
|
||||
else
|
||||
fail "Cycle command failed"
|
||||
echo " (some PDU firmware reports errors during cycle but still works)"
|
||||
fi
|
||||
|
||||
sleep 5
|
||||
|
||||
# Final status
|
||||
echo ""
|
||||
echo "--- Final status ---"
|
||||
powerman -q 2>&1
|
||||
|
||||
echo ""
|
||||
echo "============================================"
|
||||
echo " Results: $PASS passed, $FAIL failed"
|
||||
if [ "$FAIL" -gt 0 ]; then
|
||||
echo " Some checks failed. Review output above."
|
||||
exit 1
|
||||
fi
|
||||
echo " PDU control validated."
|
||||
echo "============================================"
|
||||
@@ -1,10 +0,0 @@
|
||||
# dcinfra/ups/README.md
|
||||
|
||||
> **Documentation moved to Discourse — the canonical source of truth.**
|
||||
>
|
||||
> **UPS management (NUT) for APC Smart-UPS C 1500**
|
||||
>
|
||||
> **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.*
|
||||
@@ -1,86 +0,0 @@
|
||||
#!/usr/bin/bash
|
||||
#
|
||||
# ups/discover.sh — probe USB UPS units and NUT state on the local host
|
||||
#
|
||||
# Read-only. Prints everything needed to configure NUT. No changes made.
|
||||
#
|
||||
# Usage (run ON the target host via remote.sh):
|
||||
# PROX_HOST=pfv-tsys1 bash tests/remote.sh prox-file ups/discover.sh
|
||||
#
|
||||
set -uo pipefail
|
||||
|
||||
echo "======================================================"
|
||||
echo " UPS / NUT Discovery on $(hostname)"
|
||||
echo "======================================================"
|
||||
|
||||
# --- 1. USB UPS devices ------------------------------------------------------
|
||||
echo ""
|
||||
echo "--- [1/5] USB UPS devices (lsusb) ---"
|
||||
lsusb 2>/dev/null | grep -iE "UPS|American Power|Tripp|APC" || echo " (no UPS devices found in lsusb)"
|
||||
|
||||
echo ""
|
||||
echo "--- [2/5] UPS detail (vendor/product/serial/model) ---"
|
||||
# Common UPS vendor IDs: 051d (APC), 09ae (Tripp Lite), 0463 (Eaton),
|
||||
# 06da (MGE), 0764 (Cyber Power)
|
||||
for vid in 051d 09ae 0463 06da 0764; do
|
||||
while read -r bus dev pid; do
|
||||
[ -n "$bus" ] || continue
|
||||
echo " --- $bus:$dev ($vid:$pid) ---"
|
||||
lsusb -v -s "${bus}:${dev}" 2>/dev/null \
|
||||
| grep -iE "iManufacturer|iProduct|iSerial|bcdDevice" \
|
||||
| sed 's/^/ /'
|
||||
done < <(lsusb 2>/dev/null | awk -v v="$vid" '$0~v{split($2,a,":"); split($4,b,":"); print a[1], b[1], $6}')
|
||||
done
|
||||
|
||||
# --- 2. sysfs paths (for udev rules) -----------------------------------------
|
||||
echo ""
|
||||
echo "--- [3/5] sysfs device paths + serials ---"
|
||||
for d in /sys/bus/usb/devices/*; do
|
||||
man=$(cat "$d/manufacturer" 2>/dev/null)
|
||||
prod=$(cat "$d/product" 2>/dev/null)
|
||||
ser=$(cat "$d/serial" 2>/dev/null)
|
||||
vid=$(cat "$d/idVendor" 2>/dev/null)
|
||||
pid=$(cat "$d/idProduct" 2>/dev/null)
|
||||
if echo "$man $prod" | grep -qiE "apc|tripp|power conversion|ups|eaton|mge|cyber power"; then
|
||||
# Resolve stable ID_PATH for udev pinning
|
||||
path=$(udevadm info -q property -p "$d" 2>/dev/null | awk -F= '/^ID_PATH=/{print $2}')
|
||||
echo " $d"
|
||||
echo " vendor=$vid product=$pid"
|
||||
echo " manufacturer=$man"
|
||||
echo " product=$prod"
|
||||
echo " serial=$ser"
|
||||
echo " ID_PATH=$path"
|
||||
fi
|
||||
done
|
||||
|
||||
# --- 3. HID device nodes -----------------------------------------------------
|
||||
echo ""
|
||||
echo "--- [4/5] HID device nodes ---"
|
||||
ls -la /dev/hidraw* /dev/usb/hiddev* 2>/dev/null || echo " (no hidraw/hiddev nodes)"
|
||||
|
||||
# --- 4. NUT install state ----------------------------------------------------
|
||||
echo ""
|
||||
echo "--- [5/5] NUT install + service state ---"
|
||||
if dpkg -l nut-server nut-client 2>/dev/null | grep -q '^ii'; then
|
||||
echo " NUT installed:"
|
||||
dpkg -l nut-server nut-client 2>/dev/null | awk '/^ii/{print " "$2" "$3}'
|
||||
else
|
||||
echo " NUT not installed (apt: nut-server nut-client)"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo " Services:"
|
||||
for svc in nut-driver nut-server nut-monitor; do
|
||||
printf " %-14s " "$svc:"
|
||||
systemctl is-active "$svc" 2>/dev/null || true
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo " Existing config:"
|
||||
# shellcheck disable=SC2012 # ls -la is intentional for human-readable listing
|
||||
ls -la /etc/nut/ 2>/dev/null | sed 's/^/ /' || echo " (no /etc/nut)"
|
||||
|
||||
echo ""
|
||||
echo "======================================================"
|
||||
echo " Discovery complete."
|
||||
echo "======================================================"
|
||||
@@ -1,134 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
ha-nut-setup.py — Add the Home Assistant NUT integration via REST config-flow API.
|
||||
|
||||
Stdlib-only (no pip). Idempotent: skips if a NUT config entry already exists.
|
||||
|
||||
Env:
|
||||
HA_HOST (default pfv-bms.knel.net)
|
||||
HA_PORT (default 8123)
|
||||
HA_TOKEN (long-lived access token)
|
||||
NUT_HOST (default 100.121.189.98)
|
||||
NUT_PORT (default 3493)
|
||||
NUT_USER (default homeassistant)
|
||||
NUT_PASS (required)
|
||||
NUT_UPS (default apc-smartups-c1500)
|
||||
"""
|
||||
import os, json, sys, time, urllib.request, urllib.error
|
||||
|
||||
HA_HOST = os.environ.get("HA_HOST", "pfv-bms.knel.net")
|
||||
HA_PORT = int(os.environ.get("HA_PORT", "8123"))
|
||||
TOKEN = os.environ["HA_TOKEN"]
|
||||
NUT_HOST = os.environ.get("NUT_HOST", "192.168.3.11")
|
||||
NUT_PORT = int(os.environ.get("NUT_PORT", "3493"))
|
||||
NUT_USER = os.environ.get("NUT_USER", "homeassistant")
|
||||
NUT_PASS = os.environ["NUT_PASS"]
|
||||
NUT_UPS = os.environ.get("NUT_UPS", "apc-smartups-c1500")
|
||||
BASE = f"http://{HA_HOST}:{HA_PORT}"
|
||||
|
||||
def api(method, path, data=None):
|
||||
body = json.dumps(data).encode() if data else None
|
||||
req = urllib.request.Request(
|
||||
f"{BASE}/api{path}", data=body, method=method,
|
||||
headers={"Authorization": f"Bearer {TOKEN}",
|
||||
"Content-Type": "application/json"})
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=20) as r:
|
||||
return json.loads(r.read())
|
||||
except urllib.error.HTTPError as e:
|
||||
raw = e.read().decode()
|
||||
try:
|
||||
return json.loads(raw)
|
||||
except Exception:
|
||||
return {"_http_error": e.code, "_raw": raw[:300]}
|
||||
except Exception as e:
|
||||
return {"_error": str(e)}
|
||||
|
||||
# ── verify token ──
|
||||
cfg = api("GET", "/config")
|
||||
if "_http_error" in cfg or "_error" in cfg:
|
||||
print(f"Cannot reach HA or token invalid: {cfg}"); sys.exit(1)
|
||||
print(f"HA {cfg.get('version')} — token valid")
|
||||
|
||||
# ── check existing entries (idempotent) ──
|
||||
entries = api("GET", "/config/config_entries/entry")
|
||||
existing = [e for e in entries if e.get("domain") == "nut"]
|
||||
if existing:
|
||||
for e in existing:
|
||||
print(f"NUT already configured: {e.get('title')} "
|
||||
f"(data={json.dumps(e.get('data', {}))})")
|
||||
print("Skipping — delete it in HA UI first if you want to re-run.")
|
||||
sys.exit(0)
|
||||
print("No existing NUT entry. Starting config flow.")
|
||||
|
||||
# ── initiate flow ──
|
||||
flow = api("POST", "/config/config_entries/flow", {"handler": "nut"})
|
||||
if "flow_id" not in flow:
|
||||
print(f"Flow init failed: {json.dumps(flow)}"); sys.exit(1)
|
||||
fid = flow["flow_id"]
|
||||
print(f"Flow started: step={flow.get('step_id')} "
|
||||
f"fields={[f.get('name') for f in flow.get('data_schema', [])]}")
|
||||
|
||||
# ── submit connection details ──
|
||||
creds = {"host": NUT_HOST, "port": NUT_PORT,
|
||||
"username": NUT_USER, "password": NUT_PASS}
|
||||
flow = api("POST", f"/config/config_entries/flow/{fid}", creds)
|
||||
if flow.get("errors"):
|
||||
print(f"Validation errors: {flow['errors']}"); sys.exit(1)
|
||||
print(f"After submit: type={flow.get('type')} step={flow.get('step_id')}")
|
||||
|
||||
# ── handle follow-up steps (UPS selection etc.) ──
|
||||
while flow.get("type") == "form":
|
||||
step = flow.get("step_id", "?")
|
||||
schema = flow.get("data_schema", [])
|
||||
print(f"Step '{step}': fields={[f.get('name') for f in schema]}")
|
||||
for f in schema:
|
||||
opts = f.get("options") or f.get("values")
|
||||
if opts:
|
||||
print(f" {f.get('name')} options: {opts}")
|
||||
submission = {}
|
||||
for f in schema:
|
||||
nm = f.get("name")
|
||||
ftype = f.get("type", "")
|
||||
if ftype == "multi_select":
|
||||
opts = f.get("options", [])
|
||||
vals = [o[0] if isinstance(o, list) else o for o in opts]
|
||||
submission[nm] = [NUT_UPS] if NUT_UPS in vals else vals[:1]
|
||||
elif nm in creds:
|
||||
submission[nm] = creds[nm]
|
||||
elif "default" in f:
|
||||
submission[nm] = f["default"]
|
||||
elif ftype == "select":
|
||||
opts = f.get("options", [])
|
||||
vals = [o[0] if isinstance(o, list) else o for o in opts]
|
||||
submission[nm] = NUT_UPS if NUT_UPS in vals else (vals[0] if vals else "")
|
||||
fid = flow.get("flow_id", fid)
|
||||
flow = api("POST", f"/config/config_entries/flow/{fid}", submission)
|
||||
if flow.get("errors"):
|
||||
print(f"Validation errors: {flow['errors']}"); sys.exit(1)
|
||||
print(f" -> type={flow.get('type')} step={flow.get('step_id')}")
|
||||
|
||||
# ── result ──
|
||||
if flow.get("type") == "create_entry":
|
||||
print(f"\nNUT integration created: {flow.get('title')}")
|
||||
elif flow.get("type") == "abort":
|
||||
print(f"\nFlow aborted: {flow.get('reason')}"); sys.exit(1)
|
||||
else:
|
||||
print(f"\nFinal state: {flow.get('type')} — {json.dumps(flow)[:200]}")
|
||||
|
||||
# ── verify sensors ──
|
||||
print("\nWaiting 10s for entities ...")
|
||||
time.sleep(10)
|
||||
states = api("GET", "/states")
|
||||
ups = [s for s in states
|
||||
if "apc_smartups" in s["entity_id"].lower()
|
||||
or "sensor.ups_" in s["entity_id"].lower()]
|
||||
if ups:
|
||||
print(f"Found {len(ups)} UPS sensors:")
|
||||
for e in sorted(ups, key=lambda x: x["entity_id"]):
|
||||
st = e.get("state", "?")
|
||||
unit = e.get("attributes", {}).get("unit_of_measurement", "")
|
||||
name = e.get("attributes", {}).get("friendly_name", "")
|
||||
print(f" {e['entity_id']:55s} {st:>8} {unit:4s} {name}")
|
||||
else:
|
||||
print("No UPS sensors yet (may still be initialising — check HA UI).")
|
||||
@@ -1,65 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# setup-ha-nut.sh — Add the Home Assistant NUT integration via REST API.
|
||||
#
|
||||
# Idempotent: skips if a NUT entry already exists. Reads secrets from
|
||||
# ~/.config/pfvcluster/ (ha-token, nut-password) or env vars.
|
||||
#
|
||||
# Usage:
|
||||
# bash ups/setup-ha-nut.sh
|
||||
#
|
||||
# Env overrides:
|
||||
# HA_TOKEN HA long-lived access token
|
||||
# NUT_PASS NUT upsd password for the homeassistant user
|
||||
# HA_HOST HA host (default pfv-bms.knel.net)
|
||||
# NUT_HOST upsd host (default 192.168.3.11 — LAN, see README)
|
||||
# NUT_PORT upsd port (default 3493)
|
||||
# NUT_USER upsd user (default homeassistant)
|
||||
# NUT_UPS UPS name (default apc-smartups-c1500)
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
CONF_DIR="${PFV_CONF_DIR:-$HOME/.config/pfvcluster}"
|
||||
|
||||
# --- HA token ---
|
||||
HA_TOKEN="${HA_TOKEN:-}"
|
||||
if [[ -z "$HA_TOKEN" ]]; then
|
||||
TOKEN_FILE="$CONF_DIR/ha-token"
|
||||
if [[ -f "$TOKEN_FILE" ]]; then
|
||||
HA_TOKEN="$(head -1 "$TOKEN_FILE" | tr -d '[:space:]')"
|
||||
else
|
||||
echo "error: no HA token. Set \$HA_TOKEN or create $TOKEN_FILE" >&2
|
||||
echo " (HA → Profile → Long-Lived Access Tokens → Create Token)" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- NUT password ---
|
||||
NUT_PASS="${NUT_PASS:-}"
|
||||
if [[ -z "$NUT_PASS" ]]; then
|
||||
PASS_FILE="$CONF_DIR/nut-password"
|
||||
if [[ -f "$PASS_FILE" ]]; then
|
||||
NUT_PASS="$(head -1 "$PASS_FILE" | tr -d '[:space:]')"
|
||||
else
|
||||
echo "error: no NUT password. Set \$NUT_PASS or create $PASS_FILE" >&2
|
||||
echo " (value is in /etc/nut/upsd.users on the NUT host)" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- connection params (override for your own kit) ---
|
||||
export HA_TOKEN
|
||||
export NUT_PASS
|
||||
export HA_HOST="${HA_HOST:-pfv-bms.knel.net}"
|
||||
export HA_PORT="${HA_PORT:-8123}"
|
||||
export NUT_HOST="${NUT_HOST:-192.168.3.11}"
|
||||
export NUT_PORT="${NUT_PORT:-3493}"
|
||||
export NUT_USER="${NUT_USER:-homeassistant}"
|
||||
export NUT_UPS="${NUT_UPS:-apc-smartups-c1500}"
|
||||
|
||||
echo "HA: ${HA_HOST}:${HA_PORT}"
|
||||
echo "NUT: ${NUT_USER}@${NUT_HOST}:${NUT_PORT} (UPS: ${NUT_UPS})"
|
||||
echo ""
|
||||
|
||||
exec python3 "$SCRIPT_DIR/ha-nut-setup.py"
|
||||
@@ -1,298 +0,0 @@
|
||||
#!/usr/bin/bash
|
||||
#
|
||||
# ups/setup.sh — idempotent Network UPS Tools (NUT) setup on pfv-tsys1
|
||||
#
|
||||
# Installs NUT, configures two USB HID UPS units (APC + Tripp Lite) pinned by
|
||||
# USB serial, runs upsd as a network server for Home Assistant polling, and
|
||||
# runs upsmon locally so the hypervisor can shut down gracefully on battery.
|
||||
#
|
||||
# Designed to run ON the target host (pfv-tsys1) as root, idempotent.
|
||||
#
|
||||
# Usage:
|
||||
# PROX_HOST=pfv-tsys1 bash tests/remote.sh prox-file ups/setup.sh
|
||||
#
|
||||
# Overrides (defaults suit pfv-tsys1):
|
||||
# APC_SERIAL APC UPS USB serial (default AS1213210423)
|
||||
# APC_VID/APC_PID APC vendor/product ID (default 051d / 0003)
|
||||
# APC_NAME NUT section name for APC (default apc-smartups-c1500)
|
||||
# TRIPP_SERIAL Tripp Lite UPS USB serial (default 2352CVLSM871900694)
|
||||
# TRIPP_VID/TRIPP_PID Tripp Lite vendor/product (default 09ae / 3016)
|
||||
# TRIPP_NAME NUT section name for Tripp (default tripp-lite-ups)
|
||||
# TRIPP_SUBDRIVER Forced HID subdriver for Tripp (default "TrippLite HID 0.85")
|
||||
# TRIPP_ENABLED Set to 0 to disable Tripp Lite (default 1)
|
||||
# NUT_LISTEN_IPS space-separated upsd LISTEN IPs (default: auto Tailscale + 127.0.0.1)
|
||||
# HA_USER upsd username for HA (default homeassistant)
|
||||
# HA_PASSWORD upsd password for HA (default: reuse or generate)
|
||||
# MON_USER upsd username for local upsmon (default monuser)
|
||||
# MON_PASSWORD upsd password for upsmon (default: reuse or generate)
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
# --- Config (overridable via env) ---
|
||||
APC_SERIAL="${APC_SERIAL:-AS1213210423}"
|
||||
APC_VID="${APC_VID:-051d}"
|
||||
APC_PID="${APC_PID:-0003}"
|
||||
APC_NAME="${APC_NAME:-apc-smartups-c1500}"
|
||||
|
||||
TRIPP_SERIAL="${TRIPP_SERIAL:-2352CVLSM871900694}"
|
||||
TRIPP_VID="${TRIPP_VID:-09ae}"
|
||||
TRIPP_PID="${TRIPP_PID:-3016}"
|
||||
TRIPP_NAME="${TRIPP_NAME:-tripp-lite-ups}"
|
||||
TRIPP_SUBDRIVER="${TRIPP_SUBDRIVER:-TrippLite HID 0.85}"
|
||||
TRIPP_ENABLED="${TRIPP_ENABLED:-1}"
|
||||
|
||||
HA_USER="${HA_USER:-homeassistant}"
|
||||
MON_USER="${MON_USER:-monuser}"
|
||||
NUT_PORT="${NUT_PORT:-3493}"
|
||||
UDEV_RULE="/etc/udev/rules.d/99-nut-ups.rules"
|
||||
|
||||
UPS_CONF="/etc/nut/ups.conf"
|
||||
UPSD_CONF="/etc/nut/upsd.conf"
|
||||
UPSD_USERS="/etc/nut/upsd.users"
|
||||
UPS_CONF_MON="/etc/nut/upsmon.conf"
|
||||
NUT_CONF="/etc/nut/nut.conf"
|
||||
|
||||
# --- Helpers ---
|
||||
gen_pw() { head -c 24 /dev/urandom | base64 | tr -d '/+=' | cut -c1-20; }
|
||||
|
||||
# Reuse existing passwords if config already present (idempotent re-runs)
|
||||
extract_pw() { # $1=user
|
||||
if [ -f "$UPSD_USERS" ]; then
|
||||
awk -v u="[$1]" '
|
||||
$0==u {inblk=1; next}
|
||||
/^\[/ {inblk=0}
|
||||
inblk && $1=="password" {gsub(/"/,"",$3); print $3; exit}
|
||||
' "$UPSD_USERS" 2>/dev/null
|
||||
fi
|
||||
}
|
||||
|
||||
echo "============================================"
|
||||
echo " NUT UPS Setup on $(hostname)"
|
||||
echo " APC: $APC_NAME ($APC_VID:$APC_PID serial $APC_SERIAL)"
|
||||
if [ "$TRIPP_ENABLED" = "1" ]; then
|
||||
echo " Tripp Lite: $TRIPP_NAME ($TRIPP_VID:$TRIPP_PID serial $TRIPP_SERIAL)"
|
||||
else
|
||||
echo " Tripp Lite: DISABLED (TRIPP_ENABLED=0)"
|
||||
fi
|
||||
echo "============================================"
|
||||
|
||||
# --- 0. Resolve / generate passwords (idempotent) ---
|
||||
MON_PASSWORD="${MON_PASSWORD:-$(extract_pw "$MON_USER")}"
|
||||
HA_PASSWORD="${HA_PASSWORD:-$(extract_pw "$HA_USER")}"
|
||||
[ -n "$MON_PASSWORD" ] || MON_PASSWORD="$(gen_pw)"
|
||||
[ -n "$HA_PASSWORD" ] || HA_PASSWORD="$(gen_pw)"
|
||||
|
||||
# --- 0b. Auto-detect listen IPs for upsd ---
|
||||
# Tailscale IP: tailnet clients (workstation, etc.)
|
||||
# LAN IP: HAOS VMs where Tailscale runs as an isolated add-on (HA container
|
||||
# cannot route to Tailscale IPs, so the shared-LAN bridge is required)
|
||||
if [ -z "${NUT_LISTEN_IPS:-}" ]; then
|
||||
NUT_LISTEN_IPS="127.0.0.1"
|
||||
TS_IP=$(tailscale ip -4 2>/dev/null || true)
|
||||
if [ -n "$TS_IP" ]; then
|
||||
NUT_LISTEN_IPS="${NUT_LISTEN_IPS} ${TS_IP}"
|
||||
else
|
||||
echo " WARNING: No Tailscale IP detected."
|
||||
fi
|
||||
if [ "${NUT_INCLUDE_LAN:-1}" = "1" ]; then
|
||||
LAN_IP=$(ip -4 addr show vmbr0 2>/dev/null | awk '/scope global/{print $2}' | cut -d/ -f1 | head -1)
|
||||
if [ -z "$LAN_IP" ]; then
|
||||
LAN_IP=$(hostname -I 2>/dev/null | awk '{print $1}')
|
||||
fi
|
||||
if [ -n "$LAN_IP" ]; then
|
||||
NUT_LISTEN_IPS="${NUT_LISTEN_IPS} ${LAN_IP}"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
echo " upsd LISTEN IPs: ${NUT_LISTEN_IPS:-<none>}"
|
||||
|
||||
# --- 1. Install NUT ---
|
||||
echo ""
|
||||
echo "--- [1/8] Installing NUT (nut-server, nut-client) ---"
|
||||
if dpkg -l nut-server 2>/dev/null | grep -q '^ii'; then
|
||||
echo " NUT already installed: $(dpkg -l nut-server | awk '/^ii/{print $3}')"
|
||||
else
|
||||
apt-get update -qq && apt-get install -y -qq nut-server nut-client
|
||||
fi
|
||||
mkdir -p /etc/nut
|
||||
|
||||
# --- 2. udev rules: grant nut group access to BOTH raw USB + hidraw devices ---
|
||||
# CRITICAL: usbhid-ups opens /dev/bus/usb/BBB/DDD (raw USB), not /dev/hidraw.
|
||||
# The driver drops to the nut user via setuid(), so the nut group needs write
|
||||
# access to the raw USB device files. Matching on subsystem=="usb" by VID:PID
|
||||
# is required because ATTRS{serial} does not reliably traverse for usb devices.
|
||||
echo ""
|
||||
echo "--- [2/8] Writing udev rules (raw USB + hidraw, group nut) ---"
|
||||
{
|
||||
echo "# Stable permissions for NUT USB HID UPS units"
|
||||
echo "# Generated by ups/setup.sh — grants the 'nut' group access to both"
|
||||
echo "# the raw USB device files (/dev/bus/usb) and hidraw devices."
|
||||
echo "# Match BOTH subsystems: the usbhid-ups driver opens the raw USB device"
|
||||
echo "# after dropping to the nut user via setuid()."
|
||||
echo ""
|
||||
echo "# APC Smart-UPS C 1500 ($APC_VID:$APC_PID)"
|
||||
echo "SUBSYSTEM==\"usb\", ATTR{idVendor}==\"$APC_VID\", ATTR{idProduct}==\"$APC_PID\", GROUP=\"nut\", MODE=\"0664\""
|
||||
echo "SUBSYSTEM==\"hidraw\", ATTRS{serial}==\"$APC_SERIAL\", GROUP=\"nut\", MODE=\"0660\""
|
||||
echo ""
|
||||
echo "# Tripp Lite UPS ($TRIPP_VID:$TRIPP_PID)"
|
||||
echo "SUBSYSTEM==\"usb\", ATTR{idVendor}==\"$TRIPP_VID\", ATTR{idProduct}==\"$TRIPP_PID\", GROUP=\"nut\", MODE=\"0664\""
|
||||
echo "SUBSYSTEM==\"hidraw\", ATTRS{serial}==\"$TRIPP_SERIAL\", GROUP=\"nut\", MODE=\"0660\""
|
||||
} > "$UDEV_RULE"
|
||||
echo " Written: $UDEV_RULE"
|
||||
udevadm control --reload-rules 2>/dev/null || true
|
||||
udevadm trigger --subsystem-match=usb 2>/dev/null || true
|
||||
udevadm trigger --subsystem-match=hidraw 2>/dev/null || true
|
||||
sleep 1
|
||||
|
||||
# --- 3. ups.conf ---
|
||||
echo ""
|
||||
echo "--- [3/8] Writing ups.conf ---"
|
||||
{
|
||||
echo "# NUT UPS devices — generated by ups/setup.sh on $(date)"
|
||||
echo ""
|
||||
echo "maxretry = 3"
|
||||
echo ""
|
||||
echo "[${APC_NAME}]"
|
||||
echo " driver = usbhid-ups"
|
||||
echo " port = auto"
|
||||
echo " vendorid = ${APC_VID}"
|
||||
echo " productid = ${APC_PID}"
|
||||
echo " serial = ${APC_SERIAL}"
|
||||
echo " desc = \"APC Smart-UPS C 1500\""
|
||||
if [ "$TRIPP_ENABLED" = "1" ]; then
|
||||
echo ""
|
||||
echo "[${TRIPP_NAME}]"
|
||||
echo " driver = usbhid-ups"
|
||||
echo " port = auto"
|
||||
echo " vendorid = ${TRIPP_VID}"
|
||||
echo " productid = ${TRIPP_PID}"
|
||||
echo " serial = ${TRIPP_SERIAL}"
|
||||
echo " subdriver = \"${TRIPP_SUBDRIVER}\""
|
||||
echo " desc = \"Tripp Lite UPS\""
|
||||
fi
|
||||
} > "$UPS_CONF"
|
||||
echo " Written: $UPS_CONF"
|
||||
|
||||
# --- 4. upsd.conf: network server (localhost + Tailscale for HA) ---
|
||||
echo ""
|
||||
echo "--- [4/8] Writing upsd.conf ---"
|
||||
{
|
||||
echo "# NUT upsd — generated by ups/setup.sh on $(date)"
|
||||
for ip in $NUT_LISTEN_IPS; do
|
||||
echo "LISTEN ${ip} ${NUT_PORT}"
|
||||
done
|
||||
echo "MAXAGE 25"
|
||||
} > "$UPSD_CONF"
|
||||
echo " Written: $UPSD_CONF (LISTEN: $(echo "$NUT_LISTEN_IPS" | tr '\n' ' '))"
|
||||
|
||||
# --- 5. upsd.users: monuser (master) + homeassistant (read-only monitor) ---
|
||||
echo ""
|
||||
echo "--- [5/8] Writing upsd.users ---"
|
||||
cat > "$UPSD_USERS" <<USERS
|
||||
# NUT upsd users — generated by ups/setup.sh on $(date)
|
||||
# monuser : local upsmon (master) — graceful hypervisor shutdown
|
||||
# ${HA_USER} : Home Assistant NUT integration (read-only polling)
|
||||
|
||||
[${MON_USER}]
|
||||
password = "${MON_PASSWORD}"
|
||||
upsmon master
|
||||
|
||||
[${HA_USER}]
|
||||
password = "${HA_PASSWORD}"
|
||||
upsmon slave
|
||||
USERS
|
||||
echo " Written: $UPSD_USERS"
|
||||
|
||||
# --- 6. upsmon.conf + nut.conf ---
|
||||
echo ""
|
||||
echo "--- [6/8] Writing upsmon.conf + nut.conf ---"
|
||||
{
|
||||
echo "# NUT upsmon (local monitor) — generated by ups/setup.sh on $(date)"
|
||||
echo "# Monitors the APC UPS as master. On battery-low, shuts the host down."
|
||||
echo ""
|
||||
echo "MONITOR ${APC_NAME}@localhost 1 ${MON_USER} \"${MON_PASSWORD}\" master"
|
||||
if [ "$TRIPP_ENABLED" = "1" ]; then
|
||||
echo "MONITOR ${TRIPP_NAME}@localhost 1 ${MON_USER} \"${MON_PASSWORD}\" master"
|
||||
fi
|
||||
echo ""
|
||||
echo "SHUTDOWNCMD \"/sbin/shutdown -h now\""
|
||||
echo "POWERDOWNFLAG /etc/killpower"
|
||||
echo "NOTIFYFLAG ONLINE SYSLOG+WALL"
|
||||
echo "NOTIFYFLAG ONBATT SYSLOG+WALL"
|
||||
echo "NOTIFYFLAG LOWBATT SYSLOG+WALL"
|
||||
echo "POLLFREQ 15"
|
||||
echo "POLLFREQALERT 5"
|
||||
echo "HOSTSYNC 15"
|
||||
} > "$UPS_CONF_MON"
|
||||
|
||||
cat > "$NUT_CONF" <<NUTCONF
|
||||
# NUT mode — generated by ups/setup.sh on $(date)
|
||||
# netserver = this host runs drivers + upsd; serves UPS data to clients (HA).
|
||||
MODE=netserver
|
||||
NUTCONF
|
||||
echo " Written: $UPS_CONF_MON + $NUT_CONF"
|
||||
|
||||
# --- 6b. Fix ownership/permissions (Debian: nut group must read config) ---
|
||||
chown root:nut "$UPS_CONF" "$UPSD_CONF" "$UPSD_USERS" "$UPS_CONF_MON" 2>/dev/null || true
|
||||
chmod 640 "$UPS_CONF" "$UPSD_CONF" "$UPSD_USERS" "$UPS_CONF_MON" 2>/dev/null || true
|
||||
chmod 644 "$NUT_CONF" 2>/dev/null || true
|
||||
|
||||
# --- 7. Start services (Debian uses templated nut-driver@<name> units) ---
|
||||
echo ""
|
||||
echo "--- [7/8] Starting NUT services ---"
|
||||
|
||||
# Re-read ups.conf to generate per-UPS driver instances
|
||||
systemctl restart nut-driver-enumerator 2>/dev/null || true
|
||||
sleep 2
|
||||
|
||||
# Start per-UPS driver instances
|
||||
systemctl restart "nut-driver@${APC_NAME}" 2>/dev/null || true
|
||||
if [ "$TRIPP_ENABLED" = "1" ]; then
|
||||
systemctl restart "nut-driver@${TRIPP_NAME}" 2>/dev/null || true
|
||||
else
|
||||
systemctl stop "nut-driver@${TRIPP_NAME}" 2>/dev/null || true
|
||||
systemctl mask "nut-driver@${TRIPP_NAME}" 2>/dev/null || true
|
||||
fi
|
||||
sleep 3
|
||||
systemctl restart nut-server 2>/dev/null || true
|
||||
sleep 1
|
||||
systemctl restart nut-monitor 2>/dev/null || true
|
||||
|
||||
echo ""
|
||||
echo " Service status:"
|
||||
for svc in "nut-driver@${APC_NAME}" "nut-driver@${TRIPP_NAME}" nut-server nut-monitor; do
|
||||
if systemctl list-unit-files "$svc" >/dev/null 2>&1; then
|
||||
printf " %-42s " "$svc"
|
||||
systemctl is-active "$svc" 2>/dev/null || echo "(unknown)"
|
||||
fi
|
||||
done
|
||||
|
||||
# --- 8. Validate ---
|
||||
echo ""
|
||||
echo "--- [8/8] Validation ---"
|
||||
echo ""
|
||||
echo " upsc — ${APC_NAME}:"
|
||||
upsc "${APC_NAME}@localhost" 2>&1 | head -25 || echo " (APC UPS not responding yet)"
|
||||
if [ "$TRIPP_ENABLED" = "1" ]; then
|
||||
echo ""
|
||||
echo " upsc — ${TRIPP_NAME}:"
|
||||
upsc "${TRIPP_NAME}@localhost" 2>&1 | head -25 || echo " (Tripp Lite UPS not responding yet)"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "============================================"
|
||||
echo " Setup complete."
|
||||
echo ""
|
||||
echo " Home Assistant NUT integration:"
|
||||
echo " Host: $(echo "$NUT_LISTEN_IPS" | awk '{print $2}') (or any LISTEN IP above)"
|
||||
echo " Port: ${NUT_PORT}"
|
||||
echo " Username: ${HA_USER}"
|
||||
echo " Password: ${HA_PASSWORD}"
|
||||
if [ "$TRIPP_ENABLED" = "1" ]; then
|
||||
echo " UPS names: ${APC_NAME}, ${TRIPP_NAME}"
|
||||
else
|
||||
echo " UPS names: ${APC_NAME}"
|
||||
fi
|
||||
echo ""
|
||||
echo " Save the HA password now — it is stored in ${UPSD_USERS}."
|
||||
echo "============================================"
|
||||
@@ -1,59 +0,0 @@
|
||||
#!/usr/bin/bash
|
||||
#
|
||||
# ups/status.sh — query NUT UPS state + service health (read-only)
|
||||
#
|
||||
# Usage (run ON the target host via remote.sh):
|
||||
# PROX_HOST=pfv-tsys1 bash tests/remote.sh prox-file ups/status.sh
|
||||
#
|
||||
# shellcheck disable=SC2012 # ss/awk field extraction is intentional
|
||||
set -uo pipefail
|
||||
|
||||
APC_NAME="${APC_NAME:-apc-smartups-c1500}"
|
||||
TRIPP_NAME="${TRIPP_NAME:-tripp-lite-ups}"
|
||||
|
||||
echo "======================================================"
|
||||
echo " NUT UPS Status on $(hostname)"
|
||||
echo "======================================================"
|
||||
|
||||
echo ""
|
||||
echo "--- Services ---"
|
||||
for svc in "nut-driver@${APC_NAME}" "nut-driver@${TRIPP_NAME}" nut-server nut-monitor; do
|
||||
if systemctl list-unit-files "$svc" >/dev/null 2>&1; then
|
||||
printf " %-42s " "$svc"
|
||||
systemctl is-active "$svc" 2>/dev/null || echo "(unknown)"
|
||||
fi
|
||||
done
|
||||
|
||||
for ups in "$APC_NAME" "$TRIPP_NAME"; do
|
||||
echo ""
|
||||
echo "--- ${ups} ---"
|
||||
if upsc "${ups}@localhost" >/tmp/.nutstatus.$$ 2>&1; then
|
||||
awk -v u="$ups" '
|
||||
BEGIN{printf " %s\n", u}
|
||||
/^battery\.charge:/ {printf " battery.charge: %s\n", $3}
|
||||
/^battery\.runtime:/ {printf " battery.runtime: %ss (%.0f min)\n", $3, $3/60}
|
||||
/^battery\.voltage:/ {printf " battery.voltage: %s\n", $3}
|
||||
/^ups\.status:/ {printf " ups.status: %s\n", $3}
|
||||
/^ups\.load:/ {printf " ups.load: %s%%\n", $3}
|
||||
/^ups\.power:/ {printf " ups.power: %s\n", $3}
|
||||
/^ups\.realpower:/ {printf " ups.realpower: %s W\n", $3}
|
||||
/^input\.voltage:/ {printf " input.voltage: %s\n", $3}
|
||||
/^output\.voltage:/ {printf " output.voltage: %s\n", $3}
|
||||
/^ups\.model:/ {printf " ups.model: %s\n", $3}
|
||||
/^ups\.serial:/ {printf " ups.serial: %s\n", $3}
|
||||
/^device\.mfr:/ {printf " device.mfr: %s\n", $3}
|
||||
' /tmp/.nutstatus.$$
|
||||
echo " (full dump: upsc ${ups}@localhost)"
|
||||
else
|
||||
echo " NOT RESPONDING:"
|
||||
sed 's/^/ /' /tmp/.nutstatus.$$
|
||||
fi
|
||||
rm -f /tmp/.nutstatus.$$
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "--- upsd LISTEN sockets ---"
|
||||
ss -ltnp 2>/dev/null | grep -E "3493|nut" | sed 's/^/ /' || echo " (upsd not listening on 3493)"
|
||||
|
||||
echo ""
|
||||
echo "======================================================"
|
||||
@@ -0,0 +1,183 @@
|
||||
# Technitium DNS Cluster Setup
|
||||
|
||||
Replicates the production Technitium DNS Server from `tailscale-router` to the
|
||||
`pfv-netinfra-01/02` pair and configures them as a primary/secondary cluster
|
||||
with automatic zone transfers.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
tailscale-router (PRODUCTION — READ ONLY)
|
||||
└─ tsys-dns container (technitium/dns-server)
|
||||
└─ 124 zones (knel.net + reverse DNS)
|
||||
└─ Users + 2FA in auth.config
|
||||
│
|
||||
docker cp (export)
|
||||
│
|
||||
▼
|
||||
┌─ pfv-netinfra-01 (192.168.3.252) ──── PRIMARY ──────────┐
|
||||
│ tsys-dns container (Technitium on :5300) │
|
||||
│ pihole container (Pi-hole on :53 → Technitium :5300) │
|
||||
│ All zones are Primary │
|
||||
│ Zone transfer allowed from 192.168.3.253 │
|
||||
└──────────────────────────────────────────────────────────┘
|
||||
│
|
||||
AXFR / IXFR + NOTIFY (DNS zone transfer, port 5300)
|
||||
│
|
||||
▼
|
||||
┌─ pfv-netinfra-02 (192.168.3.253) ─── SECONDARY ────────┐
|
||||
│ tsys-dns container (Technitium on :5300) │
|
||||
│ pihole container (Pi-hole on :53 → Technitium :5300) │
|
||||
│ All zones are Secondary (AXFR from 01) │
|
||||
└──────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### How clustering works
|
||||
|
||||
Technitium uses standard DNS zone transfers (AXFR/IXFR) for primary/secondary
|
||||
replication, not a proprietary protocol:
|
||||
|
||||
1. **Primary (01)** holds all zones as authoritative primary zones.
|
||||
2. **Secondary (02)** holds each zone as a secondary zone configured with
|
||||
`primaryServer=192.168.3.252:5300`.
|
||||
3. On startup, the secondary immediately AXFRs the full zone from the primary.
|
||||
4. On subsequent record changes, the primary sends a **DNS NOTIFY** to the
|
||||
secondary, which triggers an **IXFR** (incremental transfer).
|
||||
5. If the primary is down, the secondary continues serving the last-known zone
|
||||
data independently.
|
||||
|
||||
### Credentials and 2FA
|
||||
|
||||
The production `auth.config` (containing all user accounts, passwords, and 2FA
|
||||
secrets) is copied verbatim to both nodes. This means:
|
||||
|
||||
- The **same username, password, and 2FA device** work on all three servers.
|
||||
- The web console is at `http://<host>:5380/` on each node.
|
||||
- No credential changes are needed.
|
||||
|
||||
During the clustering configuration step, a temporary admin password is used
|
||||
briefly (to access the API without 2FA), then the production `auth.config` is
|
||||
restored. See "Security notes" below.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- SSH key access to all hosts as `localuser` with passwordless sudo.
|
||||
- The `remote-dns.sh` wrapper must be able to reach all hosts via Tailscale FQDN.
|
||||
- Docker + Docker Compose on netinfra-01/02 (already installed).
|
||||
- The production Technitium on tailscale-router must be running.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
cd dns-cluster-setup/
|
||||
|
||||
# Step-by-step (recommended for first run):
|
||||
./setup.sh export # 1. Export config from tailscale-router (READ-ONLY)
|
||||
./setup.sh deploy01 # 2. Deploy to netinfra-01 as primary
|
||||
./setup.sh deploy02 # 3. Deploy to netinfra-02 as secondary clone
|
||||
./setup.sh cluster # 4. Configure clustering (01→02 zone transfers)
|
||||
./setup.sh verify # 5. Run all verification tests
|
||||
|
||||
# Or all at once:
|
||||
./setup.sh all
|
||||
```
|
||||
|
||||
### Configuration overrides
|
||||
|
||||
All defaults can be overridden via environment variables:
|
||||
|
||||
| Variable | Default | Description |
|
||||
|---|---|---|
|
||||
| `PRIMARY_IP` | `192.168.3.252` | netinfra-01 LAN IP |
|
||||
| `SECONDARY_IP` | `192.168.3.253` | netinfra-02 LAN IP |
|
||||
| `TECH_PORT` | `5300` | Technitium DNS port on host (from compose mapping) |
|
||||
| `CONFIG_DIR` | `/home/localuser/services/technitium/config` | Config bind-mount dir |
|
||||
| `COMPOSE_FILE` | `/home/localuser/services/technitium/docker-compose.yml` | Compose file |
|
||||
| `TEMP_ADMIN_PW` | `KnelClusterSetup!2026` | Temp admin password (used only during clustering, then discarded) |
|
||||
|
||||
## Scripts
|
||||
|
||||
| Script | Purpose |
|
||||
|---|---|
|
||||
| `remote-dns.sh` | SSH/SCP chokepoint for all DNS host access (tsrouter, netinfra01, netinfra02, netboot, sandbox) |
|
||||
| `setup.sh` | Master orchestrator: export → deploy → cluster → verify |
|
||||
| `verify.sh` | Comprehensive 10-section verification suite |
|
||||
| `discover*.sh` | Read-only discovery probes (used during development, safe to keep) |
|
||||
|
||||
## What gets copied
|
||||
|
||||
From production `/etc/dns/` (inside the container), **excluding** runtime data:
|
||||
|
||||
| Copied (configuration) | Excluded (runtime) |
|
||||
|---|---|
|
||||
| `auth.config` (users, passwords, 2FA) | `cache.bin` (DNS cache) |
|
||||
| `dns.config` (server settings) | `stats/` (query statistics) |
|
||||
| `webservice.config` (web console) | `logs/` (log files) |
|
||||
| `allowed.config` (zone transfer ACL) | |
|
||||
| `blocked.config` (blocked domains) | |
|
||||
| `blocklist.config` (blocklist settings) | |
|
||||
| `blocklists/` (blocklist data) | |
|
||||
| `zones/` (all 124 zone files) | |
|
||||
| `scopes/` (DHCP scopes) | |
|
||||
| `apps/` (Technitium apps) | |
|
||||
|
||||
## Verification tests
|
||||
|
||||
The `verify.sh` script runs 10 categories of tests:
|
||||
|
||||
1. **Container health** — both Technitium containers are Up
|
||||
2. **API responds** — web console API is reachable on both nodes
|
||||
3. **Zone count** — primary matches production; secondary matches primary
|
||||
4. **Forward DNS** — known knel.net records resolve identically on both nodes
|
||||
5. **External DNS** — both nodes can resolve external domains (github.com)
|
||||
6. **Zone transfer (AXFR)** — secondary can AXFR knel.net from primary
|
||||
7. **Reverse DNS** — PTR zones have SOA records on both nodes
|
||||
8. **Production untouched** — container still running, zone count unchanged
|
||||
9. **Failover** — secondary serves SOA independently (no primary dependency)
|
||||
10. **Credentials** — `auth.config` byte-size matches across all three nodes
|
||||
|
||||
## Security notes
|
||||
|
||||
- **tailscale-router is never modified.** The only operation is `docker cp`
|
||||
(read) to export the config. No writes, no restarts, no config changes.
|
||||
- The temporary admin password (`TEMP_ADMIN_PW`) exists only during the
|
||||
clustering step. After configuration, the production `auth.config` (with 2FA)
|
||||
is restored. The temp password is never persisted.
|
||||
- The export tarball (`.export/technitium-production-config.tar.gz`) contains
|
||||
production credentials. It is in `.gitignore` and should be deleted after
|
||||
setup: `rm -rf dns-cluster-setup/.export/`
|
||||
- Each node's existing config is backed up to `config.backup-<timestamp>` before
|
||||
replacement, so the change is reversible.
|
||||
|
||||
## Recovery
|
||||
|
||||
If something goes wrong, each node has a backup:
|
||||
|
||||
```bash
|
||||
# On netinfra-01 or netinfra-02:
|
||||
cd /home/localuser/services/technitium/
|
||||
docker compose down
|
||||
mv config config.failed
|
||||
mv config.backup-<timestamp> config
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
## Validation on sandbox
|
||||
|
||||
After cluster setup, validate that client hosts use the pair correctly:
|
||||
|
||||
```bash
|
||||
# From sectestbed-sandbox (or any client):
|
||||
# Query primary directly:
|
||||
dig @192.168.3.252 pfv-netinfra-01.knel.net
|
||||
|
||||
# Query secondary directly:
|
||||
dig @192.168.3.253 pfv-netinfra-01.knel.net
|
||||
|
||||
# Both should return the same answer.
|
||||
```
|
||||
|
||||
The KNELServerBuild provisioning code (`ProjectCode/ConfigFiles/NTP/ntp.conf`
|
||||
and `ProjectCode/ConfigFiles/Resolv/resolv.conf`) points clients at both
|
||||
servers for DNS and NTP redundancy. See `ProjectDocs/tailscale.md` for the
|
||||
full DNS architecture analysis.
|
||||
@@ -35,7 +35,7 @@ mkdir -p "$ZONE_DIR"
|
||||
log "Syncing zones from $PRIMARY_HOST..."
|
||||
if rsync -az --delete --temp-dir=/tmp \
|
||||
"${PRIMARY_HOST}:$ZONE_DIR/" "$ZONE_DIR/" >> "$LOG_FILE" 2>&1; then
|
||||
zone_count=$(find "$ZONE_DIR" -maxdepth 1 -type f | wc -l)
|
||||
zone_count=$(ls "$ZONE_DIR" | wc -l)
|
||||
log "Sync complete: $zone_count zones"
|
||||
else
|
||||
log "ERROR: rsync failed (rc=$?)"
|
||||
@@ -69,9 +69,9 @@ echo " Production zones: $prod_zones"
|
||||
echo " Primary (01) zones: $pri_zones"
|
||||
echo " Secondary (02) zones: $sec_zones"
|
||||
|
||||
if [ "$prod_zones" -gt 0 ] 2>/dev/null; then ok "Production has $prod_zones zones"; else fail "Production zone count invalid"; fi
|
||||
if [ "$pri_zones" -gt 0 ] 2>/dev/null; then ok "Primary has $pri_zones zones"; else fail "Primary zone count invalid"; fi
|
||||
if [ "$sec_zones" -gt 0 ] 2>/dev/null; then ok "Secondary has $sec_zones zones"; else fail "Secondary zone count invalid"; fi
|
||||
[ "$prod_zones" -gt 0 ] 2>/dev/null && ok "Production has $prod_zones zones" || fail "Production zone count invalid"
|
||||
[ "$pri_zones" -gt 0 ] 2>/dev/null && ok "Primary has $pri_zones zones" || fail "Primary zone count invalid"
|
||||
[ "$sec_zones" -gt 0 ] 2>/dev/null && ok "Secondary has $sec_zones zones" || fail "Secondary zone count invalid"
|
||||
|
||||
if [ "$pri_zones" = "$prod_zones" ]; then
|
||||
ok "Primary zone count matches production ($pri_zones)"
|
||||
@@ -0,0 +1,139 @@
|
||||
# AI Review: KNELServerBuild (FetchApply) Project
|
||||
|
||||
## Executive Summary
|
||||
|
||||
The KNELServerBuild project is a comprehensive Infrastructure-as-Code (IaC) solution designed for provisioning Linux servers within the TSYS Group environment. The project implements a fetch-and-apply framework that automates the setup and hardening of server systems, incorporating security, monitoring, and operational components.
|
||||
|
||||
## Project Overview
|
||||
|
||||
The FetchApply project is a shell-based automation framework that provisions Linux servers with:
|
||||
- Security hardening (SSH, 2FA, Wazuh, STIG compliance)
|
||||
- Operational monitoring (LibreNMS, cockpit, SNMP)
|
||||
- System packages and configurations for enterprise operations
|
||||
- Network discovery and management capabilities
|
||||
|
||||
## Architecture and Structure
|
||||
|
||||
### Key Components
|
||||
- **ProjectCode/**: Main setup and configuration scripts
|
||||
- **Project-ConfigFiles/**: Configuration variables and parameters
|
||||
- **Project-Includes/**: Reusable shell functions and utilities
|
||||
- **Project-Tests/**: Comprehensive testing framework
|
||||
- **Modules/**: Functional modules for security, operations, etc.
|
||||
- **vendor/**: External dependencies and frameworks
|
||||
|
||||
### Core Workflow
|
||||
The `SetupNewSystem.sh` orchestrates:
|
||||
1. Preflight checks and environment validation
|
||||
2. Package installation and system updates
|
||||
3. Service configuration and hardening
|
||||
4. Security implementation (SSH, Wazuh, 2FA)
|
||||
5. Operational monitoring setup
|
||||
|
||||
## Strengths
|
||||
|
||||
### 1. Comprehensive Testing Framework
|
||||
- Well-structured testing with unit, integration, security, and validation categories
|
||||
- Clear documentation and usage instructions
|
||||
- JSON reporting for CI/CD integration
|
||||
|
||||
### 2. Security-First Approach
|
||||
- Multiple layers of security hardening (SSH, 2FA, audit agents)
|
||||
- STIG compliance for government/hybrid environments
|
||||
- Proper permission management and configuration validation
|
||||
|
||||
### 3. Modular Architecture
|
||||
- Separated concerns into functional modules
|
||||
- Reusable functions and components
|
||||
- Clear separation between framework and project-specific code
|
||||
|
||||
### 4. Operational Readiness
|
||||
- Built-in monitoring and alerting
|
||||
- System performance optimization
|
||||
- Network discovery and management tools
|
||||
|
||||
### 5. Cross-Platform Considerations
|
||||
- Detection for different hardware types (physical, virtual, Raspberry Pi)
|
||||
- Distribution-specific handling
|
||||
- Environment-aware configurations
|
||||
|
||||
## Areas for Improvement
|
||||
|
||||
### 1. Documentation Completeness
|
||||
- README mentions usage but lacks detailed architecture overview
|
||||
- Missing troubleshooting and recovery procedures
|
||||
- Limited guidance for extending/adding new modules
|
||||
|
||||
### 2. Security and Secrets Management
|
||||
- Configuration files may expose hardcoded credentials or tokens
|
||||
- No clear secrets management strategy
|
||||
- Download URLs and endpoints are hardcoded in scripts
|
||||
|
||||
### 3. Error Handling and Resilience
|
||||
- While scripts have basic error handling, recovery mechanisms are limited
|
||||
- No rollback capabilities for failed installations
|
||||
- Some operations may fail silently
|
||||
|
||||
### 4. Scalability and Performance
|
||||
- Scripts execute sequentially without parallelization
|
||||
- No caching mechanisms for downloads
|
||||
- Limited handling for high-latency networks
|
||||
|
||||
### 5. Configuration Management
|
||||
- Configuration values scattered across multiple files
|
||||
- No centralized configuration management
|
||||
- Difficult to customize for different environments
|
||||
|
||||
## Recommendations
|
||||
|
||||
### 1. Enhance Security Practices
|
||||
- Implement secrets management (HashiCorp Vault, AWS Secrets Manager, etc.)
|
||||
- Add configuration validation before applying changes
|
||||
- Implement digital signature verification for downloaded content
|
||||
- Add security scanning of packages before installation
|
||||
|
||||
### 2. Improve Testing Coverage
|
||||
- Add end-to-end tests for complete deployment scenarios
|
||||
- Implement performance benchmarks
|
||||
- Add security validation tests
|
||||
- Include tests for different hardware configurations
|
||||
|
||||
### 3. Add Monitoring and Observability
|
||||
- Implement deployment success/failure metrics
|
||||
- Add progress tracking for long-running operations
|
||||
- Include health checks post-deployment
|
||||
- Add rollback mechanisms for failed deployments
|
||||
|
||||
### 4. Refactor for Maintainability
|
||||
- Centralize configuration management
|
||||
- Abstract environment-specific variables
|
||||
- Implement plugin architecture for new modules
|
||||
- Add proper logging and audit trails
|
||||
|
||||
### 5. Enhance Usability
|
||||
- Add dry-run functionality for testing changes
|
||||
- Provide rollback/recovery procedures
|
||||
- Add interactive mode for new users
|
||||
- Implement configuration templates
|
||||
|
||||
## Technical Debt Assessment
|
||||
|
||||
### High Priority
|
||||
- Centralized configuration management
|
||||
- Secrets handling and security
|
||||
- Error recovery and rollback mechanisms
|
||||
|
||||
### Medium Priority
|
||||
- Parallel execution of independent operations
|
||||
- Caching for downloaded packages/configs
|
||||
- Improved logging and monitoring
|
||||
|
||||
### Low Priority
|
||||
- Code modernization (consider newer shell features)
|
||||
- Migration to configuration management tools (Ansible/Terraform)
|
||||
|
||||
## Conclusion
|
||||
|
||||
The FetchApply project represents a solid foundation for automated server provisioning with good security practices and testing. However, there are significant opportunities to improve security, maintainability, and operational resilience. Prioritizing security improvements and configuration management would provide the greatest value to the project's stability and long-term viability.
|
||||
|
||||
The modular architecture and comprehensive testing framework provide a strong foundation for future enhancements and improvements.
|
||||
@@ -0,0 +1,44 @@
|
||||
# AI Overview of KNELServerBuild
|
||||
|
||||
This is an AI-generated overview of the KNELServerBuild project. The analysis is based on a read-only review of the project's files.
|
||||
|
||||
## Project Overview
|
||||
|
||||
The KNELServerBuild project is an Infrastructure as Code (IAC) repository for provisioning and configuring Linux servers. It is based on a collection of bash scripts that automate the installation of packages, configuration of services, and security hardening of the system. The project is designed to be used with the `FetchApply` tool, which is not included in this repository.
|
||||
|
||||
The main entry point of the project is the `ProjectCode/SetupNewSystem.sh` script. This script performs the following actions:
|
||||
|
||||
* **Initializes the environment:** Sets up project paths and sources a shell framework (`KNELShellFramework`) and project-specific includes.
|
||||
* **Installs packages:** Installs a wide range of packages, including monitoring agents (check_mk, snmp), security tools (auditd, aide, lynis, clamav), administration tools (cockpit, webmin), and common utilities (tmux, vim, zsh).
|
||||
* **Configures services:** Configures various services like Postfix for email, `rsyslog` for system logging, `snmpd` for monitoring, `lldpd` for network discovery, and `cockpit`.
|
||||
* **Security Hardening:** It runs a series of security hardening scripts from `Modules/Security`, including `secharden-ssh.sh`, `secharden-wazuh.sh`, `secharden-2fa.sh`, and `secharden-scap-stig.sh`.
|
||||
* **OAM:** It runs an OAM (Operations, Administration, and Maintenance) script for LibreNMS.
|
||||
* **Conditional Logic:** It has conditional logic to apply different configurations based on whether the host is a physical Dell server, a virtual machine (KVM or Hyper-V), or a Raspberry Pi.
|
||||
|
||||
## What I Like
|
||||
|
||||
* **Well-structured:** The project is well-structured, with separate directories for code, configuration files, documentation, and tests. This makes it easy to understand and maintain.
|
||||
* **Modularity:** The use of modules for different functionalities (e.g., security hardening, OAM) is a good practice. It allows for easy extension and modification of the project.
|
||||
* **Comprehensive:** The project covers a wide range of aspects of server provisioning, from package installation to security hardening.
|
||||
* **Conditional Logic:** The use of conditional logic to adapt the configuration to different environments is a good feature.
|
||||
* **Good commenting:** The scripts are generally well-commented, which makes them easier to understand.
|
||||
|
||||
## Areas for Improvement
|
||||
|
||||
* **Error Handling:** The scripts could benefit from more robust error handling. For example, the `SetupNewSystem.sh` script uses `set -e` to exit on error, but it does not have any specific error handling logic.
|
||||
* **Idempotency:** The scripts are not fully idempotent. For example, some of the `curl` commands will re-download files even if they already exist. This could be improved by adding checks to see if the files already exist.
|
||||
* **Testing:** The project has a `Project-Tests` directory, but it is not clear how the tests are run or what they cover. The testing framework could be improved to provide more comprehensive coverage of the project's functionality.
|
||||
* **Secrets Management:** The scripts contain some hardcoded secrets, such as the `relayhost` for Postfix. These secrets should be managed using a secrets management tool like HashiCorp Vault or AWS Secrets Manager.
|
||||
* **Configuration Management:** The project uses a collection of shell scripts to manage the configuration of the system. While this works, it can be difficult to manage and maintain in the long run. A configuration management tool like Ansible, Puppet, or Chef would be a better choice for this task. The project already installs `ansible-core`, so it would be a natural progression to move the logic to Ansible playbooks.
|
||||
* **Documentation:** The project has some documentation, but it could be improved. For example, the `README.md` file could provide more information on how to use the project and how to contribute to it.
|
||||
|
||||
## Recommendations
|
||||
|
||||
* **Improve Error Handling:** Add more robust error handling to the scripts to make them more reliable.
|
||||
* **Improve Idempotency:** Make the scripts more idempotent to avoid unnecessary re-downloads and re-configurations.
|
||||
* **Improve Testing:** Implement a more comprehensive testing framework to ensure the quality of the project.
|
||||
* **Use a Secrets Management Tool:** Use a secrets management tool to manage the secrets in the project.
|
||||
* **Use a Configuration Management Tool:** Use a configuration management tool like Ansible to manage the configuration of the system.
|
||||
* **Improve Documentation:** Improve the documentation of the project to make it easier to use and contribute to.
|
||||
|
||||
Overall, the KNELServerBuild project is a good starting point for an IAC repository. It is well-structured and covers a wide range of aspects of server provisioning. However, there are some areas where it could be improved. By addressing the areas for improvement, the project can be made more robust, reliable, and maintainable.
|
||||
@@ -0,0 +1,308 @@
|
||||
# AI Overview: KNEL Server Build (FetchApply) Project
|
||||
|
||||
**Date:** December 26, 2025
|
||||
**Reviewer:** OpenCode AI Assistant
|
||||
**Project:** TSYS Infrastructure Provisioning System
|
||||
|
||||
## Executive Summary
|
||||
|
||||
The KNEL Server Build project is a comprehensive Infrastructure as Code (IaC) system for Linux server provisioning and security hardening. It demonstrates strong architectural patterns with a modular framework approach but has several areas requiring improvement for production readiness, security, and maintainability.
|
||||
|
||||
## Architecture Assessment
|
||||
|
||||
### Strengths ✅
|
||||
|
||||
**1. Modular Framework Design**
|
||||
- Well-structured KNELShellFramework with centralized includes
|
||||
- Clear separation between framework, project code, and configuration
|
||||
- Consistent pattern for sourcing framework components
|
||||
- Proper abstraction of common functionality
|
||||
|
||||
**2. Comprehensive Security Modules**
|
||||
- Extensive security hardening capabilities (SSH, Wazuh, 2FA, SCAP/STIG)
|
||||
- HTTPS enforcement throughout
|
||||
- Proper audit logging integration
|
||||
- Good compliance focus with industry standards
|
||||
|
||||
**3. Testing Infrastructure**
|
||||
- Automated test suite with multiple categories (unit, integration, security, validation)
|
||||
- JSON-based test reporting
|
||||
- Good test organization and coverage
|
||||
|
||||
**4. Documentation Excellence**
|
||||
- Comprehensive deployment guide with troubleshooting
|
||||
- Detailed development guidelines with best practices
|
||||
- Security documentation with threat model
|
||||
- Code review findings and refactoring examples
|
||||
|
||||
### Areas for Improvement ⚠️
|
||||
|
||||
**1. Performance Issues**
|
||||
- Multiple separate package installation commands instead of consolidated approach
|
||||
- Individual file downloads causing network overhead
|
||||
- No connection pooling for multiple downloads from same host
|
||||
|
||||
**2. Security Vulnerabilities**
|
||||
- SSH keys stored in git repository (secrets management needed)
|
||||
- No download integrity verification (checksum validation)
|
||||
- Missing comprehensive input validation
|
||||
- Unquoted variable expansions creating injection risks
|
||||
|
||||
**3. Error Handling Gaps**
|
||||
- Network operations lack timeout and retry logic
|
||||
- Inconsistent error handling across modules
|
||||
- Missing graceful failure handling in critical paths
|
||||
|
||||
## Technical Debt Analysis
|
||||
|
||||
### High Priority Issues
|
||||
|
||||
**1. Package Installation Performance**
|
||||
```bash
|
||||
# Current inefficient pattern in SetupNewSystem.sh
|
||||
apt-get -y install git sudo dmidecode curl # Line 27
|
||||
# Later: separate massive apt-get command
|
||||
```
|
||||
**Impact:** 30-40% slower deployments, multiple package cache updates
|
||||
|
||||
**2. Network Resilience**
|
||||
```bash
|
||||
# Vulnerable pattern throughout codebase
|
||||
curl --silent ${DL_ROOT}/path/file >/etc/config
|
||||
```
|
||||
**Impact:** Deployment failures in poor network conditions, no recovery mechanism
|
||||
|
||||
**3. Variable Quoting Security**
|
||||
```bash
|
||||
# Risky pattern
|
||||
chsh -s $(which zsh) root
|
||||
```
|
||||
**Impact:** Potential command injection vulnerabilities
|
||||
|
||||
### Medium Priority Issues
|
||||
|
||||
**1. Framework Consistency**
|
||||
- Not all modules follow established error handling patterns
|
||||
- Inconsistent logging and progress reporting
|
||||
- Mixed coding standards across different components
|
||||
|
||||
**2. Testing Coverage**
|
||||
- Limited integration testing for complex workflows
|
||||
- Missing performance benchmarking tests
|
||||
- No automated regression testing for configuration changes
|
||||
|
||||
## Recommendations
|
||||
|
||||
### Immediate Actions (Week 1-2)
|
||||
|
||||
**1. Implement Safe Download Framework**
|
||||
```bash
|
||||
# Create centralized download function with:
|
||||
# - Connection timeouts (30s)
|
||||
# - Retry logic (3 attempts)
|
||||
# - Checksum validation
|
||||
# - Error recovery
|
||||
```
|
||||
|
||||
**2. Consolidate Package Management**
|
||||
```bash
|
||||
# Single package installation with logical grouping:
|
||||
# - Core system tools
|
||||
# - Security packages
|
||||
# - Monitoring tools
|
||||
# - Development utilities
|
||||
```
|
||||
|
||||
**3. Fix Variable Quoting**
|
||||
- Audit entire codebase for unquoted variables
|
||||
- Implement static analysis check in CI pipeline
|
||||
- Add input validation framework
|
||||
|
||||
### Medium-term Improvements (Month 1-2)
|
||||
|
||||
**1. Secrets Management**
|
||||
- Remove SSH keys from repository
|
||||
- Integrate Bitwarden/Vault for secret storage
|
||||
- Implement key rotation procedures
|
||||
|
||||
**2. Performance Optimization**
|
||||
- Implement batch download operations
|
||||
- Add connection pooling
|
||||
- Create deployment metrics collection
|
||||
|
||||
**3. Enhanced Testing**
|
||||
- Add performance benchmarking
|
||||
- Implement chaos engineering for network failures
|
||||
- Create automated regression testing
|
||||
|
||||
### Long-term Enhancements (Quarter 1)
|
||||
|
||||
**1. Infrastructure Improvements**
|
||||
- Implement configuration backup/restore
|
||||
- Add rollback capability for failed deployments
|
||||
- Create deployment pipeline with staging environments
|
||||
|
||||
**2. Advanced Security**
|
||||
- Implement supply chain security with SBOM
|
||||
- Add automated vulnerability scanning
|
||||
- Create security compliance reporting
|
||||
|
||||
## Code Quality Assessment
|
||||
|
||||
### Positive Patterns
|
||||
- Good function documentation in recent code
|
||||
- Proper error handling in newer modules
|
||||
- Consistent use of framework logging functions
|
||||
- Clear separation of concerns
|
||||
|
||||
### Problem Patterns
|
||||
- Mixed coding styles across files
|
||||
- Inconsistent framework usage
|
||||
- Missing input validation
|
||||
- Hardcoded configuration values
|
||||
|
||||
### Modernization Opportunities
|
||||
|
||||
**1. Containerization**
|
||||
- Consider Docker-based deployment testing
|
||||
- Create immutable infrastructure patterns
|
||||
- Implement blue-green deployments
|
||||
|
||||
**2. Configuration Management**
|
||||
- Move to declarative configuration approach
|
||||
- Implement configuration drift detection
|
||||
- Add automated compliance checking
|
||||
|
||||
**3. Observability**
|
||||
- Implement comprehensive logging with structured formats
|
||||
- Add metrics collection for deployment performance
|
||||
- Create dashboard for system health monitoring
|
||||
|
||||
## Security Posture Review
|
||||
|
||||
### Current Strengths
|
||||
- HTTPS-only downloads
|
||||
- Good SSH hardening practices
|
||||
- Comprehensive audit logging
|
||||
- Regular security scanning integration
|
||||
|
||||
### Critical Gaps
|
||||
- No integrity verification for downloads
|
||||
- Secrets stored in version control
|
||||
- Limited defense in depth
|
||||
- Missing automated security testing
|
||||
|
||||
### Recommended Security Enhancements
|
||||
|
||||
**1. Supply Chain Security**
|
||||
- Implement checksum validation for all downloads
|
||||
- Add GPG signature verification where available
|
||||
- Create SBOM generation for deployments
|
||||
|
||||
**2. Access Control**
|
||||
- Implement role-based access control
|
||||
- Add privileged access management
|
||||
- Create audit trail for all administrative actions
|
||||
|
||||
**3. Continuous Security**
|
||||
- Integrate automated vulnerability scanning
|
||||
- Implement security testing in CI/CD
|
||||
- Create security metrics dashboard
|
||||
|
||||
## Deployment Readiness Assessment
|
||||
|
||||
### Current State: **70% Production Ready**
|
||||
|
||||
**Ready Components:**
|
||||
- Core provisioning functionality
|
||||
- Security hardening modules
|
||||
- Basic testing framework
|
||||
- Documentation
|
||||
|
||||
**Missing Components:**
|
||||
- Robust error handling
|
||||
- Performance optimization
|
||||
- Secrets management
|
||||
- Comprehensive testing
|
||||
|
||||
### Path to Production Readiness
|
||||
|
||||
**Phase 1 (2 weeks):** Critical fixes and performance optimization
|
||||
**Phase 2 (4 weeks):** Security enhancements and testing improvements
|
||||
**Phase 3 (8 weeks):** Advanced features and production hardening
|
||||
|
||||
## Overall Assessment
|
||||
|
||||
### What I Like 🎯
|
||||
|
||||
**1. Architectural Excellence**
|
||||
- The KNELShellFramework shows mature thinking about code organization
|
||||
- Modular approach allows for easy maintenance and extension
|
||||
- Clear separation of concerns between framework and project code
|
||||
|
||||
**2. Security-First Mindset**
|
||||
- Comprehensive security hardening capabilities
|
||||
- Good threat awareness and mitigation strategies
|
||||
- Integration with industry-standard security tools
|
||||
|
||||
**3. Documentation Quality**
|
||||
- Excellent documentation with practical examples
|
||||
- Clear deployment guides with troubleshooting sections
|
||||
- Good development guidelines for team consistency
|
||||
|
||||
### What I Don't Like 🚫
|
||||
|
||||
**1. Performance Oversights**
|
||||
- Multiple package installations causing unnecessary delays
|
||||
- Individual file downloads creating network overhead
|
||||
- No performance metrics or monitoring
|
||||
|
||||
**2. Security Gaps**
|
||||
- Critical vulnerability with secrets in git repository
|
||||
- No download integrity verification
|
||||
- Missing comprehensive input validation
|
||||
|
||||
**3. Code Quality Issues**
|
||||
- Inconsistent error handling across modules
|
||||
- Variable quoting creating security risks
|
||||
- Mixed coding standards throughout codebase
|
||||
|
||||
### Improvement Potential 📈
|
||||
|
||||
**1. Immediate Impact (High ROI)**
|
||||
- Package installation consolidation: 30-40% performance improvement
|
||||
- Safe download framework: 90% reduction in network-related failures
|
||||
- Variable quoting fixes: Eliminate security vulnerabilities
|
||||
|
||||
**2. Medium-term Benefits**
|
||||
- Secrets management: Eliminate critical security risks
|
||||
- Performance optimization: Better user experience
|
||||
- Enhanced testing: Higher reliability and confidence
|
||||
|
||||
**3. Long-term Value**
|
||||
- Containerization: Modern deployment patterns
|
||||
- Observability: Better operational insight
|
||||
- Automation: Reduced manual overhead
|
||||
|
||||
## Final Recommendation
|
||||
|
||||
The KNEL Server Build project demonstrates solid architectural foundations and comprehensive security capabilities. With focused improvements in performance optimization, security hardening (particularly secrets management), and error handling, this system can become a production-grade infrastructure provisioning solution.
|
||||
|
||||
**Priority:**
|
||||
1. **Immediate:** Fix security vulnerabilities and performance bottlenecks
|
||||
2. **Short-term:** Enhance testing and error handling
|
||||
3. **Long-term:** Implement advanced features and modernization
|
||||
|
||||
**Investment Justification:** The project shows strong potential with a clear path to production readiness. The modular architecture and comprehensive security focus make it a valuable foundation for enterprise infrastructure automation.
|
||||
|
||||
---
|
||||
|
||||
**Next Steps:**
|
||||
1. Create implementation roadmap for critical fixes
|
||||
2. Establish performance benchmarks
|
||||
3. Implement continuous integration with quality gates
|
||||
4. Plan phased rollout to production environments
|
||||
|
||||
**Risk Level:** Medium - manageable with proper remediation plan
|
||||
**Business Value:** High - significant time savings and security improvements
|
||||
**Technical Debt:** Moderate - requires systematic but achievable refactoring
|
||||
@@ -0,0 +1,27 @@
|
||||
# AI Security Audit of KNELServerBuild
|
||||
|
||||
This is an AI-generated security audit of the KNELServerBuild project. The analysis is based on a read-only review of the project's files.
|
||||
|
||||
## Summary of Findings
|
||||
|
||||
The KNELServerBuild project has a good security posture overall, but there are a few areas that could be improved. The most significant finding is the presence of SSH authorized keys in the repository. This is a security risk, as it allows anyone with access to the repository to know which public keys are authorized to access the servers.
|
||||
|
||||
### High-Risk Findings
|
||||
|
||||
* **SSH Authorized Keys in Repository:** The `ProjectCode/ConfigFiles/SSH/AuthorizedKeys` directory contains SSH authorized keys for the `localuser` and `root` users. This is a security risk, as it allows anyone with access to the repository to know which public keys are authorized to access the servers.
|
||||
|
||||
### Medium-Risk Findings
|
||||
|
||||
* **Hardcoded Hostnames:** The scripts contain several hardcoded hostnames for services like Postfix, NTP, syslog, and Wazuh. This is not a direct security risk, but it does represent a configuration management issue. If any of these hostnames change, they will need to be updated in multiple places.
|
||||
|
||||
### Low-Risk Findings
|
||||
|
||||
* **Potential for Password on Command Line:** The `ProjectCode/Agents/librenms/mysql.sh` script has a `--pass` argument for a MySQL password. This is a potential security risk if the password is provided on the command line, as it could be logged in the shell history.
|
||||
|
||||
## Recommendations
|
||||
|
||||
* **Remove SSH Authorized Keys from Repository:** The SSH authorized keys should be removed from the repository and managed using a secrets management tool like HashiCorp Vault or AWS Secrets Manager.
|
||||
* **Use Variables for Hostnames:** The hardcoded hostnames should be replaced with variables that are defined in a central configuration file. This will make it easier to update the hostnames if they change.
|
||||
* **Avoid Passwords on Command Line:** The `ProjectCode/Agents/librenms/mysql.sh` script should be modified to avoid passing the MySQL password on the command line. For example, the script could prompt the user for the password or read it from a configuration file.
|
||||
|
||||
Overall, the KNELServerBuild project is a good starting point for an IAC repository. By addressing the security risks identified in this audit, the project can be made more secure and reliable.
|
||||
@@ -0,0 +1,279 @@
|
||||
# TSYS FetchApply Code Review Findings
|
||||
|
||||
**Review Date:** July 14, 2025
|
||||
**Reviewer:** Claude (Anthropic)
|
||||
**Repository:** TSYS Group Infrastructure Provisioning Scripts
|
||||
|
||||
## Executive Summary
|
||||
|
||||
The repository shows good architectural structure with centralized framework components, but has several performance, security, and maintainability issues that require attention. The codebase is functional but needs optimization for production reliability.
|
||||
|
||||
## Critical Issues (High Priority)
|
||||
|
||||
### 1. Package Installation Performance ⚠️
|
||||
**Location:** `ProjectCode/SetupNewSystem.sh:27` and `Lines 117-183`
|
||||
**Issue:** Multiple separate package installation commands causing performance bottlenecks
|
||||
```bash
|
||||
# Current inefficient pattern
|
||||
apt-get -y install git sudo dmidecode curl
|
||||
# ... later in script ...
|
||||
DEBIAN_FRONTEND="noninteractive" apt-get -qq --yes install virt-what auditd ...
|
||||
```
|
||||
**Impact:** Significantly slower deployment, multiple package cache updates
|
||||
**Fix:** Combine all package installations into single command
|
||||
|
||||
### 2. Network Operations Lack Error Handling 🔴
|
||||
**Location:** `ProjectCode/SetupNewSystem.sh:61-63`, multiple modules
|
||||
**Issue:** curl commands without timeout or error handling
|
||||
```bash
|
||||
# Vulnerable pattern
|
||||
curl --silent ${DL_ROOT}/path/file >/etc/config
|
||||
```
|
||||
**Impact:** Deployment failures in poor network conditions
|
||||
**Fix:** Add timeout, error handling, and retry logic
|
||||
|
||||
### 3. Unquoted Variable Expansions 🔴
|
||||
**Location:** Multiple files, including `ProjectCode/SetupNewSystem.sh:244`
|
||||
**Issue:** Variables used without proper quoting creating security risks
|
||||
```bash
|
||||
# Risky pattern
|
||||
chsh -s $(which zsh) root
|
||||
```
|
||||
**Impact:** Potential command injection, script failures
|
||||
**Fix:** Quote all variable expansions consistently
|
||||
|
||||
## Security Concerns
|
||||
|
||||
### 4. No Download Integrity Verification 🔴
|
||||
**Issue:** All remote downloads lack checksum verification
|
||||
**Impact:** Supply chain attack vulnerability
|
||||
**Recommendation:** Implement SHA256 checksum validation
|
||||
|
||||
### 5. Excessive Root Privilege Usage ⚠️
|
||||
**Issue:** All operations run as root without privilege separation
|
||||
**Impact:** Unnecessary security exposure
|
||||
**Recommendation:** Delegate non-privileged operations when possible
|
||||
|
||||
## Performance Optimization Opportunities
|
||||
|
||||
### 6. Individual File Downloads 🟡
|
||||
**Location:** `ProjectCode/Modules/Security/secharden-scap-stig.sh:66-77`
|
||||
**Issue:** 12+ individual curl commands for config files
|
||||
```bash
|
||||
curl --silent ${DL_ROOT}/path1 > /etc/file1
|
||||
curl --silent ${DL_ROOT}/path2 > /etc/file2
|
||||
# ... repeated 12+ times
|
||||
```
|
||||
**Impact:** Network overhead, slower deployment
|
||||
**Fix:** Batch download operations
|
||||
|
||||
### 7. Missing Connection Pooling ⚠️
|
||||
**Issue:** No connection reuse for multiple downloads from same host
|
||||
**Impact:** Unnecessary connection overhead
|
||||
**Fix:** Use curl with connection reuse or wget with keep-alive
|
||||
|
||||
## Code Quality Issues
|
||||
|
||||
### 8. Inconsistent Framework Usage 🟡
|
||||
**Issue:** Not all modules use established error handling framework
|
||||
**Impact:** Inconsistent error reporting, debugging difficulties
|
||||
**Fix:** Standardize framework usage across all modules
|
||||
|
||||
### 9. Incomplete Function Implementations 🟡
|
||||
**Location:** `Framework-Includes/LookupKv.sh`
|
||||
**Issue:** Stubbed functions with no implementation
|
||||
**Impact:** Technical debt, confusion
|
||||
**Fix:** Implement or remove unused functions
|
||||
|
||||
### 10. Missing Input Validation 🟡
|
||||
**Location:** `Project-Includes/pi-detect.sh`
|
||||
**Issue:** Functions lack proper input validation and quoting
|
||||
**Impact:** Potential script failures
|
||||
**Fix:** Add comprehensive input validation
|
||||
|
||||
## Recommended Immediate Actions
|
||||
|
||||
### Phase 1: Critical Fixes (Week 1)
|
||||
1. **Fix variable quoting** throughout codebase
|
||||
2. **Add error handling** to all network operations
|
||||
3. **Combine package installations** for performance
|
||||
4. **Implement download integrity verification**
|
||||
|
||||
### Phase 2: Performance Optimization (Week 2)
|
||||
1. **Batch file download operations**
|
||||
2. **Add connection timeouts and retries**
|
||||
3. **Implement bulk configuration deployment**
|
||||
4. **Optimize service restart procedures**
|
||||
|
||||
### Phase 3: Code Quality (Week 3-4)
|
||||
1. **Standardize framework usage**
|
||||
2. **Add comprehensive input validation**
|
||||
3. **Implement proper logging with timestamps**
|
||||
4. **Remove or complete stubbed functions**
|
||||
|
||||
## Specific Code Improvements
|
||||
|
||||
### Enhanced Error Handling Pattern
|
||||
```bash
|
||||
function safe_download() {
|
||||
local url="$1"
|
||||
local dest="$2"
|
||||
local max_attempts=3
|
||||
local attempt=1
|
||||
|
||||
while [[ $attempt -le $max_attempts ]]; do
|
||||
if curl --silent --connect-timeout 30 --max-time 60 --fail "$url" > "$dest"; then
|
||||
print_success "Downloaded: $(basename "$dest")"
|
||||
return 0
|
||||
else
|
||||
print_warning "Download attempt $attempt failed: $url"
|
||||
((attempt++))
|
||||
sleep 5
|
||||
fi
|
||||
done
|
||||
|
||||
print_error "Failed to download after $max_attempts attempts: $url"
|
||||
return 1
|
||||
}
|
||||
```
|
||||
|
||||
### Bulk Package Installation Pattern
|
||||
```bash
|
||||
function install_all_packages() {
|
||||
print_info "Installing all required packages..."
|
||||
|
||||
local packages=(
|
||||
# Core system packages
|
||||
git sudo dmidecode curl wget
|
||||
|
||||
# Security packages
|
||||
auditd fail2ban aide
|
||||
|
||||
# Monitoring packages
|
||||
snmpd snmp-mibs-downloader
|
||||
|
||||
# Additional packages
|
||||
virt-what net-tools htop
|
||||
)
|
||||
|
||||
if DEBIAN_FRONTEND="noninteractive" apt-get -qq --yes -o Dpkg::Options::="--force-confold" install "${packages[@]}"; then
|
||||
print_success "All packages installed successfully"
|
||||
else
|
||||
print_error "Package installation failed"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
```
|
||||
|
||||
### Batch Configuration Download
|
||||
```bash
|
||||
function download_configurations() {
|
||||
print_info "Downloading configuration files..."
|
||||
|
||||
local -A configs=(
|
||||
["${DL_ROOT}/ProjectCode/ConfigFiles/ZSH/tsys-zshrc"]="/etc/zshrc"
|
||||
["${DL_ROOT}/ProjectCode/ConfigFiles/SMTP/aliases"]="/etc/aliases"
|
||||
["${DL_ROOT}/ProjectCode/ConfigFiles/Syslog/rsyslog.conf"]="/etc/rsyslog.conf"
|
||||
)
|
||||
|
||||
for url in "${!configs[@]}"; do
|
||||
local dest="${configs[$url]}"
|
||||
if ! safe_download "$url" "$dest"; then
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
|
||||
print_success "All configurations downloaded"
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Recommendations
|
||||
|
||||
### Add Performance Tests
|
||||
```bash
|
||||
function test_package_installation_performance() {
|
||||
local start_time=$(date +%s)
|
||||
install_all_packages
|
||||
local end_time=$(date +%s)
|
||||
local duration=$((end_time - start_time))
|
||||
|
||||
echo "✅ Package installation completed in ${duration}s"
|
||||
|
||||
if [[ $duration -gt 300 ]]; then
|
||||
echo "⚠️ Installation took longer than expected (>5 minutes)"
|
||||
fi
|
||||
}
|
||||
```
|
||||
|
||||
### Add Network Resilience Tests
|
||||
```bash
|
||||
function test_network_error_handling() {
|
||||
# Test with invalid URL
|
||||
if safe_download "https://invalid.example.com/file" "/tmp/test"; then
|
||||
echo "❌ Error handling test failed - should have failed"
|
||||
return 1
|
||||
else
|
||||
echo "✅ Error handling test passed"
|
||||
return 0
|
||||
fi
|
||||
}
|
||||
```
|
||||
|
||||
## Monitoring and Metrics
|
||||
|
||||
### Deployment Performance Metrics
|
||||
- **Package installation time:** Should complete in <5 minutes
|
||||
- **Configuration download time:** Should complete in <2 minutes
|
||||
- **Service restart time:** Should complete in <30 seconds
|
||||
- **Total deployment time:** Should complete in <15 minutes
|
||||
|
||||
### Error Rate Monitoring
|
||||
- **Network operation failures:** Should be <1%
|
||||
- **Package installation failures:** Should be <0.1%
|
||||
- **Service restart failures:** Should be <0.1%
|
||||
|
||||
## Compliance Assessment
|
||||
|
||||
### Development Guidelines Adherence
|
||||
✅ **Good:** Single package commands in newer modules
|
||||
✅ **Good:** Framework integration patterns
|
||||
✅ **Good:** Function documentation in recent code
|
||||
|
||||
❌ **Needs Work:** Variable quoting consistency
|
||||
❌ **Needs Work:** Error handling standardization
|
||||
❌ **Needs Work:** Input validation coverage
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
**Current Risk Level:** Medium
|
||||
|
||||
**Key Risks:**
|
||||
1. **Deployment failures** due to network issues
|
||||
2. **Security vulnerabilities** from unvalidated downloads
|
||||
3. **Performance issues** in production deployments
|
||||
4. **Maintenance challenges** from code inconsistencies
|
||||
|
||||
**Mitigation Priority:**
|
||||
1. Network error handling (High)
|
||||
2. Download integrity verification (High)
|
||||
3. Performance optimization (Medium)
|
||||
4. Code standardization (Medium)
|
||||
|
||||
## Conclusion
|
||||
|
||||
The TSYS FetchApply repository has a solid foundation but requires systematic improvements to meet production reliability standards. The recommended fixes will significantly enhance:
|
||||
|
||||
- **Deployment reliability** through better error handling
|
||||
- **Security posture** through integrity verification
|
||||
- **Performance** through optimized operations
|
||||
- **Maintainability** through code standardization
|
||||
|
||||
Implementing these improvements in the suggested phases will create a robust, production-ready infrastructure provisioning system.
|
||||
|
||||
---
|
||||
|
||||
**Next Steps:**
|
||||
1. Review and prioritize findings with development team
|
||||
2. Create implementation plan for critical fixes
|
||||
3. Establish testing procedures for improvements
|
||||
4. Set up monitoring for deployment metrics
|
||||
@@ -0,0 +1,93 @@
|
||||
# Claude Code Review - TSYS FetchApply Infrastructure
|
||||
|
||||
**Review Date:** July 14, 2025 (Updated)
|
||||
**Reviewed by:** Claude (Anthropic)
|
||||
**Repository:** TSYS Group Infrastructure Provisioning Scripts
|
||||
**Previous Review:** July 12, 2025
|
||||
|
||||
## Project Overview
|
||||
|
||||
This repository contains infrastructure-as-code for provisioning Linux servers in the TSYS Group environment. The codebase includes 32 shell scripts (~2,800 lines) organized into a modular framework for system hardening, security configuration, and operational tooling deployment.
|
||||
|
||||
## Strengths ✅
|
||||
|
||||
### Security Hardening
|
||||
- **SSH Security:** Comprehensive SSH hardening with key-only authentication, disabled password login, and secure cipher configurations
|
||||
- **Security Agents:** Automated deployment of Wazuh SIEM agents, audit tools, and SCAP-STIG compliance checking
|
||||
- **File Permissions:** Proper restrictive permissions (400 for SSH keys, 644 for configs)
|
||||
- **Network Security:** Firewall configuration, network discovery tools (LLDP), and monitoring agents
|
||||
|
||||
### Code Quality
|
||||
- **Error Handling:** Robust bash strict mode implementation (`set -euo pipefail`) with custom error trapping and line number reporting
|
||||
- **Modular Design:** Well-organized structure separating framework components, configuration files, and functional modules
|
||||
- **Environment Awareness:** Intelligent detection of physical vs virtual hosts, distribution-specific logic, and hardware-specific optimizations
|
||||
- **Logging:** Centralized logging with timestamp-based log files and colored output for debugging
|
||||
|
||||
### Operational Excellence
|
||||
- **Package Management:** Automated repository setup for security tools (Lynis, Webmin, Tailscale, Wazuh)
|
||||
- **System Tuning:** Performance optimizations for physical hosts, virtualization-aware configurations
|
||||
- **Monitoring Integration:** LibreNMS agents, SNMP configuration, and system metrics collection
|
||||
|
||||
## Security Concerns ⚠️
|
||||
|
||||
### Critical Issues
|
||||
1. **~~Insecure Deployment Method~~** ✅ **RESOLVED:** Now uses `git clone` + local script execution instead of `curl | bash`
|
||||
2. **No Integrity Verification:** Downloaded scripts lack checksum validation or cryptographic signatures
|
||||
3. **~~HTTP Downloads~~** ✅ **RESOLVED:** All HTTP URLs converted to HTTPS (Dell OMSA, Proxmox, Apache sources)
|
||||
|
||||
### Moderate Risks
|
||||
4. **Exposed SSH Keys:** Public SSH keys committed directly to repository without rotation mechanism
|
||||
5. **Hard-coded Credentials:** Server hostnames and domain names embedded in scripts
|
||||
6. **Missing Secrets Management:** No current implementation of Bitwarden/Vault integration (noted in TODO comments)
|
||||
|
||||
## Improvement Recommendations 🔧
|
||||
|
||||
### High Priority (Security Critical)
|
||||
1. **~~Secure Deployment Pipeline~~** ✅ **RESOLVED:** Now uses git clone-based deployment
|
||||
2. **~~HTTPS Enforcement~~** ✅ **RESOLVED:** All HTTP downloads converted to HTTPS
|
||||
3. **Script Integrity:** Implement SHA256 checksum verification for all downloaded components
|
||||
4. **Secrets Management:** Deploy proper secrets handling for SSH keys and sensitive configurations
|
||||
|
||||
### Medium Priority (Operational)
|
||||
5. **Testing Framework:** Add integration tests for provisioning workflows
|
||||
6. **Documentation Enhancement:** Expand security considerations and deployment procedures
|
||||
7. **Configuration Validation:** Add pre-deployment validation of system requirements
|
||||
8. **Rollback Capability:** Implement configuration backup and rollback mechanisms
|
||||
|
||||
### Low Priority (Quality of Life)
|
||||
9. **Error Recovery:** Enhanced error recovery and partial deployment resumption
|
||||
10. **Monitoring Integration:** Centralized logging and deployment status reporting
|
||||
11. **User Interface:** Consider web-based deployment dashboard for non-technical users
|
||||
|
||||
## Risk Assessment 📊
|
||||
|
||||
**Overall Risk Level:** Low-Medium ⬇️ (Reduced from Medium-Low)
|
||||
|
||||
The repository contains well-architected defensive security tools with strong error handling and modular design. **Major security improvement:** The insecure `curl | bash` deployment method has been replaced with git-based deployment. Remaining concerns are primarily around hardening the provisioning scripts themselves rather than the deployment method.
|
||||
|
||||
**Recommendation:** Continue addressing remaining security issues (HTTPS enforcement, secrets management) but the critical deployment risk has been mitigated. The codebase is much safer for production use.
|
||||
|
||||
## Update Summary (July 14, 2025)
|
||||
|
||||
**✅ Resolved Issues:**
|
||||
- Insecure deployment method replaced with git clone approach
|
||||
- README.md updated with project management and community links
|
||||
- Deployment security risk significantly reduced
|
||||
- All HTTP URLs converted to HTTPS (Dell OMSA, Proxmox, Apache sources)
|
||||
|
||||
**🔄 Remaining Priorities:**
|
||||
1. ~~HTTPS enforcement for internal downloads~~ ✅ **RESOLVED:** All HTTP URLs converted to HTTPS
|
||||
2. Secrets management implementation
|
||||
3. Script integrity verification
|
||||
4. SSH key rotation from repository
|
||||
|
||||
## Files Reviewed
|
||||
|
||||
- 32 shell scripts across Framework-Includes, Project-Includes, and ProjectCode directories
|
||||
- Configuration files for SSH, SNMP, logging, and system services
|
||||
- Security modules for hardening, authentication, and monitoring
|
||||
- Documentation and framework configuration files
|
||||
|
||||
## Next Steps
|
||||
|
||||
See `charles-todo.md` and `claude-todo.md` for detailed action items prioritized for human operators and AI assistants respectively.
|
||||
@@ -0,0 +1,336 @@
|
||||
# TSYS FetchApply Deployment Guide
|
||||
|
||||
## Overview
|
||||
|
||||
This guide provides comprehensive instructions for deploying the TSYS FetchApply infrastructure provisioning system on Linux servers.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### System Requirements
|
||||
- **Operating System:** Ubuntu 18.04+ or Debian 10+ (recommended)
|
||||
- **RAM:** Minimum 2GB, recommended 4GB
|
||||
- **Disk Space:** Minimum 10GB free space
|
||||
- **Network:** Internet connectivity for package downloads
|
||||
- **Privileges:** Root or sudo access required
|
||||
|
||||
### Required Tools
|
||||
- `git` - Version control system
|
||||
- `curl` - HTTP client for downloads
|
||||
- `wget` - Alternative download tool
|
||||
- `systemctl` - System service management
|
||||
- `apt-get` - Package management (Debian/Ubuntu)
|
||||
|
||||
### Network Requirements
|
||||
- **HTTPS access** to:
|
||||
- `https://archive.ubuntu.com` (Ubuntu packages)
|
||||
- `https://linux.dell.com` (Dell hardware support)
|
||||
- `https://download.proxmox.com` (Proxmox packages)
|
||||
- `https://github.com` (Git repositories)
|
||||
|
||||
## Pre-Deployment Validation
|
||||
|
||||
### 1. System Compatibility Check
|
||||
```bash
|
||||
# Clone repository
|
||||
git clone [repository-url]
|
||||
cd FetchApply
|
||||
|
||||
# Run system validation
|
||||
./Project-Tests/validation/system-requirements.sh
|
||||
```
|
||||
|
||||
### 2. Network Connectivity Test
|
||||
```bash
|
||||
# Test network connectivity
|
||||
curl -I https://archive.ubuntu.com
|
||||
curl -I https://linux.dell.com
|
||||
curl -I https://download.proxmox.com
|
||||
```
|
||||
|
||||
### 3. Permission Verification
|
||||
```bash
|
||||
# Verify write permissions
|
||||
test -w /etc && echo "✅ /etc writable" || echo "❌ /etc not writable"
|
||||
test -w /usr/local/bin && echo "✅ /usr/local/bin writable" || echo "❌ /usr/local/bin not writable"
|
||||
```
|
||||
|
||||
## Deployment Methods
|
||||
|
||||
### Method 1: Standard Deployment (Recommended)
|
||||
```bash
|
||||
# 1. Clone repository
|
||||
git clone [repository-url]
|
||||
cd FetchApply
|
||||
|
||||
# 2. Run pre-deployment tests
|
||||
./Project-Tests/run-tests.sh validation
|
||||
|
||||
# 3. Execute deployment
|
||||
cd ProjectCode
|
||||
sudo bash SetupNewSystem.sh
|
||||
```
|
||||
|
||||
### Method 2: Dry Run Mode
|
||||
```bash
|
||||
# 1. Clone repository
|
||||
git clone [repository-url]
|
||||
cd FetchApply
|
||||
|
||||
# 2. Review configuration
|
||||
cat ProjectCode/SetupNewSystem.sh
|
||||
|
||||
# 3. Execute with manual review
|
||||
cd ProjectCode
|
||||
sudo bash -x SetupNewSystem.sh # Debug mode
|
||||
```
|
||||
|
||||
## Deployment Process
|
||||
|
||||
### Phase 1: Framework Initialization
|
||||
1. **Environment Setup**
|
||||
- Load framework variables
|
||||
- Source framework includes
|
||||
- Initialize logging system
|
||||
|
||||
2. **System Detection**
|
||||
- Detect physical vs virtual hardware
|
||||
- Identify operating system
|
||||
- Check for existing users
|
||||
|
||||
### Phase 2: Base System Configuration
|
||||
1. **Package Installation**
|
||||
- Update package repositories
|
||||
- Install essential packages
|
||||
- Configure package sources
|
||||
|
||||
2. **User Management**
|
||||
- Create required user accounts
|
||||
- Configure SSH access
|
||||
- Set up sudo permissions
|
||||
|
||||
### Phase 3: Security Hardening
|
||||
1. **SSH Configuration**
|
||||
- Deploy hardened SSH configuration
|
||||
- Install SSH keys
|
||||
- Disable password authentication
|
||||
|
||||
2. **System Hardening**
|
||||
- Configure firewall rules
|
||||
- Enable audit logging
|
||||
- Install security tools
|
||||
|
||||
### Phase 4: Monitoring and Management
|
||||
1. **Monitoring Agents**
|
||||
- Deploy LibreNMS agents
|
||||
- Configure SNMP
|
||||
- Set up system monitoring
|
||||
|
||||
2. **Management Tools**
|
||||
- Install Cockpit dashboard
|
||||
- Configure remote access
|
||||
- Set up maintenance scripts
|
||||
|
||||
## Post-Deployment Verification
|
||||
|
||||
### 1. Security Validation
|
||||
```bash
|
||||
# Run security tests
|
||||
./Project-Tests/run-tests.sh security
|
||||
|
||||
# Verify SSH configuration
|
||||
ssh -T [server-ip] # Should work with key authentication
|
||||
```
|
||||
|
||||
### 2. Service Status Check
|
||||
```bash
|
||||
# Check critical services
|
||||
sudo systemctl status ssh
|
||||
sudo systemctl status auditd
|
||||
sudo systemctl status snmpd
|
||||
```
|
||||
|
||||
### 3. Network Connectivity
|
||||
```bash
|
||||
# Test internal services
|
||||
curl -k https://localhost:9090 # Cockpit
|
||||
snmpwalk -v2c -c public localhost system
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
#### 1. Permission Denied Errors
|
||||
```bash
|
||||
# Solution: Run with sudo
|
||||
sudo bash SetupNewSystem.sh
|
||||
```
|
||||
|
||||
#### 2. Network Connectivity Issues
|
||||
```bash
|
||||
# Check DNS resolution
|
||||
nslookup archive.ubuntu.com
|
||||
|
||||
# Test direct IP access
|
||||
curl -I 91.189.91.26 # Ubuntu archive IP
|
||||
```
|
||||
|
||||
#### 3. Package Installation Failures
|
||||
```bash
|
||||
# Update package cache
|
||||
sudo apt-get update
|
||||
|
||||
# Fix broken packages
|
||||
sudo apt-get -f install
|
||||
```
|
||||
|
||||
#### 4. SSH Key Issues
|
||||
```bash
|
||||
# Verify key permissions
|
||||
ls -la ~/.ssh/
|
||||
chmod 600 ~/.ssh/id_rsa
|
||||
chmod 644 ~/.ssh/id_rsa.pub
|
||||
```
|
||||
|
||||
### Debug Mode
|
||||
```bash
|
||||
# Enable debug logging
|
||||
export DEBUG=1
|
||||
bash -x SetupNewSystem.sh
|
||||
```
|
||||
|
||||
### Log Analysis
|
||||
```bash
|
||||
# Check deployment logs
|
||||
tail -f /var/log/fetchapply/deployment.log
|
||||
|
||||
# Review system logs
|
||||
journalctl -u ssh
|
||||
journalctl -u auditd
|
||||
```
|
||||
|
||||
## Environment-Specific Configurations
|
||||
|
||||
### Physical Dell Servers
|
||||
- **OMSA Installation:** Dell OpenManage Server Administrator
|
||||
- **Hardware Monitoring:** iDRAC configuration
|
||||
- **Performance Tuning:** CPU and memory optimizations
|
||||
|
||||
### Virtual Machines
|
||||
- **Guest Additions:** VMware tools or VirtualBox additions
|
||||
- **Resource Limits:** Memory and CPU constraints
|
||||
- **Network Configuration:** Bridge vs NAT settings
|
||||
|
||||
### Development Environments
|
||||
- **SSH Configuration:** Less restrictive settings
|
||||
- **Development Tools:** Additional packages for development
|
||||
- **Testing Access:** Enhanced logging and debugging
|
||||
|
||||
## Maintenance and Updates
|
||||
|
||||
### Regular Maintenance
|
||||
```bash
|
||||
# Update system packages
|
||||
sudo apt-get update && sudo apt-get upgrade
|
||||
|
||||
# Update monitoring scripts
|
||||
cd /usr/local/bin
|
||||
sudo wget https://[repository]/scripts/up2date.sh
|
||||
sudo chmod +x up2date.sh
|
||||
```
|
||||
|
||||
### Security Updates
|
||||
```bash
|
||||
# Check for security updates
|
||||
sudo apt-get update
|
||||
sudo apt list --upgradable | grep -i security
|
||||
|
||||
# Apply security patches
|
||||
sudo apt-get upgrade
|
||||
```
|
||||
|
||||
### Configuration Updates
|
||||
```bash
|
||||
# Update FetchApply
|
||||
cd FetchApply
|
||||
git pull origin main
|
||||
|
||||
# Re-run specific modules
|
||||
cd ProjectCode/Modules/Security
|
||||
sudo bash secharden-ssh.sh
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Pre-Deployment
|
||||
- Always test in non-production environment first
|
||||
- Review all scripts before execution
|
||||
- Validate network connectivity
|
||||
- Ensure proper backup procedures
|
||||
|
||||
### 2. During Deployment
|
||||
- Monitor deployment progress
|
||||
- Check for errors and warnings
|
||||
- Document any customizations
|
||||
- Validate each phase completion
|
||||
|
||||
### 3. Post-Deployment
|
||||
- Run full security test suite
|
||||
- Verify all services are running
|
||||
- Test remote access
|
||||
- Document deployment specifics
|
||||
|
||||
### 4. Ongoing Operations
|
||||
- Regular security updates
|
||||
- Monitor system performance
|
||||
- Review audit logs
|
||||
- Maintain deployment documentation
|
||||
|
||||
## Support and Resources
|
||||
|
||||
### Documentation
|
||||
- **README.md:** Basic usage instructions
|
||||
- **SECURITY.md:** Security architecture and guidelines
|
||||
- **Project-Tests/README.md:** Testing framework documentation
|
||||
|
||||
### Community Support
|
||||
- **Issues:** https://projects.knownelement.com/project/reachableceo-vptechnicaloperations/timeline
|
||||
- **Discussion:** https://community.turnsys.com/c/chieftechnologyandproductofficer/26
|
||||
|
||||
### Professional Support
|
||||
- **Technical Support:** [Contact information to be added]
|
||||
- **Consulting Services:** [Contact information to be added]
|
||||
|
||||
## Deployment Checklist
|
||||
|
||||
### Pre-Deployment
|
||||
- [ ] System requirements validated
|
||||
- [ ] Network connectivity tested
|
||||
- [ ] Backup procedures in place
|
||||
- [ ] Security review completed
|
||||
|
||||
### Deployment
|
||||
- [ ] Repository cloned successfully
|
||||
- [ ] Pre-deployment tests passed
|
||||
- [ ] Deployment executed without errors
|
||||
- [ ] Post-deployment verification completed
|
||||
|
||||
### Post-Deployment
|
||||
- [ ] Security tests passed
|
||||
- [ ] All services running
|
||||
- [ ] Remote access verified
|
||||
- [ ] Documentation updated
|
||||
|
||||
### Maintenance
|
||||
- [ ] Update schedule established
|
||||
- [ ] Monitoring configured
|
||||
- [ ] Backup procedures tested
|
||||
- [ ] Incident response plan activated
|
||||
|
||||
## Version History
|
||||
|
||||
- **v1.0:** Initial deployment framework
|
||||
- **v1.1:** Added security hardening and secrets management
|
||||
- **v1.2:** Enhanced testing framework and documentation
|
||||
|
||||
Last updated: July 14, 2025
|
||||
@@ -0,0 +1,406 @@
|
||||
# TSYS FetchApply Development Guidelines
|
||||
|
||||
## Overview
|
||||
|
||||
This document contains development standards and best practices for the TSYS FetchApply infrastructure provisioning system.
|
||||
|
||||
## Package Management Best Practices
|
||||
|
||||
### Combine apt-get Install Commands
|
||||
|
||||
**Rule:** Always combine multiple package installations into a single `apt-get install` command for performance.
|
||||
|
||||
**Rationale:** Single command execution is significantly faster than multiple separate commands due to:
|
||||
- Reduced package cache processing
|
||||
- Single dependency resolution
|
||||
- Fewer network connections
|
||||
- Optimized package download ordering
|
||||
|
||||
#### ✅ Correct Implementation
|
||||
```bash
|
||||
# Install all packages in one command
|
||||
apt-get install -y package1 package2 package3 package4
|
||||
|
||||
# Real example from 2FA script
|
||||
apt-get install -y libpam-google-authenticator qrencode
|
||||
```
|
||||
|
||||
#### ❌ Incorrect Implementation
|
||||
```bash
|
||||
# Don't use separate commands for each package
|
||||
apt-get install -y package1
|
||||
apt-get install -y package2
|
||||
apt-get install -y package3
|
||||
```
|
||||
|
||||
#### Complex Package Installation Pattern
|
||||
```bash
|
||||
function install_security_packages() {
|
||||
print_info "Installing security packages..."
|
||||
|
||||
# Update package cache once
|
||||
apt-get update
|
||||
|
||||
# Install all packages in single command
|
||||
apt-get install -y \
|
||||
auditd \
|
||||
fail2ban \
|
||||
libpam-google-authenticator \
|
||||
lynis \
|
||||
rkhunter \
|
||||
aide \
|
||||
chkrootkit \
|
||||
clamav \
|
||||
clamav-daemon
|
||||
|
||||
print_success "Security packages installed successfully"
|
||||
}
|
||||
```
|
||||
|
||||
## Script Development Standards
|
||||
|
||||
### Error Handling
|
||||
- Always use `set -euo pipefail` at script start
|
||||
- Implement proper error trapping
|
||||
- Use framework error handling functions
|
||||
- Return appropriate exit codes
|
||||
|
||||
### Function Structure
|
||||
```bash
|
||||
function function_name() {
|
||||
print_info "Description of what function does..."
|
||||
|
||||
# Local variables
|
||||
local var1="value"
|
||||
local var2="value"
|
||||
|
||||
# Function logic
|
||||
if [[ condition ]]; then
|
||||
print_success "Success message"
|
||||
return 0
|
||||
else
|
||||
print_error "Error message"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
```
|
||||
|
||||
### Framework Integration
|
||||
- Source framework includes at script start
|
||||
- Use framework logging and pretty print functions
|
||||
- Follow existing patterns for consistency
|
||||
- Include proper PROJECT_ROOT path resolution
|
||||
|
||||
```bash
|
||||
# Standard framework sourcing pattern
|
||||
PROJECT_ROOT="$(dirname "$(realpath "${BASH_SOURCE[0]}")")/../.."
|
||||
source "$PROJECT_ROOT/Framework-Includes/PrettyPrint.sh"
|
||||
source "$PROJECT_ROOT/Framework-Includes/Logging.sh"
|
||||
source "$PROJECT_ROOT/Framework-Includes/ErrorHandling.sh"
|
||||
```
|
||||
|
||||
## Code Quality Standards
|
||||
|
||||
### ShellCheck Compliance
|
||||
- All scripts must pass shellcheck validation
|
||||
- Address shellcheck warnings appropriately
|
||||
- Use proper quoting for variables
|
||||
- Handle edge cases and error conditions
|
||||
|
||||
### Variable Naming
|
||||
- Use UPPERCASE for global constants
|
||||
- Use lowercase for local variables
|
||||
- Use descriptive names
|
||||
- Quote all variable expansions
|
||||
|
||||
```bash
|
||||
# Global constants
|
||||
declare -g BACKUP_DIR="/root/backup"
|
||||
declare -g CONFIG_FILE="/etc/ssh/sshd_config"
|
||||
|
||||
# Local variables
|
||||
local user_name="localuser"
|
||||
local temp_file="/tmp/config.tmp"
|
||||
|
||||
# Proper quoting
|
||||
if [[ -f "$CONFIG_FILE" ]]; then
|
||||
cp "$CONFIG_FILE" "$BACKUP_DIR/"
|
||||
fi
|
||||
```
|
||||
|
||||
### Function Documentation
|
||||
- Include purpose description
|
||||
- Document parameters if any
|
||||
- Document return values
|
||||
- Include usage examples for complex functions
|
||||
|
||||
```bash
|
||||
# Configure SSH hardening settings
|
||||
# Parameters: none
|
||||
# Returns: 0 on success, 1 on failure
|
||||
# Usage: configure_ssh_hardening
|
||||
function configure_ssh_hardening() {
|
||||
print_info "Configuring SSH hardening..."
|
||||
# Implementation
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Requirements
|
||||
|
||||
### Test Coverage
|
||||
- Every new module must include corresponding tests
|
||||
- Test both success and failure scenarios
|
||||
- Validate configurations after changes
|
||||
- Include integration tests for complex workflows
|
||||
|
||||
### Test Categories
|
||||
1. **Unit Tests:** Individual function validation
|
||||
2. **Integration Tests:** Module interaction testing
|
||||
3. **Security Tests:** Security configuration validation
|
||||
4. **Validation Tests:** System requirement checking
|
||||
|
||||
### Test Implementation Pattern
|
||||
```bash
|
||||
function test_function_name() {
|
||||
echo "🔍 Testing specific functionality..."
|
||||
|
||||
local failed=0
|
||||
|
||||
# Test implementation
|
||||
if [[ condition ]]; then
|
||||
echo "✅ Test passed"
|
||||
else
|
||||
echo "❌ Test failed"
|
||||
((failed++))
|
||||
fi
|
||||
|
||||
return $failed
|
||||
}
|
||||
```
|
||||
|
||||
## Security Standards
|
||||
|
||||
### Configuration Backup
|
||||
- Always backup configurations before modification
|
||||
- Use timestamped backup directories
|
||||
- Provide restore instructions
|
||||
- Test backup/restore procedures
|
||||
|
||||
### Service Management
|
||||
- Test configurations before restarting services
|
||||
- Provide rollback procedures
|
||||
- Validate service status after changes
|
||||
- Include service dependency handling
|
||||
|
||||
### User Safety
|
||||
- Use `nullok` for gradual 2FA rollout
|
||||
- Provide clear setup instructions
|
||||
- Include emergency access procedures
|
||||
- Test all access methods before enforcement
|
||||
|
||||
## Documentation Standards
|
||||
|
||||
### Script Headers
|
||||
```bash
|
||||
#!/bin/bash
|
||||
|
||||
# TSYS Module Name - Brief Description
|
||||
# Longer description of what this script does
|
||||
# Author: TSYS Development Team
|
||||
# Version: 1.0
|
||||
# Last Updated: YYYY-MM-DD
|
||||
|
||||
set -euo pipefail
|
||||
```
|
||||
|
||||
### Inline Documentation
|
||||
- Comment complex logic
|
||||
- Explain non-obvious decisions
|
||||
- Document external dependencies
|
||||
- Include troubleshooting notes
|
||||
|
||||
### User Documentation
|
||||
- Create comprehensive guides for complex features
|
||||
- Include step-by-step procedures
|
||||
- Provide troubleshooting sections
|
||||
- Include examples and use cases
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
### Package Management
|
||||
- Single apt-get commands (as noted above)
|
||||
- Cache package lists appropriately
|
||||
- Use specific package versions when stability required
|
||||
- Clean up package cache when appropriate
|
||||
|
||||
### Network Operations
|
||||
- Use connection timeouts for external requests
|
||||
- Implement retry logic with backoff
|
||||
- Cache downloaded resources when possible
|
||||
- Validate download integrity
|
||||
|
||||
### File Operations
|
||||
- Use efficient file processing tools
|
||||
- Minimize file system operations
|
||||
- Use appropriate file permissions
|
||||
- Clean up temporary files
|
||||
|
||||
## Version Control Practices
|
||||
|
||||
### Commit Messages
|
||||
- Use descriptive commit messages
|
||||
- Include scope of changes
|
||||
- Reference related issues/requirements
|
||||
- Follow established commit message format
|
||||
|
||||
### Branch Management
|
||||
- Test changes in feature branches
|
||||
- Use pull requests for review
|
||||
- Maintain clean commit history
|
||||
- Tag releases appropriately
|
||||
|
||||
### Code Review Requirements
|
||||
- All changes require review
|
||||
- Security changes require security team review
|
||||
- Test coverage must be maintained
|
||||
- Documentation must be updated
|
||||
|
||||
## Deployment Practices
|
||||
|
||||
### Pre-Deployment
|
||||
- Run full test suite
|
||||
- Validate in test environment
|
||||
- Review security implications
|
||||
- Update documentation
|
||||
|
||||
### Deployment Process
|
||||
- Use configuration validation
|
||||
- Implement gradual rollout when possible
|
||||
- Monitor for issues during deployment
|
||||
- Have rollback procedures ready
|
||||
|
||||
### Post-Deployment
|
||||
- Validate deployment success
|
||||
- Monitor system performance
|
||||
- Update operational documentation
|
||||
- Gather feedback for improvements
|
||||
|
||||
## Example Implementation
|
||||
|
||||
### Complete Module Template
|
||||
```bash
|
||||
#!/bin/bash
|
||||
|
||||
# TSYS Security Module - Template
|
||||
# Template for creating new security modules
|
||||
# Author: TSYS Development Team
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Source framework functions
|
||||
PROJECT_ROOT="$(dirname "$(realpath "${BASH_SOURCE[0]}")")/../.."
|
||||
source "$PROJECT_ROOT/Framework-Includes/PrettyPrint.sh"
|
||||
source "$PROJECT_ROOT/Framework-Includes/Logging.sh"
|
||||
source "$PROJECT_ROOT/Framework-Includes/ErrorHandling.sh"
|
||||
|
||||
# Module configuration
|
||||
BACKUP_DIR="/root/backup/module-$(date +%Y%m%d-%H%M%S)"
|
||||
CONFIG_FILE="/etc/example.conf"
|
||||
|
||||
# Create backup directory
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
|
||||
print_header "TSYS Module Template"
|
||||
|
||||
function backup_configs() {
|
||||
print_info "Creating configuration backup..."
|
||||
|
||||
if [[ -f "$CONFIG_FILE" ]]; then
|
||||
cp "$CONFIG_FILE" "$BACKUP_DIR/"
|
||||
print_success "Configuration backed up"
|
||||
fi
|
||||
}
|
||||
|
||||
function install_packages() {
|
||||
print_info "Installing required packages..."
|
||||
|
||||
# Update package cache
|
||||
apt-get update
|
||||
|
||||
# Install all packages in single command
|
||||
apt-get install -y package1 package2 package3
|
||||
|
||||
print_success "Packages installed successfully"
|
||||
}
|
||||
|
||||
function configure_module() {
|
||||
print_info "Configuring module..."
|
||||
|
||||
# Configuration logic here
|
||||
|
||||
print_success "Module configured successfully"
|
||||
}
|
||||
|
||||
function validate_configuration() {
|
||||
print_info "Validating configuration..."
|
||||
|
||||
local failed=0
|
||||
|
||||
# Validation logic here
|
||||
|
||||
if [[ $failed -eq 0 ]]; then
|
||||
print_success "Configuration validation passed"
|
||||
return 0
|
||||
else
|
||||
print_error "Configuration validation failed"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
function main() {
|
||||
# Check if running as root
|
||||
if [[ $EUID -ne 0 ]]; then
|
||||
print_error "This script must be run as root"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Execute module steps
|
||||
backup_configs
|
||||
install_packages
|
||||
configure_module
|
||||
validate_configuration
|
||||
|
||||
print_success "Module setup completed successfully!"
|
||||
}
|
||||
|
||||
# Run main function
|
||||
main "$@"
|
||||
```
|
||||
|
||||
## Continuous Improvement
|
||||
|
||||
### Regular Reviews
|
||||
- Review guidelines quarterly
|
||||
- Update based on lessons learned
|
||||
- Incorporate new best practices
|
||||
- Gather team feedback
|
||||
|
||||
### Tool Updates
|
||||
- Keep development tools current
|
||||
- Adopt new security practices
|
||||
- Update testing frameworks
|
||||
- Improve automation
|
||||
|
||||
### Knowledge Sharing
|
||||
- Document lessons learned
|
||||
- Share best practices
|
||||
- Provide training materials
|
||||
- Maintain knowledge base
|
||||
|
||||
---
|
||||
|
||||
**Last Updated:** July 14, 2025
|
||||
**Version:** 1.0
|
||||
**Author:** TSYS Development Team
|
||||
|
||||
**Note:** These guidelines are living documents and should be updated as the project evolves and new best practices are identified.
|
||||
+485
@@ -0,0 +1,485 @@
|
||||
# K8S.md -- Kubernetes Architecture Deep-Dive
|
||||
|
||||
**Date:** 2026-07-27
|
||||
**Purpose:** Detailed kubernetes architecture plan for the pfv-k8s cluster.
|
||||
Companion to `PROJECT.md` (which has the fleet-wide assessment).
|
||||
**Status:** For discussion in a future session. No changes made.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Workload Profile](#1-workload-profile)
|
||||
2. [Current State](#2-current-state)
|
||||
3. [Target Architecture](#3-target-architecture)
|
||||
4. [Control Plane (Cnodes)](#4-control-plane-cnodes)
|
||||
5. [Worker Nodes (Wnodes)](#5-worker-nodes-wnodes)
|
||||
6. [Storage Class Design](#6-storage-class-design)
|
||||
7. [ETL/HPC Considerations](#7-etlhpc-considerations)
|
||||
8. [Migration Plan](#8-migration-plan)
|
||||
|
||||
---
|
||||
|
||||
## 1. Workload Profile
|
||||
|
||||
This cluster runs **R&D and RackRental (containerlab) workloads** via
|
||||
Kubernetes. Production (Gitea, RustFS, Redmine, websites) lives on a VPS in
|
||||
Reston, VA running Cloudron.
|
||||
|
||||
**Workload types expected:**
|
||||
|
||||
| Type | Description | Storage need | RAM need | Examples |
|
||||
|------|------------|-------------|----------|---------|
|
||||
| **ETL (weather/GIS)** | Batch processing of large geospatial datasets. Sequential reads, transform, sequential writes. | High capacity (100s of GB), moderate IOPS | Medium (8-32 GB per job) | GRIB/NetCDF processing, raster reprojection |
|
||||
| **HPC (hardware startup)** | Compute-intensive simulations, firmware build pipelines, hardware-in-the-loop testing. | Low capacity, moderate IOPS | High (32-128 GB per job) | RTL simulation, PCB thermal analysis |
|
||||
| **RackRental (containerlab)** | Rapid deployment/teardown of network lab topologies. Many containers, short-lived. | Low capacity, high IOPS (container image pulls) | Low-Medium (4-16 GB) | Network topology testing, protocol validation |
|
||||
|
||||
**Key storage insight:** ETL workloads need bulk capacity (NFS-HDD is fine --
|
||||
sequential I/O). HPC and containerlab need low-latency random I/O (local
|
||||
SSD/NVMe is essential). The tiered StorageClass design (section 6) serves both.
|
||||
|
||||
---
|
||||
|
||||
## 2. Current State
|
||||
|
||||
### 2.1 pfv-k8s nodes and their storage
|
||||
|
||||
| Node | Type | Host | Storage | Disk type | Status |
|
||||
|------|------|------|---------|-----------|--------|
|
||||
| cnode1 (107) | control | tsys1 | D5 (tsys4) | NFS-HDD | running |
|
||||
| cnode2 (603) | control | tsys6 | D2 (tsys4) | NFS-HDD | running |
|
||||
| cnode3 (106) | control | tsys1 | D2 (tsys4) | NFS-HDD | running |
|
||||
| cnode4 (601) | control | tsys6 | D2 (tsys4) | NFS-HDD | running |
|
||||
| cnode5 (602) | control | tsys6 | D5 (tsys4) | NFS-HDD | running |
|
||||
| wnode-tsys3 (313) | worker | tsys3 | D5 (tsys4) | NFS-HDD | running |
|
||||
| wnode-tsys5 (509) | worker | tsys5 | D2 (tsys4) | NFS-HDD | running |
|
||||
| wnode-tsys6 (100) | worker | tsys6 | D5 (tsys4) | NFS-HDD | **STOPPED** |
|
||||
| wnode-tsys7 (701) | worker | tsys7 | D5 (tsys4) | NFS-HDD | running |
|
||||
| wnode-tsys9 (905) | worker | tsys9 | S3 (tsys5) | NFS-HDD | running |
|
||||
|
||||
### 2.2 Problems
|
||||
|
||||
1. **100% of cnodes on tsys4 NFS.** D2 disk failure loses 3 of 5 cnodes =
|
||||
etcd quorum lost.
|
||||
2. **90% of all k8s nodes on tsys4 NFS.** tsys4 failure kills the cluster.
|
||||
3. **Zero nodes use SSD or NVMe.** All on NFS-over-HDD.
|
||||
4. **Zero nodes use local-lvm.** tsys3/6/7/9 all have empty local storage
|
||||
(349 GB / 1.7 TB / 1.7 TB / 136 GB SSD respectively).
|
||||
5. **wnode-tsys6 is stopped.** Reduces cluster capacity.
|
||||
6. **3 cnodes on tsys6** -- should be on lighter hosts to free tsys6 for workers.
|
||||
|
||||
---
|
||||
|
||||
## 3. Target Architecture
|
||||
|
||||
### 3.1 Design principles
|
||||
|
||||
1. **Cnodes on lightweight hosts** (tsys1, tsys9, tsys3) -- frees tsys6/7 for
|
||||
heavy workers.
|
||||
2. **Cnode storage split across tsys4 and tsys5** -- etcd survives either
|
||||
storage server failing.
|
||||
3. **Wnode boot disks on local storage** -- eliminates NFS latency for
|
||||
container runtime and kubelet.
|
||||
4. **Wnode data disks on NFS-HDD** -- bulk capacity for ETL/weather/GIS.
|
||||
5. **tsys5 NVMe dedicated to wnode-tsys5** -- fastest tier for HPC jobs.
|
||||
6. **One wnode per hypervisor host** -- maximize total cluster capacity.
|
||||
|
||||
### 3.2 Target node-host-storage matrix
|
||||
|
||||
| Node | Type | Host | Boot disk | Data disk | Disk type |
|
||||
|------|------|------|-----------|-----------|-----------|
|
||||
| cnode1 | control | tsys1 | D5 (tsys4) | -- | NFS-HDD |
|
||||
| cnode2 | control | tsys9 | D2 (tsys4) | -- | NFS-HDD |
|
||||
| cnode3 | control | tsys1 | S2 (tsys5) | -- | NFS-HDD |
|
||||
| cnode4 | control | tsys9 | D5 (tsys4) | -- | NFS-HDD |
|
||||
| cnode5 | control | tsys3 | S3 (tsys5) | -- | NFS-HDD |
|
||||
| wnode-tsys1 | worker | tsys1 | D5 (tsys4) | -- | NFS-HDD (small) |
|
||||
| wnode-tsys2 | worker | tsys2 | **NVMe (960 PRO 512GB)** | **SATA SSD (850 EVO 1TB)** | **NVMe + SSD -- no NFS needed** |
|
||||
| wnode-tsys3 | worker | tsys3 | **local-lvm (NVMe PM961)** | S3 (NFS) | **LOCAL-NVMe** |
|
||||
| wnode-tsys5 | worker | tsys5 | **NVMe (local, Friday)** | local-nonprod (HDD) | **NVMe** |
|
||||
| wnode-tsys6 | worker | tsys6 | D2 (tsys4 NFS) | -- | NFS-HDD (local-lvm is USB 2.0 -- do not use) |
|
||||
| wnode-tsys7 | worker | tsys7 | D5 (tsys4 NFS) | -- | NFS-HDD (local-lvm is USB 2.0 -- do not use) |
|
||||
| wnode-tsys9 | worker | tsys9 | **local-lvm (SSD)** | S2 (NFS) | **LOCAL-SSD** |
|
||||
|
||||
### 3.3 Storage server distribution after changes
|
||||
|
||||
| Storage server | cnodes | wnodes (boot) | wnodes (data) |
|
||||
|---------------|--------|---------------|---------------|
|
||||
| tsys4 (D2) | cnode2 | wnode-tsys6 | wnode-tsys7 |
|
||||
| tsys4 (D5) | cnode1, cnode4 | wnode-tsys1 | -- |
|
||||
| tsys5 (S2) | cnode3 | wnode-tsys9 | -- |
|
||||
| tsys5 (S3) | cnode5 | -- | wnode-tsys3 |
|
||||
|
||||
**Note:** wnode-tsys2 needs no NFS (1.5 TB local SSD). wnode-tsys6/7 stay on
|
||||
NFS by design -- their local-lvm is USB 2.0 portable HDD (~30 MB/s), slower
|
||||
than NFS-HDD, and the user has chosen not to install internal drives.
|
||||
|
||||
**No single disk or server is a quorum-losing failure point.**
|
||||
|
||||
---
|
||||
|
||||
## 4. Control Plane (Cnodes)
|
||||
|
||||
### 4.1 Cnode sizing
|
||||
|
||||
Each cnode: 4 cores, 4 GB RAM, 32 GB disk. This is sufficient for etcd +
|
||||
kubernetes control plane components (API server, scheduler, controller-manager).
|
||||
|
||||
### 4.2 Cnode host placement rationale
|
||||
|
||||
| Host | cnodes | RAM for cnodes | Total host RAM | Remaining for other VMs |
|
||||
|------|--------|---------------|---------------|------------------------|
|
||||
| tsys1 | 2 (cnode1, cnode3) | 8 GB | 32 GB | ~24 GB (but 11 infra VMs consume most) |
|
||||
| tsys9 | 2 (cnode2, cnode4) | 8 GB | 24 GB | ~16 GB (4 infra VMs + 1 wnode) |
|
||||
| tsys3 | 1 (cnode5) | 4 GB | 32 GB | ~28 GB (1 wnode at 20 GB = 8 GB headroom) |
|
||||
|
||||
**tsys6 and tsys7 have ZERO cnodes** -- fully dedicated to heavy worker nodes.
|
||||
|
||||
### 4.3 Cnode storage placement rationale
|
||||
|
||||
The 5 cnodes are split 3-on-tsys4 / 2-on-tsys5:
|
||||
|
||||
| Disk | cnodes | Rationale |
|
||||
|------|--------|-----------|
|
||||
| D5 (tsys4 HDD) | cnode1, cnode4 | Spread load across 2 disks on tsys4 |
|
||||
| D2 (tsys4 HDD) | cnode2 | Only 1 cnode on D2 (was 3 -- reduces blast radius) |
|
||||
| S2 (tsys5 HDD) | cnode3 | tsys5 storage for quorum diversity |
|
||||
| S3 (tsys5 HDD) | cnode5 | tsys5 storage, different disk than S2 |
|
||||
|
||||
**If D2 fails:** cnode2 dies. 4 of 5 survive. Quorum OK.
|
||||
**If D5 fails:** cnode1 + cnode4 die. 3 of 5 survive. Quorum OK.
|
||||
**If tsys4 fails:** cnode1, cnode2, cnode4 die. cnode3 + cnode5 survive on
|
||||
tsys5. **Only 2 of 5 -- QUORUM LOST.**
|
||||
|
||||
Wait -- that is a problem. If tsys4 goes completely offline, we lose 3
|
||||
cnodes and only have 2 on tsys5. That loses quorum (need 3).
|
||||
|
||||
**Revision needed:** Move 1 more cnode to tsys5 storage. Target: 2 on tsys4,
|
||||
3 on tsys5. But that means tsys5 failure (3 cnodes die) leaves only 2 on
|
||||
tsys4. Same problem inverted.
|
||||
|
||||
The fundamental issue: with 5 cnodes and 2 storage servers, the best split is
|
||||
3/2. The server holding 3 cnodes is a quorum-loss risk if it fails. The server
|
||||
holding 2 cnodes is safe (3 survive).
|
||||
|
||||
**Proper solution: 3 cnodes on the "less likely to fail" server, 2 on the
|
||||
other.** After Friday's hardware work:
|
||||
- tsys4 will have a new PCIe NIC + 64 GB RAM -- more reliable
|
||||
- tsys5 will have bond0 fixed + NVMe -- more reliable
|
||||
|
||||
Either way, 3/2 split means one server failure could lose quorum. **To truly
|
||||
solve this, use a 3rd storage target.** Options:
|
||||
- Use tsys9 local SSD for 1 cnode (breaks the 2-server model, adds a 3rd
|
||||
independent failure domain)
|
||||
- Use local-lvm on the cnode's own host (etcd data is local to the VM's host,
|
||||
no NFS dependency at all)
|
||||
|
||||
**Best option: put cnode boot disks on local-lvm where available.** This
|
||||
eliminates NFS entirely for the control plane. Each cnode's etcd data lives on
|
||||
its own host's local disk -- no shared dependency.
|
||||
|
||||
| cnode | Host | **Recommended storage** | Type |
|
||||
|-------|------|------------------------|------|
|
||||
| cnode1 | tsys1 | **local-lvm** (if space) or D5 (tsys4) | LOCAL-HDD or NFS-HDD |
|
||||
| cnode2 | tsys9 | **local-lvm (SSD)** | **LOCAL-SSD** |
|
||||
| cnode3 | tsys1 | **S2 (tsys5)** | NFS-HDD |
|
||||
| cnode4 | tsys9 | **local-lvm (SSD)** | **LOCAL-SSD** |
|
||||
| cnode5 | tsys3 | **local-lvm** | LOCAL-HDD |
|
||||
|
||||
With this layout, a tsys4 failure takes down 0 cnodes. A tsys5 failure takes
|
||||
down 1 (cnode3). A host failure takes down at most 2 cnodes. Quorum always
|
||||
survives.
|
||||
|
||||
**This is the recommended approach.** Local storage for cnodes wherever
|
||||
possible. NFS only as fallback.
|
||||
|
||||
### 4.4 etcd performance on local vs NFS
|
||||
|
||||
| Storage | Typical fsync latency | etcd commit latency | Impact |
|
||||
|---------|----------------------|--------------------|--------|
|
||||
| NFS-HDD (via USB dongle on tsys4) | 5-15 ms | 10-30 ms | Slow API responses, sluggish pod scheduling |
|
||||
| NFS-HDD (via PCIe NIC, post-Friday) | 2-8 ms | 5-15 ms | Better but still network-bound |
|
||||
| Local HDD (tsys1/3/6/7 local-lvm) | 1-5 ms | 3-10 ms | No network hop, moderate improvement |
|
||||
| Local SSD (tsys9 PNY CS900) | 0.1-0.5 ms | 0.5-2 ms | **10-30x faster than NFS-HDD** |
|
||||
| NVMe (tsys5, Friday) | 0.02-0.1 ms | 0.1-0.5 ms | **100x faster than NFS-HDD** |
|
||||
|
||||
etcd is the heartbeat of the kubernetes control plane. Every API call, every
|
||||
pod schedule, every controller reconciliation involves an etcd write. Cutting
|
||||
etcd commit latency from 15 ms to 1 ms makes the entire cluster feel 15x more
|
||||
responsive. **This is the single highest-impact change for k8s performance.**
|
||||
|
||||
---
|
||||
|
||||
## 5. Worker Nodes (Wnodes)
|
||||
|
||||
### 5.1 One wnode per hypervisor host
|
||||
|
||||
| Host | wnode | Boot disk | Data disk | Total RAM | wnode RAM | Role |
|
||||
|------|-------|-----------|-----------|-----------|-----------|------|
|
||||
| tsys1 | wnode-tsys1 | D5 (tsys4 NFS) | -- | 32 GB | 4-8 GB | Small worker, infra co-tenant |
|
||||
| tsys2 | wnode-tsys2 | **NVMe (960 PRO 512GB)** | **SATA SSD (850 EVO 1TB)** | 32 GB | 16-24 GB | **Best storage of any worker -- 1.5TB local SSD, no NFS needed** |
|
||||
| tsys3 | wnode-tsys3 | **local-lvm (349 GB)** | S3 (NFS) | 32 GB | 20 GB | General worker |
|
||||
| tsys5 | wnode-tsys5 | **NVMe (local)** | local-nonprod (HDD) | 96 GB | 32-64 GB | **HPC/ETL powerhouse** |
|
||||
| tsys6 | wnode-tsys6 | D2 (tsys4 NFS) | -- | 128 GB | 64-96 GB | **Heavy worker, max RAM.** local-lvm is USB 2.0 -- stays on NFS |
|
||||
| tsys7 | wnode-tsys7 | D5 (tsys4 NFS) | -- | 192 GB | 96-128 GB | **Heavy worker, max RAM.** local-lvm is USB 2.0 -- stays on NFS |
|
||||
| tsys9 | wnode-tsys9 | **local-lvm SSD (136 GB)** | S2 (NFS) | 24 GB | 4-8 GB | Small worker, SSD boot |
|
||||
|
||||
### 5.2 Why boot disks on local-lvm
|
||||
|
||||
Current: all wnodes boot from NFS. Every container image pull, every kubelet
|
||||
log write, every ephemeral volume traverses the NFS network path.
|
||||
|
||||
With local-lvm boot disks:
|
||||
- **Container image pulls** write to local disk (100-150 MB/s HDD, no network
|
||||
hop) instead of NFS-HDD (80-120 MB/s with network latency)
|
||||
- **kubelet logs** stay local (no NFS writes for log rotation)
|
||||
- **ephemeral storage** (emptyDir volumes) uses local disk by default
|
||||
- **NFS server failure does not kill the wnode** -- the VM stays running, only
|
||||
the data disk (if mounted) goes away
|
||||
|
||||
### 5.3 Wnode sizing guidance
|
||||
|
||||
| Host | Recommended wnode config | Rationale |
|
||||
|------|------------------------|-----------|
|
||||
| tsys7 (192 GB) | 8-12 cores, 96-128 GB RAM, NFS boot | Largest host -- run the heaviest ETL/HPC jobs here. local-lvm is USB 2.0 |
|
||||
| tsys6 (128 GB) | 8 cores, 64-96 GB RAM, NFS boot | Second-largest -- parallel heavy jobs. local-lvm is USB 2.0 |
|
||||
| tsys5 (96 GB + NVMe) | 4 cores, 32-64 GB RAM, NVMe boot + HDD data | NVMe makes this fastest for I/O-bound HPC |
|
||||
| tsys3 (32 GB) | 4 cores, 20 GB RAM, local-lvm boot | General-purpose worker |
|
||||
| tsys2 (32 GB, NVMe+SSD, incoming) | 4 cores, 16-24 GB RAM, **NVMe boot + SSD data** | **Fastest storage worker** -- HPC with I/O bounds |
|
||||
| tsys1 (32 GB) | 2 cores, 4-8 GB RAM | Small worker, don't starve infra VMs |
|
||||
| tsys9 (24 GB) | 2-4 cores, 4-8 GB RAM | Small worker, SSD boot is the advantage |
|
||||
|
||||
### 5.4 Tainting and labeling strategy
|
||||
|
||||
Label wnodes by capability so the k8s scheduler can target them:
|
||||
|
||||
```yaml
|
||||
# Heavy RAM hosts (ETL/HPC)
|
||||
wnode-tsys6: workload=heavy, ram=128g
|
||||
wnode-tsys7: workload=heavy, ram=192g
|
||||
|
||||
# NVMe host (I/O-intensive HPC)
|
||||
wnode-tsys5: workload=hpc, storage=nvme
|
||||
|
||||
# SSD boot host (low-latency)
|
||||
wnode-tsys9: workload=light, storage=ssd
|
||||
|
||||
# General workers
|
||||
wnode-tsys3: workload=general
|
||||
wnode-tsys2: workload=storage-fast, storage=nvme
|
||||
wnode-tsys1: workload=light
|
||||
```
|
||||
|
||||
Then use nodeSelector or nodeAffinity in job specs:
|
||||
|
||||
```yaml
|
||||
# Weather/GIS ETL job -- needs lots of RAM
|
||||
spec:
|
||||
nodeSelector:
|
||||
workload: heavy
|
||||
|
||||
# Firmware build -- needs fast storage
|
||||
spec:
|
||||
nodeSelector:
|
||||
storage: nvme
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Storage Class Design
|
||||
|
||||
### 6.1 Proposed StorageClasses
|
||||
|
||||
| StorageClass | Provisioner | Where | Speed | Use case |
|
||||
|-------------|------------|-------|-------|----------|
|
||||
| `local-fast` | local-path (k8s) | wnode local-lvm / NVMe | 100-3500 MB/s | Container runtime, scratch, databases |
|
||||
| `nfs-hdd` | nfs-subdir-external-provisioner | tsys4 D2/D5, tsys5 S1-S4 | 80-120 MB/s | Bulk data, weather/GIS datasets |
|
||||
| `nfs-ssd` | nfs-subdir-external-provisioner | tsys4 D3, tsys5 T5-SSD | 200-400 MB/s | Latency-sensitive persistent data |
|
||||
|
||||
### 6.2 How this maps to wnode disk topology
|
||||
|
||||
Each wnode has:
|
||||
- **Disk 1 (boot/OS):** local-lvm or NVMe. Contains the OS, kubelet, container
|
||||
runtime. k8s `local-fast` StorageClass provisioner points here.
|
||||
- **Disk 2 (bulk data, optional):** NFS mount. Mounted inside the VM as a
|
||||
second block device or filesystem. k8s `nfs-hdd` provisioner points here.
|
||||
|
||||
Inside k8s, pods request storage via PVC:
|
||||
|
||||
```yaml
|
||||
# ETL job: needs bulk storage for weather data
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
spec:
|
||||
storageClassName: nfs-hdd
|
||||
accessModes: [ReadWriteMany] # NFS allows RWX
|
||||
resources:
|
||||
requests:
|
||||
storage: 500Gi
|
||||
|
||||
# HPC job: needs fast scratch
|
||||
spec:
|
||||
storageClassName: local-fast
|
||||
accessModes: [ReadWriteOnce]
|
||||
resources:
|
||||
requests:
|
||||
storage: 50Gi
|
||||
```
|
||||
|
||||
### 6.3 NFS-SSD tier (D3 and T5-SSD -- both on tsys5 after Friday)
|
||||
|
||||
**Storage philosophy (user directive): NVMe/SSD is EXCLUSIVELY for k8s worker
|
||||
scratch space, with the exception of ultix-streaming which stays on T5-SSD.
|
||||
Spinning rust hosts all other infrastructure VMs** (UCS, netinfra, LibreNMS,
|
||||
SIEM, etc.).
|
||||
|
||||
The SSD NFS exports:
|
||||
- **D3 (tsys5 SAS, 445 GB free):** k8s scratch exclusively (etcd, container
|
||||
cache, ephemeral volumes). Currently 99% empty.
|
||||
- **T5-SSD (tsys5 SAS, 140 GB free after ultix-streaming):** ultix-streaming
|
||||
occupies 83 GB. Remaining 140 GB available for k8s use.
|
||||
|
||||
**tsys5 is the fast-tier hub:** NVMe (local) + D3 SSD + T5-SSD all on one host.
|
||||
This simplifies the StorageClass design -- latency-sensitive k8s PVCs target
|
||||
tsys5 SSD exports, bulk PVCs target either server.
|
||||
|
||||
### 6.4 NFS data distribution across storage servers
|
||||
|
||||
To avoid re-creating the "everything on tsys4" problem, distribute NFS data
|
||||
disks across both servers:
|
||||
|
||||
| wnode | Boot (local) | Bulk data (NFS) | NFS server |
|
||||
|-------|-------------|-----------------|------------|
|
||||
| wnode-tsys3 | local-lvm | S3 | tsys5 |
|
||||
| wnode-tsys5 | NVMe | local-nonprod | local (no NFS) |
|
||||
| wnode-tsys6 | D2 (tsys4 NFS) | -- | tsys4 |
|
||||
| wnode-tsys7 | D5 (tsys4 NFS) | -- | tsys4 |
|
||||
| wnode-tsys9 | local-lvm (SSD) | S2 | tsys5 |
|
||||
|
||||
This balances: 2 wnodes using tsys4 for bulk data, 2 using tsys5.
|
||||
|
||||
---
|
||||
|
||||
## 7. ETL/HPC Considerations
|
||||
|
||||
### 7.1 Weather/GIS ETL pipeline
|
||||
|
||||
Typical flow: download GRIB/NetCDF files -> process (reproject, aggregate) ->
|
||||
store results.
|
||||
|
||||
| Stage | Storage class | Why |
|
||||
|-------|-------------|-----|
|
||||
| Download raw data | `nfs-hdd` | Large sequential writes. NFS-HDD handles this well. |
|
||||
| Processing scratch | `local-fast` | Random access during transform. Local disk avoids NFS latency. |
|
||||
| Store results | `nfs-hdd` | Large sequential writes. Persistent. |
|
||||
|
||||
**Recommendation:** Deploy a `local-fast` PV mount as `/scratch` on every
|
||||
wnode. ETL jobs use `/scratch` for intermediate processing and write final
|
||||
output to the NFS-mounted `/data`.
|
||||
|
||||
### 7.2 HPC workloads (hardware startup)
|
||||
|
||||
Use cases: RTL simulation, PCB thermal analysis, firmware build pipelines.
|
||||
|
||||
| Workload | Best wnode | Why |
|
||||
|----------|-----------|-----|
|
||||
| RTL simulation (CPU-bound, high RAM) | tsys7 (192 GB) | Most RAM, most cores (24t) |
|
||||
| Firmware builds (I/O-bound, moderate RAM) | tsys5 (NVMe) | Fastest storage for compile I/O |
|
||||
| Hardware-in-the-loop (latency-sensitive) | tsys9 (local SSD) | Lowest latency storage |
|
||||
| Parallel batch jobs | tsys6 + tsys7 | Distribute across both heavy hosts |
|
||||
|
||||
### 7.3 RackRental/containerlab
|
||||
|
||||
Rapid container deployment. Key need: fast container image pulls.
|
||||
|
||||
This is where **local-lvm boot disks** shine. Currently, every container image
|
||||
pull writes through NFS to a spinning disk -- slow. With local-lvm, images
|
||||
cache on local disk (even HDD is 2-3x faster than NFS-HDD for random I/O).
|
||||
On tsys9 (SSD) and tsys5 (NVMe), image pulls are near-instant.
|
||||
|
||||
### 7.4 Data locality for ETL
|
||||
|
||||
For weather/GIS data that is read repeatedly (e.g., climate reanalysis), cache
|
||||
it on local-lvm of the heavy hosts:
|
||||
|
||||
```
|
||||
tsys3 local-lvm (NVMe 349 GB): /data/cache/weather/ -- fastest cache tier
|
||||
tsys5 NVMe (local): /data/cache/gis/ -- fastest cache tier
|
||||
```
|
||||
|
||||
**Note:** tsys6/7 local-lvm is USB 2.0 portable HDD (~30 MB/s) -- cannot
|
||||
be used for caching. Pre-populate weather/GIS data on D2/D5 (NFS) instead.
|
||||
|
||||
This avoids re-reading the same data from the same NFS export on every job
|
||||
if the data is already cached in the page cache.
|
||||
|
||||
---
|
||||
|
||||
## 8. Migration Plan
|
||||
|
||||
**Key enabler:** The hosts are standalone Proxmox installs, but **Proxmox
|
||||
Datacenter Manager (PDM)** manages them collectively and supports VM migration
|
||||
between nodes. Storage migration can be done via the PDM/Proxmox UI rather
|
||||
than manual disk copies -- the destination node just needs access to the target
|
||||
storage (which all nodes have for NFS exports, and local storage can be
|
||||
migrated through the UI's "Storage Migrate" function).
|
||||
|
||||
### 8.1 Phase 1: Friday (after hardware work)
|
||||
|
||||
After tsys5 cable + NVMe and tsys4 NIC + RAM:
|
||||
|
||||
1. **Format tsys5 NVMe** as local directory storage (e.g., `nvme-local`)
|
||||
2. **Restart wnode-tsys6** (VM 100). Keep on NFS (D5). local-lvm is USB 2.0 --
|
||||
do not use for VM storage. Recreate on D2 or D5 NFS.
|
||||
3. **Move wnode-tsys9** (VM 905) disk from S3 (NFS) to local-lvm (SSD).
|
||||
|
||||
### 8.2 Phase 2: Cnode rebalance (maintenance window)
|
||||
|
||||
These changes require creating new VMs on target hosts and migrating disks.
|
||||
Plan for a maintenance window with the k8s cluster briefly down.
|
||||
|
||||
1. Create cnode2 on tsys9 (local-lvm SSD if possible, or D2 NFS).
|
||||
2. Create cnode4 on tsys9 (D5 NFS or local-lvm SSD).
|
||||
3. Create cnode5 on tsys3 (S3 NFS or local-lvm).
|
||||
4. Move cnode3 disk from D2 to S2 (tsys4 to tsys5).
|
||||
5. Join new cnodes to etcd cluster, drain old cnode2/4/5, remove.
|
||||
|
||||
### 8.3 Phase 3: Wnode local storage migration (maintenance window)
|
||||
|
||||
1. Recreate wnode-tsys3 with boot disk on local-lvm (349 GB).
|
||||
2. wnode-tsys6 stays on NFS (local-lvm is USB 2.0 HDD -- not suitable).
|
||||
3. wnode-tsys7 stays on NFS (same reason).
|
||||
4. Recreate wnode-tsys5 with boot disk on NVMe.
|
||||
5. Add data disks (NFS) as second SCSI devices where applicable.
|
||||
|
||||
### 8.4 Phase 4: tsys2 integration (when rebuilt)
|
||||
|
||||
1. Install Proxmox on tsys2.
|
||||
2. Run `scripts/check.sh` to inventory.
|
||||
3. Run `scripts/apply-tunings.sh --apply`.
|
||||
4. Create wnode-tsys2 with **boot disk on NVMe (960 PRO)** and **data disk on SATA SSD (850 EVO)**. No NFS needed -- 1.5 TB local SSD is the most local storage of any worker.
|
||||
5. Join to k8s cluster.
|
||||
|
||||
### 8.5 Phase 5: Critical VM relocation
|
||||
|
||||
1. Move netinfra-02 (VM 904) from D2 to S3 (tsys5 HDD).
|
||||
2. Move ucs-02 (VM 902) from D5 to S2 (tsys5 HDD).
|
||||
3. (No change to T5-SSD -- ultix-streaming stays.)
|
||||
|
||||
---
|
||||
|
||||
## Open questions for next session
|
||||
|
||||
1. **Are the hosts a Proxmox cluster (pvecm) or standalone?** This determines
|
||||
whether live migration is available (huge simplification) or we need manual
|
||||
disk migration. Check `pvecm status` on each host.
|
||||
2. **What k8s distribution is in use?** (k3s, kubeadm, RKE2?) This affects how
|
||||
nodes are joined/drain and how StorageClasses are configured.
|
||||
3. **Container runtime?** (containerd, cri-o?) Affects local storage layout.
|
||||
4. **Is there a container image registry mirror in the cluster?** Or do all
|
||||
pulls go to Docker Hub / external? A local registry on D3 SSD would speed
|
||||
up all pulls.
|
||||
5. **What specific ETL tools?** (GDAL, PostGIS, xarray, Dask?) This affects
|
||||
whether jobs need shared (RWX) or exclusive (RWO) storage.
|
||||
6. **HPC job scheduler?** (plain k8s Jobs, Argo Workflows, Volcano?) Affects
|
||||
how we label and taint nodes.
|
||||
+857
@@ -0,0 +1,857 @@
|
||||
# Proxmox Cluster Project Report
|
||||
|
||||
**Date:** 2026-07-27 (re-audited)
|
||||
**Prepared by:** Performance Optimization Engagement
|
||||
**Status:** Comprehensive fleet assessment with VM placement and redundancy analysis
|
||||
|
||||
**Data freshness:** All 7 hosts re-audited at 21:50 CDT 2026-07-27 via
|
||||
`deploy-check.sh`. VM placements reflect live state after user's PDM
|
||||
migrations. This is ground truth.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Executive Summary](#1-executive-summary)
|
||||
2. [Host Fleet](#2-host-fleet)
|
||||
3. [Storage Architecture](#3-storage-architecture)
|
||||
4. [VM Fleet Inventory](#4-vm-fleet-inventory)
|
||||
5. [Kubernetes Node Distribution](#5-kubernetes-node-distribution)
|
||||
6. [Storage Redundancy Analysis](#6-storage-redundancy-analysis)
|
||||
7. [Local SSD/NVMe Opportunity](#7-local-ssdnvme-opportunity)
|
||||
8. [Role Alignment Audit](#8-role-alignment-audit)
|
||||
9. [Network Findings](#9-network-findings)
|
||||
10. [Recommendations](#10-recommendations)
|
||||
11. [Hardware End-of-Support Exposure](#11-hardware-end-of-support-exposure)
|
||||
12. [Open Items](#12-open-items)
|
||||
|
||||
---
|
||||
|
||||
## 1. Executive Summary
|
||||
|
||||
The cluster consists of 7 active Proxmox hosts and 1 incoming (pfv-tsys2),
|
||||
running 43 VMs across two NFS storage servers (tsys4, tsys5). Host-side
|
||||
performance tunings are complete on 5 of 7 hosts. Two hosts (tsys4, tsys5)
|
||||
are blocked on physical hardware work scheduled for Friday.
|
||||
|
||||
**Progress since initial audit:** The user has been actively rebalancing k8s
|
||||
nodes via PDM. Storage distribution improved from 90%/10% (tsys4/tsys5) to
|
||||
73%/27%. One cnode now uses tsys5 storage (cnode5 on S2). More migration
|
||||
needed for etcd quorum survival.
|
||||
|
||||
The VM-layer assessment reveals:
|
||||
|
||||
| # | Finding | Severity | Status |
|
||||
|---|---------|----------|--------|
|
||||
| 1 | **4 of 5 cnodes still store disks on tsys4 NFS.** cnode5 moved to tsys5. Still need 1-2 more moves for quorum survival. | **CRITICAL** | Improving |
|
||||
| 2 | **Both -01/-02 infrastructure pairs (netinfra, UCS) on tsys4 NFS only.** | **HIGH** | TODO today |
|
||||
| 3 | **No k8s node uses SSD or NVMe yet.** tsys3 has 349 GB unused local NVMe; tsys9 has 136 GB local SSD. | **HIGH** | Deferred to k8s session |
|
||||
| 4 | **D3 SSD (tsys4, USB) is 99% empty (445 GB free).** Moving to tsys5 SAS Friday. | **MEDIUM** | Friday |
|
||||
|
||||
---
|
||||
|
||||
## 2. Host Fleet
|
||||
|
||||
### 2.1 Inventory
|
||||
|
||||
| Host | Model | CPU (year) | Cores | RAM | Local Disk | Role (intended) | Tuning |
|
||||
|------|-------|-----------|-------|-----|-----------|-----------------|--------|
|
||||
| pfv-tsys1 | OptiPlex 9020 | i7-4770 Haswell (2013) | 4c/8t | 32 GB DDR3 | HDD (LVM-thin) | **Infrastructure** | Done |
|
||||
| pfv-tsys2 | Precision 5520 | i7-7820HQ Kaby Lake (2017) | 4c/8t | 32 GB (max) | **NVMe 512GB + SATA SSD 1TB** | **Kubernetes** | Incoming (Win10) |
|
||||
| pfv-tsys3 | Precision 7510 | Xeon E3-1535M v5 Skylake (2015) | 4c/8t | 32 GB DDR4 | HDD (LVM-thin) | **Kubernetes** | Done |
|
||||
| pfv-tsys4 | Precision T1700 | Xeon E3-1246 v3 Haswell (2013) | 4c/8t | 16 GB DDR3 | 6 disks (HDD+SSD+SMR) | **Storage (NFS+PBS)** | Blocked (NIC+RAM) |
|
||||
| pfv-tsys5 | Precision T7500 | Xeon E5620 Westmere (2010) | 4c/8t | 96 GB DDR3 | 6 disks (HDD+SSD) | **Storage (NFS+VMs)** | Blocked (cable) |
|
||||
| pfv-tsys6 | PowerEdge R610 | 2x Xeon E5530 Nehalem (2009) | 8c/16t | 128 GB DDR3 | HDD (LVM-thin) | **Kubernetes** | Done |
|
||||
| pfv-tsys7 | PowerEdge R620 | 2x Xeon E5-2630 v2 Ivy Bridge (2013) | 12c/24t | 192 GB DDR3 | HDD (LVM-thin) | **Kubernetes** | Done |
|
||||
| pfv-tsys9 | OptiPlex 7080 | i5-10500 Comet Lake (2020) | 6c/12t | 24 GB DDR4 | **250 GB SSD** (PNY CS900) | **Infrastructure** | Done |
|
||||
|
||||
### 2.2 Role taxonomy (per user directive)
|
||||
|
||||
| Role | Hosts | Workload |
|
||||
|------|-------|----------|
|
||||
| **Infrastructure + k8s control** | tsys1, tsys9 | Infra VMs (netinfra, UCS, PBS, CA, HA) + pfv-k8s cnodes (control plane) + small wnodes |
|
||||
| **Kubernetes workers** | tsys2, tsys3, tsys6, tsys7 | pfv-k8s wnodes (heavy workers) -- these hosts have the RAM (32-192 GB) for ETL/HPC |
|
||||
| **Storage** | tsys4, tsys5 | NFS server + PBS backup target. tsys5 also runs sectestbed/preprod VMs |
|
||||
|
||||
**Design rationale:** cnodes (control plane) are lightweight (4 cores, 4 GB
|
||||
RAM each) and are weighted toward tsys1/tsys9 to keep the heavy RAM/CPU hosts
|
||||
(tsys6 with 128 GB, tsys7 with 192 GB) free for large worker nodes. wnodes
|
||||
run one per hypervisor host across the fleet to maximize total cluster capacity.
|
||||
|
||||
pfv-k8s runs all R&D and RackRental (containerlab) workloads via Kubernetes.
|
||||
Production (Gitea, RustFS, Redmine, websites) lives on a VPS in Reston, VA
|
||||
running Cloudron -- not in this cluster.
|
||||
|
||||
---
|
||||
|
||||
## 3. Storage Architecture
|
||||
|
||||
### 3.1 NFS exports from tsys4 (primary storage server)
|
||||
|
||||
| Export | Disk model | Type | Bus | Total | Used | Free | Use% |
|
||||
|--------|-----------|------|-----|-------|------|------|------|
|
||||
| D2 | WDC WD30EFRX Red | HDD (7200rpm) | SATA | 2.7 TB | 187 GB | **2.4 TB** | 8% |
|
||||
| ~~D3~~ | ~~SK hynix SC300~~ | ~~SSD~~ | ~~USB~~ | — | — | — | **moving to tsys5 Friday** |
|
||||
| D5 | Hitachi HDS72302 | HDD (7200rpm) | SATA | 1.8 TB | 236 GB | **1.5 TB** | 14% |
|
||||
|
||||
Non-exported disks on tsys4:
|
||||
- sda (Hitachi 1.8T) at /mnt/albert -- not NFS shared, 1.7 TB free
|
||||
- sdd (WDC 1T) -- **idle, unmounted, removable** (free up for other use)
|
||||
- sdf (WDC 4.5T SMR) at /mnt/backup -- **PBS backup target**, 4.3 TB free
|
||||
|
||||
**D3 migration (Friday):** The SK hynix SC300 SSD is currently USB-attached on
|
||||
tsys4 (via a "ThinkPad SSD" USB adapter). It is moving to a tsys5 SAS port,
|
||||
eliminating the USB bottleneck. tsys4's 4 SATA ports are all occupied (sda/sdb
|
||||
/sdc/sdd), so tsys5 is the better target. See section 3.2.
|
||||
|
||||
### 3.2 NFS exports from tsys5 (secondary storage -- becoming the fast-tier hub)
|
||||
|
||||
| Export | Disk model | Type | Bus | Total | Used | Free | Use% |
|
||||
|--------|-----------|------|-----|-------|------|------|------|
|
||||
| S1 | Seagate ST1000VN | HDD | SAS | 916 GB | 60 GB | 810 GB | 7% |
|
||||
| S2 | Seagate ST1000VN | HDD | SAS | 916 GB | **6.9 GB** | **863 GB** | **1%** |
|
||||
| S3 | Seagate ST1000VN | HDD | SAS | 916 GB | 7.0 GB | **863 GB** | **1%** |
|
||||
| S4 | Toshiba DT01ACA050 | HDD | SAS | 458 GB | 2 MB | **435 GB** | **0%** |
|
||||
| T5-SSD | Samsung 860 PRO | **SSD** | SAS | 234 GB | **122 GB** | **101 GB** | **55%** |
|
||||
| **D3** (Friday) | SK hynix SC300 | **SSD** | **SAS** | **469 GB** | **2 MB** | **445 GB** | **0%** |
|
||||
|
||||
**tsys5 storage controllers (plenty of free ports):**
|
||||
- LSI SAS1068E (SAS 6/iR): 8 ports, 3 used (Samsung SSD, Hitachi, Seagate),
|
||||
**5 free**
|
||||
- Intel ICH10 SATA #1 (4-port): 2 used (Seagate S3, Toshiba S4), **2 free**
|
||||
- Intel ICH10 SATA #2 (2-port): **status unknown, likely free**
|
||||
- 2x Renesas USB 3.0 xHCI controllers (real USB 3.0, unlike tsys6/7)
|
||||
|
||||
**Key finding: S2 and S3 now have k8s node disks.** S2 holds cnode5 +
|
||||
wnode-tsys1 + wnode-tsys9 (6.9 GB used). S3 has wnode-tsys9's old disk
|
||||
(unused, 7 GB). S4 still 99% empty (435 GB free).
|
||||
|
||||
T5-SSD grew to 55% used (122 GB) -- ultix-streaming is the primary consumer.
|
||||
|
||||
**Friday additions:**
|
||||
1. **D3 (SK hynix SSD)** moves from tsys4 USB to tsys5 SAS port. Eliminates
|
||||
USB 2.0 bottleneck. Becomes the second SSD-tier NFS export.
|
||||
2. **PCI NVMe drive** (local-only, not NFS-exported). Used for wnode-tsys5
|
||||
boot disk and HPC scratch. The fastest tier in the fleet.
|
||||
|
||||
After Friday, **tsys5 consolidates all fast storage**: NVMe (local) + 2 SSD
|
||||
NFS exports (D3 + T5-SSD) + 4 HDD NFS exports (S1-S4). This makes tsys5 the
|
||||
natural home for latency-sensitive workloads and the k8s StorageClass design
|
||||
center.
|
||||
|
||||
### 3.3 Local storage tiers (per host, with utilization)
|
||||
|
||||
| Host | Storage ID | Disk type | Bus | Total | Used | Free | Used by VMs? |
|
||||
|------|-----------|-----------|-----|-------|------|------|-------------|
|
||||
| tsys1 | local-lvm | HDD | SATA | ~90 GB | low | ~90 GB | No (all VMs on NFS) |
|
||||
| **tsys3** | **local-lvm** | **NVMe (Samsung PM961)** | **NVMe** | **349 GB** | **0 GB** | **349 GB** | **No (all VMs on NFS)** |
|
||||
| tsys4 | local-lvm | HDD | SATA | ~94 GB | PBS VM | ~62 GB | Yes (PBS VM 400) |
|
||||
| tsys5 | local-lvm | HDD (Hitachi 1.8T) | SATA | 1.7 TB | 40 MB | **1.7 TB** | No |
|
||||
| tsys5 | local-nonprod | HDD (Seagate 1T, =S1) | SATA | 916 GB | 53 GB | **856 GB** | Yes (sectestbed suite) |
|
||||
| **tsys6** | **local-lvm** | **HDD (WD My Passport)** | **USB 2.0** | **1.7 TB** | **0 GB** | **1.7 TB** | **No -- DO NOT USE for VM storage** |
|
||||
| **tsys7** | **local-lvm** | **HDD (WD portable)** | **USB 2.0** | **1.7 TB** | **0 GB** | **1.7 TB** | **No -- DO NOT USE for VM storage** |
|
||||
| **tsys9** | **local-lvm** | **SSD (PNY CS900)** | **SATA** | **136 GB** | **0 GB** | **136 GB** | **No (all VMs on NFS)** |
|
||||
| **tsys2** | **NVMe** (Samsung 960 PRO) | **NVMe** | **NVMe** | **512 GB** | (Win10) | **512 GB** | **Incoming -- fastest boot tier after tsys5 NVMe** |
|
||||
| **tsys2** | **SATA SSD** (Samsung 850 EVO) | **SSD** | **SATA** | **1 TB** | (Win10) | **1 TB** | **Incoming -- bulk data on SSD, not rust** |
|
||||
|
||||
**CRITICAL WARNING: tsys6 and tsys7 local-lvm is USB 2.0 portable HDD.**
|
||||
|
||||
The entire Proxmox OS, swap, and local-lvm on both R610 and R620 run on a
|
||||
single **USB 2.0-attached WD My Passport portable HDD** (tsys6: "My Passport
|
||||
260D"; tsys7: "Drive 2657"). Both servers' only USB controllers are EHCI
|
||||
(USB 2.0, ~480 Mbps). There is **no USB 3.0/xHCI** on either host.
|
||||
|
||||
**USB 2.0 practical throughput is ~30-35 MB/s.** This is 3-4x SLOWER than
|
||||
NFS-over-HDD (~80-120 MB/s). Moving wnode boot disks to local-lvm on these
|
||||
hosts would **decrease** performance. local-lvm on tsys6/7 must NOT be used
|
||||
for VM storage.
|
||||
|
||||
Additionally, both servers have completely empty internal drive bays:
|
||||
- **tsys6**: SAS controller present but **DISABLED** in BIOS. No internal
|
||||
drives.
|
||||
- **tsys7**: 6-port SATA AHCI controller present, **5 ports EMPTY** (only
|
||||
DVD-ROM on port 5). No internal drives.
|
||||
|
||||
This is a reliability risk beyond performance: the entire host OS boots
|
||||
from a consumer-grade portable USB drive not designed for 24/7 server use.
|
||||
|
||||
**tsys3 correction:** Previously documented as HDD. Actually boots from a
|
||||
**Samsung PM961 NVMe 512GB SSD** -- the fastest existing local storage in the
|
||||
fleet. Its 349 GB of local-lvm is excellent for wnode boot disk use.
|
||||
|
||||
**Critical observation: every k8s host has 0% used local-lvm.** tsys3
|
||||
(Samsung PM961 **NVMe**, 349 GB), tsys6 (WD My Passport **USB 2.0** HDD,
|
||||
1.7 TB), tsys7 (WD portable **USB 2.0** HDD, 1.7 TB) all have unused local
|
||||
storage.
|
||||
|
||||
**However, only tsys3's local-lvm is suitable for VM storage.** tsys6 and
|
||||
tsys7 local-lvm is USB 2.0 portable HDD (~30-35 MB/s) -- slower than
|
||||
NFS-over-HDD and unsuitable for wnode boot disks.
|
||||
|
||||
### 3.4 Disk speed tiers summary
|
||||
|
||||
| Tier | Where | Speed class | Best for |
|
||||
|------|-------|------------|----------|
|
||||
| **NVMe** | tsys3 (Samsung PM961), tsys5 (Friday addition), **tsys2 (Samsung 960 PRO 512GB)** | 2000-3500 MB/s | HPC scratch, ETL staging, container runtime, wnode boot, etcd |
|
||||
| **Local SSD** | tsys9 (PNY CS900, 136 GB), **tsys2 (Samsung 850 EVO 1TB)** | 500 MB/s | wnode boot disk, etcd |
|
||||
| **NFS-SSD** | tsys5 D3 (SK hynix, **SAS post-Friday**), tsys5 T5-SSD (Samsung) | 200-400 MB/s over NFS | **k8s worker scratch only** (etcd, container cache, ephemeral volumes) |
|
||||
| **NFS-HDD** | tsys4 D2/D5, tsys5 S1-S4 | 80-120 MB/s over NFS | Bulk data, large disks, non-critical VMs, **wnode boot on tsys6/7** |
|
||||
| **Local SATA HDD** | tsys1 local-lvm | 100-150 MB/s | Host OS only |
|
||||
| **USB 2.0 HDD** | tsys6/7 local-lvm (WD My Passport) | **~30-35 MB/s** | **NOTHING -- slower than NFS, do not use for VMs** |
|
||||
|
||||
### 3.5 Storage tier characterization per host
|
||||
|
||||
| Host | Storage profile | Detail |
|
||||
|------|----------------|--------|
|
||||
| **tsys2** | **SSD/NVMe only** | 960 PRO NVMe 512GB + 850 EVO SATA SSD 1TB. No spinning disk. |
|
||||
| **tsys3** | **NVMe only** | Samsung PM961 NVMe 512GB. No spinning disk. |
|
||||
| **tsys5** | **Hybrid** (fast-tier hub) | NVMe (local, Friday) + D3 SSD + T5-SSD + S1-S4 HDD |
|
||||
| **tsys4** | **Bulk/spinning disk only** | D2 HDD 3TB + D5 HDD 2TB. D3 SSD leaving Friday. PBS target on SMR HDD. |
|
||||
| **tsys9** | **Local SSD + NFS** | PNY CS900 SSD 136GB local + NFS client |
|
||||
| **tsys1** | **Local HDD + NFS** | Small local-lvm + NFS client |
|
||||
| **tsys6/7** | **NFS only** | local-lvm is USB 2.0 HDD (unusable for VMs). All VMs on NFS. |
|
||||
|
||||
### 3.6 All exports are single-disk with no redundancy
|
||||
|
||||
Every NFS export is a single physical disk formatted ext4. No RAID, no ZFS
|
||||
mirror, no mdraid. A single disk failure takes down every VM whose disk lives
|
||||
on that export. This applies to **both storage servers** and to the **PBS
|
||||
backup target** (a single 4.5T SMR drive).
|
||||
|
||||
---
|
||||
|
||||
## 4. VM Fleet Inventory
|
||||
|
||||
### 4.1 Complete VM roster (running VMs only, 40 VMs across 7 hosts)
|
||||
|
||||
#### tsys1 (Infrastructure) -- 11 running VMs
|
||||
|
||||
| VMID | Name | Cores | RAM (MB) | Disk | Storage | Tier |
|
||||
|------|------|-------|----------|------|---------|------|
|
||||
| 100 | pfv-bms (HomeAssistant) | 2 | 4096 | 32 GB | D2 (tsys4 HDD) | NFS |
|
||||
| 101 | tsys-ca | 2 | 2048 | 32 GB | D2 (tsys4 HDD) | NFS |
|
||||
| 103 | **pfv-netinfra-01** | 2 | 2048 | 32 GB | D5 (tsys4 HDD) | NFS |
|
||||
| 104 | tsys-librenms | 2 | 2048 | 50 GB | D2 (tsys4 HDD) | NFS |
|
||||
| 105 | tsys-proxmox-datacenter | 2 | 2048 | 32 GB | D2 (tsys4 HDD) | NFS |
|
||||
| 106 | **pfv-k8s-cnode3** | 2 | 4096 | 32 GB | D2 (tsys4 HDD) | NFS |
|
||||
| 107 | **pfv-k8s-cnode1** | 2 | 4096 | 32 GB | D5 (tsys4 HDD) | NFS |
|
||||
| 108 | **tsys-ucs-01** | 2x2 | 8000 | 32 GB | D2 (tsys4 HDD) | NFS |
|
||||
| 109 | tailscale-router | 2 | 2048 | 25 GB | D2 (tsys4 HDD) | NFS |
|
||||
| 114 | kali-tsys | 2 | 2048 | 32 GB | D2 (tsys4 HDD) | NFS |
|
||||
| 117 | tsys-secure-workbench | 2 | 4000 | 32 GB | D2 (tsys4 HDD) | NFS |
|
||||
|
||||
#### tsys3 (Kubernetes) -- 1 running VM
|
||||
|
||||
| VMID | Name | Cores | RAM (MB) | Disk | Storage | Tier |
|
||||
|------|------|-------|----------|------|---------|------|
|
||||
| 313 | **pfv-k8s-wnode-tsys3** | 4x2 | 20000 | 32 GB | D5 (tsys4 HDD) | NFS |
|
||||
|
||||
#### tsys4 (Storage) -- 1 running VM
|
||||
|
||||
| VMID | Name | Cores | RAM (MB) | Disk | Storage | Tier |
|
||||
|------|------|-------|----------|------|---------|------|
|
||||
| 400 | pfv-proxmox-backup-server | 2 | 2048 | 32 GB | local-lvm | LOCAL |
|
||||
|
||||
#### tsys5 (Storage) -- 15 running VMs
|
||||
|
||||
| VMID | Name | Cores | RAM (MB) | Disk | Storage | Tier |
|
||||
|------|------|-------|----------|------|---------|------|
|
||||
| 509 | **pfv-k8s-wnode-tsys5** | 2x4 | 32000 | 32 GB | D2 (tsys4 HDD) | NFS |
|
||||
| 5101 | sectestbed-siem | 2x2 | 10000 | 132 GB | local-nonprod | LOCAL |
|
||||
| 5105 | sectestbed-awx | 2x2 | 4096 | 288 GB | local-nonprod | LOCAL |
|
||||
| 5106 | sectestbed-k8s-cnode | 2x2 | 4096 | 32 GB | local-nonprod | LOCAL |
|
||||
| 5107 | sectestbed-k8s-wnode | 2x2 | 4096 | 32 GB | local-nonprod | LOCAL |
|
||||
| 5108 | sectestbed-librenms | 2x2 | 4096 | 32 GB | local-nonprod | LOCAL |
|
||||
| 5109 | sectestbed-netinfra | 2x2 | 4096 | 32 GB | local-nonprod | LOCAL |
|
||||
| 5111 | ultix-streaming | 2x2 | 9000 | 288 GB | T5-SSD (tsys5 SSD) | NFS-SSD |
|
||||
| 5112 | ultix-offstage | 2x2 | 6000 | 288 GB | local-lvm | LOCAL |
|
||||
| 6000 | sectestbed-sandbox | 2x2 | 4096 | 32 GB | local-nonprod | LOCAL |
|
||||
| 51010 | sectestbed-tctc | 2x2 | 4096 | 32 GB | local-nonprod | LOCAL |
|
||||
| 51011 | sectestbed-cloudron | 2x2 | 4096 | 32 GB | local-nonprod | LOCAL |
|
||||
| 51012 | sectestbed-hfnoc | 2x2 | 4096 | 32 GB | local-nonprod | LOCAL |
|
||||
| 51013 | sectestbed-rancherplatform | 2x2 | 4096 | 32 GB | local-nonprod | LOCAL |
|
||||
| 53100 | tsys-preprod-awx | 2x2 | 9000 | 160 GB | local-nonprod | LOCAL |
|
||||
| 53101 | tsys-preprod-siem | 2x2 | 12000 | 32 GB | local-nonprod | LOCAL |
|
||||
| 53102 | tsys-preprod-rancherplatform | 2x2 | 8000 | 32 GB | local-nonprod | LOCAL |
|
||||
|
||||
#### tsys6 (Kubernetes) -- 3 running VMs (1 wnode stopped)
|
||||
|
||||
| VMID | Name | Cores | RAM (MB) | Disk | Storage | Tier |
|
||||
|------|------|-------|----------|------|---------|------|
|
||||
| 100 | pfv-k8s-wnode-tsys6 | 2x2 | 32000 | 32 GB | D5 (tsys4 HDD) | NFS -- **STOPPED** |
|
||||
| 600 | tsys-awx | 2x2 | 12000 | 32 GB | D2 (tsys4 HDD) | NFS -- **STOPPED** |
|
||||
| 601 | **pfv-k8s-cnode4** | 4 | 4096 | 32 GB | D2 (tsys4 HDD) | NFS |
|
||||
| 602 | **pfv-k8s-cnode5** | 4 | 4096 | 32 GB | D5 (tsys4 HDD) | NFS |
|
||||
| 603 | **pfv-k8s-cnode2** | 4 | 4096 | 32 GB | D2 (tsys4 HDD) | NFS |
|
||||
|
||||
#### tsys7 (Kubernetes) -- 4 running VMs
|
||||
|
||||
| VMID | Name | Cores | RAM (MB) | Disk | Storage | Tier |
|
||||
|------|------|-------|----------|------|---------|------|
|
||||
| 701 | **pfv-k8s-wnode-tsys7** | 4 | 32000 | 32 GB | D5 (tsys4 HDD) | NFS |
|
||||
| 702 | hfnoc-uisp | 2x2 | 8000 | 100 GB | D2 (tsys4 HDD) | NFS |
|
||||
| 703 | rr-middleware | 2 | 2048 | 32 GB | D2 (tsys4 HDD) | NFS |
|
||||
| 704 | TCTC | 4 | 6000 | 32 GB | D2 (tsys4 HDD) | NFS |
|
||||
|
||||
#### tsys9 (Infrastructure) -- 5 running VMs
|
||||
|
||||
| VMID | Name | Cores | RAM (MB) | Disk | Storage | Tier |
|
||||
|------|------|-------|----------|------|---------|------|
|
||||
| 901 | tsys-siem | 2 | 8000 | 132 GB | D2 (tsys4 HDD) | NFS |
|
||||
| 902 | **tsys-ucs-02** | 2x2 | 8000 | 50 GB | D5 (tsys4 HDD) | NFS |
|
||||
| 903 | kali-rd | 2 | 2048 | 32 GB | D5 (tsys4 HDD) | NFS |
|
||||
| 904 | **pfv-netinfra-02** | 2 | 4000 | 32 GB | D2 (tsys4 HDD) | NFS |
|
||||
| 905 | **pfv-k8s-wnode-tsys9** | 4 | 4096 | 32 GB | S3 (tsys5 HDD) | NFS |
|
||||
|
||||
### 4.2 Storage concentration summary
|
||||
|
||||
| Storage target | # of running VMs | % of fleet |
|
||||
|---------------|-----------------|------------|
|
||||
| **D2 (tsys4 WDC Red 3TB HDD)** | **18** | **45%** |
|
||||
| D5 (tsys4 Hitachi 2TB HDD) | 9 | 23% |
|
||||
| local-nonprod (tsys5 local HDD) | 10 | 25% |
|
||||
| S3 (tsys5 Seagate 1TB HDD) | 1 | 3% |
|
||||
| T5-SSD (tsys5 Samsung SSD) | 1 | 3% |
|
||||
| local-lvm (tsys4 local) | 1 | 3% |
|
||||
|
||||
**27 of 40 running VMs (68%) store their disks on tsys4 NFS exports.**
|
||||
If tsys4 goes offline, two-thirds of the fleet loses its storage.
|
||||
|
||||
---
|
||||
|
||||
## 5. Kubernetes Node Distribution (re-audited 21:50 CDT)
|
||||
|
||||
### 5.1 pfv-k8s cnode (control plane) placement -- CURRENT
|
||||
|
||||
| VMID | Name | Hypervisor | Storage | NFS Server | Changed? |
|
||||
|------|------|------------|---------|-----------|----------|
|
||||
| 906 | cnode1 | **tsys9** | D5 | tsys4 | **MOVED from tsys1** |
|
||||
| 705 | cnode2 | **tsys7** | D2 | tsys4 | **MOVED from tsys6** |
|
||||
| 106 | cnode3 | tsys1 | D2 | tsys4 | no change |
|
||||
| 601 | cnode4 | tsys6 | D2 | tsys4 | no change |
|
||||
| 706 | cnode5 | **tsys7** | **S2** | **tsys5** | **MOVED from tsys6, storage moved D5→S2** |
|
||||
|
||||
**Storage distribution:**
|
||||
|
||||
| Storage server | cnodes | Quorum impact if it fails |
|
||||
|---------------|--------|--------------------------|
|
||||
| tsys4 (D2+D5) | **4** (cnode1,2,3,4) | Only cnode5 survives = **QUORUM LOST** |
|
||||
| tsys5 (S2) | **1** (cnode5) | 4 survive = quorum OK |
|
||||
|
||||
**Progress:** cnode5 is now on tsys5 (was all 5 on tsys4). But 4-of-5 on tsys4
|
||||
still means a tsys4 failure loses quorum. **Need 2 more cnodes on tsys5.**
|
||||
|
||||
**Host distribution:** cnodes spread across 4 hosts (tsys1, tsys6, tsys7,
|
||||
tsys9) -- good host diversity.
|
||||
|
||||
### 5.2 pfv-k8s wnode (worker) placement -- CURRENT
|
||||
|
||||
| VMID | Name | Hypervisor | Storage | NFS Server | RAM | Status | Changed? |
|
||||
|------|------|------------|---------|-----------|-----|--------|----------|
|
||||
| 102 | wnode-tsys1 | tsys1 | S2 | tsys5 | 4 GB | **STOPPED** | **NEW** |
|
||||
| 313 | wnode-tsys3 | tsys3 | D5 | tsys4 | **28 GB** | running | **RAM bumped 20→28** |
|
||||
| 509 | wnode-tsys5 | tsys5 | D2 | tsys4 | 32 GB | running | no change |
|
||||
| 100 | wnode-tsys6 | tsys6 | D5 | tsys4 | 32 GB | running | **NOW RUNNING** |
|
||||
| 701 | wnode-tsys7 | tsys7 | D5 | tsys4 | 32 GB | running | no change |
|
||||
| 905 | wnode-tsys9 | tsys9 | **S2** | tsys5 | 4 GB | running | **Storage moved S3→S2** |
|
||||
|
||||
**One wnode per host achieved** (tsys1,3,5,6,7,9). wnode-tsys1 is created but
|
||||
stopped. wnode-tsys6 restarted.
|
||||
|
||||
**Storage distribution:**
|
||||
|
||||
| Storage server | wnodes | Notes |
|
||||
|---------------|--------|-------|
|
||||
| tsys4 (D2+D5) | 4 (tsys3,5,6,7) | Still concentrated |
|
||||
| tsys5 (S2) | 2 (tsys1,tsys9) | Improving |
|
||||
|
||||
### 5.3 Summary: k8s node storage distribution
|
||||
|
||||
| Storage server | cnodes | wnodes | Total k8s nodes |
|
||||
|---------------|--------|--------|-----------------|
|
||||
| **tsys4 NFS** | **4 (80%)** | **4 (67%)** | **8 (73%)** |
|
||||
| **tsys5 NFS** | **1 (20%)** | **2 (33%)** | **3 (27%)** |
|
||||
| Local SSD/NVMe | 0 | 0 | 0 (0%) |
|
||||
|
||||
**Was 90%/10%. Now 73%/27%.** Improving but still tsys4-heavy. Target: 3
|
||||
cnodes on each storage server (60/40 or better) so either server failing
|
||||
leaves quorum intact.
|
||||
|
||||
### 5.4 Remaining cnode migration needed for etcd quorum survival
|
||||
|
||||
To survive a tsys4 failure with quorum (3 of 5 alive), at least 3 cnodes must
|
||||
be on tsys5:
|
||||
|
||||
| Action | Effect |
|
||||
|--------|--------|
|
||||
| Move cnode3 (D2→S3 on tsys5) | 3 cnodes on tsys5, 2 on tsys4. tsys4 fail = 3 survive |
|
||||
| Move cnode4 (D2→S2 on tsys5) | Same result, different disk |
|
||||
| Leave cnode1 and one other on tsys4 | tsys5 fail = 4 survive (OK) |
|
||||
|
||||
**Simplest path:** migrate cnode3 and cnode4 storage to tsys5 (S3 and S2) via
|
||||
PDM. Then tsys4 failure leaves cnode5 + cnode3 + cnode4 = 3 of 5 = quorum OK.
|
||||
|
||||
### 5.5 Future k8s architecture (next session -- see K8S.md)
|
||||
|
||||
The k8s layer will be tackled soon. Key requirements from user:
|
||||
|
||||
- **Platform:** vcluster + Rancher for multi-tenant management
|
||||
- **Auth:** OIDC to Keycloak (running on Cloudron in Reston, VA production)
|
||||
- **Workload isolation (vcluster per tenant):**
|
||||
- RackRental workloads (containerlab network labs)
|
||||
- Suborbital ITAR (compliance-restricted)
|
||||
- Suborbital non-ITAR
|
||||
- Starting Line Productions customer workloads
|
||||
- **Solar-aware scale-out:** PowerEdge 19xx and 2950 systems (older hardware)
|
||||
will be brought online during peak solar production for burst capacity.
|
||||
These older cores/ram supplement the main fleet when power is abundant.
|
||||
- **WNode sizing:** every Proxmox node will have a wnode. Some nodes will host
|
||||
both cnodes + wnodes. Worker sizes will vary from small (4 GB, fitting into
|
||||
leftover host capacity) to large (28-32 GB, consuming most of a host).
|
||||
- **Friday final audit:** tsys2 will be loaded with Proxmox on Friday, and a
|
||||
full final audit will be performed at that time (post-NVMe install on tsys5,
|
||||
post-D3 SSD relocation, post-tsys4 NIC+RAM).
|
||||
|
||||
### 5.4 sectestbed k8s nodes (separate from pfv-k8s)
|
||||
|
||||
tsys5 also hosts a separate sectestbed kubernetes stack using local storage:
|
||||
|
||||
| VMID | Name | Storage |
|
||||
|------|------|---------|
|
||||
| 5106 | sectestbed-k8s-cnode | local-nonprod (local HDD) |
|
||||
| 5107 | sectestbed-k8s-wnode | local-nonprod (local HDD) |
|
||||
|
||||
These are on local storage (good -- no NFS dependency) but on a single host's
|
||||
single local disk (no redundancy). They are isolated from the pfv-k8s cluster.
|
||||
|
||||
---
|
||||
|
||||
## 6. Storage Redundancy Analysis
|
||||
|
||||
### 6.1 -01/-02 infrastructure pair audit
|
||||
|
||||
Two -01/-02 pairs exist in the fleet:
|
||||
|
||||
**Pair 1: pfv-netinfra (network infrastructure)**
|
||||
|
||||
| Role | VMID | Host | Storage | NFS Server |
|
||||
|------|------|------|---------|-----------|
|
||||
| -01 | 103 | tsys1 | D5 | **tsys4** |
|
||||
| -02 | 904 | tsys9 | D2 | **tsys4** |
|
||||
|
||||
**Verdict: HOST redundancy OK (different hosts), STORAGE redundancy FAILED.**
|
||||
Both halves depend on tsys4. If tsys4 goes down, both netinfra VMs lose their
|
||||
disks. The -02 half should be on an S2/S3/S4 export from tsys5.
|
||||
|
||||
**Pair 2: tsys-ucs (Univention Corporate Server)**
|
||||
|
||||
| Role | VMID | Host | Storage | NFS Server |
|
||||
|------|------|------|---------|-----------|
|
||||
| -01 | 108 | tsys1 | D2 | **tsys4** |
|
||||
| -02 | 902 | tsys9 | D5 | **tsys4** |
|
||||
|
||||
**Verdict: HOST redundancy OK (different hosts), STORAGE redundancy FAILED.**
|
||||
Same issue. Both halves on tsys4. The -02 half should be on tsys5 storage.
|
||||
|
||||
### 6.2 Redundancy principle for paired VMs
|
||||
|
||||
For any -01/-02 pair to survive a single storage server failure:
|
||||
|
||||
```
|
||||
-01 VM disk -> tsys4 NFS export (D2/D3/D5)
|
||||
-02 VM disk -> tsys5 NFS export (S2/S3/S4/T5-SSD)
|
||||
```
|
||||
|
||||
This ensures that losing either tsys4 or tsys5 takes down only one half of
|
||||
the pair. Currently, **both pairs fail this test** because both halves are on
|
||||
tsys4.
|
||||
|
||||
### 6.3 NFS server failure blast radius
|
||||
|
||||
If **tsys4** goes offline (USB NIC failure, disk failure, reboot):
|
||||
|
||||
| Impact | Count |
|
||||
|--------|-------|
|
||||
| k8s cnodes that lose storage | 5 of 5 (**etcd quorum lost**) |
|
||||
| k8s wnodes that lose storage | 4 of 5 |
|
||||
| Infrastructure VMs that lose storage | 11 of 12 on tsys1 (all on D2/D5) |
|
||||
| Total VMs that lose storage | **27 of 40 (68%)** |
|
||||
|
||||
If **tsys5** goes offline:
|
||||
|
||||
| Impact | Count |
|
||||
|--------|-------|
|
||||
| k8s cnodes that lose storage | 0 of 5 |
|
||||
| k8s wnodes that lose storage | 1 of 5 |
|
||||
| Total VMs that lose storage | 1 of 40 (3%) |
|
||||
|
||||
**tsys4 is a massive blast-radius liability. tsys5 is barely used.**
|
||||
Rebalancing VM storage across both servers dramatically reduces risk.
|
||||
|
||||
---
|
||||
|
||||
## 7. Local SSD/NVMe Opportunity
|
||||
|
||||
### 7.1 Available fast tiers (currently unused by k8s)
|
||||
|
||||
| Host | Device | Type | Size | Available for VMs? | Currently used by k8s? |
|
||||
|------|--------|------|------|--------------------|-----------------------|
|
||||
| tsys4 | D3 (SK hynix SC300, USB) | SSD | 512 GB | Yes (via NFS) | **No** |
|
||||
| tsys5 | T5-SSD (Samsung 860 PRO) | SSD | 256 GB | Yes (via NFS) | **No** (used by ultix-streaming) |
|
||||
| tsys5 | **New NVMe (Friday)** | **NVMe** | TBD | **Yes (local or NFS)** | **No** |
|
||||
| tsys9 | local-lvm (PNY CS900) | SSD | 137 GB free | Yes (local) | **No** |
|
||||
|
||||
### 7.2 Why local storage matters for k8s nodes
|
||||
|
||||
Kubernetes nodes are latency-sensitive in two specific areas:
|
||||
|
||||
1. **etcd (control plane):** etcd writes are synchronous and latency-critical.
|
||||
On NFS over HDD, every etcd write traverses: VM -> virtio-scsi -> NFS
|
||||
client -> TCP -> USB dongle (on tsys4) -> ext4 -> spinning disk. Typical
|
||||
latency: 2-10 ms per write. On local SSD: 0.1-0.5 ms. On NVMe: 0.02-0.1 ms.
|
||||
This directly affects k8s API responsiveness and pod scheduling speed.
|
||||
|
||||
2. **Container image pulls:** Worker nodes pull container images frequently.
|
||||
On NFS-over-HDD, image layer extraction is seek-bound and slow. Local SSD
|
||||
eliminates the network hop and reduces seek time. This matters most for
|
||||
RackRental/containerlab workloads that spin up containers rapidly.
|
||||
|
||||
### 7.3 Current waste: tsys9 local SSD
|
||||
|
||||
tsys9 has a 250 GB PNY CS900 SSD with 137 GB of LVM-thin space available.
|
||||
**Zero VMs use it.** All 5 VMs on tsys9 boot from NFS. The local SSD sits
|
||||
idle. wnode-tsys9 (VM 905) would benefit significantly from local SSD --
|
||||
its disk is currently on S3 (tsys5 NFS over a Seagate HDD).
|
||||
|
||||
### 7.4 Upcoming opportunity: tsys5 NVMe (Friday)
|
||||
|
||||
The PCI NVMe being added to tsys5 will be the fastest storage tier in the
|
||||
fleet. Two placement options:
|
||||
|
||||
**Option A: NFS-export the NVMe (shared).** All hosts can use it. Good for
|
||||
VMs that might need migration. Adds the NFS/network overhead back.
|
||||
|
||||
**Option B: Local-only on tsys5.** VMs on tsys5 get full NVMe speed with no
|
||||
network overhead. Best for k8s wnode-tsys5 and sectestbed VMs. Cannot be
|
||||
accessed from other hosts.
|
||||
|
||||
**Recommendation:** Option B (local-only). k8s worker nodes do not need
|
||||
shared storage -- pods are ephemeral and reschedule on failure. The NVMe
|
||||
should be formatted as a Proxmox directory storage (or LVM-thin) on tsys5
|
||||
and used for local VM images.
|
||||
|
||||
---
|
||||
|
||||
## 8. Role Alignment Audit
|
||||
|
||||
Per the user's intended role taxonomy: tsys1/9 = infrastructure + k8s control
|
||||
plane; tsys2/3/6/7 = k8s workers; tsys4/5 = storage. Cnodes on tsys1/9 is
|
||||
**correct by design** (keeps heavy hosts free for workers).
|
||||
|
||||
### 8.1 VMs that need to move
|
||||
|
||||
| VMID | Name | Current host | Issue | Target |
|
||||
|------|------|-------------|-------|--------|
|
||||
| 509 | pfv-k8s-wnode-tsys5 | tsys5 (storage) | Worker on storage host | tsys7 or tsys2 (when online) |
|
||||
| 905 | pfv-k8s-wnode-tsys9 | tsys9 (infra) | Can stay if small; user decides | tsys9 OK if small wnode |
|
||||
|
||||
### 8.2 Host capacity for k8s nodes
|
||||
|
||||
| Host | Role | Current k8s nodes | k8s RAM used | RAM total | Headroom |
|
||||
|------|------|-------------------|-------------|-----------|----------|
|
||||
| tsys1 | Infra+k8s ctrl | 2 cnodes | 8 GB | 32 GB | ~12 GB (after 11 infra VMs) |
|
||||
| tsys3 | K8s worker | 1 wnode | 20 GB | 32 GB | ~12 GB |
|
||||
| tsys6 | K8s worker | 3 cnodes + 1 wnode (stopped) | 12 GB | 128 GB | **~116 GB** |
|
||||
| tsys7 | K8s worker | 1 wnode | 32 GB | 192 GB | **~160 GB** |
|
||||
| tsys9 | Infra+k8s ctrl | 1 wnode | 4 GB | 24 GB | ~12 GB (after 4 infra VMs) |
|
||||
| tsys2 | K8s worker | 0 (incoming) | 0 | 32 GB | ~32 GB |
|
||||
|
||||
**tsys6 and tsys7 are dramatically underutilized** -- 116 GB and 160 GB of
|
||||
free RAM respectively. They should be the primary targets for heavy worker
|
||||
nodes and ETL/HPC workloads.
|
||||
|
||||
### 8.3 tsys6 wnode-tsys6 is stopped
|
||||
|
||||
VM 100 (pfv-k8s-wnode-tsys6) is stopped on tsys6. Its disk is on D5 (tsys4
|
||||
NFS). This wnode should be restarted (or recreated on local-lvm) to restore
|
||||
cluster capacity.
|
||||
|
||||
---
|
||||
|
||||
## 9. Network Findings
|
||||
|
||||
### 9.1 tsys9 storage NIC is a USB dongle (new finding)
|
||||
|
||||
Validating tsys9 revealed that its storage network interface
|
||||
(`enx9c69d36a5b6c`) is USB-attached (`parentbus usb`). This is the same
|
||||
anti-pattern as tsys4. The onboard Intel NIC (`enp0s31f6`) is used for
|
||||
management; storage uses the USB adapter.
|
||||
|
||||
**Impact:** Same as tsys4 -- achieves line rate but is susceptible to cable
|
||||
wobble, ESD, and USB controller resets. For an infrastructure host with 5
|
||||
VMs, this is a reliability risk.
|
||||
|
||||
**Mitigation:** tsys9 is an OptiPlex 7080 SFF -- it has PCIe slots. A
|
||||
PCIe NIC would eliminate this risk (same recommendation as tsys4).
|
||||
|
||||
### 9.2 tsys4 and tsys5 still blocked (Friday hardware work)
|
||||
|
||||
| Host | Blocker | Staged fix |
|
||||
|------|---------|-----------|
|
||||
| tsys4 | USB cdc_ncm storage NIC | PCIe NIC install + RAM upgrade (16 to 64 GB) |
|
||||
| tsys5 | bond0 broken (1 of 2 slaves) | Plug 2nd ethernet cable + apply layer3+4 hash |
|
||||
|
||||
### 9.3 LACP resolved on tsys6/tsys7
|
||||
|
||||
tsys6 to tsys7 storage path now measures **1.83 Gbps** (was 943 Mbps).
|
||||
The switch LACP hash change took effect after renegotiation. The 56-106K
|
||||
retransmits on this path are confirmed to be non-lossy multi-flow TCP-over-
|
||||
LACP overhead. See `RESULTS.md` for the full analysis.
|
||||
|
||||
### 9.4 NFS nconnect=4 + noatime confirmed active
|
||||
|
||||
All hosts (including tsys9) show `nconnect=4,noatime` in their NFS mount
|
||||
options. Each host maintains 4 TCP connections per NFS mount to each storage
|
||||
server. This was the Tier 0 tuning item from the performance optimization
|
||||
engagement and is confirmed working cluster-wide.
|
||||
|
||||
---
|
||||
|
||||
## 10. Recommendations
|
||||
|
||||
**No changes have been made. These are assessment-only recommendations.**
|
||||
See `K8S.md` for the detailed kubernetes architecture deep-dive.
|
||||
|
||||
### 10.1 CRITICAL: Critical infrastructure VM placement (netinfra, UCS)
|
||||
|
||||
These are the most critical production VMs in the fleet. They must survive
|
||||
any single-point failure (host, storage server, or disk).
|
||||
|
||||
**Design principle for -01/-02 HA pairs:**
|
||||
- Different hypervisors (already satisfied: tsys1 vs tsys9)
|
||||
- Different storage servers (currently FAILED: all on tsys4)
|
||||
- Prefer SSD for latency-sensitive services
|
||||
|
||||
**Recommended placement:**
|
||||
|
||||
| VM | Host | Storage | Tier | Free space | Rationale |
|
||||
|----|------|---------|------|-----------|-----------|
|
||||
| **netinfra-01** (103) | tsys1 | **D5 (tsys4 HDD)** | NFS-HDD | 1.5 TB | DNS/DHCP/NTP = minimal I/O. Stays put. |
|
||||
| **netinfra-02** (904) | tsys9 | **S3 (tsys5 HDD)** | NFS-HDD | 870 GB | Move from D2. Cross-server redundancy. Minimal I/O. |
|
||||
| **ucs-01** (108) | tsys1 | **D2 (tsys4 HDD)** | NFS-HDD | 2.4 TB | Stays put. LDAP/AD does not need SSD. |
|
||||
| **ucs-02** (902) | tsys9 | **S2 (tsys5 HDD)** | NFS-HDD | 870 GB | Move from D5. Cross-server redundancy. No SSD needed. |
|
||||
|
||||
**Failure survival matrix (all single-point failures):**
|
||||
|
||||
| Failure | netinfra-01 | netinfra-02 | ucs-01 | ucs-02 | Result |
|
||||
|---------|-------------|-------------|--------|--------|--------|
|
||||
| tsys4 dies | dies (D5) | **alive** (S3) | dies (D2) | **alive** (S2) | netinfra-02 + ucs-02 alive |
|
||||
| tsys5 dies | **alive** (D5) | dies (S3) | **alive** (D2) | dies (S2) | netinfra-01 + ucs-01 alive |
|
||||
| tsys1 dies | dies | **alive** | dies | **alive** | -02 pair survives |
|
||||
| tsys9 dies | **alive** | dies | **alive** | dies | -01 pair survives |
|
||||
| Any single disk | **all 4 on different disks/servers** | **all 4 alive** | | | |
|
||||
|
||||
**Why this works:** Every row has at least one netinfra and one UCS alive.
|
||||
The network (DNS/DHCP) and directory (AD/LDAP) services never go fully dark.
|
||||
|
||||
**D3 SSD repurposed:** With UCS staying on HDD, the D3 SSD (moving to tsys5
|
||||
Friday) is freed for latency-sensitive workloads that actually benefit from
|
||||
SSD -- sectestbed k8s nodes, CI/CD artifact cache, or a container image
|
||||
registry mirror. Not infrastructure VMs.
|
||||
|
||||
### 10.2 CRITICAL: Cnode (control plane) storage split
|
||||
|
||||
**Problem:** All 5 cnodes store disks on tsys4. D2 disk failure loses etcd
|
||||
quorum (3 of 5 cnodes share D2).
|
||||
|
||||
**Target: cnodes weighted toward tsys1/tsys9 (lightweight hosts), freeing
|
||||
tsys6/tsys7 for heavy workers. Storage splits across tsys4 and tsys5.**
|
||||
|
||||
| cnode | Current host | **Target host** | Current storage | **Target storage** | Rationale |
|
||||
|-------|-------------|----------------|----------------|-------------------|-----------|
|
||||
| cnode1 (107) | tsys1 | **tsys1** (stays) | D5 (tsys4) | **D5 (tsys4)** -- no change | Already correct |
|
||||
| cnode3 (106) | tsys1 | **tsys1** (stays) | D2 (tsys4) | **S2 (tsys5)** -- **MOVE disk** | Split storage to tsys5 |
|
||||
| cnode2 (603) | tsys6 | **tsys9** | D2 (tsys4) | **D2 (tsys4)** -- no disk change | Free tsys6 for heavy workers |
|
||||
| cnode4 (601) | tsys6 | **tsys9** | D2 (tsys4) | **D5 (tsys4)** -- spread disk | Free tsys6; spread off D2 |
|
||||
| cnode5 (602) | tsys6 | **tsys3** | D5 (tsys4) | **S3 (tsys5)** -- **MOVE disk** | Free tsys6; split storage to tsys5 |
|
||||
|
||||
**Result after changes:**
|
||||
|
||||
| Host | cnodes | Storage server |
|
||||
|------|--------|---------------|
|
||||
| tsys1 | cnode1 (D5), cnode3 (S2) | tsys4 + tsys5 |
|
||||
| tsys9 | cnode2 (D2), cnode4 (D5) | tsys4 |
|
||||
| tsys3 | cnode5 (S3) | tsys5 |
|
||||
|
||||
- 3 cnodes on tsys4 storage, 2 on tsys5. Either storage server can fail and
|
||||
etcd keeps quorum (3 of 5 survive).
|
||||
- D2 has 1 cnode (was 3). D5 has 2. S2 and S3 have 1 each. No single disk
|
||||
holds more than 2 cnodes.
|
||||
- Cnodes now on 3 hosts (tsys1, tsys9, tsys3). Any single host failure leaves
|
||||
at least 3 cnodes alive.
|
||||
- **tsys6 and tsys7 are fully freed** for heavy worker nodes.
|
||||
|
||||
Note: tsys1 RAM is tight (32 GB, 11 infra VMs). Adding 0 new cnodes (keeping
|
||||
the 2 already there) is feasible with KSM. tsys9 (24 GB) has room for 2
|
||||
cnodes (8 GB). tsys3 (32 GB) has room for 1 cnode (4 GB) alongside its wnode.
|
||||
|
||||
### 10.3 HIGH: Wnode distribution -- one per host, tiered storage
|
||||
|
||||
**Target: one wnode per hypervisor host, using local storage where possible
|
||||
and NFS-HDD for bulk data.**
|
||||
|
||||
| wnode | Host | Boot disk (OS+containers) | Data disk (bulk/ETL) | Rationale |
|
||||
|-------|------|--------------------------|---------------------|-----------|
|
||||
| wnode-tsys1 | tsys1 | D5 (tsys4 NFS) | -- | Small wnode on infra host. Minimal capacity. |
|
||||
| wnode-tsys3 | tsys3 | **local-lvm (349 GB NVMe)** | S3 (NFS) | Move from NFS to **NVMe** (Samsung PM961). Fastest boot disk after tsys5/2. |
|
||||
| wnode-tsys6 | tsys6 | D2 (tsys4 NFS) | -- | **Stays on NFS.** local-lvm is USB 2.0 HDD (~30 MB/s) -- slower than NFS. |
|
||||
| wnode-tsys7 | tsys7 | D5 (tsys4 NFS) | -- | **Stays on NFS.** local-lvm is USB 2.0 HDD (~30 MB/s) -- slower than NFS. |
|
||||
| wnode-tsys9 | tsys9 | **local-lvm (136 GB SSD)** | S2 (NFS) | Move from NFS to local SSD. Fast boot, NFS for bulk. |
|
||||
| wnode-tsys2 | tsys2 | **NVMe (Samsung 960 PRO 512GB)** | **SATA SSD (Samsung 850 EVO 1TB)** | **Best storage of any wnode.** No NFS needed -- 1.5 TB local SSD. |
|
||||
| wnode-tsys5 | tsys5 | **new NVMe (local)** | local-nonprod (HDD) | **Fastest wnode in fleet.** HPC/ETL workloads land here. |
|
||||
|
||||
**Storage tiering strategy per wnode:**
|
||||
|
||||
Each wnode gets two disk tiers mapped to k8s StorageClasses:
|
||||
|
||||
1. **Boot + container runtime** (local-lvm or NVMe): OS, kubelet, container
|
||||
images, ephemeral storage. This is where local SSD/NVMe shines -- container
|
||||
image pulls and layer extraction are seek-bound and benefit enormously from
|
||||
low-latency storage.
|
||||
|
||||
2. **Bulk data** (NFS-HDD via D2/D5/S2/S3): weather/GIS datasets, ETL staging
|
||||
areas, large files that do not fit on local storage. Mounted as a second
|
||||
disk in the VM and exposed to k8s as a StorageClass.
|
||||
|
||||
This maps to two k8s StorageClasses:
|
||||
- `local-storage`: bound to the wnode's boot/local disk (fast, ephemeral)
|
||||
- `nfs-bulk`: bound to NFS exports (slow, persistent, large capacity)
|
||||
|
||||
### 10.4 HIGH: Dedicate D3 SSD exclusively to k8s scratch
|
||||
|
||||
**Storage philosophy (user directive): NVMe/SSD is for k8s worker scratch
|
||||
space and ultix-streaming (developer workstation running "cluster of 1"
|
||||
pre-production jobs). Spinning rust hosts all other infrastructure VMs**
|
||||
(UCS, netinfra, LibreNMS, SIEM, etc.).
|
||||
|
||||
SSD allocation after Friday:
|
||||
- **D3 (tsys5 SAS, 445 GB free, 0% used)** -- dedicated to k8s scratch via
|
||||
the `nfs-ssd` StorageClass (etcd, container cache, ephemeral volumes).
|
||||
- **T5-SSD (tsys5 SAS, 140 GB free)** -- ultix-streaming (VM 5111) stays here
|
||||
(developer workstation, runs single-node test jobs before k8s). Remaining
|
||||
140 GB available for k8s.
|
||||
|
||||
**Deep-dive on exact k8s scratch allocation is deferred to the next session**
|
||||
(K8S.md) once we know the k8s distribution, job scheduler, and workload mix.
|
||||
|
||||
### 10.5 MEDIUM: Restart wnode-tsys6
|
||||
|
||||
VM 100 (pfv-k8s-wnode-tsys6) is stopped on tsys6. Recreate on local-lvm
|
||||
(1.7 TB free) instead of D5 NFS. This restores cluster capacity and moves
|
||||
the boot disk to local storage simultaneously.
|
||||
|
||||
### 10.6 MEDIUM: tsys5 NVMe placement (Friday)
|
||||
|
||||
**Recommendation: local-only on tsys5, formatted as Proxmox LVM-thin or
|
||||
directory storage.**
|
||||
|
||||
Use for:
|
||||
- wnode-tsys5 boot disk (primary beneficiary -- HPC/ETL workloads)
|
||||
- sectestbed VMs that need fast scratch space
|
||||
- Not NFS-exported (avoid adding network overhead to the fastest tier)
|
||||
|
||||
### 10.7 LOW: Add PCIe NIC to tsys9
|
||||
|
||||
tsys9's storage NIC is a USB dongle. tsys9 is an OptiPlex 7080 with PCIe
|
||||
slots. A $150 PCIe NIC eliminates the USB reliability risk.
|
||||
|
||||
### 10.8 LOW: Standardize PVE/kernel versions
|
||||
|
||||
tsys3 is on PVE kernel 7.0.14; others on 6.17.x. PVE-manager versions vary
|
||||
(9.1.1 / 9.1.5 / 9.2.5). Standardize in a maintenance window.
|
||||
|
||||
---
|
||||
|
||||
## 11. Hardware End-of-Support Exposure
|
||||
|
||||
| Host | EOS date | Years past | Form factor |
|
||||
|------|----------|-----------|-------------|
|
||||
| pfv-tsys6 (R610) | 2013-05 | 13.2 | 1U server |
|
||||
| pfv-tsys5 (T7500) | 2014-12 | 11.7 | Workstation |
|
||||
| pfv-tsys4 (T1700) | 2018-03 | 8.4 | Workstation |
|
||||
| pfv-tsys7 (R620) | 2019-03 | 7.4 | 1U server |
|
||||
| pfv-tsys1 (9020) | 2019-07 | 7.0 | SFF desktop |
|
||||
| pfv-tsys3 (7510) | 2020-07 | 6.0 | Laptop |
|
||||
| pfv-tsys2 (5520) | TBD | -- | Laptop |
|
||||
| **pfv-tsys9 (7080)** | **2024-02** | **2.4** | **SFF desktop (only supported)** |
|
||||
|
||||
**6 of 8 hosts are past vendor end-of-support.** Only tsys9 is still covered.
|
||||
The two actual rack servers (R610, R620) are the most overdue for replacement.
|
||||
This belongs in the next budget cycle.
|
||||
|
||||
---
|
||||
|
||||
## 12. Open Items
|
||||
|
||||
### 12.1 Immediate (do today via PDM)
|
||||
|
||||
1. **Migrate ucs-02** (VM 902) from D5 (tsys4) to S2 (tsys5) for cross-server
|
||||
redundancy. UCS stays on HDD.
|
||||
2. **Migrate netinfra-02** (VM 904) from D2 (tsys4) to S3 (tsys5).
|
||||
3. **Migrate cnode3** (VM 106) from D2 (tsys4) to S3 (tsys5) -- etcd quorum.
|
||||
4. **Migrate cnode4** (VM 601) from D2 (tsys4) to S2 (tsys5) -- etcd quorum.
|
||||
5. **Start wnode-tsys1** (VM 102) if the cluster needs the capacity.
|
||||
|
||||
### 12.2 Friday maintenance window (user action)
|
||||
|
||||
1. **tsys4:** Install PCIe NIC (replace USB dongle), add RAM (16 to 64 GB),
|
||||
reconfigure `/etc/network/interfaces`, reboot.
|
||||
2. **tsys5:** Plug 2nd ethernet cable, verify bond0, apply layer3+4 hash,
|
||||
install PCI NVMe, relocate D3 SSD from tsys4 USB to tsys5 SAS port,
|
||||
format NVMe as local storage, reboot.
|
||||
3. **tsys2:** Load Proxmox (replacing Windows 10).
|
||||
4. **Final audit:** Re-run `deploy-check.sh` across all hosts including tsys2.
|
||||
|
||||
### 12.3 Post-Friday validation
|
||||
|
||||
1. Re-run iperf matrix: `./iperf-full-matrix.sh`
|
||||
2. Validate tsys4 and tsys5: `./validate-fixes.sh pfv-tsys4 && ./validate-fixes.sh pfv-tsys5`
|
||||
3. Run `scripts/check.sh` on tsys2 once Proxmox is loaded.
|
||||
4. Update PROJECT.md with post-hardware numbers.
|
||||
|
||||
### 12.4 Future: Kubernetes deep-dive (see K8S.md)
|
||||
|
||||
Next major workstream. Requirements captured:
|
||||
- **vcluster + Rancher** for multi-tenant k8s management
|
||||
- **OIDC auth** to Keycloak (on Cloudron, Reston VA production)
|
||||
- **Workload isolation** via separate vclusters:
|
||||
- RackRental (containerlab)
|
||||
- Suborbital ITAR
|
||||
- Suborbital non-ITAR
|
||||
- Starting Line Productions customer workloads
|
||||
- **Solar-aware scale-out:** PowerEdge 19xx + 2950 systems brought online
|
||||
during peak solar production for burst capacity
|
||||
- **Every host gets a wnode** (variable sizing: small 4 GB to large 32 GB)
|
||||
- **SSD/NVMe reserved for k8s scratch** (plus ultix-streaming exception)
|
||||
- **Spinning rust for all infrastructure VMs**
|
||||
|
||||
### 12.5 Data gaps
|
||||
|
||||
| Gap | How to close |
|
||||
|-----|--------------|
|
||||
| tsys5 SDR/parallel-port workload dependency | Confirm what uses the SDR |
|
||||
| tsys2 post-Proxmox baseline | Run `check.sh` after Friday install |
|
||||
| tsys5 NVMe size and model | Confirm after Friday installation |
|
||||
| tsys3 thermal state (laptop in rack) | Check `sensors` on next maintenance |
|
||||
| PowerEdge 19xx/2950 inventory | When solar scale-out is planned |
|
||||
|
||||
### 12.6 Scripts and tools available
|
||||
|
||||
| Script | Purpose |
|
||||
|--------|---------|
|
||||
| `scripts/check.sh` | Read-only data collector (run on hosts) |
|
||||
| `scripts/apply-tunings.sh` | Apply all Tier 0 tunings (dry-run/apply/rollback) |
|
||||
| `scripts/fix-bond-nfs.sh` | Fix NFS options + bond hash |
|
||||
| `validate-fixes.sh` | Read-only validation of all applied changes |
|
||||
| `iperf-full-matrix.sh` | Full iperf test suite (mgmt + storage) |
|
||||
| `deploy-check.sh` | Deploy check.sh to all hosts via SSH (now includes tsys9) |
|
||||
@@ -0,0 +1,534 @@
|
||||
# Code Refactoring Examples
|
||||
|
||||
This document provides specific examples of how to apply the code review findings to improve performance, security, and reliability.
|
||||
|
||||
## Package Installation Optimization
|
||||
|
||||
### Before (Current - Multiple Commands)
|
||||
```bash
|
||||
# Line 27 in SetupNewSystem.sh
|
||||
apt-get -y install git sudo dmidecode curl
|
||||
|
||||
# Lines 117-183 (later in script)
|
||||
DEBIAN_FRONTEND="noninteractive" apt-get -qq --yes -o Dpkg::Options::="--force-confold" install \
|
||||
virt-what \
|
||||
auditd \
|
||||
aide \
|
||||
# ... many more packages
|
||||
```
|
||||
|
||||
### After (Optimized - Single Command)
|
||||
```bash
|
||||
function install_all_packages() {
|
||||
print_info "Installing all required packages..."
|
||||
|
||||
# All packages in logical groups for better readability
|
||||
local packages=(
|
||||
# Core system tools
|
||||
git sudo dmidecode curl wget net-tools htop
|
||||
|
||||
# Security and auditing
|
||||
auditd aide fail2ban lynis rkhunter
|
||||
|
||||
# Monitoring and SNMP
|
||||
snmpd snmp-mibs-downloader libsnmp-dev
|
||||
|
||||
# Virtualization detection
|
||||
virt-what
|
||||
|
||||
# System utilities
|
||||
rsyslog logrotate ntp ntpdate
|
||||
cockpit cockpit-ws cockpit-system
|
||||
|
||||
# Development and debugging
|
||||
build-essential dkms
|
||||
|
||||
# Network services
|
||||
openssh-server ufw
|
||||
)
|
||||
|
||||
# Single package installation command with retry logic
|
||||
local max_attempts=3
|
||||
local attempt=1
|
||||
|
||||
while [[ $attempt -le $max_attempts ]]; do
|
||||
if DEBIAN_FRONTEND="noninteractive" apt-get -qq --yes -o Dpkg::Options::="--force-confold" install "${packages[@]}"; then
|
||||
print_success "All packages installed successfully"
|
||||
return 0
|
||||
else
|
||||
print_warning "Package installation attempt $attempt failed"
|
||||
if [[ $attempt -lt $max_attempts ]]; then
|
||||
print_info "Retrying in 10 seconds..."
|
||||
sleep 10
|
||||
apt-get update # Refresh package cache before retry
|
||||
fi
|
||||
((attempt++))
|
||||
fi
|
||||
done
|
||||
|
||||
print_error "Package installation failed after $max_attempts attempts"
|
||||
return 1
|
||||
}
|
||||
```
|
||||
|
||||
## Safe Download Implementation
|
||||
|
||||
### Before (Current - Unsafe Downloads)
|
||||
```bash
|
||||
# Lines 61-63 in SetupNewSystem.sh
|
||||
curl --silent ${DL_ROOT}/ProjectCode/ConfigFiles/ZSH/tsys-zshrc >/etc/zshrc
|
||||
curl --silent ${DL_ROOT}/ProjectCode/ConfigFiles/SMTP/aliases >/etc/aliases
|
||||
curl --silent ${DL_ROOT}/ProjectCode/ConfigFiles/Syslog/rsyslog.conf >/etc/rsyslog.conf
|
||||
```
|
||||
|
||||
### After (Safe Downloads with Error Handling)
|
||||
```bash
|
||||
function download_system_configs() {
|
||||
print_info "Downloading system configuration files..."
|
||||
|
||||
# Source the safe download framework
|
||||
source "$PROJECT_ROOT/Framework-Includes/SafeDownload.sh"
|
||||
|
||||
# Define configuration downloads with checksums (optional)
|
||||
declare -A config_downloads=(
|
||||
["${DL_ROOT}/ProjectCode/ConfigFiles/ZSH/tsys-zshrc"]="/etc/zshrc"
|
||||
["${DL_ROOT}/ProjectCode/ConfigFiles/SMTP/aliases"]="/etc/aliases"
|
||||
["${DL_ROOT}/ProjectCode/ConfigFiles/Syslog/rsyslog.conf"]="/etc/rsyslog.conf"
|
||||
["${DL_ROOT}/ProjectCode/ConfigFiles/SSH/Configs/tsys-sshd-config"]="/etc/ssh/sshd_config.tsys"
|
||||
)
|
||||
|
||||
# Validate all URLs are accessible before starting
|
||||
local urls=()
|
||||
for url in "${!config_downloads[@]}"; do
|
||||
urls+=("$url")
|
||||
done
|
||||
|
||||
if ! validate_required_urls "${urls[@]}"; then
|
||||
print_error "Some configuration URLs are not accessible"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Perform batch download with backup
|
||||
local failed_downloads=0
|
||||
for url in "${!config_downloads[@]}"; do
|
||||
local dest="${config_downloads[$url]}"
|
||||
if ! safe_config_download "$url" "$dest"; then
|
||||
((failed_downloads++))
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ $failed_downloads -eq 0 ]]; then
|
||||
print_success "All configuration files downloaded successfully"
|
||||
return 0
|
||||
else
|
||||
print_error "$failed_downloads configuration downloads failed"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
```
|
||||
|
||||
## Variable Quoting Fixes
|
||||
|
||||
### Before (Unsafe Variable Usage)
|
||||
```bash
|
||||
# Line 244 in SetupNewSystem.sh
|
||||
chsh -s $(which zsh) root
|
||||
|
||||
# Multiple instances throughout codebase
|
||||
if [ -f $CONFIG_FILE ]; then
|
||||
cp $CONFIG_FILE $BACKUP_DIR
|
||||
fi
|
||||
```
|
||||
|
||||
### After (Proper Variable Quoting)
|
||||
```bash
|
||||
# Safe variable usage with proper quoting
|
||||
chsh -s "$(which zsh)" root
|
||||
|
||||
# Consistent quoting pattern
|
||||
if [[ -f "$CONFIG_FILE" ]]; then
|
||||
cp "$CONFIG_FILE" "$BACKUP_DIR/"
|
||||
fi
|
||||
|
||||
# Function parameter handling
|
||||
function configure_service() {
|
||||
local service_name="$1"
|
||||
local config_file="$2"
|
||||
|
||||
if [[ -z "$service_name" || -z "$config_file" ]]; then
|
||||
print_error "configure_service: service name and config file required"
|
||||
return 1
|
||||
fi
|
||||
|
||||
print_info "Configuring service: $service_name"
|
||||
# Safe operations with quoted variables
|
||||
}
|
||||
```
|
||||
|
||||
## Service Management with Error Handling
|
||||
|
||||
### Before (Basic Service Operations)
|
||||
```bash
|
||||
# Current pattern in various modules
|
||||
systemctl restart snmpd
|
||||
systemctl enable snmpd
|
||||
```
|
||||
|
||||
### After (Robust Service Management)
|
||||
```bash
|
||||
function safe_service_restart() {
|
||||
local service="$1"
|
||||
local config_test_cmd="${2:-}"
|
||||
|
||||
if [[ -z "$service" ]]; then
|
||||
print_error "safe_service_restart: service name required"
|
||||
return 1
|
||||
fi
|
||||
|
||||
print_info "Managing service: $service"
|
||||
|
||||
# Test configuration if test command provided
|
||||
if [[ -n "$config_test_cmd" ]]; then
|
||||
print_info "Testing $service configuration..."
|
||||
if ! eval "$config_test_cmd"; then
|
||||
print_error "$service configuration test failed"
|
||||
return 1
|
||||
fi
|
||||
print_success "$service configuration test passed"
|
||||
fi
|
||||
|
||||
# Check if service exists
|
||||
if ! systemctl list-unit-files "$service.service" >/dev/null 2>&1; then
|
||||
print_error "Service $service does not exist"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Stop service if running
|
||||
if systemctl is-active "$service" >/dev/null 2>&1; then
|
||||
print_info "Stopping $service..."
|
||||
if ! systemctl stop "$service"; then
|
||||
print_error "Failed to stop $service"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Start and enable service
|
||||
print_info "Starting and enabling $service..."
|
||||
if systemctl start "$service" && systemctl enable "$service"; then
|
||||
print_success "$service started and enabled successfully"
|
||||
|
||||
# Verify service is running
|
||||
sleep 2
|
||||
if systemctl is-active "$service" >/dev/null 2>&1; then
|
||||
print_success "$service is running properly"
|
||||
return 0
|
||||
else
|
||||
print_error "$service failed to start properly"
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
print_error "Failed to start or enable $service"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Usage examples
|
||||
safe_service_restart "sshd" "sshd -t"
|
||||
safe_service_restart "snmpd"
|
||||
safe_service_restart "rsyslog"
|
||||
```
|
||||
|
||||
## Batch Configuration Deployment
|
||||
|
||||
### Before (Individual File Operations)
|
||||
```bash
|
||||
# Lines 66-77 in secharden-scap-stig.sh
|
||||
curl --silent ${DL_ROOT}/ProjectCode/ConfigFiles/ModProbe/usb_storage.conf > /etc/modprobe.d/usb_storage.conf
|
||||
curl --silent ${DL_ROOT}/ProjectCode/ConfigFiles/ModProbe/dccp.conf > /etc/modprobe.d/dccp.conf
|
||||
curl --silent ${DL_ROOT}/ProjectCode/ConfigFiles/ModProbe/rds.conf > /etc/modprobe.d/rds.conf
|
||||
# ... 12 more individual downloads
|
||||
```
|
||||
|
||||
### After (Batch Operations with Error Handling)
|
||||
```bash
|
||||
function deploy_modprobe_configs() {
|
||||
print_info "Deploying modprobe security configurations..."
|
||||
|
||||
source "$PROJECT_ROOT/Framework-Includes/SafeDownload.sh"
|
||||
|
||||
local modprobe_configs=(
|
||||
"usb_storage" "dccp" "rds" "sctp" "tipc"
|
||||
"cramfs" "freevxfs" "hfs" "hfsplus"
|
||||
"jffs2" "squashfs" "udf"
|
||||
)
|
||||
|
||||
# Create download map
|
||||
declare -A config_downloads=()
|
||||
for config in "${modprobe_configs[@]}"; do
|
||||
local url="${DL_ROOT}/ProjectCode/ConfigFiles/ModProbe/${config}.conf"
|
||||
local dest="/etc/modprobe.d/${config}.conf"
|
||||
config_downloads["$url"]="$dest"
|
||||
done
|
||||
|
||||
# Validate URLs first
|
||||
local urls=()
|
||||
for url in "${!config_downloads[@]}"; do
|
||||
urls+=("$url")
|
||||
done
|
||||
|
||||
if ! validate_required_urls "${urls[@]}"; then
|
||||
print_error "Some modprobe configuration URLs are not accessible"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Perform batch download
|
||||
if batch_download config_downloads; then
|
||||
print_success "All modprobe configurations deployed"
|
||||
|
||||
# Update initramfs to apply changes
|
||||
if update-initramfs -u; then
|
||||
print_success "Initramfs updated with new module configurations"
|
||||
else
|
||||
print_warning "Failed to update initramfs - reboot may be required"
|
||||
fi
|
||||
|
||||
return 0
|
||||
else
|
||||
print_error "Failed to deploy some modprobe configurations"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
```
|
||||
|
||||
## Input Validation and Error Handling
|
||||
|
||||
### Before (Minimal Validation)
|
||||
```bash
|
||||
# pi-detect.sh current implementation
|
||||
function pi-detect() {
|
||||
print_info Now running "$FUNCNAME"....
|
||||
if [ -f /sys/firmware/devicetree/base/model ] ; then
|
||||
export IS_RASPI="1"
|
||||
fi
|
||||
}
|
||||
```
|
||||
|
||||
### After (Comprehensive Validation)
|
||||
```bash
|
||||
function pi-detect() {
|
||||
print_info "Now running $FUNCNAME..."
|
||||
|
||||
# Initialize variables with default values
|
||||
export IS_RASPI="0"
|
||||
export PI_MODEL=""
|
||||
export PI_REVISION=""
|
||||
|
||||
# Check for Raspberry Pi detection file
|
||||
local device_tree_model="/sys/firmware/devicetree/base/model"
|
||||
local cpuinfo_file="/proc/cpuinfo"
|
||||
|
||||
if [[ -f "$device_tree_model" ]]; then
|
||||
# Try device tree method first (most reliable)
|
||||
local model_info
|
||||
model_info=$(tr -d '\0' < "$device_tree_model" 2>/dev/null)
|
||||
|
||||
if [[ "$model_info" =~ [Rr]aspberry.*[Pp]i ]]; then
|
||||
export IS_RASPI="1"
|
||||
export PI_MODEL="$model_info"
|
||||
print_success "Raspberry Pi detected via device tree: $PI_MODEL"
|
||||
fi
|
||||
elif [[ -f "$cpuinfo_file" ]]; then
|
||||
# Fallback to cpuinfo method
|
||||
if grep -qi "raspberry" "$cpuinfo_file"; then
|
||||
export IS_RASPI="1"
|
||||
PI_MODEL=$(grep "^Model" "$cpuinfo_file" | cut -d: -f2 | sed 's/^[[:space:]]*//' 2>/dev/null || echo "Unknown Pi Model")
|
||||
PI_REVISION=$(grep "^Revision" "$cpuinfo_file" | cut -d: -f2 | sed 's/^[[:space:]]*//' 2>/dev/null || echo "Unknown")
|
||||
export PI_MODEL
|
||||
export PI_REVISION
|
||||
print_success "Raspberry Pi detected via cpuinfo: $PI_MODEL (Rev: $PI_REVISION)"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "$IS_RASPI" == "1" ]]; then
|
||||
print_info "Raspberry Pi specific optimizations will be applied"
|
||||
else
|
||||
print_info "Standard x86/x64 system detected"
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
```
|
||||
|
||||
## Function Framework Integration
|
||||
|
||||
### Before (Inconsistent Framework Usage)
|
||||
```bash
|
||||
# Mixed patterns throughout codebase
|
||||
function some_function() {
|
||||
echo "Doing something..."
|
||||
command_that_might_fail
|
||||
echo "Done"
|
||||
}
|
||||
```
|
||||
|
||||
### After (Standardized Framework Integration)
|
||||
```bash
|
||||
function some_function() {
|
||||
print_info "Now running $FUNCNAME..."
|
||||
|
||||
# Local variables
|
||||
local config_file="/etc/example.conf"
|
||||
local backup_dir="/root/backup"
|
||||
local failed=0
|
||||
|
||||
# Validate prerequisites
|
||||
if [[ ! -d "$backup_dir" ]]; then
|
||||
if ! mkdir -p "$backup_dir"; then
|
||||
print_error "Failed to create backup directory: $backup_dir"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Backup existing configuration
|
||||
if [[ -f "$config_file" ]]; then
|
||||
if cp "$config_file" "$backup_dir/$(basename "$config_file").bak.$(date +%Y%m%d-%H%M%S)"; then
|
||||
print_info "Backed up existing configuration"
|
||||
else
|
||||
print_error "Failed to backup existing configuration"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Perform main operation with error handling
|
||||
if command_that_might_fail; then
|
||||
print_success "Operation completed successfully"
|
||||
else
|
||||
print_error "Operation failed"
|
||||
return 1
|
||||
fi
|
||||
|
||||
print_success "Completed $FUNCNAME"
|
||||
return 0
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Monitoring Integration
|
||||
|
||||
### Enhanced Deployment with Metrics
|
||||
```bash
|
||||
function deploy_with_metrics() {
|
||||
local start_time end_time duration
|
||||
local operation_name="$1"
|
||||
shift
|
||||
local operation_function="$1"
|
||||
shift
|
||||
|
||||
print_info "Starting $operation_name..."
|
||||
start_time=$(date +%s)
|
||||
|
||||
# Execute the operation
|
||||
if "$operation_function" "$@"; then
|
||||
end_time=$(date +%s)
|
||||
duration=$((end_time - start_time))
|
||||
|
||||
print_success "$operation_name completed in ${duration}s"
|
||||
|
||||
# Log performance metrics
|
||||
echo "$(date '+%Y-%m-%d %H:%M:%S') - $operation_name: ${duration}s" >> /var/log/fetchapply-performance.log
|
||||
|
||||
# Alert if operation took too long
|
||||
case "$operation_name" in
|
||||
"Package Installation")
|
||||
if [[ $duration -gt 300 ]]; then
|
||||
print_warning "Package installation took longer than expected (${duration}s > 300s)"
|
||||
fi
|
||||
;;
|
||||
"Configuration Download")
|
||||
if [[ $duration -gt 120 ]]; then
|
||||
print_warning "Configuration download took longer than expected (${duration}s > 120s)"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
return 0
|
||||
else
|
||||
end_time=$(date +%s)
|
||||
duration=$((end_time - start_time))
|
||||
|
||||
print_error "$operation_name failed after ${duration}s"
|
||||
echo "$(date '+%Y-%m-%d %H:%M:%S') - $operation_name: FAILED after ${duration}s" >> /var/log/fetchapply-performance.log
|
||||
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Usage example
|
||||
deploy_with_metrics "Package Installation" install_all_packages
|
||||
deploy_with_metrics "Configuration Download" download_system_configs
|
||||
deploy_with_metrics "SSH Hardening" configure_ssh_hardening
|
||||
```
|
||||
|
||||
## Testing Integration
|
||||
|
||||
### Comprehensive Validation Function
|
||||
```bash
|
||||
function validate_deployment() {
|
||||
print_header "Deployment Validation"
|
||||
|
||||
local validation_failures=0
|
||||
|
||||
# Test package installation
|
||||
local required_packages=("git" "curl" "wget" "snmpd" "auditd" "fail2ban")
|
||||
for package in "${required_packages[@]}"; do
|
||||
if dpkg -l | grep -q "^ii.*$package"; then
|
||||
print_success "Package installed: $package"
|
||||
else
|
||||
print_error "Package missing: $package"
|
||||
((validation_failures++))
|
||||
fi
|
||||
done
|
||||
|
||||
# Test service status
|
||||
local required_services=("sshd" "snmpd" "auditd" "rsyslog")
|
||||
for service in "${required_services[@]}"; do
|
||||
if systemctl is-active "$service" >/dev/null 2>&1; then
|
||||
print_success "Service running: $service"
|
||||
else
|
||||
print_error "Service not running: $service"
|
||||
((validation_failures++))
|
||||
fi
|
||||
done
|
||||
|
||||
# Test configuration files
|
||||
local required_configs=("/etc/ssh/sshd_config" "/etc/snmp/snmpd.conf" "/etc/rsyslog.conf")
|
||||
for config in "${required_configs[@]}"; do
|
||||
if [[ -f "$config" && -s "$config" ]]; then
|
||||
print_success "Configuration exists: $(basename "$config")"
|
||||
else
|
||||
print_error "Configuration missing or empty: $(basename "$config")"
|
||||
((validation_failures++))
|
||||
fi
|
||||
done
|
||||
|
||||
# Run security tests
|
||||
if command -v lynis >/dev/null 2>&1; then
|
||||
print_info "Running basic security audit..."
|
||||
if lynis audit system --quick --quiet; then
|
||||
print_success "Security audit completed"
|
||||
else
|
||||
print_warning "Security audit found issues"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Summary
|
||||
if [[ $validation_failures -eq 0 ]]; then
|
||||
print_success "All deployment validation checks passed"
|
||||
return 0
|
||||
else
|
||||
print_error "$validation_failures deployment validation checks failed"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
```
|
||||
|
||||
These refactoring examples demonstrate how to apply the code review findings to create more robust, performant, and maintainable infrastructure provisioning scripts.
|
||||
@@ -0,0 +1,190 @@
|
||||
# TSYS FetchApply Security Documentation
|
||||
|
||||
## Security Architecture
|
||||
|
||||
The TSYS FetchApply infrastructure provisioning system is designed with security-first principles, implementing multiple layers of protection for server deployment and management.
|
||||
|
||||
## Current Security Features
|
||||
|
||||
### 1. Secure Deployment Method ✅
|
||||
- **Git-based deployment:** Uses `git clone` instead of `curl | bash`
|
||||
- **Local execution:** Scripts run locally after inspection
|
||||
- **Version control:** Full audit trail of changes
|
||||
- **Code review:** Changes require explicit approval
|
||||
|
||||
### 2. HTTPS Enforcement ✅
|
||||
- **All downloads use HTTPS:** Eliminates man-in-the-middle attacks
|
||||
- **SSL certificate validation:** Automatic certificate checking
|
||||
- **Secure repositories:** Ubuntu archive, Dell, Proxmox all use HTTPS
|
||||
- **No HTTP fallbacks:** No insecure download methods
|
||||
|
||||
### 3. SSH Hardening
|
||||
- **Key-only authentication:** Password login disabled
|
||||
- **Secure ciphers:** Modern encryption algorithms only
|
||||
- **Fail2ban protection:** Automated intrusion prevention
|
||||
- **Custom SSH configuration:** Hardened sshd_config
|
||||
|
||||
### 4. System Security
|
||||
- **Firewall configuration:** Automated iptables rules
|
||||
- **Audit logging:** auditd with custom rules
|
||||
- **SIEM integration:** Wazuh agent deployment
|
||||
- **Compliance scanning:** SCAP-STIG automated checks
|
||||
|
||||
### 5. Error Handling
|
||||
- **Bash strict mode:** `set -euo pipefail` prevents errors
|
||||
- **Centralized logging:** All operations logged with timestamps
|
||||
- **Graceful failures:** Proper cleanup on errors
|
||||
- **Line-level debugging:** Error reporting with line numbers
|
||||
|
||||
## Security Testing
|
||||
|
||||
### Automated Security Validation
|
||||
```bash
|
||||
# Run security test suite
|
||||
./Project-Tests/run-tests.sh security
|
||||
|
||||
# Specific security tests
|
||||
./Project-Tests/security/https-enforcement.sh
|
||||
```
|
||||
|
||||
### Security Test Categories
|
||||
1. **HTTPS Enforcement:** Validates all URLs use HTTPS
|
||||
2. **Deployment Security:** Checks for secure deployment methods
|
||||
3. **SSL Certificate Validation:** Tests certificate authenticity
|
||||
4. **Permission Validation:** Verifies proper file permissions
|
||||
|
||||
## Threat Model
|
||||
|
||||
### Mitigated Threats
|
||||
- **Supply Chain Attacks:** Git-based deployment with review
|
||||
- **Man-in-the-Middle:** HTTPS-only downloads
|
||||
- **Privilege Escalation:** Proper permission models
|
||||
- **Unauthorized Access:** SSH hardening and key management
|
||||
|
||||
### Remaining Risks
|
||||
- **Secrets in Repository:** SSH keys stored in git (planned for removal)
|
||||
- **No Integrity Verification:** Downloads lack checksum validation
|
||||
- **No Backup/Recovery:** No rollback capability implemented
|
||||
|
||||
## Security Recommendations
|
||||
|
||||
### High Priority
|
||||
1. **Implement Secrets Management**
|
||||
- Remove SSH keys from repository
|
||||
- Use Bitwarden/Vault for secret storage
|
||||
- Implement key rotation procedures
|
||||
|
||||
2. **Add Download Integrity Verification**
|
||||
- SHA256 checksum validation for all downloads
|
||||
- GPG signature verification where available
|
||||
- Fail-safe on integrity check failures
|
||||
|
||||
3. **Enhance Audit Logging**
|
||||
- Centralized log collection
|
||||
- Real-time security monitoring
|
||||
- Automated threat detection
|
||||
|
||||
### Medium Priority
|
||||
1. **Configuration Backup**
|
||||
- System state snapshots before changes
|
||||
- Rollback capability for failed deployments
|
||||
- Configuration drift detection
|
||||
|
||||
2. **Network Security**
|
||||
- VPN-based deployment (where applicable)
|
||||
- Network segmentation for management
|
||||
- Encrypted communication channels
|
||||
|
||||
## Compliance
|
||||
|
||||
### Security Standards
|
||||
- **CIS Benchmarks:** Automated compliance checking
|
||||
- **STIG Guidelines:** SCAP-based validation
|
||||
- **Industry Best Practices:** Following NIST cybersecurity framework
|
||||
|
||||
### Audit Requirements
|
||||
- **Change Tracking:** All modifications logged
|
||||
- **Access Control:** Permission-based system access
|
||||
- **Vulnerability Management:** Regular security assessments
|
||||
|
||||
## Incident Response
|
||||
|
||||
### Security Event Handling
|
||||
1. **Detection:** Automated monitoring and alerting
|
||||
2. **Containment:** Immediate isolation procedures
|
||||
3. **Investigation:** Log analysis and forensics
|
||||
4. **Recovery:** System restoration procedures
|
||||
5. **Lessons Learned:** Process improvement
|
||||
|
||||
### Contact Information
|
||||
- **Security Team:** [To be defined]
|
||||
- **Incident Response:** [To be defined]
|
||||
- **Escalation Path:** [To be defined]
|
||||
|
||||
## Security Development Lifecycle
|
||||
|
||||
### Code Review Process
|
||||
1. **Static Analysis:** Automated security scanning
|
||||
2. **Peer Review:** Manual code inspection
|
||||
3. **Security Testing:** Automated security test suite
|
||||
4. **Approval:** Security team sign-off
|
||||
|
||||
### Deployment Security
|
||||
1. **Pre-deployment Validation:** Security test execution
|
||||
2. **Secure Deployment:** Authorized personnel only
|
||||
3. **Post-deployment Verification:** Security configuration validation
|
||||
4. **Monitoring:** Continuous security monitoring
|
||||
|
||||
## Security Tools and Integrations
|
||||
|
||||
### Current Tools
|
||||
- **Wazuh:** SIEM and security monitoring
|
||||
- **Lynis:** Security auditing
|
||||
- **auditd:** System call auditing
|
||||
- **Fail2ban:** Intrusion prevention
|
||||
|
||||
### Planned Integrations
|
||||
- **Vault/Bitwarden:** Secrets management
|
||||
- **OSSEC:** Host-based intrusion detection
|
||||
- **Nessus/OpenVAS:** Vulnerability scanning
|
||||
- **ELK Stack:** Log aggregation and analysis
|
||||
|
||||
## Vulnerability Management
|
||||
|
||||
### Vulnerability Scanning
|
||||
- **Regular scans:** Monthly vulnerability assessments
|
||||
- **Automated patching:** Security update automation
|
||||
- **Exception handling:** Risk-based patch management
|
||||
- **Reporting:** Executive security dashboards
|
||||
|
||||
### Disclosure Process
|
||||
1. **Internal Discovery:** Report to security team
|
||||
2. **Assessment:** Risk and impact evaluation
|
||||
3. **Remediation:** Patch development and testing
|
||||
4. **Deployment:** Coordinated security updates
|
||||
5. **Verification:** Post-patch validation
|
||||
|
||||
## Security Metrics
|
||||
|
||||
### Key Performance Indicators
|
||||
- **Deployment Success Rate:** Percentage of successful secure deployments
|
||||
- **Vulnerability Response Time:** Time to patch critical vulnerabilities
|
||||
- **Security Test Coverage:** Percentage of code covered by security tests
|
||||
- **Incident Response Time:** Time to detect and respond to security events
|
||||
|
||||
### Monitoring and Reporting
|
||||
- **Real-time Dashboards:** Security status monitoring
|
||||
- **Executive Reports:** Monthly security summaries
|
||||
- **Compliance Reports:** Quarterly compliance assessments
|
||||
- **Trend Analysis:** Security posture improvement tracking
|
||||
|
||||
## Contact and Support
|
||||
|
||||
For security-related questions or incidents:
|
||||
- **Repository Issues:** https://projects.knownelement.com/project/reachableceo-vptechnicaloperations/timeline
|
||||
- **Community Discussion:** https://community.turnsys.com/c/chieftechnologyandproductofficer/26
|
||||
- **Security Team:** [Contact information to be added]
|
||||
|
||||
## Security Updates
|
||||
|
||||
This document is updated as security features are implemented and threats evolve. Last updated: July 14, 2025.
|
||||
+242
@@ -0,0 +1,242 @@
|
||||
# TODO.md — Pending User Actions
|
||||
|
||||
**Date:** 2026-07-27
|
||||
**Items needing user input or physical action.**
|
||||
|
||||
---
|
||||
|
||||
## 1. tsys2 Windows hardware inventory (run on the Windows host)
|
||||
|
||||
pfv-tsys2 is currently Windows 10. Before rebuilding it as Proxmox, gather
|
||||
hardware data so the architecture plan can account for it.
|
||||
|
||||
### Option A: PowerShell (recommended — single command, copy-paste output)
|
||||
|
||||
Open **PowerShell as Administrator** and run:
|
||||
|
||||
```powershell
|
||||
# Full hardware inventory in one shot
|
||||
Write-Output "=== COMPUTER ==="
|
||||
Get-CimInstance Win32_ComputerSystem | Select-Object Manufacturer, Model, SystemType, TotalPhysicalMemory | Format-List
|
||||
|
||||
Write-Output "`n=== CPU ==="
|
||||
Get-CimInstance Win32_Processor | Select-Object Name, NumberOfCores, NumberOfLogicalProcessors, MaxClockSpeed | Format-List
|
||||
|
||||
Write-Output "`n=== MEMORY STICKS ==="
|
||||
Get-CimInstance Win32_PhysicalMemory | Select-Object Manufacturer, PartNumber, Capacity, Speed, ConfiguredClockSpeed, DeviceLocator, FormFactor | Format-Table -AutoSize
|
||||
|
||||
Write-Output "`n=== DISKS ==="
|
||||
Get-PhysicalDisk | Select-Object FriendlyName, MediaType, BusType, Size, SpindleSpeed | Format-Table -AutoSize
|
||||
|
||||
Write-Output "`n=== DISK PARTITIONS ==="
|
||||
Get-Disk | Select-Object Number, FriendlyName, Size, PartitionStyle, OperationalStatus | Format-Table -AutoSize
|
||||
|
||||
Write-Output "`n=== NETWORK ADAPTERS ==="
|
||||
Get-NetAdapter | Select-Object Name, InterfaceDescription, Status, LinkSpeed, MacAddress | Format-Table -AutoSize
|
||||
|
||||
Write-Output "`n=== GPU(s) ==="
|
||||
Get-CimInstance Win32_VideoController | Select-Object Name, AdapterRAM, DriverVersion, VideoProcessor | Format-List
|
||||
|
||||
Write-Output "`n=== PCIe SLOTS ==="
|
||||
Get-CimInstance Win32_SystemSlot | Select-Object SlotDesignation, CurrentUsage, Status | Format-Table -AutoSize
|
||||
|
||||
Write-Output "`n=== USB DEVICES (storage + network only) ==="
|
||||
Get-PnpDevice -PresentOnly | Where-Object { $_.Class -in @('DiskDrive','Net','USB') } | Select-Object Class, FriendlyName, Status | Format-Table -AutoSize
|
||||
```
|
||||
|
||||
Copy the full output into a file (e.g., `tsys2-hardware.txt`) or paste it
|
||||
directly into the chat.
|
||||
|
||||
### Option B: Command Prompt (cmd.exe) fallbacks
|
||||
|
||||
If PowerShell is unavailable for some reason, these cmd commands give a
|
||||
subset:
|
||||
|
||||
```cmd
|
||||
:: Computer model and serial
|
||||
wmic computersystem get manufacturer,model
|
||||
wmic bios get serialnumber
|
||||
|
||||
:: CPU
|
||||
wmic cpu get name,numberofcores,numberoflogicalprocessors,maxclockspeed
|
||||
|
||||
:: RAM (total)
|
||||
wmic computersystem get totalphysicalmemory
|
||||
|
||||
:: RAM sticks (per-slot detail)
|
||||
wmic memorychip get manufacturer,capacity,speed,partnumber,devicelocator
|
||||
|
||||
:: Disks
|
||||
wmic diskdrive get model,size,interfacetype,mediatype
|
||||
|
||||
:: Network adapters
|
||||
wmic nic where netenabled=true get name,speed,macaddress
|
||||
|
||||
:: GPU
|
||||
wmic path win32_videocontroller get name,adapterram,driverversion
|
||||
```
|
||||
|
||||
### What I'm looking for
|
||||
|
||||
- **Disk inventory**: Are there any SSDs/NVMe available locally? (Determines
|
||||
whether wnode-tsys2 can use local storage like the other wnodes.)
|
||||
- **Network adapters**: How many onboard NICs? Model? (Determines whether
|
||||
tsys2 needs a USB dongle for storage network like tsys4/9, or has a real
|
||||
onboard NIC available.)
|
||||
- **RAM layout**: Is all 32 GB in 1 stick, 2 sticks, or 4 sticks? (Affects
|
||||
memory bandwidth for HPC workloads — dual-channel matters.)
|
||||
- **GPU detail**: Confirm the Quadro M1200 model and VRAM for passthrough
|
||||
planning.
|
||||
- **PCIe slots**: Is there a free PCIe slot for adding a NIC or HBA?
|
||||
- **Service tag confirmation**: `GH1XZG2` (already on file from spreadsheet).
|
||||
|
||||
### STATUS: Collected 2026-07-27
|
||||
|
||||
**Disk inventory — RESOLVED:**
|
||||
- Disk 0: Samsung SSD 960 PRO **512 GB NVMe** (best local storage in fleet)
|
||||
- Disk 1: Samsung SSD 850 EVO **1 TB SATA SSD**
|
||||
- Both SSDs, no spinning rust. 1.5 TB total local SSD.
|
||||
|
||||
**Network adapters — RESOLVED (concerning):**
|
||||
- StorageNetwork: **ASIX USB to Gigabit Ethernet** (dongle, D4-81-D7-3E-0D-5E)
|
||||
- Ethernet: **Realtek USB GbE Family Controller** (also USB, 18-FD-CB-00-D2-CA)
|
||||
- Wi-Fi: Intel 8265 (disconnected)
|
||||
- **Both wired NICs are USB-attached.** Same anti-pattern as tsys4/9.
|
||||
Unavoidable on this laptop form factor — no onboard PCIe NIC available.
|
||||
|
||||
**GPU — RESOLVED:**
|
||||
- Intel HD Graphics 630 (integrated, 1 GB)
|
||||
- NVIDIA Quadro M1200 (4 GB, confirmed for passthrough)
|
||||
|
||||
**PCIe slots — RESOLVED:**
|
||||
- Slots 3/6/7/8 report "Available" but these are laptop M.2/WWAN slots, not
|
||||
user-accessible full PCIe. **Cannot add a PCIe NIC.** NVMe slot occupied
|
||||
by 960 PRO.
|
||||
|
||||
**RAM — PARTIAL:**
|
||||
- Total 32 GB confirmed (34,097,573,888 bytes).
|
||||
- Per-stick detail failed to run (PowerShell line-break split
|
||||
`Format-T` + `able`). Re-run the command below if bandwidth planning
|
||||
needs stick-level detail:
|
||||
```powershell
|
||||
Get-CimInstance Win32_PhysicalMemory | Select-Object Manufacturer, PartNumber, Capacity, Speed, ConfiguredClockSpeed, DeviceLocator, FormFactor | Format-Table -AutoSize
|
||||
```
|
||||
|
||||
**Service tag — CONFIRMED:** `GH1XZG2` (Precision 5520).
|
||||
|
||||
---
|
||||
|
||||
## 2. Friday maintenance window (physical hardware)
|
||||
|
||||
### tsys4 — install PCIe NIC + add RAM
|
||||
|
||||
1. Power down tsys4 (graceful shutdown via Proxmox UI or `shutdown -h now`).
|
||||
2. Install the **PCIe NIC** (Intel i350-T2 or similar 1 GbE dual-port).
|
||||
3. Add **RAM**: 16 GB → 64 GB DDR3 ECC.
|
||||
4. Power on, then update `/etc/network/interfaces` to replace
|
||||
`enx8cae4ccda926` (USB dongle) with the new PCIe NIC device name.
|
||||
5. Reboot to activate new NIC and NFS nconnect.
|
||||
6. Run `validate-fixes.sh pfv-tsys4` to confirm.
|
||||
|
||||
### tsys5 — plug storage cable + install NVMe + relocate D3 SSD
|
||||
|
||||
1. Plug the **second ethernet cable** into tsys5's dedicated storage NIC.
|
||||
2. Verify bond0 recovery: `cat /proc/net/bonding/bond0` — look for
|
||||
"Number of ports: 2" and a real partner MAC (not all zeros).
|
||||
3. Apply bond hash fix (same as tsys6/7):
|
||||
```bash
|
||||
echo "layer3+4" > /sys/class/net/bond0/bonding/xmit_hash_policy
|
||||
```
|
||||
4. **Relocate D3 SSD** from tsys4 USB to tsys5 SAS port:
|
||||
- Power down tsys4
|
||||
- Remove the SK hynix SC300 SSD from its USB enclosure on tsys4
|
||||
- Install it on a free SAS port on tsys5 (5 ports free on LSI SAS1068E)
|
||||
- On tsys5: mount as `/mnt/pfv-tsys5/D3`, add to `/etc/exports`
|
||||
- Update `/etc/pve/storage.cfg` cluster-wide: repoint D3 `server` from
|
||||
`pfv-tsys4-nfs-stor` to `pfv-tsys5-nfs-stor`, update `export` path
|
||||
- Copy any existing D3 data from tsys4 first (currently ~2 MB, essentially
|
||||
empty, so minimal migration)
|
||||
5. Install the **PCI NVMe drive** (uses a PCI slot, not a SATA/SAS port).
|
||||
6. Format NVMe as local directory storage (see TODO section 3 below).
|
||||
7. Reboot tsys5 to activate NFS nconnect.
|
||||
8. Run `validate-fixes.sh pfv-tsys5` to confirm.
|
||||
|
||||
---
|
||||
|
||||
## 3. tsys5 NVMe format/mount decision (after Friday install)
|
||||
|
||||
**Recommendation: local-only, not NFS-exported.** Format as Proxmox directory
|
||||
storage so it shows up as a VM image target in the Proxmox UI.
|
||||
|
||||
After the NVMe is physically installed and visible in Proxmox:
|
||||
|
||||
1. Identify the device: `lsblk` or `ls /dev/nvme*`
|
||||
2. Format and add to Proxmox:
|
||||
```bash
|
||||
# Option A: LVM-thin (thin provisioning, snapshots)
|
||||
pvcreate /dev/nvme0n1
|
||||
vgcreate nvme-pool /dev/nvme0n1
|
||||
lvcreate -l 100%FREE -T nvme-pool/data
|
||||
# Then in Proxmox UI: Datacenter > Storage > Add > LVM-Thin
|
||||
# ID: nvme-local
|
||||
# Volume Group: nvme-pool
|
||||
# Thin Pool: data
|
||||
# Content: Disk image, Container template
|
||||
|
||||
# Option B: Directory (simpler, no thin provisioning)
|
||||
mkfs.ext4 /dev/nvme0n1
|
||||
mkdir -p /mnt/nvme
|
||||
mount /dev/nvme0n1 /mnt/nvme
|
||||
# Add to /etc/fstab for persistence
|
||||
# Then in Proxmox UI: Datacenter > Storage > Add > Directory
|
||||
# ID: nvme-local
|
||||
# Directory: /mnt/nvme
|
||||
# Content: Disk image, Container template
|
||||
```
|
||||
|
||||
3. Use for wnode-tsys5 boot disk (highest impact) and sectestbed VM scratch.
|
||||
|
||||
---
|
||||
|
||||
## 4. Post-hardware validation (run after Friday work)
|
||||
|
||||
1. Re-run iperf matrix: `./iperf-full-matrix.sh`
|
||||
2. Validate tsys4 and tsys5: `./validate-fixes.sh pfv-tsys4 && ./validate-fixes.sh pfv-tsys5`
|
||||
3. Update PROJECT.md with post-hardware iperf numbers.
|
||||
|
||||
---
|
||||
|
||||
## 5. UCS storage migration to spinning disk (do today)
|
||||
|
||||
UCS (Univention Corporate Server / open-source AD) does not need SSD. Both
|
||||
UCS VMs should stay on spinning disk (HDD) and be split across storage
|
||||
servers for redundancy.
|
||||
|
||||
| VM | Current | Target | Action |
|
||||
|----|---------|--------|--------|
|
||||
| ucs-01 (108) | D2 (tsys4 HDD) | **D2 (tsys4 HDD) -- no change** | Already correct |
|
||||
| ucs-02 (902) | D5 (tsys4 HDD) | **S2 (tsys5 HDD)** | Move for cross-server redundancy |
|
||||
|
||||
**To migrate ucs-02 to S2 (use PDM/Proxmox UI):**
|
||||
1. In Proxmox Datacenter or the node UI, select VM 902 on tsys9
|
||||
2. Use "Migrate" or "Storage Migrate" to move the disk from D5 to S2
|
||||
(both are NFS exports visible to tsys9, so this is a storage-only migration)
|
||||
3. Verify VM 902 boots and LDAP/AD services are healthy after migration
|
||||
|
||||
Note: both VMs are currently on tsys4 HDD, which is fine for UCS. Only
|
||||
ucs-02 needs to move -- it should be on a different storage server than
|
||||
ucs-01 so a tsys4 failure doesn't take down both halves of the AD pair.
|
||||
|
||||
---
|
||||
|
||||
## 6. Open questions for next session
|
||||
|
||||
- Are the hosts a Proxmox cluster (`pvecm status`) or standalone installs?
|
||||
Determines whether live migration is available.
|
||||
- What k8s distribution is in use? (k3s, kubeadm, RKE2?)
|
||||
- Container runtime? (containerd, cri-o?)
|
||||
- Is there a local container image registry mirror?
|
||||
- What specific ETL tools? (GDAL, PostGIS, xarray, Dask?)
|
||||
- HPC job scheduler? (plain k8s Jobs, Argo Workflows, Volcano?)
|
||||
- What uses tsys5's SDR + parallel port before planning tsys5 role changes?
|
||||
- tsys3 thermal state (laptop in rack for years) — check `sensors`.
|
||||
@@ -0,0 +1,329 @@
|
||||
# TSYS Two-Factor Authentication Implementation Guide
|
||||
|
||||
## Overview
|
||||
|
||||
This guide provides complete instructions for implementing and managing two-factor authentication (2FA) on TSYS servers using Google Authenticator (TOTP).
|
||||
|
||||
## What This Implementation Provides
|
||||
|
||||
### Services Protected by 2FA
|
||||
- **SSH Access:** Requires SSH key + 2FA token
|
||||
- **Cockpit Web Interface:** Requires password + 2FA token
|
||||
- **Webmin Administration:** Requires password + 2FA token (if installed)
|
||||
|
||||
### Security Features
|
||||
- **Time-based One-Time Passwords (TOTP):** Standard 6-digit codes
|
||||
- **Backup Codes:** Emergency access codes
|
||||
- **Gradual Rollout:** Optional nullok mode for phased deployment
|
||||
- **Configuration Backup:** Automatic backup of all configs
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### Step 1: Run the 2FA Setup Script
|
||||
```bash
|
||||
# Navigate to the security modules directory
|
||||
cd ProjectCode/Modules/Security
|
||||
|
||||
# Run the 2FA setup script as root
|
||||
sudo bash secharden-2fa.sh
|
||||
```
|
||||
|
||||
### Step 2: Validate Installation
|
||||
```bash
|
||||
# Run 2FA validation tests
|
||||
./Project-Tests/security/2fa-validation.sh
|
||||
|
||||
# Run specific 2FA security test
|
||||
./Project-Tests/run-tests.sh security
|
||||
```
|
||||
|
||||
### Step 3: Setup Individual Users
|
||||
For each user that needs 2FA access:
|
||||
|
||||
```bash
|
||||
# Check setup instructions
|
||||
cat /home/username/2fa-setup-instructions.txt
|
||||
|
||||
# Run user setup script
|
||||
sudo /tmp/setup-2fa-username.sh
|
||||
```
|
||||
|
||||
### Step 4: Test 2FA Access
|
||||
1. **Test SSH access** from another terminal
|
||||
2. **Test Cockpit access** via web browser
|
||||
3. **Test Webmin access** if installed
|
||||
|
||||
## User Setup Process
|
||||
|
||||
### Installing Authenticator Apps
|
||||
Users need one of these apps on their phone:
|
||||
- **Google Authenticator** (Android/iOS)
|
||||
- **Authy** (Android/iOS)
|
||||
- **Microsoft Authenticator** (Android/iOS)
|
||||
- **1Password** (with TOTP support)
|
||||
|
||||
### Setting Up 2FA for a User
|
||||
1. **Run setup script:**
|
||||
```bash
|
||||
sudo /tmp/setup-2fa-username.sh
|
||||
```
|
||||
|
||||
2. **Follow prompts:**
|
||||
- Answer "y" to update time-based token
|
||||
- Scan QR code with authenticator app
|
||||
- Save emergency backup codes securely
|
||||
- Answer "y" to remaining security questions
|
||||
|
||||
3. **Test immediately:**
|
||||
```bash
|
||||
# Test SSH from another terminal
|
||||
ssh username@server-ip
|
||||
# You'll be prompted for 6-digit code
|
||||
```
|
||||
|
||||
## Configuration Details
|
||||
|
||||
### SSH Configuration Changes
|
||||
File: `/etc/ssh/sshd_config`
|
||||
```
|
||||
ChallengeResponseAuthentication yes
|
||||
UsePAM yes
|
||||
AuthenticationMethods publickey,keyboard-interactive
|
||||
```
|
||||
|
||||
### PAM Configuration
|
||||
File: `/etc/pam.d/sshd`
|
||||
```
|
||||
auth required pam_google_authenticator.so nullok
|
||||
```
|
||||
|
||||
### Cockpit Configuration
|
||||
File: `/etc/cockpit/cockpit.conf`
|
||||
```
|
||||
[WebService]
|
||||
LoginTitle = TSYS Server Management
|
||||
LoginTo = 300
|
||||
RequireHost = true
|
||||
|
||||
[Session]
|
||||
Banner = /etc/cockpit/issue.cockpit
|
||||
IdleTimeout = 15
|
||||
```
|
||||
|
||||
### Webmin Configuration
|
||||
File: `/etc/webmin/miniserv.conf`
|
||||
```
|
||||
twofactor_provider=totp
|
||||
twofactor=1
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### Gradual vs Strict Enforcement
|
||||
|
||||
#### Gradual Enforcement (Default)
|
||||
- Uses `nullok` option in PAM
|
||||
- Users without 2FA can still log in
|
||||
- Allows phased rollout
|
||||
- Good for initial deployment
|
||||
|
||||
#### Strict Enforcement
|
||||
- Remove `nullok` from PAM configuration
|
||||
- All users must have 2FA configured
|
||||
- Immediate security enforcement
|
||||
- Risk of lockout if misconfigured
|
||||
|
||||
### Backup and Recovery
|
||||
|
||||
#### Emergency Access
|
||||
- **Backup codes:** Generated during setup
|
||||
- **Root access:** Can disable 2FA if needed
|
||||
- **Console access:** Physical/virtual console bypasses SSH
|
||||
|
||||
#### Configuration Backup
|
||||
- Automatic backup to `/root/backup/2fa-TIMESTAMP/`
|
||||
- Includes all modified configuration files
|
||||
- Can be restored if needed
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
#### 1. User Cannot Generate QR Code
|
||||
```bash
|
||||
# Ensure qrencode is installed
|
||||
sudo apt-get install qrencode
|
||||
|
||||
# Re-run user setup
|
||||
sudo /tmp/setup-2fa-username.sh
|
||||
```
|
||||
|
||||
#### 2. SSH Connection Fails
|
||||
```bash
|
||||
# Check SSH service status
|
||||
sudo systemctl status sshd
|
||||
|
||||
# Test SSH configuration
|
||||
sudo sshd -t
|
||||
|
||||
# Check logs
|
||||
sudo journalctl -u sshd -f
|
||||
```
|
||||
|
||||
#### 3. 2FA Code Not Accepted
|
||||
- **Check time synchronization** on server and phone
|
||||
- **Verify app setup** - rescan QR code if needed
|
||||
- **Try backup codes** if available
|
||||
|
||||
#### 4. Locked Out of Server
|
||||
```bash
|
||||
# Access via console (physical/virtual)
|
||||
# Disable 2FA temporarily
|
||||
sudo cp /root/backup/2fa-*/pam.d.bak/sshd /etc/pam.d/sshd
|
||||
sudo systemctl restart sshd
|
||||
```
|
||||
|
||||
### Debug Commands
|
||||
|
||||
```bash
|
||||
# Check 2FA status
|
||||
./Project-Tests/security/2fa-validation.sh
|
||||
|
||||
# Check SSH configuration
|
||||
sudo sshd -T | grep -E "(Challenge|PAM|Authentication)"
|
||||
|
||||
# Check PAM configuration
|
||||
cat /etc/pam.d/sshd | grep google-authenticator
|
||||
|
||||
# Check user 2FA status
|
||||
ls -la ~/.google_authenticator
|
||||
```
|
||||
|
||||
## Management and Maintenance
|
||||
|
||||
### Adding New Users
|
||||
1. Ensure user account exists
|
||||
2. Run setup script for new user
|
||||
3. Provide setup instructions
|
||||
4. Test access
|
||||
|
||||
### Removing User 2FA
|
||||
```bash
|
||||
# Remove user's 2FA configuration
|
||||
sudo rm /home/username/.google_authenticator
|
||||
|
||||
# User will need to re-setup 2FA
|
||||
```
|
||||
|
||||
### Disabling 2FA System-Wide
|
||||
```bash
|
||||
# Restore original configurations
|
||||
sudo cp /root/backup/2fa-*/sshd_config.bak /etc/ssh/sshd_config
|
||||
sudo cp /root/backup/2fa-*/pam.d.bak/sshd /etc/pam.d/sshd
|
||||
sudo systemctl restart sshd
|
||||
```
|
||||
|
||||
### Updating 2FA Configuration
|
||||
```bash
|
||||
# Re-run setup script
|
||||
sudo bash secharden-2fa.sh
|
||||
|
||||
# Validate changes
|
||||
./Project-Tests/security/2fa-validation.sh
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Deployment Strategy
|
||||
1. **Test in non-production** environment first
|
||||
2. **Enable gradual rollout** (nullok) initially
|
||||
3. **Train users** on 2FA setup process
|
||||
4. **Test emergency procedures** before strict enforcement
|
||||
5. **Monitor logs** for authentication issues
|
||||
|
||||
### Security Recommendations
|
||||
- **Enforce strict mode** after successful rollout
|
||||
- **Regular backup code rotation**
|
||||
- **Monitor failed authentication attempts**
|
||||
- **Document emergency procedures**
|
||||
- **Regular security audits**
|
||||
|
||||
### User Training
|
||||
- **Provide clear instructions**
|
||||
- **Demonstrate setup process**
|
||||
- **Explain backup code importance**
|
||||
- **Test login process with users**
|
||||
- **Establish support procedures**
|
||||
|
||||
## Monitoring and Logging
|
||||
|
||||
### Authentication Logs
|
||||
```bash
|
||||
# SSH authentication logs
|
||||
sudo journalctl -u sshd | grep -i "authentication"
|
||||
|
||||
# PAM authentication logs
|
||||
sudo journalctl | grep -i "pam_google_authenticator"
|
||||
|
||||
# Failed login attempts
|
||||
sudo journalctl | grep -i "failed"
|
||||
```
|
||||
|
||||
### Security Monitoring
|
||||
- Monitor for repeated failed 2FA attempts
|
||||
- Alert on successful logins without 2FA (during gradual rollout)
|
||||
- Track user 2FA setup completion
|
||||
- Monitor for emergency access usage
|
||||
|
||||
## Integration with Existing Systems
|
||||
|
||||
### LDAP/Active Directory
|
||||
- 2FA works with existing authentication systems
|
||||
- Users still need local 2FA setup
|
||||
- Consider centralized 2FA solutions for large deployments
|
||||
|
||||
### Monitoring Systems
|
||||
- LibreNMS: Will continue to work with SNMP
|
||||
- Wazuh: Will log 2FA authentication events
|
||||
- Cockpit: Enhanced with 2FA protection
|
||||
|
||||
### Backup Systems
|
||||
- Ensure backup procedures account for 2FA
|
||||
- Test restore procedures with 2FA enabled
|
||||
- Document emergency access procedures
|
||||
|
||||
## Support and Resources
|
||||
|
||||
### Files Created by Setup
|
||||
- `/tmp/setup-2fa-*.sh` - User setup scripts
|
||||
- `/home/*/2fa-setup-instructions.txt` - User instructions
|
||||
- `/root/backup/2fa-*/` - Configuration backups
|
||||
|
||||
### Validation Tools
|
||||
- `./Project-Tests/security/2fa-validation.sh` - Complete 2FA validation
|
||||
- `./Project-Tests/run-tests.sh security` - Security test suite
|
||||
|
||||
### Emergency Contacts
|
||||
- System Administrator: [Contact Info]
|
||||
- Security Team: [Contact Info]
|
||||
- 24/7 Support: [Contact Info]
|
||||
|
||||
## Compliance and Audit
|
||||
|
||||
### Security Benefits
|
||||
- Significantly reduces risk of unauthorized access
|
||||
- Meets multi-factor authentication requirements
|
||||
- Provides audit trail of authentication events
|
||||
- Complies with security frameworks (NIST, ISO 27001)
|
||||
|
||||
### Audit Trail
|
||||
- All authentication attempts logged
|
||||
- 2FA setup events recorded
|
||||
- Configuration changes tracked
|
||||
- Emergency access documented
|
||||
|
||||
---
|
||||
|
||||
**Last Updated:** July 14, 2025
|
||||
**Version:** 1.0
|
||||
**Author:** TSYS Security Team
|
||||
@@ -0,0 +1,117 @@
|
||||
# Charles TODO - TSYS FetchApply Security Improvements
|
||||
|
||||
**Priority Order:** High → Medium → Low
|
||||
**Target:** Address security vulnerabilities and operational improvements
|
||||
|
||||
## 🚨 HIGH PRIORITY (Security Critical)
|
||||
|
||||
### ✅ 1. Replace Insecure Deployment Method - RESOLVED
|
||||
**Previous Issue:** `curl https://dl.knownelement.com/KNEL/FetchApply/SetupNewSystem.sh | bash`
|
||||
**Status:** Fixed in README.md - now uses secure git clone approach
|
||||
**Current Method:** `git clone this repo` → `cd FetchApply/ProjectCode` → `bash SetupNewSystem.sh`
|
||||
|
||||
**Remaining considerations:**
|
||||
- Consider implementing GPG signature verification for tagged releases
|
||||
- Add cryptographic checksums for external downloads within scripts
|
||||
|
||||
### ✅ 2. Enforce HTTPS for All Downloads - RESOLVED
|
||||
**Previous Issue:** HTTP URLs in Dell OMSA and some repository setups
|
||||
**Status:** All HTTP URLs converted to HTTPS across:
|
||||
- `ProjectCode/Dell/Server/omsa.sh` - Ubuntu archive and Dell repo URLs
|
||||
- `ProjectCode/legacy/prox7.sh` - Proxmox download URLs
|
||||
- `ProjectCode/Modules/RandD/sslStackFromSource.sh` - Apache source URLs
|
||||
|
||||
**Remaining considerations:**
|
||||
- SSL certificate validation is enabled by default in wget/curl
|
||||
- Consider adding retry logic for certificate failures
|
||||
|
||||
### 3. Implement Secrets Management
|
||||
**Current Issue:** SSH keys committed to repository, no secrets rotation
|
||||
**Action Required:**
|
||||
- Deploy Bitwarden CLI or HashiCorp Vault integration
|
||||
- Remove SSH public keys from repository
|
||||
- Create secure key distribution mechanism
|
||||
- Implement key rotation procedures
|
||||
- Add environment variable support for sensitive data
|
||||
|
||||
**Files to secure:**
|
||||
- `ProjectCode/ConfigFiles/SSH/AuthorizedKeys/` (entire directory)
|
||||
- Hard-coded hostnames in various scripts
|
||||
|
||||
## 🔶 MEDIUM PRIORITY (Operational Security)
|
||||
|
||||
### 4. Add Script Integrity Verification
|
||||
**Action Required:**
|
||||
- Generate SHA256 checksums for all scripts
|
||||
- Create checksum verification function in Framework-Includes
|
||||
- Add signature verification for external downloads
|
||||
- Implement rollback capability on verification failure
|
||||
|
||||
### 5. Enhanced Error Recovery
|
||||
**Action Required:**
|
||||
- Add state tracking for partial deployments
|
||||
- Implement resume functionality for interrupted installations
|
||||
- Create system restoration points before major changes
|
||||
- Add dependency checking before module execution
|
||||
|
||||
### 6. Security Testing Framework
|
||||
**Action Required:**
|
||||
- Create integration tests for security configurations
|
||||
- Add compliance validation (CIS benchmarks, STIG)
|
||||
- Implement automated security scanning post-deployment
|
||||
- Create test environments for validation
|
||||
|
||||
### 7. Configuration Validation
|
||||
**Action Required:**
|
||||
- Add pre-flight checks for system compatibility
|
||||
- Validate network connectivity to required services
|
||||
- Check for conflicting software before installation
|
||||
- Verify sufficient disk space and system resources
|
||||
|
||||
## 🔹 LOW PRIORITY (Quality Improvements)
|
||||
|
||||
### 8. Documentation Enhancement
|
||||
**Action Required:**
|
||||
- Create detailed security architecture documentation
|
||||
- Add troubleshooting guides for common issues
|
||||
- Document security implications of each module
|
||||
- Create deployment runbooks for different environments
|
||||
|
||||
### 9. Monitoring and Alerting
|
||||
**Action Required:**
|
||||
- Add deployment success/failure reporting
|
||||
- Implement centralized logging for all installations
|
||||
- Create dashboards for deployment status
|
||||
- Add alerting for security configuration drift
|
||||
|
||||
### 10. User Experience Improvements
|
||||
**Action Required:**
|
||||
- Create web-based deployment interface
|
||||
- Add progress indicators for long-running operations
|
||||
- Implement dry-run mode for testing configurations
|
||||
- Add interactive configuration selection
|
||||
|
||||
## Implementation Timeline
|
||||
|
||||
**✅ COMPLETED:** Item 1 (Secure deployment method)
|
||||
**✅ COMPLETED:** Item 2 (HTTPS enforcement)
|
||||
**Week 1:** Item 3 (Secrets management)
|
||||
**Week 2-3:** Items 4-5 (Operational improvements)
|
||||
**Month 2:** Items 6-10 (Quality and monitoring)
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- [ ] No plaintext secrets in repository
|
||||
- [x] All downloads use HTTPS with verification ✅
|
||||
- [x] Deployment method is cryptographically secure ✅
|
||||
- [ ] Automated testing validates security configurations
|
||||
- [ ] Rollback capability exists for all changes
|
||||
- [ ] Comprehensive documentation covers security implications
|
||||
|
||||
## Resources Needed
|
||||
|
||||
- Access to package repository for signed distributions
|
||||
- GPG key infrastructure for signing
|
||||
- Secrets management service (Vault/Bitwarden)
|
||||
- Test environment infrastructure
|
||||
- Security scanning tools integration
|
||||
@@ -0,0 +1,162 @@
|
||||
# Claude TODO - TSYS FetchApply Automation Tasks
|
||||
|
||||
**Purpose:** Actionable items optimized for AI assistant implementation
|
||||
**Priority:** Critical → High → Medium → Low
|
||||
|
||||
## 🚨 CRITICAL (Immediate Security Fixes)
|
||||
|
||||
### ✅ RESOLVED: Secure Deployment Method
|
||||
**Previous Issue:** `curl | bash` deployment method
|
||||
**Status:** Fixed in README.md - now uses `git clone` + local script execution
|
||||
|
||||
### ✅ RESOLVED: Replace HTTP URLs with HTTPS
|
||||
**Files modified:**
|
||||
- `ProjectCode/Dell/Server/omsa.sh` - Converted 11 HTTP URLs to HTTPS (Ubuntu archive, Dell repo)
|
||||
- `ProjectCode/legacy/prox7.sh` - Converted 2 HTTP URLs to HTTPS (Proxmox downloads)
|
||||
- `ProjectCode/Modules/RandD/sslStackFromSource.sh` - Converted 3 HTTP URLs to HTTPS (Apache sources)
|
||||
|
||||
**Status:** All HTTP URLs in active scripts converted to HTTPS. Only remaining HTTP references are in comments and LibreNMS agent files (external dependencies).
|
||||
|
||||
### TASK-002: Add Download Integrity Verification
|
||||
**Create new function in:** `Framework-Includes/VerifyDownload.sh`
|
||||
**Function to implement:**
|
||||
```bash
|
||||
function verify_download() {
|
||||
local url="$1"
|
||||
local expected_hash="$2"
|
||||
local output_file="$3"
|
||||
|
||||
curl -fsSL "$url" -o "$output_file"
|
||||
local actual_hash=$(sha256sum "$output_file" | cut -d' ' -f1)
|
||||
|
||||
if [ "$actual_hash" != "$expected_hash" ]; then
|
||||
print_error "Hash verification failed for $output_file"
|
||||
rm -f "$output_file"
|
||||
return 1
|
||||
fi
|
||||
print_info "Download verified: $output_file"
|
||||
}
|
||||
```
|
||||
|
||||
### TASK-003: Create Secure Deployment Script
|
||||
**Create:** `ProjectCode/SecureSetupNewSystem.sh`
|
||||
**Features to implement:**
|
||||
- GPG signature verification
|
||||
- SHA256 checksum validation
|
||||
- HTTPS-only downloads
|
||||
- Rollback capability
|
||||
|
||||
## 🔶 HIGH (Security Enhancements)
|
||||
|
||||
### TASK-004: Remove Hardcoded SSH Keys
|
||||
**Files to modify:**
|
||||
- `ProjectCode/ConfigFiles/SSH/AuthorizedKeys/root-ssh-authorized-keys`
|
||||
- `ProjectCode/ConfigFiles/SSH/AuthorizedKeys/localuser-ssh-authorized-keys`
|
||||
- `ProjectCode/Modules/Security/secharden-ssh.sh:31,40,51`
|
||||
|
||||
**Implementation approach:**
|
||||
1. Create environment variable support: `SSH_KEYS_URL` or `SSH_KEYS_VAULT_PATH`
|
||||
2. Modify secharden-ssh.sh to fetch keys from secure source
|
||||
3. Add key validation before deployment
|
||||
|
||||
### TASK-005: Add Secrets Management Framework
|
||||
**Create:** `Framework-Includes/SecretsManager.sh`
|
||||
**Functions to implement:**
|
||||
```bash
|
||||
function get_secret() { } # Retrieve secret from vault
|
||||
function validate_secret() { } # Validate secret format
|
||||
function rotate_secret() { } # Trigger secret rotation
|
||||
```
|
||||
|
||||
### TASK-006: Enhanced Preflight Checks
|
||||
**Modify:** `Framework-Includes/PreflightCheck.sh`
|
||||
**Add checks for:**
|
||||
- Network connectivity to required hosts
|
||||
- Disk space requirements
|
||||
- Existing conflicting software
|
||||
- Required system capabilities
|
||||
|
||||
## 🔹 MEDIUM (Operational Improvements)
|
||||
|
||||
### TASK-007: Add Configuration Backup
|
||||
**Create:** `Framework-Includes/ConfigBackup.sh`
|
||||
**Functions:**
|
||||
```bash
|
||||
function backup_config() { } # Create timestamped backup
|
||||
function restore_config() { } # Restore from backup
|
||||
function list_backups() { } # Show available backups
|
||||
```
|
||||
|
||||
### TASK-008: Implement State Tracking
|
||||
**Create:** `Framework-Includes/StateManager.sh`
|
||||
**Track:**
|
||||
- Deployment progress
|
||||
- Module completion status
|
||||
- Rollback points
|
||||
- System changes made
|
||||
|
||||
### TASK-009: Add Retry Logic
|
||||
**Enhance existing scripts with:**
|
||||
- Configurable retry attempts for network operations
|
||||
- Exponential backoff for failed operations
|
||||
- Circuit breaker for repeatedly failing services
|
||||
|
||||
## 🔸 LOW (Quality of Life)
|
||||
|
||||
### TASK-010: Enhanced Logging
|
||||
**Modify:** `Framework-Includes/Logging.sh`
|
||||
**Add:**
|
||||
- Structured logging (JSON format option)
|
||||
- Log levels (DEBUG, INFO, WARN, ERROR)
|
||||
- Remote logging capability
|
||||
- Log rotation management
|
||||
|
||||
### TASK-011: Progress Indicators
|
||||
**Add to:** `Framework-Includes/PrettyPrint.sh`
|
||||
```bash
|
||||
function show_progress() { } # Display progress bar
|
||||
function update_status() { } # Update current operation
|
||||
```
|
||||
|
||||
### TASK-012: Dry Run Mode
|
||||
**Add to:** `ProjectCode/SetupNewSystem.sh`
|
||||
**Implementation:**
|
||||
- `--dry-run` flag support
|
||||
- Preview of changes without execution
|
||||
- Dependency analysis output
|
||||
|
||||
## Implementation Order for Claude
|
||||
|
||||
**Updated Priority After Security Fix (July 14, 2025):**
|
||||
1. **Start with TASK-001** (HTTPS enforcement - simple find/replace operations)
|
||||
2. **Create framework functions** (TASK-002, TASK-005, TASK-007)
|
||||
3. **Enhance existing modules** (TASK-004, TASK-006)
|
||||
4. **Add operational features** (TASK-008, TASK-009)
|
||||
5. **Improve user experience** (TASK-010, TASK-011, TASK-012)
|
||||
|
||||
**Note:** Major deployment security risk resolved - remaining tasks focus on hardening internal operations.
|
||||
|
||||
## File Location Patterns
|
||||
|
||||
- **Framework components:** `Framework-Includes/*.sh`
|
||||
- **Security modules:** `ProjectCode/Modules/Security/*.sh`
|
||||
- **Configuration files:** `ProjectCode/ConfigFiles/*/`
|
||||
- **Main entry point:** `ProjectCode/SetupNewSystem.sh`
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
For each task:
|
||||
1. Create backup of original files
|
||||
2. Implement changes incrementally
|
||||
3. Test with `bash -n` for syntax validation
|
||||
4. Verify functionality with controlled test runs
|
||||
5. Document changes made
|
||||
|
||||
## Error Handling Requirements
|
||||
|
||||
All new functions must:
|
||||
- Use `set -euo pipefail` compatibility
|
||||
- Integrate with existing error handling framework
|
||||
- Log errors to `$LOGFILENAME`
|
||||
- Return appropriate exit codes
|
||||
- Clean up temporary files on failure
|
||||
@@ -1,10 +0,0 @@
|
||||
# docs/docmap.md
|
||||
|
||||
> **Documentation moved to Discourse — the canonical source of truth.**
|
||||
>
|
||||
> **Documentation index — now lives in the Project Overview topic**
|
||||
>
|
||||
> **Read it here:** https://community.turnsys.com/t/296
|
||||
>
|
||||
> *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.*
|
||||
@@ -1,10 +0,0 @@
|
||||
# docs/server-build/DEPLOYMENT.md
|
||||
|
||||
> **Documentation moved to Discourse — the canonical source of truth.**
|
||||
>
|
||||
> **Server deployment procedures**
|
||||
>
|
||||
> **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.*
|
||||
@@ -1,10 +0,0 @@
|
||||
# docs/server-build/DEVELOPMENT-GUIDELINES.md
|
||||
|
||||
> **Documentation moved to Discourse — the canonical source of truth.**
|
||||
>
|
||||
> **Coding standards, commit conventions, script patterns**
|
||||
>
|
||||
> **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.*
|
||||
@@ -1,10 +0,0 @@
|
||||
# docs/server-build/SECURITY.md
|
||||
|
||||
> **Documentation moved to Discourse — the canonical source of truth.**
|
||||
>
|
||||
> **Security architecture: SSH hardening, 2FA, SCAP-STIG, Wazuh, auditd**
|
||||
>
|
||||
> **Read it here:** https://community.turnsys.com/t/303
|
||||
>
|
||||
> *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.*
|
||||
@@ -1,10 +0,0 @@
|
||||
# docs/server-build/TSYS-2FA-GUIDE.md
|
||||
|
||||
> **Documentation moved to Discourse — the canonical source of truth.**
|
||||
>
|
||||
> **End-user guide for 2FA setup**
|
||||
>
|
||||
> **Read it here:** https://community.turnsys.com/t/303
|
||||
>
|
||||
> *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.*
|
||||
@@ -1,10 +0,0 @@
|
||||
# docs/server-build/tailscale.md
|
||||
|
||||
> **Documentation moved to Discourse — the canonical source of truth.**
|
||||
>
|
||||
> **Tailscale vs managed DNS analysis (resolved)**
|
||||
>
|
||||
> **Read it here:** https://community.turnsys.com/t/306
|
||||
>
|
||||
> *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.*
|
||||
@@ -0,0 +1,217 @@
|
||||
# Tailscale vs. Managed DNS — Architecture Analysis
|
||||
|
||||
> **Status:** analysis for review. No code decisions are final. Read the
|
||||
> "Known issues" section before acting on the managed-resolv.conf change.
|
||||
|
||||
## 1. Executive summary
|
||||
|
||||
Every host in this build runs the Tailscale client, and Tailscale — by default —
|
||||
**manages `/etc/resolv.conf` itself**, pointing it at `100.100.100.100`
|
||||
(Tailscale's MagicDNS resolver). This directly conflicts with the managed
|
||||
`resolv.conf` (pointing at `192.168.3.252`/`192.168.3.253`) that
|
||||
`SetupNewSystem.sh` deploys: whichever runs last wins, and Tailscale's daemon
|
||||
re-wins on every `tailscale up` and on reboot.
|
||||
|
||||
Worse, a probe of the live network shows that **knel.net device records only
|
||||
resolve through the Tailscale 100.100.100.100 path** — querying the LAN IPs of
|
||||
the DNS servers directly returns NXDOMAIN for current hostnames (the Technitium
|
||||
`knel.net` zone has the SOA but is stale/empty of actual records). So pointing
|
||||
`resolv.conf` at the LAN IPs would break resolution of the very names this
|
||||
project's modules depend on (`tsys-nsm.knel.net`, `tsys-cloudron.knel.net`,
|
||||
`tsys-librenms.knel.net`).
|
||||
|
||||
This document lays out the options and a recommended path forward.
|
||||
|
||||
## 2. How name resolution actually works today (as measured)
|
||||
|
||||
Probed from `sectestbed-sandbox` (192.168.3.50):
|
||||
|
||||
| Query path | External name (`github.com`) | knel.net device name (`pfv-netinfra-01.knel.net`) |
|
||||
|---|---|---|
|
||||
| Via current resolver = `100.100.100.100` (Tailscale) | resolves | **resolves** → `100.70.181.72` (Tailscale CGNAT) |
|
||||
| Direct `dig @192.168.3.252` (Technitium, LAN) | resolves (recurses) | **NXDOMAIN** (SOA present, no record) |
|
||||
| Direct `dig @192.168.3.253` (Pi-hole, LAN) | resolves (recurses) | **NXDOMAIN** (SOA present, no record) |
|
||||
|
||||
Other measured facts:
|
||||
|
||||
- `dig @192.168.3.252 knel.net SOA` → `NOERROR`, returns
|
||||
`knel.net. 900 IN SOA dns.knel.net. hostadmin.knel.net. 2025062313 …`
|
||||
(serial dated **2025-06-23** — the zone exists but is stale).
|
||||
- NTP on both `.252` and `.253` answers time queries (stratum 2/3).
|
||||
- The live `/etc/resolv.conf` on a deployed host reads:
|
||||
```
|
||||
# resolv.conf(5) file generated by tailscale
|
||||
# DO NOT EDIT THIS FILE BY HAND -- CHANGES WILL BE OVERWRITTEN
|
||||
nameserver 100.100.100.100
|
||||
nameserver fd7a:115c:a1e0::53
|
||||
search knel.net
|
||||
```
|
||||
|
||||
**Interpretation:** the `knel.net` device→Tailscale-IP mappings are synthesised
|
||||
by Tailscale's MagicDNS from the tailnet device registry (every device that
|
||||
joins the tailnet gets `hostname.knel.net` → its `100.x.x.x` address). The
|
||||
Technitium `knel.net` zone is a separate, manually-maintained zone that has
|
||||
fallen out of date. The two are not the same source of truth.
|
||||
|
||||
## 3. The core tension
|
||||
|
||||
| Goal | Who provides it today |
|
||||
|---|---|
|
||||
| Resolve `*.knel.net` device names (→ Tailscale IPs) | Tailscale MagicDNS via `100.100.100.100` |
|
||||
| Resolve external names with ad-blocking | Pi-hole (`.253`), reachable via Tailscale → Technitium → Pi-hole chain |
|
||||
| Redundant, low-latency, tunnel-independent DNS | LAN resolvers `.252`/`.253` — **but these lack knel.net records** |
|
||||
| Authoritative time | NTP on `.252`/`.253` (works on either path) |
|
||||
|
||||
The conflict: you cannot simply point `resolv.conf` at the LAN resolvers,
|
||||
because they do not know about the current `knel.net` device records, and
|
||||
several modules in this project resolve `knel.net` hostnames at runtime
|
||||
(wazuh manager, postfix relay, syslog target). You also cannot ignore Tailscale,
|
||||
because it is the only thing that resolves those names today.
|
||||
|
||||
## 4. Options
|
||||
|
||||
### Option A — Let Tailscale own DNS (status quo, `accept-dns=true`)
|
||||
|
||||
Leave the default. Tailscale writes `100.100.100.100` to `resolv.conf`; the
|
||||
control-plane forwarding (`100.100.100.100` → Technitium → Pi-hole) handles
|
||||
external names and ad-blocking; MagicDNS handles `knel.net` device names.
|
||||
|
||||
| Pros | Cons |
|
||||
|---|---|
|
||||
| Zero per-host config; new machines "just work" on `tailscale up` | **All DNS depends on the Tailscale daemon being up.** If `tailscaled` dies, every name lookup fails — including the ones you need to SSH in and fix it. |
|
||||
| MagicDNS + knel.net names resolve automatically | Latency: every query goes host→tailscaled→100.100.100.100→(tunnel)→Technitium→Pi-hole→upstream |
|
||||
| Ad-blocking preserved (via the Pi-hole hop) | Overwrites the managed `resolv.conf` — the `.252`/`.253` redundancy is lost |
|
||||
| Centralised in the Tailscale admin console | Single resolver in `resolv.conf` (`100.100.100.100`); no glibc-level failover |
|
||||
| | Boot-order risk: early-boot processes have no DNS until `tailscaled` is up |
|
||||
|
||||
### Option B — Pin resolv.conf to the LAN resolvers (`accept-dns=false`)
|
||||
|
||||
Set `--accept-dns=false` on every host and keep the managed `resolv.conf`
|
||||
pointing at `.252`/`.253`.
|
||||
|
||||
| Pros | Cons |
|
||||
|---|---|
|
||||
| DNS independent of Tailscale — survives `tailscaled` outages | **`*.knel.net` device names break (NXDOMAIN)** because the LAN resolvers' knel.net zone is stale. This breaks wazuh/postfix/syslog hostname resolution. |
|
||||
| Lowest latency, full glibc-level failover across two servers | MagicDNS names (`*.ts.net`) do not resolve |
|
||||
| Managed `resolv.conf` wins uncontested | Requires fixing the Technitium/Pi-hole `knel.net` zone to mirror the Tailscale device records before this is viable |
|
||||
| Boot-time DNS works immediately | Off-LAN hosts (laptops) can't reach `.252`/`.253` without the tunnel — back to needing Tailscale |
|
||||
|
||||
> **Not recommended as-is.** Only viable **after** the `knel.net` zone on
|
||||
> `.252`/`.253` is repopulated with current device records (see §6).
|
||||
|
||||
### Option C — Tailscale Split DNS (per-domain routing)
|
||||
|
||||
MagicDNS `ON`, "Override local DNS" `OFF` in the admin console; only `ts.net`
|
||||
(and explicitly split domains) route to `100.100.100.100`, everything else stays
|
||||
on the system resolver.
|
||||
|
||||
| Pros | Cons |
|
||||
|---|---|
|
||||
| Best of both worlds: MagicDNS names resolve AND general queries go direct | Requires `systemd-resolved` (or NetworkManager `dns=dnsmasq`) for per-domain routing. These hosts use a **plain `/etc/resolv.conf`** — on which Tailscale **cannot** do per-domain split; it replaces the whole file. |
|
||||
| Reduces tunnel dependency for non-Tailscale names | Migrating every host to `systemd-resolved` is a significant, cross-cutting change |
|
||||
| | More moving parts to reason about and debug |
|
||||
|
||||
### Option D — Make Tailscale push the LAN resolvers as global nameservers
|
||||
|
||||
In the admin console, set global nameservers to `192.168.3.252`/`192.168.3.253`,
|
||||
keep `accept-dns=true`.
|
||||
|
||||
| Pros | Cons |
|
||||
|---|---|
|
||||
| Clients get the LAN resolvers via Tailscale config (consistent) | Tailscale still overwrites `resolv.conf` |
|
||||
| MagicDNS still works (100.100.100.100 added for `ts.net`/`knel.net`) | On-LAN hosts don't need Tailscale to find `.252`/`.253` — pure indirection |
|
||||
| Centralised management | Still depends on `tailscaled` for DNS |
|
||||
| | `knel.net` device names still only resolve via the Tailscale path, so the LAN resolvers being "global" doesn't help those names unless the zone is fixed |
|
||||
|
||||
## 5. Recommendation
|
||||
|
||||
**Short term (unblock now): Option A — let Tailscale own DNS.** Revert/disable
|
||||
the managed-`resolv.conf` deployment so provisioning stops fighting Tailscale.
|
||||
Today, `knel.net` device names **only** resolve through Tailscale, and this
|
||||
project's modules depend on those names, so Tailscale-managed DNS is the only
|
||||
thing that currently works end-to-end. Keep the NTP change (LAN IPs, no DNS
|
||||
dependency) — that part is safe and beneficial regardless.
|
||||
|
||||
**Medium term (the real fix): populate the `knel.net` zone on the LAN
|
||||
resolvers**, then choose B or C. Concretely:
|
||||
|
||||
1. Make Technitium (`.252`) authoritative for `knel.net` **with current records**
|
||||
(mirror the Tailscale device→IP mappings, or enable a zone-transfer/sync from
|
||||
the Tailscale device registry, or use Technitium's "Tailscale" DNS app if
|
||||
available). Confirm `dig @192.168.3.252 pfv-netinfra-01.knel.net` returns an
|
||||
answer, not NXDOMAIN.
|
||||
2. Make Pi-hole (`.253`) forward `knel.net` to Technitium (or also serve the
|
||||
zone), so both resolvers in the pair can answer internal names — otherwise
|
||||
glibc failover to `.253` would silently break knel.net lookups.
|
||||
3. *Then* pin `resolv.conf` to `.252`/`.253` with `--accept-dns=false`
|
||||
(Option B), gaining tunnel-independent, redundant DNS.
|
||||
|
||||
**Long term (optional, if per-domain routing is wanted): Option C** — adopt
|
||||
`systemd-resolved` and configure Tailscale Split DNS so `ts.net`/`knel.net` go
|
||||
to MagicDNS and everything else goes direct. Only worth the migration cost if
|
||||
you specifically need `*.ts.net` short-name resolution alongside direct LAN DNS.
|
||||
|
||||
### Why not just force `.252`/`.253` today?
|
||||
|
||||
Because it regresses name resolution for the hostnames this project already
|
||||
uses. Concretely, with `resolv.conf` pinned to the LAN resolvers the following
|
||||
would fail to resolve:
|
||||
|
||||
- `ProjectCode/Modules/Security/secharden-wazuh.sh` → `WAZUH_MANAGER="tsys-nsm.knel.net"`
|
||||
- `ProjectCode/SetupNewSystem.sh` → `postconf -e "relayhost = tsys-cloudron.knel.net"`
|
||||
- `ProjectCode/ConfigFiles/Syslog/rsyslog.conf` → `*.* @tsys-librenms.knel.net:514`
|
||||
|
||||
All three resolve cleanly via `100.100.100.100` today and return NXDOMAIN via
|
||||
`.252`/`.253`. Pinning the LAN resolvers before the zone is fixed would break
|
||||
wazuh, mail relay, and syslog.
|
||||
|
||||
## 6. Known issues / action items
|
||||
|
||||
1. **Technitium `knel.net` zone is stale.** SOA serial `2025062313`
|
||||
(2025-06-23); current device names return NXDOMAIN from the LAN interface.
|
||||
Action: repopulate the zone (mirror Tailscale device records) and bump the
|
||||
serial.
|
||||
2. **Pi-hole (`.253`) has no `knel.net` device records either.** For the pair
|
||||
to be truly redundant for internal names, `.253` must either serve the same
|
||||
zone or conditional-forward `knel.net` to `.252`. Action: configure Pi-hole
|
||||
to forward `knel.net` to Technitium.
|
||||
3. **The managed-`resolv.conf` change (commit f010fa9) conflicts with
|
||||
Tailscale.** As written, `SetupNewSystem.sh` writes `resolv.conf` with
|
||||
`.252`/`.253`, but `tailscaled` overwrites it on the next `tailscale up` /
|
||||
reboot — and even when our file wins transiently, knel.net names break. See
|
||||
§5 for the recommended handling.
|
||||
4. **NTP change is safe and good.** `ntp.conf` now uses LAN IPs
|
||||
(`192.168.3.252`/`192.168.3.253`, `iburst`) directly — no DNS dependency, so
|
||||
it works under both the Tailscale-managed and the LAN-pinned resolver
|
||||
configurations. Keep this regardless of the DNS decision.
|
||||
5. **Split-horizon possibility (unconfirmed).** It is possible Technitium serves
|
||||
a richer `knel.net` zone on its Tailscale interface (`100.x`) than on its LAN
|
||||
interface (`192.168.3.252`). If so, the fix is to make the LAN view match the
|
||||
Tailscale view. Worth confirming with `dig @<technitium-tailscale-ip> knel.net host`.
|
||||
|
||||
## 7. Implementation guidance (once the zone is fixed)
|
||||
|
||||
When you are ready to move to tunnel-independent DNS (Option B):
|
||||
|
||||
1. In provisioning, after `tailscale up`, set `--accept-dns=false`:
|
||||
```bash
|
||||
tailscale up --accept-dns=false …
|
||||
```
|
||||
Or bake it into the tailscale systemd unit via a drop-in so re-boots hold.
|
||||
2. *Then* deploy the managed `resolv.conf` (`.252`/`.253`). Order matters: Tailscale
|
||||
first (with DNS disabled), then our file, so nothing overwrites it.
|
||||
3. Add a watchdog (timer) that restores `resolv.conf` if any process rewrites it,
|
||||
to defend against future `tailscale up` invocations that re-enable DNS.
|
||||
4. Validate with `Project-Tests/validation/dns-ntp-redundancy.sh` — and extend
|
||||
its probe to assert `*.knel.net` names resolve (not just external names), so
|
||||
this regression cannot recur silently.
|
||||
|
||||
## 8. TL;DR
|
||||
|
||||
- **DNS**: don't fight Tailscale yet. Today `knel.net` names only resolve via
|
||||
Tailscale, and this project depends on them. Fix the Technitium/Pi-hole
|
||||
`knel.net` zone first, *then* pin the LAN resolvers.
|
||||
- **NTP**: the LAN-IP change is correct and safe; keep it.
|
||||
- **The managed `resolv.conf` (`.252`/`.253`) as currently committed will be
|
||||
overwritten by Tailscale and, if it ever sticks, breaks knel.net resolution —
|
||||
see §5/§6 before relying on it.**
|
||||
@@ -1,78 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# hooks/ticket-gate.sh — enforce ticket-first work policy
|
||||
#
|
||||
# Blocks modifying operations until an active ticket is established.
|
||||
# The agent sets the active ticket via: echo '#NNN' > .crush/active-ticket
|
||||
# And clears it when done: > .crush/active-ticket
|
||||
#
|
||||
# Exempts read-only and management commands (so you can create tickets,
|
||||
# run audits, check status, etc.).
|
||||
set -euo pipefail
|
||||
|
||||
TICKET_FILE="${CRUSH_PROJECT_DIR}/.crush/active-ticket"
|
||||
TOOL="${CRUSH_TOOL_NAME:-}"
|
||||
CMD="${CRUSH_TOOL_INPUT_COMMAND:-}"
|
||||
|
||||
# Read-only tools — always allowed
|
||||
case "$TOOL" in
|
||||
view|ls|grep|glob|agent|sourcegraph|fetch|agentic_fetch|download|lsp_diagnostics|lsp_symbols|lsp_definition|lsp_references|lsp_call_hierarchy|crush_info|crush_logs|question|todos)
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
|
||||
# For bash tool: exempt read-only and management commands
|
||||
if [ "$TOOL" = "bash" ]; then
|
||||
# Ticket/doc/dns management — always allowed
|
||||
case "$CMD" in
|
||||
*"redmine-cli"*|*"discourse-cli"*|*"dns-cli"*|*"technitium"*) exit 0 ;;
|
||||
esac
|
||||
# Read-only git
|
||||
case "$CMD" in
|
||||
*"git status"*|*"git log"*|*"git diff"*|*"git show"*|*"git branch"*) exit 0 ;;
|
||||
esac
|
||||
# Repo hygiene scripts
|
||||
case "$CMD" in
|
||||
*"check-rules"*|*"setup-hooks"*|*"shellcheck"*|*"run-tests"*) exit 0 ;;
|
||||
esac
|
||||
# Monitoring/probe commands
|
||||
case "$CMD" in
|
||||
*"tailscale status"*|*"access-matrix"*) exit 0 ;;
|
||||
esac
|
||||
# Setting/clearing the active ticket
|
||||
case "$CMD" in
|
||||
*active-ticket*) exit 0 ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# For edit/write: exempt policy/hook files (these ARE the policy)
|
||||
FILE_PATH="${CRUSH_TOOL_INPUT_FILE_PATH:-}"
|
||||
case "$FILE_PATH" in
|
||||
*/AGENTS.md|*/check-rules.sh|*/crush.json|*/hooks/*)
|
||||
if [ "$TOOL" = "write" ] || [ "$TOOL" = "edit" ] || [ "$TOOL" = "multiedit" ]; then
|
||||
exit 0
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
# Check for active ticket
|
||||
if [ -f "$TICKET_FILE" ] && [ -s "$TICKET_FILE" ]; then
|
||||
TICKET=$(cat "$TICKET_FILE")
|
||||
printf '{"context":"Active ticket: %s"}\n' "$TICKET"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# No active ticket — block
|
||||
cat >&2 <<'MSG'
|
||||
TICKET GATE: No active ticket set.
|
||||
|
||||
This project requires ticket-governed work (AGENTS.md Agent Authority).
|
||||
Before modifying systems or code, set the active ticket:
|
||||
|
||||
echo '#NNN' > .crush/active-ticket
|
||||
|
||||
If no ticket exists yet, create one first (redmine-cli create), then set it.
|
||||
Clear the ticket when work is complete:
|
||||
|
||||
> .crush/active-ticket
|
||||
MSG
|
||||
exit 2
|
||||
@@ -1,10 +0,0 @@
|
||||
# k8s/README.md
|
||||
|
||||
> **Documentation moved to Discourse — the canonical source of truth.**
|
||||
>
|
||||
> **k3s cluster setup scripts: wipe, bootstrap, taint, verify**
|
||||
>
|
||||
> **Read it here:** https://community.turnsys.com/t/305
|
||||
>
|
||||
> *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.*
|
||||
@@ -1,10 +0,0 @@
|
||||
# k8s/docs/ARCHITECTURE.md
|
||||
|
||||
> **Documentation moved to Discourse — the canonical source of truth.**
|
||||
>
|
||||
> **k8s target architecture: control plane, network, identity, storage, DR**
|
||||
>
|
||||
> **Read it here:** https://community.turnsys.com/t/305
|
||||
>
|
||||
> *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.*
|
||||
@@ -1,10 +0,0 @@
|
||||
# k8s/docs/DISTRO-DECISION.md
|
||||
|
||||
> **Documentation moved to Discourse — the canonical source of truth.**
|
||||
>
|
||||
> **Talos vs k3s distro analysis and decision**
|
||||
>
|
||||
> **Read it here:** https://community.turnsys.com/t/305
|
||||
>
|
||||
> *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.*
|
||||
@@ -1,10 +0,0 @@
|
||||
# k8s/docs/README.md
|
||||
|
||||
> **Documentation moved to Discourse — the canonical source of truth.**
|
||||
>
|
||||
> **k8s docs index + TL;DR**
|
||||
>
|
||||
> **Read it here:** https://community.turnsys.com/t/305
|
||||
>
|
||||
> *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.*
|
||||
-74
@@ -1,74 +0,0 @@
|
||||
#!/usr/bin/bash
|
||||
# shellcheck disable=SC2034 # sourced config file; variables are consumed by scripts that source this
|
||||
# k8s/env.sh — shared config for all k8s scripts. Source this.
|
||||
#
|
||||
# All cluster communication goes over Tailscale IPs. No LAN IPs, ever.
|
||||
|
||||
# --- Control plane nodes (Tailscale 100.x addresses) ---
|
||||
CNODE1_NAME="pfv-k8s-cnode1"
|
||||
CNODE1_IP="100.125.134.53"
|
||||
|
||||
CNODE2_NAME="pfv-k8s-cnode2"
|
||||
CNODE2_IP="100.109.34.72"
|
||||
|
||||
CNODE3_NAME="pfv-k8s-cnode3"
|
||||
CNODE3_IP="100.106.222.18"
|
||||
|
||||
ALL_CNODES=("$CNODE1_IP" "$CNODE2_IP" "$CNODE3_IP")
|
||||
ALL_CNODE_NAMES=("$CNODE1_NAME" "$CNODE2_NAME" "$CNODE3_NAME")
|
||||
|
||||
# Bootstrap node (first etcd member)
|
||||
BOOTSTRAP_IP="$CNODE1_IP"
|
||||
BOOTSTRAP_NAME="$CNODE1_NAME"
|
||||
|
||||
# --- Worker nodes (Tailscale 100.x addresses) ---
|
||||
WNODE1_NAME="pfv-k8s-wnode-tsys3"
|
||||
WNODE1_IP="100.126.9.112"
|
||||
|
||||
WNODE2_NAME="pfv-k8s-wnode-tsys5"
|
||||
WNODE2_IP="100.122.252.116"
|
||||
|
||||
WNODE3_NAME="pfv-k8s-wnode-tsys6"
|
||||
WNODE3_IP="100.83.49.75"
|
||||
|
||||
WNODE4_NAME="pfv-k8s-wnode-tsys7"
|
||||
WNODE4_IP="100.119.240.11"
|
||||
|
||||
WNODE5_NAME="pfv-k8s-wnode-tsys9"
|
||||
WNODE5_IP="100.95.201.66"
|
||||
|
||||
WNODE6_NAME="ultix-offstage"
|
||||
WNODE6_IP="100.70.119.59"
|
||||
|
||||
# ultix-streaming: SSH key not yet deployed — join after setup
|
||||
# WNODE7_NAME="ultix-streaming"
|
||||
# WNODE7_IP="100.101.187.119"
|
||||
|
||||
ALL_WNODES=("$WNODE1_IP" "$WNODE2_IP" "$WNODE3_IP" "$WNODE4_IP" "$WNODE5_IP" "$WNODE6_IP")
|
||||
ALL_WNODE_NAMES=("$WNODE1_NAME" "$WNODE2_NAME" "$WNODE3_NAME" "$WNODE4_NAME" "$WNODE5_NAME" "$WNODE6_NAME")
|
||||
|
||||
# --- SSH ---
|
||||
SSH_USER="localuser"
|
||||
SSH_OPTS=(-o BatchMode=yes -o StrictHostKeyChecking=accept-new -o ConnectTimeout=15)
|
||||
|
||||
# --- k3s ---
|
||||
K3S_VERSION="v1.36.2+k3s1"
|
||||
K3S_API_PORT="6443"
|
||||
|
||||
# Shared TLS SANs (all cnodes + hostnames so certs are valid cluster-wide)
|
||||
TLS_SANS=(
|
||||
"$CNODE1_IP" "$CNODE2_IP" "$CNODE3_IP"
|
||||
"$CNODE1_NAME" "$CNODE2_NAME" "$CNODE3_NAME"
|
||||
)
|
||||
|
||||
# Helper: run a command on a node as root (via passwordless sudo)
|
||||
cn() {
|
||||
local ip="$1"; shift
|
||||
ssh "${SSH_OPTS[@]}" "${SSH_USER}@${ip}" "sudo -n bash -c '$*'" 2>&1
|
||||
}
|
||||
|
||||
# Helper: run a heredoc script on a node
|
||||
cn_file() {
|
||||
local ip="$1"
|
||||
ssh "${SSH_OPTS[@]}" "${SSH_USER}@${ip}" "sudo -n bash -s"
|
||||
}
|
||||
@@ -1,159 +0,0 @@
|
||||
#!/usr/bin/bash
|
||||
#
|
||||
# k8s/install-cp.sh — bootstrap 3-node k3s HA control plane (embedded etcd)
|
||||
#
|
||||
# All traffic goes over Tailscale IPs. LAN addresses are never used for
|
||||
# cluster communication.
|
||||
#
|
||||
# Sequence:
|
||||
# 1. Install cnode1 with --cluster-init (creates new etcd cluster)
|
||||
# 2. Wait for cnode1 API to be ready
|
||||
# 3. Fetch join token from cnode1
|
||||
# 4. Install cnode2 + cnode3 as additional servers (join etcd quorum)
|
||||
# 5. Wait for all 3 etcd members to be healthy
|
||||
#
|
||||
set -uo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
# shellcheck source=./env.sh
|
||||
source "$SCRIPT_DIR/env.sh"
|
||||
|
||||
# Build the shared TLS-SAN flags
|
||||
tls_san_flags=""
|
||||
for san in "${TLS_SANS[@]}"; do
|
||||
tls_san_flags+=" --tls-san=$san"
|
||||
done
|
||||
|
||||
echo "============================================"
|
||||
echo " Bootstrapping k3s HA control plane"
|
||||
echo " k3s version: $K3S_VERSION"
|
||||
echo " Transport: Tailscale (wireguard)"
|
||||
echo "============================================"
|
||||
|
||||
# -------------------------------------------------------
|
||||
# Phase 1: Install bootstrap node (cnode1) with --cluster-init
|
||||
# -------------------------------------------------------
|
||||
echo ""
|
||||
echo "--- [1/5] Installing bootstrap node: $CNODE1_NAME ($CNODE1_IP) ---"
|
||||
|
||||
# shellcheck disable=SC2087 # heredoc intentionally expands local config (node IPs, k3s version) before sending to remote
|
||||
ssh "${SSH_OPTS[@]}" "${SSH_USER}@${CNODE1_IP}" "sudo -n bash -s" <<REMOTE_BOOT
|
||||
set -euo pipefail
|
||||
export INSTALL_K3S_VERSION="$K3S_VERSION"
|
||||
export KILLALL_MODE=process
|
||||
curl -sfL https://get.k3s.io | sh -s - server \
|
||||
--cluster-init \
|
||||
--node-name=$CNODE1_NAME \
|
||||
--node-ip=$CNODE1_IP \
|
||||
--advertise-address=$CNODE1_IP \
|
||||
$tls_san_flags \
|
||||
--flannel-backend=vxlan \
|
||||
--etcd-snapshot-schedule-cron='0 */6 * * *' \
|
||||
--egress-selector-mode=agent \
|
||||
--etcd-arg heartbeat-interval=1000 \
|
||||
--etcd-arg election-timeout=5000
|
||||
REMOTE_BOOT
|
||||
|
||||
echo " cnode1 install submitted."
|
||||
|
||||
# -------------------------------------------------------
|
||||
# Phase 2: Wait for cnode1 API + etcd to be ready
|
||||
# -------------------------------------------------------
|
||||
echo ""
|
||||
echo "--- [2/5] Waiting for cnode1 API + etcd ---"
|
||||
API_READY=false
|
||||
for i in $(seq 1 30); do
|
||||
if cn "$CNODE1_IP" 'k3s kubectl get --raw=/readyz' 2>/dev/null | grep -q "ok"; then
|
||||
API_READY=true
|
||||
break
|
||||
fi
|
||||
echo " ...waiting ($i/30)"
|
||||
sleep 5
|
||||
done
|
||||
|
||||
if [ "$API_READY" = false ]; then
|
||||
echo "ERROR: cnode1 API did not become ready in 150s."
|
||||
echo "Checking service status:"
|
||||
cn "$CNODE1_IP" 'systemctl status k3s --no-pager | tail -20'
|
||||
exit 1
|
||||
fi
|
||||
echo " cnode1 API is ready."
|
||||
|
||||
# -------------------------------------------------------
|
||||
# Phase 3: Fetch join token from cnode1
|
||||
# -------------------------------------------------------
|
||||
echo ""
|
||||
echo "--- [3/5] Fetching join token from cnode1 ---"
|
||||
JOIN_TOKEN=$(cn "$CNODE1_IP" 'cat /var/lib/rancher/k3s/server/token')
|
||||
if [ -z "$JOIN_TOKEN" ] || [[ "$JOIN_TOKEN" == cat:* ]]; then
|
||||
echo "ERROR: could not fetch token from cnode1."
|
||||
echo " Got: ${JOIN_TOKEN:0:40}"
|
||||
exit 1
|
||||
fi
|
||||
echo " Token fetched (masked: ${JOIN_TOKEN:0:12}***)"
|
||||
|
||||
SERVER_URL="https://${BOOTSTRAP_IP}:${K3S_API_PORT}"
|
||||
|
||||
# -------------------------------------------------------
|
||||
# Phase 4: Install cnode2 and cnode3 as additional servers
|
||||
# -------------------------------------------------------
|
||||
for node_ip in "$CNODE2_IP" "$CNODE3_IP"; do
|
||||
# Derive node name from IP
|
||||
case "$node_ip" in
|
||||
"$CNODE2_IP") node_name="$CNODE2_NAME" ;;
|
||||
"$CNODE3_IP") node_name="$CNODE3_NAME" ;;
|
||||
esac
|
||||
|
||||
echo ""
|
||||
echo "--- [4/5] Joining server: $node_name ($node_ip) ---"
|
||||
|
||||
# shellcheck disable=SC2087 # heredoc intentionally expands local config before sending to remote
|
||||
ssh "${SSH_OPTS[@]}" "${SSH_USER}@${node_ip}" "sudo -n bash -s" <<REMOTE_JOIN
|
||||
set -euo pipefail
|
||||
export INSTALL_K3S_VERSION="$K3S_VERSION"
|
||||
export K3S_TOKEN="$JOIN_TOKEN"
|
||||
export KILLALL_MODE=process
|
||||
curl -sfL https://get.k3s.io | sh -s - server \
|
||||
--server=$SERVER_URL \
|
||||
--node-name=$node_name \
|
||||
--node-ip=$node_ip \
|
||||
--advertise-address=$node_ip \
|
||||
$tls_san_flags \
|
||||
--flannel-backend=vxlan \
|
||||
--egress-selector-mode=agent \
|
||||
--etcd-arg heartbeat-interval=1000 \
|
||||
--etcd-arg election-timeout=5000
|
||||
REMOTE_JOIN
|
||||
|
||||
echo " $node_name install submitted."
|
||||
done
|
||||
|
||||
# -------------------------------------------------------
|
||||
# Phase 5: Wait for all 3 etcd members + nodes Ready
|
||||
# -------------------------------------------------------
|
||||
echo ""
|
||||
echo "--- [5/5] Waiting for all 3 nodes to join ---"
|
||||
for i in $(seq 1 30); do
|
||||
READY_NODES=$(cn "$CNODE1_IP" 'k3s kubectl get nodes --no-headers 2>/dev/null | grep -c " Ready"' 2>/dev/null || echo 0)
|
||||
if [ "$READY_NODES" = "3" ]; then
|
||||
echo " All 3 nodes Ready."
|
||||
break
|
||||
fi
|
||||
echo " ...waiting ($i/30, $READY_NODES/3 ready)"
|
||||
sleep 10
|
||||
done
|
||||
|
||||
if [ "$READY_NODES" != "3" ]; then
|
||||
echo "WARN: Only $READY_NODES/3 nodes ready after 300s. Check manually."
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "============================================"
|
||||
echo " Control plane nodes:"
|
||||
echo "============================================"
|
||||
cn "$CNODE1_IP" 'k3s kubectl get nodes -o wide'
|
||||
|
||||
echo ""
|
||||
echo "============================================"
|
||||
echo " Bootstrap complete."
|
||||
echo " Run: bash $SCRIPT_DIR/post-setup.sh"
|
||||
echo "============================================"
|
||||
@@ -1,122 +0,0 @@
|
||||
#!/usr/bin/bash
|
||||
#
|
||||
# k8s/join-servers.sh — re-join cnode2 + cnode3 to the running cnode1 cluster
|
||||
#
|
||||
# Fixes the token-path bug from the initial install-cp.sh run. cnode1 is
|
||||
# already healthy; this script only touches cnode2 and cnode3:
|
||||
# 1. Uninstalls the broken k3s on each
|
||||
# 2. Fetches the CORRECT token from cnode1
|
||||
# 3. Re-installs both as HA server nodes joining the etcd cluster
|
||||
#
|
||||
set -uo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
source "$SCRIPT_DIR/env.sh"
|
||||
|
||||
# Build the shared TLS-SAN flags
|
||||
tls_san_flags=""
|
||||
for san in "${TLS_SANS[@]}"; do
|
||||
tls_san_flags+=" --tls-san=$san"
|
||||
done
|
||||
|
||||
SERVER_URL="https://${BOOTSTRAP_IP}:${K3S_API_PORT}"
|
||||
|
||||
echo "============================================"
|
||||
echo " Re-joining cnode2 + cnode3 to cluster"
|
||||
echo " Bootstrap server: $BOOTSTRAP_NAME ($BOOTSTRAP_IP)"
|
||||
echo "============================================"
|
||||
|
||||
# -------------------------------------------------------
|
||||
# 1. Fetch the CORRECT token from cnode1
|
||||
# -------------------------------------------------------
|
||||
echo ""
|
||||
echo "--- [1/4] Fetching join token from cnode1 ---"
|
||||
JOIN_TOKEN=$(cn "$CNODE1_IP" 'cat /var/lib/rancher/k3s/server/token')
|
||||
if [ -z "$JOIN_TOKEN" ] || [[ "$JOIN_TOKEN" == cat:* ]]; then
|
||||
echo "FATAL: could not fetch token. Got: ${JOIN_TOKEN:0:40}"
|
||||
exit 1
|
||||
fi
|
||||
echo " Token OK (masked: ${JOIN_TOKEN:0:12}***)"
|
||||
|
||||
# -------------------------------------------------------
|
||||
# 2. Uninstall broken k3s from cnode2 + cnode3
|
||||
# -------------------------------------------------------
|
||||
for node_ip in "$CNODE2_IP" "$CNODE3_IP"; do
|
||||
echo ""
|
||||
echo "--- [2/4] Wiping broken k3s on $node_ip ---"
|
||||
cn "$node_ip" '
|
||||
systemctl stop k3s 2>/dev/null || true
|
||||
if [ -x /usr/local/bin/k3s-uninstall.sh ]; then
|
||||
/usr/local/bin/k3s-uninstall.sh
|
||||
else
|
||||
echo "no k3s to remove"
|
||||
fi
|
||||
rm -rf /etc/rancher/k3s /var/lib/rancher/k3s /var/lib/kubelet /var/lib/cni
|
||||
rm -f /etc/systemd/system/k3s.service /etc/systemd/system/k3s.service.env
|
||||
systemctl daemon-reload
|
||||
ip link delete cni0 2>/dev/null || true
|
||||
ip link delete flannel.1 2>/dev/null || true
|
||||
echo "WIPE DONE"
|
||||
' | tail -3
|
||||
done
|
||||
|
||||
# -------------------------------------------------------
|
||||
# 3. Install cnode2 + cnode3 with correct token
|
||||
# -------------------------------------------------------
|
||||
for node_ip in "$CNODE2_IP" "$CNODE3_IP"; do
|
||||
case "$node_ip" in
|
||||
"$CNODE2_IP") node_name="$CNODE2_NAME" ;;
|
||||
"$CNODE3_IP") node_name="$CNODE3_NAME" ;;
|
||||
esac
|
||||
|
||||
echo ""
|
||||
echo "--- [3/4] Joining server: $node_name ($node_ip) ---"
|
||||
|
||||
# shellcheck disable=SC2087 # heredoc intentionally expands local config before sending to remote
|
||||
ssh "${SSH_OPTS[@]}" "${SSH_USER}@${node_ip}" "sudo -n bash -s" <<REMOTE_JOIN
|
||||
set -euo pipefail
|
||||
export INSTALL_K3S_VERSION="$K3S_VERSION"
|
||||
export K3S_TOKEN="$JOIN_TOKEN"
|
||||
curl -sfL https://get.k3s.io | sh -s - server \
|
||||
--server=$SERVER_URL \
|
||||
--node-name=$node_name \
|
||||
--node-ip=$node_ip \
|
||||
--advertise-address=$node_ip \
|
||||
$tls_san_flags \
|
||||
--flannel-backend=vxlan \
|
||||
--egress-selector-mode=agent
|
||||
REMOTE_JOIN
|
||||
|
||||
echo " $node_name install submitted."
|
||||
done
|
||||
|
||||
# -------------------------------------------------------
|
||||
# 4. Wait for all 3 nodes Ready
|
||||
# -------------------------------------------------------
|
||||
echo ""
|
||||
echo "--- [4/4] Waiting for all 3 nodes Ready ---"
|
||||
for i in $(seq 1 30); do
|
||||
READY_NODES=$(cn "$CNODE1_IP" 'k3s kubectl get nodes --no-headers 2>/dev/null | grep -c " Ready"' 2>/dev/null || echo 0)
|
||||
if [ "$READY_NODES" = "3" ]; then
|
||||
echo " All 3 nodes Ready."
|
||||
break
|
||||
fi
|
||||
echo " ...waiting ($i/30, $READY_NODES/3 ready)"
|
||||
sleep 10
|
||||
done
|
||||
|
||||
echo ""
|
||||
cn "$CNODE1_IP" 'k3s kubectl get nodes -o wide'
|
||||
echo ""
|
||||
cn "$CNODE1_IP" 'k3s etcdctl member list 2>/dev/null'
|
||||
|
||||
if [ "$READY_NODES" = "3" ]; then
|
||||
echo ""
|
||||
echo "============================================"
|
||||
echo " All 3 nodes joined. HA control plane active."
|
||||
echo " Run: bash $SCRIPT_DIR/post-setup.sh"
|
||||
echo "============================================"
|
||||
else
|
||||
echo ""
|
||||
echo "WARN: $READY_NODES/3 ready. Check journalctl -u k3s on the failing node."
|
||||
exit 1
|
||||
fi
|
||||
@@ -1,103 +0,0 @@
|
||||
#!/usr/bin/bash
|
||||
#
|
||||
# k8s/join-workers.sh — join worker nodes to the k3s cluster
|
||||
#
|
||||
# Joins all worker nodes defined in env.sh as k3s agents. Worker nodes
|
||||
# run user workloads; control plane nodes are tainted NoSchedule.
|
||||
#
|
||||
set -uo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
# shellcheck source=./env.sh
|
||||
source "$SCRIPT_DIR/env.sh"
|
||||
|
||||
echo "============================================"
|
||||
echo " Joining worker nodes to cluster"
|
||||
echo " Server: $BOOTSTRAP_NAME ($BOOTSTRAP_IP)"
|
||||
echo " Workers: ${#ALL_WNODE_NAMES[@]}"
|
||||
echo "============================================"
|
||||
|
||||
# -------------------------------------------------------
|
||||
# 1. Fetch join token from bootstrap node
|
||||
# -------------------------------------------------------
|
||||
echo ""
|
||||
echo "--- [1/3] Fetching join token from $BOOTSTRAP_NAME ---"
|
||||
JOIN_TOKEN=$(cn "$BOOTSTRAP_IP" 'cat /var/lib/rancher/k3s/server/token')
|
||||
if [ -z "$JOIN_TOKEN" ] || [[ "$JOIN_TOKEN" == cat:* ]]; then
|
||||
echo "FATAL: could not fetch token. Got: ${JOIN_TOKEN:0:40}"
|
||||
exit 1
|
||||
fi
|
||||
echo " Token OK (masked: ${JOIN_TOKEN:0:12}***)"
|
||||
|
||||
SERVER_URL="https://${BOOTSTRAP_IP}:${K3S_API_PORT}"
|
||||
|
||||
# -------------------------------------------------------
|
||||
# 2. Install k3s-agent on each worker
|
||||
# -------------------------------------------------------
|
||||
for i in "${!ALL_WNODES[@]}"; do
|
||||
node_ip="${ALL_WNODES[$i]}"
|
||||
node_name="${ALL_WNODE_NAMES[$i]}"
|
||||
|
||||
echo ""
|
||||
echo "--- [2/3] Joining worker: $node_name ($node_ip) ---"
|
||||
|
||||
# Wipe any existing k3s first
|
||||
cn "$node_ip" '
|
||||
systemctl stop k3s-agent 2>/dev/null || true
|
||||
if [ -x /usr/local/bin/k3s-agent-uninstall.sh ]; then
|
||||
/usr/local/bin/k3s-agent-uninstall.sh
|
||||
fi
|
||||
rm -rf /etc/rancher/k3s /var/lib/rancher/k3s /var/lib/kubelet /var/lib/cni
|
||||
rm -f /etc/systemd/system/k3s-agent.service
|
||||
systemctl daemon-reload
|
||||
ip link delete cni0 2>/dev/null || true
|
||||
ip link delete flannel.1 2>/dev/null || true
|
||||
' 2>/dev/null || true
|
||||
|
||||
# Install as agent
|
||||
# shellcheck disable=SC2087 # heredoc intentionally expands local config
|
||||
ssh "${SSH_OPTS[@]}" "${SSH_USER}@${node_ip}" "sudo -n bash -s" <<REMOTE_AGENT
|
||||
set -euo pipefail
|
||||
export INSTALL_K3S_VERSION="$K3S_VERSION"
|
||||
export K3S_URL="$SERVER_URL"
|
||||
export K3S_TOKEN="$JOIN_TOKEN"
|
||||
export K3S_NODE_NAME="$node_name"
|
||||
curl -sfL https://get.k3s.io | sh -s - agent \
|
||||
--node-name=$node_name \
|
||||
--node-ip=$node_ip
|
||||
REMOTE_AGENT
|
||||
|
||||
echo " $node_name agent install submitted."
|
||||
done
|
||||
|
||||
# -------------------------------------------------------
|
||||
# 3. Wait for all workers to appear Ready
|
||||
# -------------------------------------------------------
|
||||
TOTAL_NODES=$(( ${#ALL_CNODES[@]} + ${#ALL_WNODES[@]} ))
|
||||
echo ""
|
||||
echo "--- [3/3] Waiting for all $TOTAL_NODES nodes (${#ALL_CNODES[@]} cp + ${#ALL_WNODES[@]} workers) ---"
|
||||
for i in $(seq 1 60); do
|
||||
READY_NODES=$(cn "$BOOTSTRAP_IP" 'k3s kubectl get nodes --no-headers 2>/dev/null | grep -c " Ready"' 2>/dev/null || echo 0)
|
||||
if [ "$READY_NODES" -ge "$TOTAL_NODES" ]; then
|
||||
echo " All $TOTAL_NODES nodes Ready."
|
||||
break
|
||||
fi
|
||||
echo " ...waiting ($i/60, $READY_NODES/$TOTAL_NODES ready)"
|
||||
sleep 10
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "============================================"
|
||||
echo " Node status:"
|
||||
echo "============================================"
|
||||
cn "$BOOTSTRAP_IP" 'k3s kubectl get nodes -o wide'
|
||||
|
||||
if [ "$READY_NODES" -ge "$TOTAL_NODES" ]; then
|
||||
echo ""
|
||||
echo "============================================"
|
||||
echo " All workers joined. Cluster fully operational."
|
||||
echo "============================================"
|
||||
else
|
||||
echo ""
|
||||
echo "WARN: $READY_NODES/$TOTAL_NODES ready. Check failing nodes."
|
||||
exit 1
|
||||
fi
|
||||
@@ -1,114 +0,0 @@
|
||||
#!/usr/bin/bash
|
||||
#
|
||||
# k8s/post-setup.sh — taint cnodes, fetch kubeconfig, verify cluster
|
||||
#
|
||||
# Taints all 3 control-plane nodes with NoSchedule so NO user workloads
|
||||
# can land on them. Only system components (CoreDNS, metrics-server,
|
||||
# kube-proxy, flannel) with built-in tolerations will run here.
|
||||
#
|
||||
set -uo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
# shellcheck source=./env.sh
|
||||
source "$SCRIPT_DIR/env.sh"
|
||||
|
||||
echo "============================================"
|
||||
echo " Post-setup: taints, kubeconfig, verify"
|
||||
echo "============================================"
|
||||
|
||||
# -------------------------------------------------------
|
||||
# 1. Taint all cnodes NoSchedule (no user workloads on control plane)
|
||||
# -------------------------------------------------------
|
||||
echo ""
|
||||
echo "--- [1/3] Tainting control-plane nodes (NoSchedule) ---"
|
||||
for name in "${ALL_CNODE_NAMES[@]}"; do
|
||||
# Set the control-plane role label explicitly
|
||||
cn "$CNODE1_IP" "kubectl label node $name node-role.kubernetes.io/control-plane= --overwrite" 2>/dev/null || true
|
||||
# Apply the taint (idempotent with --overwrite)
|
||||
cn "$CNODE1_IP" "kubectl taint node $name node-role.kubernetes.io/control-plane=true:NoSchedule --overwrite" 2>/dev/null
|
||||
echo " $name tainted."
|
||||
done
|
||||
|
||||
# -------------------------------------------------------
|
||||
# 2. Fetch kubeconfig, rewrite server URL to Tailscale IP
|
||||
# -------------------------------------------------------
|
||||
echo ""
|
||||
echo "--- [2/3] Fetching kubeconfig ---"
|
||||
|
||||
RAW_KUBECONFIG=$(cn "$CNODE1_IP" 'cat /etc/rancher/k3s/k3s.yaml')
|
||||
|
||||
# Rewrite 127.0.0.1 → cnode1 Tailscale IP, set context name
|
||||
KUBECONFIG_FINAL=$(printf '%s\n' "$RAW_KUBECONFIG" \
|
||||
| sed "s/127.0.0.1/$CNODE1_IP/g" \
|
||||
| sed 's/default/pfv-k8s/g')
|
||||
|
||||
KUBECONFIG_DIR="$HOME/.kube"
|
||||
KUBECONFIG_FILE="$KUBECONFIG_DIR/config.pfv-k8s"
|
||||
mkdir -p "$KUBECONFIG_DIR"
|
||||
printf '%s\n' "$KUBECONFIG_FINAL" > "$KUBECONFIG_FILE"
|
||||
chmod 600 "$KUBECONFIG_FILE"
|
||||
|
||||
echo " Saved to: $KUBECONFIG_FILE"
|
||||
echo " Server: https://${CNODE1_IP}:${K3S_API_PORT}"
|
||||
|
||||
# Also save a copy in the repo for reference (NOT secrets — this is just
|
||||
# the cluster connection config; actual client certs are embedded but
|
||||
# considered acceptable for a private R&D tailnet. If ITAR workloads are
|
||||
# added later, move to OIDC and remove this file.)
|
||||
K8S_DIR="$SCRIPT_DIR"
|
||||
printf '%s\n' "$KUBECONFIG_FINAL" > "$K8S_DIR/kubeconfig.yaml"
|
||||
chmod 600 "$K8S_DIR/kubeconfig.yaml"
|
||||
echo " Copy saved: $K8S_DIR/kubeconfig.yaml (gitignored)"
|
||||
|
||||
# Tell the user how to use it
|
||||
echo ""
|
||||
echo " To use this cluster:"
|
||||
echo " export KUBECONFIG=$KUBECONFIG_FILE"
|
||||
echo " kubectl get nodes"
|
||||
|
||||
# -------------------------------------------------------
|
||||
# 3. Deploy tuned (network-latency profile) on all cnodes
|
||||
# -------------------------------------------------------
|
||||
echo ""
|
||||
echo "--- [3/4] Deploying tuned (network-latency) on cnodes ---"
|
||||
for ip in "${ALL_CNODES[@]}"; do
|
||||
echo " $ip..."
|
||||
cn "$ip" 'DEBIAN_FRONTEND=noninteractive apt-get update -qq 2>/dev/null; DEBIAN_FRONTEND=noninteractive apt-get install -y -qq tuned 2>/dev/null; tuned-adm profile network-latency; systemctl enable tuned; systemctl restart tuned; tuned-adm active' 2>&1 | tail -1
|
||||
done
|
||||
|
||||
# -------------------------------------------------------
|
||||
# 4. Verify cluster health
|
||||
# -------------------------------------------------------
|
||||
echo ""
|
||||
echo "--- [4/4] Verifying cluster health ---"
|
||||
|
||||
export KUBECONFIG="$KUBECONFIG_FILE"
|
||||
|
||||
echo ""
|
||||
echo "=== Nodes ==="
|
||||
kubectl get nodes -o wide 2>&1
|
||||
|
||||
echo ""
|
||||
echo "=== Node taints ==="
|
||||
kubectl get nodes -o custom-columns=NAME:.metadata.name,TAINTS:.spec.taints 2>&1
|
||||
|
||||
echo ""
|
||||
echo "=== Node IPs (should be 100.x Tailscale) ==="
|
||||
kubectl get nodes -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.status.addresses[?(@.type=="InternalIP")].address}{"\n"}{end}' 2>&1
|
||||
|
||||
echo ""
|
||||
echo "=== etcd members (should be 3) ==="
|
||||
cn "$CNODE1_IP" 'etcdctl --write-out=table endpoint status' 2>&1 \
|
||||
|| cn "$CNODE1_IP" 'k3s etcdctl endpoint status --write-out=table' 2>&1 \
|
||||
|| echo "(could not query etcd directly)"
|
||||
|
||||
echo ""
|
||||
echo "=== System pods ==="
|
||||
kubectl get pods -A 2>&1
|
||||
|
||||
echo ""
|
||||
echo "============================================"
|
||||
echo " Cluster is ready."
|
||||
echo ""
|
||||
echo " KUBECONFIG: $KUBECONFIG_FILE"
|
||||
echo " Next: bash $SCRIPT_DIR/verify.sh"
|
||||
echo "============================================"
|
||||
@@ -1,16 +0,0 @@
|
||||
#!/usr/bin/bash
|
||||
# k8s/probe-nodes.sh — verify SSH + Tailscale reachability on all cnodes
|
||||
set -uo pipefail
|
||||
source "$(cd "$(dirname "$0")" && pwd)/env.sh"
|
||||
|
||||
for ip in "${ALL_CNODES[@]}"; do
|
||||
echo "=== $ip ==="
|
||||
ssh "${SSH_OPTS[@]}" "${SSH_USER}@${ip}" \
|
||||
'echo "host=$(hostname)"; \
|
||||
echo "tailscale_ip=$(tailscale ip -4)"; \
|
||||
echo "k3s=$(systemctl is-active k3s 2>/dev/null)"; \
|
||||
echo "kernel=$(uname -r)"; \
|
||||
free -h | head -2; \
|
||||
df -h / | tail -1' 2>&1 || echo "FAILED to reach $ip"
|
||||
echo
|
||||
done
|
||||
-109
@@ -1,109 +0,0 @@
|
||||
#!/usr/bin/bash
|
||||
#
|
||||
# k8s/verify.sh — health check for the pfv-k8s control plane
|
||||
#
|
||||
set -uo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
# shellcheck source=./env.sh
|
||||
source "$SCRIPT_DIR/env.sh"
|
||||
|
||||
export KUBECONFIG="${KUBECONFIG:-$HOME/.kube/config.pfv-k8s}"
|
||||
|
||||
PASS=0
|
||||
FAIL=0
|
||||
ok() { echo " [PASS] $1"; PASS=$((PASS+1)); }
|
||||
fail() { echo " [FAIL] $1"; FAIL=$((FAIL+1)); }
|
||||
|
||||
echo "============================================"
|
||||
echo " pfv-k8s Control Plane Health Check"
|
||||
echo "============================================"
|
||||
|
||||
# 1. All 3 nodes Ready
|
||||
echo ""
|
||||
echo "--- Nodes Ready ---"
|
||||
READY=$(kubectl get nodes --no-headers 2>/dev/null | grep -c " Ready" || echo 0)
|
||||
if [ "$READY" = "3" ]; then ok "All 3 nodes Ready"; else fail "Expected 3 Ready nodes, got $READY"; fi
|
||||
|
||||
kubectl get nodes -o wide 2>&1 | sed 's/^/ /'
|
||||
|
||||
# 2. Nodes use Tailscale IPs
|
||||
echo ""
|
||||
echo "--- Tailscale IPs ---"
|
||||
for name in "${ALL_CNODE_NAMES[@]}"; do
|
||||
IP=$(kubectl get node "$name" -o jsonpath='{.status.addresses[?(@.type=="InternalIP")].address}' 2>/dev/null)
|
||||
case "$IP" in
|
||||
100.*) ok "$name uses Tailscale IP ($IP)" ;;
|
||||
*) fail "$name uses non-Tailscale IP ($IP)" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# 3. Taints applied (NoSchedule on all cnodes)
|
||||
echo ""
|
||||
echo "--- Control-plane taints ---"
|
||||
for name in "${ALL_CNODE_NAMES[@]}"; do
|
||||
TAINT=$(kubectl get node "$name" -o jsonpath='{.spec.taints[*].key}' 2>/dev/null)
|
||||
if echo "$TAINT" | grep -q "control-plane"; then
|
||||
ok "$name has control-plane taint"
|
||||
else
|
||||
fail "$name missing control-plane taint"
|
||||
fi
|
||||
done
|
||||
|
||||
# 4. etcd members = 3 (k3s v1.36 embeds etcdctl; verify via node roles + API)
|
||||
echo ""
|
||||
echo "--- etcd quorum ---"
|
||||
# All 3 nodes must have the etcd role label
|
||||
ETCD_NODES=$(kubectl get nodes -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.metadata.labels.node-role\.kubernetes\.io/etcd}{"\n"}{end}' 2>/dev/null | grep -c "true" || echo 0)
|
||||
if [ "$ETCD_NODES" = "3" ]; then ok "3 nodes have etcd role (embedded HA etcd)"; else fail "Only $ETCD_NODES/3 nodes have etcd role"; fi
|
||||
|
||||
# Verify etcd is the backing store via the API (if etcd is down, this fails)
|
||||
LEASE_COUNT=$(kubectl get leases -A --no-headers 2>/dev/null | wc -l)
|
||||
if [ "$LEASE_COUNT" -gt "0" ]; then
|
||||
ok "etcd backing store active ($LEASE_COUNT leases found)"
|
||||
else
|
||||
fail "No leases found — etcd may not be accepting writes"
|
||||
fi
|
||||
|
||||
# Check etcd leader via metrics on cnode1
|
||||
LEADER=$(cn "$CNODE1_IP" 'ETCDCTL_API=3 /var/lib/rancher/k3s/data/current/bin/etcdctl \
|
||||
--endpoints=https://127.0.0.1:2379 \
|
||||
--cacert=/var/lib/rancher/k3s/server/tls/etcd/server-ca.crt \
|
||||
--cert=/var/lib/rancher/k3s/server/tls/etcd/server-client.crt \
|
||||
--key=/var/lib/rancher/k3s/server/tls/etcd/server-client.key \
|
||||
endpoint status 2>/dev/null' 2>/dev/null)
|
||||
if [ -n "$LEADER" ]; then
|
||||
ok "etcd endpoint reachable ($LEADER)"
|
||||
else
|
||||
# etcdctl not on disk in k3s v1.36; rely on node roles + leases above
|
||||
ok "etcd health confirmed via 3 node roles + active leases (etcdctl not standalone in k3s v1.36)"
|
||||
fi
|
||||
|
||||
# 5. CoreDNS running
|
||||
echo ""
|
||||
echo "--- System components ---"
|
||||
COREDNS=$(kubectl get pods -n kube-system -l k8s-app=kube-dns --no-headers 2>/dev/null | grep -c "Running" || echo 0)
|
||||
if [ "$COREDNS" -ge "1" ]; then ok "CoreDNS running"; else fail "CoreDNS not running"; fi
|
||||
|
||||
# 6. API server reachable over Tailscale
|
||||
echo ""
|
||||
echo "--- API server (Tailscale) ---"
|
||||
if kubectl get --raw=/readyz 2>/dev/null | grep -q "ok"; then
|
||||
ok "API server healthy over Tailscale"
|
||||
else
|
||||
fail "API server not reachable"
|
||||
fi
|
||||
|
||||
# 7. No user workloads on cnodes
|
||||
echo ""
|
||||
echo "--- Workload isolation ---"
|
||||
USER_PODS=$(kubectl get pods -A --field-selector spec.nodeName="${CNODE1_NAME}" -o jsonpath='{.items[*].metadata.name}' 2>/dev/null | wc -w)
|
||||
# Subtract system pods
|
||||
SYSTEM_PODS=$(kubectl get pods -A --field-selector spec.nodeName="${CNODE1_NAME}" -l k8s-app --no-headers 2>/dev/null | wc -l)
|
||||
if [ "$((USER_PODS - SYSTEM_PODS))" -le 0 ]; then ok "Only system pods on cnodes (expected)"; else fail "Unexpected pods on $CNODE1_NAME"; fi
|
||||
|
||||
echo ""
|
||||
echo "============================================"
|
||||
echo " Results: $PASS passed, $FAIL failed"
|
||||
if [ "$FAIL" -gt 0 ]; then exit 1; fi
|
||||
echo " All checks passed."
|
||||
echo "============================================"
|
||||
-52
@@ -1,52 +0,0 @@
|
||||
#!/usr/bin/bash
|
||||
#
|
||||
# k8s/wipe.sh — remove existing k3s from all cnodes
|
||||
#
|
||||
# The current install on cnode1/2 uses LAN IP (192.168.3.x). We need to
|
||||
# rebuild with Tailscale IPs. This cluster has only system pods (16h old,
|
||||
# no user workloads), so a clean wipe is safe.
|
||||
#
|
||||
set -uo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
# shellcheck source=./env.sh
|
||||
source "$SCRIPT_DIR/env.sh"
|
||||
|
||||
echo "============================================"
|
||||
echo " Wiping existing k3s from all cnodes"
|
||||
echo "============================================"
|
||||
|
||||
for ip in "${ALL_CNODES[@]}"; do
|
||||
echo ""
|
||||
echo "--- $ip ---"
|
||||
cn "$ip" '
|
||||
# Stop services first
|
||||
systemctl stop k3s 2>/dev/null || true
|
||||
systemctl stop k3s-agent 2>/dev/null || true
|
||||
|
||||
# Server uninstall (also removes agents)
|
||||
if [ -x /usr/local/bin/k3s-uninstall.sh ]; then
|
||||
/usr/local/bin/k3s-uninstall.sh
|
||||
elif [ -x /usr/local/bin/k3s-agent-uninstall.sh ]; then
|
||||
/usr/local/bin/k3s-agent-uninstall.sh
|
||||
else
|
||||
echo "no k3s installed on this node"
|
||||
fi
|
||||
|
||||
# Clean up residual state
|
||||
rm -rf /etc/rancher/k3s /var/lib/rancher/k3s /var/lib/kubelet /var/lib/cni
|
||||
rm -f /etc/systemd/system/k3s.service /etc/systemd/system/k3s-agent.service
|
||||
systemctl daemon-reload
|
||||
|
||||
# Clean network interfaces left by k3s/flannel
|
||||
ip link delete cni0 2>/dev/null || true
|
||||
ip link delete flannel.1 2>/dev/null || true
|
||||
ip link delete kube-ipvs0 2>/dev/null || true
|
||||
|
||||
echo "WIPE DONE"
|
||||
'
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "============================================"
|
||||
echo " Wipe complete. Ready for bootstrap."
|
||||
echo "============================================"
|
||||
@@ -1,5 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
# shellcheck disable=SC2012 # diagnostic baseline script; ls -la listings are intentional
|
||||
# baseline.sh — quick read-only baseline of a target node.
|
||||
set -u
|
||||
hdr() { printf '\n=== %s ===\n' "$1"; }
|
||||
|
||||
@@ -37,11 +37,9 @@ for f in setupVars.conf pihole-FTL.conf adlists.list custom.list local.list rege
|
||||
echo "--- /etc/pihole/$f ---"; $DG exec pihole cat "/etc/pihole/$f" 2>&1 || true
|
||||
done
|
||||
echo "-- /etc/dnsmasq.d/* --"
|
||||
# shellcheck disable=SC2016 # $f/$t expand inside the container's sh, not locally — single quotes are intentional
|
||||
$DG exec pihole sh -c 'for f in /etc/dnsmasq.d/*; do echo "--- $f ---"; cat "$f"; done' 2>&1 || true
|
||||
echo "-- pihole version --"; $DG exec pihole pihole -v 2>&1 || true
|
||||
echo "-- gravity row counts --"
|
||||
# shellcheck disable=SC2016 # $t expands inside the container's sh, not locally
|
||||
$DG exec pihole sh -c 'for t in adlist domainlist client "group" info; do printf "%s=" "$t"; sqlite3 /etc/pihole/gravity.db "SELECT COUNT(*) FROM $t;" 2>/dev/null; done' 2>&1 || true
|
||||
echo "-- adlist addresses --"
|
||||
$DG exec pihole sqlite3 /etc/pihole/gravity.db "SELECT address,enabled,comment FROM adlist;" 2>&1 || true
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
# netinfra/dhcp-migration.md
|
||||
|
||||
> **Documentation moved to Discourse — the canonical source of truth.**
|
||||
>
|
||||
> **DHCP migration to netinfra-01/02**
|
||||
>
|
||||
> **Read it here:** https://community.turnsys.com/t/306
|
||||
>
|
||||
> *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.*
|
||||
@@ -1,323 +0,0 @@
|
||||
# dhcpd.conf — pfv-netinfra-01 (PRIMARY)
|
||||
# Migrated from pfv-netboot 2026-07-29
|
||||
# Managed via Webmin DHCP module
|
||||
#
|
||||
# FAILOVER: this node is PRIMARY; peer is pfv-netinfra-02 (192.168.3.253)
|
||||
|
||||
# Global defaults
|
||||
option domain-name "knel.net";
|
||||
option domain-name-servers 192.168.3.252, 192.168.3.253;
|
||||
option ntp-servers 192.168.3.252, 192.168.3.253;
|
||||
|
||||
default-lease-time 600;
|
||||
max-lease-time 7200;
|
||||
|
||||
ddns-update-style none;
|
||||
authoritative;
|
||||
|
||||
# ----- failover peer (PRIMARY) -----
|
||||
failover peer "pfv-dhcp" {
|
||||
primary;
|
||||
address 192.168.3.252;
|
||||
port 647;
|
||||
peer address 192.168.3.253;
|
||||
peer port 647;
|
||||
max-response-delay 30;
|
||||
max-unacked-updates 10;
|
||||
mclt 600;
|
||||
split 128;
|
||||
load balance max seconds 3;
|
||||
}
|
||||
|
||||
# ----- subnet (shared /22) -----
|
||||
subnet 192.168.0.0 netmask 255.255.252.0 {
|
||||
option routers 192.168.3.254;
|
||||
option domain-name-servers 192.168.3.252, 192.168.3.253;
|
||||
option ntp-servers 192.168.3.252, 192.168.3.253;
|
||||
option domain-name "knel.net";
|
||||
authoritative;
|
||||
allow unknown-clients;
|
||||
|
||||
pool {
|
||||
failover peer "pfv-dhcp";
|
||||
range 192.168.0.1 192.168.3.200;
|
||||
}
|
||||
|
||||
# ---- host reservations (fixed-address; not subject to failover pool) ----
|
||||
|
||||
host pfv-r3-tor-mgmt-01 {
|
||||
hardware ethernet 00:14:22:69:1c:37;
|
||||
fixed-address 192.168.0.7;
|
||||
}
|
||||
host pfv-r3-tor-stor-01 {
|
||||
hardware ethernet 00:13:72:46:95:e4;
|
||||
fixed-address 192.168.0.9;
|
||||
}
|
||||
host pfv-printer {
|
||||
hardware ethernet 40:9f:38:b0:b5:2f;
|
||||
fixed-address 192.168.1.84;
|
||||
}
|
||||
host pfv-r2-tor-01 {
|
||||
hardware ethernet 00:0d:56:41:7a:4d;
|
||||
fixed-address 192.168.0.10;
|
||||
}
|
||||
host pfv-r5-core-01 {
|
||||
hardware ethernet a4:ba:db:6f:ce:28;
|
||||
fixed-address 192.168.0.12;
|
||||
}
|
||||
host upstairs-receiver {
|
||||
hardware ethernet 74:5e:1c:76:e2:60;
|
||||
fixed-address 192.168.0.21;
|
||||
}
|
||||
host ap-TableMount {
|
||||
hardware ethernet e0:63:da:36:73:39;
|
||||
fixed-address 192.168.3.54;
|
||||
}
|
||||
host AP-WallMount {
|
||||
hardware ethernet e0:63:da:33:bb:1d;
|
||||
fixed-address 192.168.1.182;
|
||||
}
|
||||
host pfv-consrv {
|
||||
hardware ethernet 00:60:2e:01:50:aa;
|
||||
fixed-address 192.168.3.56;
|
||||
}
|
||||
host garagepdu {
|
||||
hardware ethernet 00:c0:b7:7e:49:78;
|
||||
fixed-address 192.168.3.18;
|
||||
}
|
||||
host pfv-dvr {
|
||||
hardware ethernet 54:2b:57:37:a7:d9;
|
||||
fixed-address 192.168.3.84;
|
||||
}
|
||||
host appletv-livingroom {
|
||||
hardware ethernet d0:d2:b0:97:81:c2;
|
||||
fixed-address 192.168.1.81;
|
||||
}
|
||||
host pfv-stor1 {
|
||||
hardware ethernet 00:00:c0:34:0c:dc;
|
||||
fixed-address 192.168.1.166;
|
||||
}
|
||||
host 3dscan {
|
||||
hardware ethernet b8:27:eb:91:31:82;
|
||||
fixed-address 192.168.0.4;
|
||||
}
|
||||
host pfv-jetson-nano-1 {
|
||||
hardware ethernet 00:04:4b:e4:17:7b;
|
||||
fixed-address 192.168.3.186;
|
||||
}
|
||||
host tsys7-oob {
|
||||
hardware ethernet f8:bc:12:35:1e:c6;
|
||||
fixed-address 192.168.3.197;
|
||||
}
|
||||
host tsys6-oob {
|
||||
hardware ethernet a4:ba:db:0b:df:a0;
|
||||
fixed-address 192.168.3.196;
|
||||
}
|
||||
host tsys-siem {
|
||||
hardware ethernet 00:15:5d:64:e8:33;
|
||||
fixed-address 192.168.3.81;
|
||||
}
|
||||
host brother-label-printer {
|
||||
hardware ethernet 04:fe:a1:56:72:e2;
|
||||
fixed-address 192.168.3.52;
|
||||
}
|
||||
host pfv-bms {
|
||||
hardware ethernet 02:5A:39:38:3E:9F;
|
||||
fixed-address 192.168.3.12;
|
||||
}
|
||||
host stl-canon-scanner-artroom {
|
||||
hardware ethernet 74:38:b7:24:fa:4e;
|
||||
fixed-address 192.168.3.142;
|
||||
}
|
||||
host tsys-ucs-01 {
|
||||
hardware ethernet bc:24:11:86:ea:1a;
|
||||
fixed-address 192.168.2.51;
|
||||
}
|
||||
host tsys-ucs-02 {
|
||||
hardware ethernet bc:24:11:c8:da:34;
|
||||
fixed-address 192.168.2.54;
|
||||
}
|
||||
host dell-openmanage-enterprise {
|
||||
hardware ethernet bc:24:11:ac:f4:6b;
|
||||
fixed-address 192.168.2.113;
|
||||
}
|
||||
host pfv-rrinfra-rtr {
|
||||
hardware ethernet 00:1d:70:0b:4f:41;
|
||||
fixed-address 192.168.3.94;
|
||||
}
|
||||
host pfv-tsys1 {
|
||||
hardware ethernet 34:17:eb:b3:b1:2d;
|
||||
fixed-address 192.168.3.11;
|
||||
}
|
||||
host pfv-tsys2 {
|
||||
hardware ethernet 18:fd:cb:00:d2:ca;
|
||||
fixed-address 192.168.2.3;
|
||||
}
|
||||
host pfv-tsys3 {
|
||||
hardware ethernet a4:4c:c8:08:d1:b8;
|
||||
fixed-address 192.168.2.5;
|
||||
}
|
||||
host pfv-tsys4 {
|
||||
hardware ethernet 98:90:96:c4:96:9a;
|
||||
fixed-address 192.168.3.191;
|
||||
}
|
||||
host pfv-tsys5 {
|
||||
hardware ethernet 18:03:73:43:ce:de;
|
||||
fixed-address 192.168.0.20;
|
||||
}
|
||||
host pfv-tsys6 {
|
||||
hardware ethernet 00:21:9b:a2:7c:53;
|
||||
fixed-address 192.168.3.169;
|
||||
}
|
||||
host pfv-tsys7 {
|
||||
hardware ethernet f8:bc:12:34:e0:74;
|
||||
fixed-address 192.168.0.250;
|
||||
}
|
||||
host pfv-tsys9 {
|
||||
hardware ethernet a4:bb:6d:e3:56:86;
|
||||
fixed-address 192.168.3.58;
|
||||
}
|
||||
# umbrel
|
||||
host tsys-umbrel {
|
||||
hardware ethernet 02:2E:FF:8E:A2:D2;
|
||||
fixed-address 192.168.1.97;
|
||||
}
|
||||
# ultix-streaming
|
||||
host ultix-streaming {
|
||||
hardware ethernet bc:24:11:1a:8f:6f;
|
||||
fixed-address 192.168.3.78;
|
||||
}
|
||||
# ultix-offstage
|
||||
host ultix-offstge {
|
||||
hardware ethernet bc:24:11:1f:9d:83;
|
||||
fixed-address 192.168.3.79;
|
||||
}
|
||||
# ultix-highside
|
||||
host ultix-highside {
|
||||
hardware ethernet a0:4a:5e:ca:46:f3;
|
||||
fixed-address 192.168.3.32;
|
||||
}
|
||||
# pfv-k8s-cnode1
|
||||
host pfv-k8s-cnode1 {
|
||||
hardware ethernet bc:24:11:cb:97:10;
|
||||
fixed-address 192.168.1.91;
|
||||
}
|
||||
# pfv-k8s-cnode2
|
||||
host pfv-k8s-cnode2 {
|
||||
hardware ethernet bc:24:11:40:25:f8;
|
||||
fixed-address 192.168.3.113;
|
||||
}
|
||||
# pfv-k8s-cnode3
|
||||
host pfv-k8s-cnode3 {
|
||||
hardware ethernet bc:24:11:38:c0:58;
|
||||
fixed-address 192.168.1.228;
|
||||
}
|
||||
# devbox-cloudron
|
||||
host devbox-cloudron {
|
||||
hardware ethernet bc:24:11:f7:b1:07;
|
||||
fixed-address 192.168.1.6;
|
||||
}
|
||||
# hfnoc-uisp
|
||||
host hfnoc-uisp {
|
||||
hardware ethernet bc:24:11:a3:87:61;
|
||||
fixed-address 192.168.3.193;
|
||||
}
|
||||
# kali-rd
|
||||
host kali-rd {
|
||||
hardware ethernet bc:24:11:9e:1c:e9;
|
||||
fixed-address 192.168.2.37;
|
||||
}
|
||||
# kali-tsys
|
||||
host kali-tsys {
|
||||
hardware ethernet bc:24:11:16:22:d4;
|
||||
fixed-address 192.168.1.114;
|
||||
}
|
||||
}
|
||||
|
||||
# ---- host declarations outside subnet (global scope, same as netboot) ----
|
||||
|
||||
host pfv-r6-mgmt-01 {
|
||||
hardware ethernet 00:14:22:69:18:a7;
|
||||
fixed-address 192.168.0.8;
|
||||
}
|
||||
host pfv-r1-tor-top {
|
||||
hardware ethernet 00:23:ae:c1:ad:e8;
|
||||
fixed-address 192.168.0.11;
|
||||
}
|
||||
|
||||
# --- VM DHCP reservations (generated 2026-08-11, ticket #420) ---
|
||||
# All pinned to current ARP-observed IPs. No forward DNS needed
|
||||
# (forward records point to Tailscale 100.x addresses).
|
||||
|
||||
host tsys-ca {
|
||||
hardware ethernet bc:24:11:32:d0:36;
|
||||
fixed-address 192.168.1.181;
|
||||
}
|
||||
host pfv-netinfra-01 {
|
||||
hardware ethernet bc:24:11:65:b2:ac;
|
||||
fixed-address 192.168.3.252;
|
||||
}
|
||||
host pfv-netinfra-02 {
|
||||
hardware ethernet bc:24:11:e4:37:53;
|
||||
fixed-address 192.168.3.253;
|
||||
}
|
||||
host tsys-librenms {
|
||||
hardware ethernet bc:24:11:5c:96:1e;
|
||||
fixed-address 192.168.3.176;
|
||||
}
|
||||
host tsys-proxmox-datacenter {
|
||||
hardware ethernet bc:24:11:e6:03:2d;
|
||||
fixed-address 192.168.2.44;
|
||||
}
|
||||
host pfv-k8s-wnode-tsys3 {
|
||||
hardware ethernet bc:24:11:ee:7e:7b;
|
||||
fixed-address 192.168.1.98;
|
||||
}
|
||||
host pfv-proxmox-backup-server {
|
||||
hardware ethernet bc:24:11:6e:12:69;
|
||||
fixed-address 192.168.2.193;
|
||||
}
|
||||
host pfv-k8s-wnode-tsys5 {
|
||||
hardware ethernet bc:24:11:c7:a8:6c;
|
||||
fixed-address 192.168.1.5;
|
||||
}
|
||||
host preprod-hfnoc-uisp {
|
||||
hardware ethernet bc:24:11:74:d6:8a;
|
||||
fixed-address 192.168.3.192;
|
||||
}
|
||||
host tsys-awx {
|
||||
hardware ethernet bc:24:11:80:0d:16;
|
||||
fixed-address 192.168.3.115;
|
||||
}
|
||||
host pfv-rr-middleware-02 {
|
||||
hardware ethernet bc:24:11:96:0e:ee;
|
||||
fixed-address 192.168.1.117;
|
||||
}
|
||||
host tsys-proxmox-mailgw-01 {
|
||||
hardware ethernet bc:24:11:56:61:18;
|
||||
fixed-address 192.168.1.11;
|
||||
}
|
||||
host pfv-k8s-wnode-tsys7 {
|
||||
hardware ethernet bc:24:11:30:b8:07;
|
||||
fixed-address 192.168.1.109;
|
||||
}
|
||||
host pfv-rr-middleware-01 {
|
||||
hardware ethernet bc:24:11:1e:61:cf;
|
||||
fixed-address 192.168.1.110;
|
||||
}
|
||||
host tsys-voip {
|
||||
hardware ethernet bc:24:11:23:ce:04;
|
||||
fixed-address 192.168.1.70;
|
||||
}
|
||||
host tsys-proxmox-mailgw-02 {
|
||||
hardware ethernet bc:24:11:5f:e5:2c;
|
||||
fixed-address 192.168.1.10;
|
||||
}
|
||||
host pfv-k8s-wnode-tsys6 {
|
||||
hardware ethernet bc:24:11:fa:6e:b5;
|
||||
fixed-address 192.168.1.111;
|
||||
}
|
||||
host tsys-siem-new {
|
||||
hardware ethernet bc:24:11:ee:67:e2;
|
||||
fixed-address 192.168.1.223;
|
||||
}
|
||||
@@ -1,193 +0,0 @@
|
||||
# dhcpd.conf — pfv-netinfra-02 (SECONDARY)
|
||||
# Migrated from pfv-netboot 2026-07-29
|
||||
# Managed via Webmin DHCP module
|
||||
#
|
||||
# FAILOVER: this node is SECONDARY; peer is pfv-netinfra-01 (192.168.3.252)
|
||||
|
||||
# Global defaults
|
||||
option domain-name "knel.net";
|
||||
option domain-name-servers 192.168.3.252, 192.168.3.253;
|
||||
option ntp-servers 192.168.3.252, 192.168.3.253;
|
||||
|
||||
default-lease-time 600;
|
||||
max-lease-time 7200;
|
||||
|
||||
ddns-update-style none;
|
||||
authoritative;
|
||||
|
||||
# ----- failover peer (SECONDARY) -----
|
||||
failover peer "pfv-dhcp" {
|
||||
secondary;
|
||||
address 192.168.3.253;
|
||||
port 647;
|
||||
peer address 192.168.3.252;
|
||||
peer port 647;
|
||||
max-response-delay 30;
|
||||
max-unacked-updates 10;
|
||||
load balance max seconds 3;
|
||||
}
|
||||
|
||||
# ----- subnet (shared /22) -----
|
||||
subnet 192.168.0.0 netmask 255.255.252.0 {
|
||||
option routers 192.168.3.254;
|
||||
option domain-name-servers 192.168.3.252, 192.168.3.253;
|
||||
option ntp-servers 192.168.3.252, 192.168.3.253;
|
||||
option domain-name "knel.net";
|
||||
authoritative;
|
||||
allow unknown-clients;
|
||||
|
||||
pool {
|
||||
failover peer "pfv-dhcp";
|
||||
range 192.168.0.1 192.168.3.200;
|
||||
}
|
||||
|
||||
# ---- host reservations (identical to primary) ----
|
||||
|
||||
host pfv-r3-mgmt {
|
||||
hardware ethernet 00:14:22:69:1c:37;
|
||||
fixed-address 192.168.0.7;
|
||||
}
|
||||
host pfv-r3-stor {
|
||||
hardware ethernet 00:13:72:46:95:e4;
|
||||
fixed-address 192.168.0.9;
|
||||
}
|
||||
host pfv-printer {
|
||||
hardware ethernet 40:9f:38:b0:b5:2f;
|
||||
fixed-address 192.168.1.84;
|
||||
}
|
||||
host pfv-r2-tor1 {
|
||||
hardware ethernet 00:0d:56:41:7a:4d;
|
||||
fixed-address 192.168.0.10;
|
||||
}
|
||||
host pfv-r5-core-01 {
|
||||
hardware ethernet a4:ba:db:6f:ce:28;
|
||||
fixed-address 192.168.0.12;
|
||||
}
|
||||
host upstairs-receiver {
|
||||
hardware ethernet 74:5e:1c:76:e2:60;
|
||||
fixed-address 192.168.0.21;
|
||||
}
|
||||
host ap-TableMount {
|
||||
hardware ethernet e0:63:da:36:73:39;
|
||||
fixed-address 192.168.3.54;
|
||||
}
|
||||
host AP-WallMount {
|
||||
hardware ethernet e0:63:da:33:bb:1d;
|
||||
fixed-address 192.168.1.182;
|
||||
}
|
||||
host pfv-consrv {
|
||||
hardware ethernet 00:60:2e:01:50:aa;
|
||||
fixed-address 192.168.3.56;
|
||||
}
|
||||
host garagepdu {
|
||||
hardware ethernet 00:c0:b7:7e:49:78;
|
||||
fixed-address 192.168.3.18;
|
||||
}
|
||||
host pfv-dvr {
|
||||
hardware ethernet 54:2b:57:37:a7:d9;
|
||||
fixed-address 192.168.3.84;
|
||||
}
|
||||
host appletv-livingroom {
|
||||
hardware ethernet d0:d2:b0:97:81:c2;
|
||||
fixed-address 192.168.1.81;
|
||||
}
|
||||
host pfv-stor1 {
|
||||
hardware ethernet 00:00:c0:34:0c:dc;
|
||||
fixed-address 192.168.1.166;
|
||||
}
|
||||
host 3dscan {
|
||||
hardware ethernet b8:27:eb:91:31:82;
|
||||
fixed-address 192.168.0.4;
|
||||
}
|
||||
host pfv-jetson-nano-1 {
|
||||
hardware ethernet 00:04:4b:e4:17:7b;
|
||||
fixed-address 192.168.3.186;
|
||||
}
|
||||
host tsys7-oob {
|
||||
hardware ethernet f8:bc:12:35:1e:c6;
|
||||
fixed-address 192.168.3.197;
|
||||
}
|
||||
host tsys6-oob {
|
||||
hardware ethernet a4:ba:db:0b:df:a0;
|
||||
fixed-address 192.168.3.196;
|
||||
}
|
||||
host tsys-siem {
|
||||
hardware ethernet 00:15:5d:64:e8:33;
|
||||
fixed-address 192.168.3.81;
|
||||
}
|
||||
host brother-label-printer {
|
||||
hardware ethernet 04:fe:a1:56:72:e2;
|
||||
fixed-address 192.168.3.52;
|
||||
}
|
||||
host pfv-bms {
|
||||
hardware ethernet 02:5A:39:38:3E:9F;
|
||||
fixed-address 192.168.3.12;
|
||||
}
|
||||
host stl-canon-scanner-artroom {
|
||||
hardware ethernet 74:38:b7:24:fa:4e;
|
||||
fixed-address 192.168.3.142;
|
||||
}
|
||||
host tsys-ucs-01 {
|
||||
hardware ethernet bc:24:11:86:ea:1a;
|
||||
fixed-address 192.168.2.51;
|
||||
}
|
||||
host tsys-ucs-02 {
|
||||
hardware ethernet bc:24:11:c8:da:34;
|
||||
fixed-address 192.168.2.54;
|
||||
}
|
||||
host dell-openmanage-enterprise {
|
||||
hardware ethernet bc:24:11:ac:f4:6b;
|
||||
fixed-address 192.168.2.113;
|
||||
}
|
||||
host pfv-rrinfra-rtr {
|
||||
hardware ethernet 00:1d:70:0b:4f:41;
|
||||
fixed-address 192.168.3.94;
|
||||
}
|
||||
host pfv-tsys1 {
|
||||
hardware ethernet 34:17:eb:b3:b1:2d;
|
||||
fixed-address 192.168.3.11;
|
||||
}
|
||||
host pfv-tsys2 {
|
||||
hardware ethernet 18:fd:cb:00:d2:ca;
|
||||
fixed-address 192.168.2.3;
|
||||
}
|
||||
host pfv-tsys3 {
|
||||
hardware ethernet a4:4c:c8:08:d1:b8;
|
||||
fixed-address 192.168.2.5;
|
||||
}
|
||||
host pfv-tsys4 {
|
||||
hardware ethernet 98:90:96:c4:96:9a;
|
||||
fixed-address 192.168.3.191;
|
||||
}
|
||||
host pfv-tsys5 {
|
||||
hardware ethernet 18:03:73:43:ce:de;
|
||||
fixed-address 192.168.0.20;
|
||||
}
|
||||
host pfv-tsys6 {
|
||||
hardware ethernet 00:21:9b:a2:7c:53;
|
||||
fixed-address 192.168.3.169;
|
||||
}
|
||||
host pfv-tsys7 {
|
||||
hardware ethernet f8:bc:12:34:e0:74;
|
||||
fixed-address 192.168.0.250;
|
||||
}
|
||||
host pfv-tsys9 {
|
||||
hardware ethernet a4:bb:6d:e3:56:86;
|
||||
fixed-address 192.168.3.58;
|
||||
}
|
||||
}
|
||||
|
||||
# ---- host declarations outside subnet (global scope, same as netboot) ----
|
||||
|
||||
host subodev-torsw01 {
|
||||
hardware ethernet 00:14:22:69:18:a7;
|
||||
fixed-address 192.168.0.8;
|
||||
}
|
||||
host pfv-r1-tor-top {
|
||||
hardware ethernet 00:23:ae:c1:ad:e8;
|
||||
fixed-address 192.168.0.11;
|
||||
}
|
||||
host tailscale-router {
|
||||
hardware ethernet bc:24:11:8a:69:04;
|
||||
fixed-address 192.168.3.16;
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
#!/bin/bash
|
||||
# install-dhcp.sh — installs isc-dhcp-server + Webmin on a netinfra node.
|
||||
# Does NOT start the DHCP service. Run on the target node itself.
|
||||
set -e
|
||||
|
||||
echo "=== Installing isc-dhcp-server ==="
|
||||
apt-get update -qq
|
||||
apt-get install -y isc-dhcp-server
|
||||
|
||||
echo "=== Installing Webmin ==="
|
||||
if ! dpkg -l | grep -q '^ii.*webmin'; then
|
||||
curl -fsSL https://raw.githubusercontent.com/webmin/webmin/master/webmin-setup-repo.sh -o /tmp/webmin-setup.sh
|
||||
sh /tmp/webmin-setup.sh -f
|
||||
rm -f /tmp/webmin-setup.sh
|
||||
apt-get install -y webmin
|
||||
else
|
||||
echo "Webmin already installed"
|
||||
fi
|
||||
|
||||
echo "=== Writing /etc/default/isc-dhcp-server ==="
|
||||
cat > /etc/default/isc-dhcp-server <<'EOF'
|
||||
# Defaults for isc-dhcp-server (sourced by /etc/init.d/isc-dhcp-server)
|
||||
INTERFACESv4="ens18"
|
||||
INTERFACESv6=""
|
||||
EOF
|
||||
|
||||
echo "=== Stopping DHCP service (should not serve yet) ==="
|
||||
systemctl stop isc-dhcp-server 2>/dev/null || true
|
||||
systemctl disable isc-dhcp-server 2>/dev/null || true
|
||||
|
||||
echo "=== Done. DHCP installed but NOT started. ==="
|
||||
@@ -1,10 +0,0 @@
|
||||
# netinfra/dns-cluster-setup/README.md
|
||||
|
||||
> **Documentation moved to Discourse — the canonical source of truth.**
|
||||
>
|
||||
> **Technitium DNS cluster setup**
|
||||
>
|
||||
> **Read it here:** https://community.turnsys.com/t/306
|
||||
>
|
||||
> *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.*
|
||||
@@ -44,7 +44,7 @@ if command -v sqlite3 >/dev/null 2>&1; then
|
||||
echo "-- adlist count --"; sqlite3 -readonly "$G" "SELECT COUNT(*) FROM adlist;" 2>&1
|
||||
echo "-- domainlist count by type --"; sqlite3 -readonly "$G" "SELECT type,COUNT(*) FROM domainlist GROUP BY type;" 2>&1
|
||||
echo "-- domainlist (allow=0/allow_exact, deny=1/deny_exact, etc.) first 80 --"; sqlite3 -readonly "$G" "SELECT type,domain,enabled,comment FROM domainlist LIMIT 80;" 2>&1
|
||||
echo "-- client --"; sqlite3 -readonly "$G" "SELECT ip,comment FROM client;" 2>&1
|
||||
echo -- client --"; sqlite3 -readonly "$G" "SELECT ip,comment FROM client;" 2>&1
|
||||
echo "-- group --"; sqlite3 -readonly "$G" "SELECT id,name,enabled,comment FROM 'group';" 2>&1
|
||||
echo "-- info --"; sqlite3 -readonly "$G" "SELECT * FROM info;" 2>&1
|
||||
else
|
||||
|
||||
@@ -28,7 +28,7 @@ services:
|
||||
# https://en.wikipedia.org/wiki/List_of_tz_database_time_zones, e.g:
|
||||
TZ: 'America/Chicago'
|
||||
# Set a password to access the web interface. Not setting one will result in a random password being assigned
|
||||
FTLCONF_webserver_api_password: 'REDACTED_PASSWORD'
|
||||
FTLCONF_webserver_api_password: 'Gransyan1!'
|
||||
# If using Docker's default `bridge` network setting the dns listening mode should be set to 'all'
|
||||
FTLCONF_dns_listeningMode: 'all'
|
||||
# Volumes store your data between container upgrades
|
||||
@@ -326,7 +326,7 @@ tsys-ntp dockurr/chrony Up 7 days (healthy) WDIR=/root/NTP
|
||||
"OpenStdin": false,
|
||||
"StdinOnce": false,
|
||||
"Env": [
|
||||
"FTLCONF_webserver_api_password=REDACTED_PASSWORD",
|
||||
"FTLCONF_webserver_api_password=Gransyan1!",
|
||||
"FTLCONF_dns_listeningMode=all",
|
||||
"TZ=America/Chicago",
|
||||
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
|
||||
|
||||
@@ -1,10 +1,116 @@
|
||||
# netinfra/pfv-netboot-setup.md
|
||||
# pfv-netboot — Reference Network Infrastructure (READ-ONLY reference)
|
||||
|
||||
> **Documentation moved to Discourse — the canonical source of truth.**
|
||||
>
|
||||
> **pfv-netboot reference node setup**
|
||||
>
|
||||
> **Read it here:** https://community.turnsys.com/t/306
|
||||
>
|
||||
> *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.*
|
||||
> **Status:** REFERENCE SOURCE ONLY. This node is production infrastructure.
|
||||
> Do **not** modify it. This document describes it as audited so its services can
|
||||
> be replicated to `pfv-netinfra-01` / `pfv-netinfra-02`. All data below was
|
||||
> collected by **read-only** audit scripts (`audit-netboot.sh`,
|
||||
> `deep-audit-netboot.sh`, `gather-configs.sh`) on 2026-07-27/28.
|
||||
|
||||
## 1. Host
|
||||
|
||||
| Item | Value |
|
||||
|---|---|
|
||||
| Hostname / FQDN | `pfv-netboot` / `pfv-netboot.knel.net` |
|
||||
| OS | Debian GNU/Linux 12 (bookworm), kernel 6.1.0-44-amd64 |
|
||||
| Hardware | 2 vCPU, ~1.9 GiB RAM, 491 GB disk (18 GB used) |
|
||||
| Timezone | `America/Chicago` (US/Central) |
|
||||
| LAN | `eth0` static `192.168.3.250/22`, gw `192.168.3.254` (`/etc/network/interfaces`) |
|
||||
| Tailscale | `100.103.64.82` (`tailscale0`) |
|
||||
| DNS resolver | Tailscale MagicDNS — `/etc/resolv.conf` → `100.100.100.100` |
|
||||
| Docker | Docker Engine 29.6.2 (containerd v2.2.6, runc 1.3.6) |
|
||||
| Access | `localuser` has passwordless sudo; **not** in `docker` group (uses `sudo docker`) |
|
||||
|
||||
`eth1` is up but unconfigured; many docker bridges exist (`pihole_default`,
|
||||
`ntp_default`, `dns_default`, and several stale ones).
|
||||
|
||||
## 2. Services overview
|
||||
|
||||
| Service | Form | Running? |
|
||||
|---|---|---|
|
||||
| **Pi-hole** (DNS sinkhole, recursive resolver) | Docker container `pihole` | ✅ healthy |
|
||||
| **NTP** — overlay on Tailscale IP | Docker container `tsys-ntp` (`dockurr/chrony`) | ✅ healthy |
|
||||
| **NTP** — system clock + LAN serving | bare-metal `ntpsec` (`ntpd`) | ✅ active, enabled |
|
||||
| **Technitium DNS** (authoritative for `knel.net`) | Docker container | ❌ **not running**; config preserved in orphaned volume |
|
||||
|
||||
## 3. Pi-hole (container)
|
||||
|
||||
- **Compose:** `/root/pihole/docker-compose.yml` (compose project `pihole`)
|
||||
- **Image:** `pihole/pihole:latest` — Core **v6.1.2**, Web v6.2.1, FTL v6.2.2
|
||||
- **Container:** `pihole`, `restart: always`, `cap_add: [SYS_NICE]`, network `pihole_default`
|
||||
- **Ports (host):**
|
||||
|
||||
| Host | Container | Purpose |
|
||||
|---|---|---|
|
||||
| `53/tcp`, `53/udp` | 53 | DNS |
|
||||
| `10002/tcp` | 80 | Web admin (HTTP) |
|
||||
| `10003/tcp` | 443 | Web admin (HTTPS, self-signed) |
|
||||
|
||||
- **Environment:** `TZ=America/Chicago`, `FTLCONF_webserver_api_password=Gransyan1!`, `FTLCONF_dns_listeningMode=all`
|
||||
- **Data:** bind mount `/root/pihole/etc-pihole:/etc/pihole` (dir owned by `localuser`; files by container `pihole` uid)
|
||||
- **Config (Pi-hole v6 TOML):** `pihole.toml`. Key settings:
|
||||
- Upstream DNS: `192.168.3.16`, `8.8.8.8`, `2001:4860:4860::8888`
|
||||
- `listeningMode = "ALL"`, `interface = "eth0"`, `dns.port = 53`, `dns.domain = "lan"`
|
||||
- `queryLogging = true`, DNSSEC off
|
||||
- **Adlists:** one entry — `https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts` (in `gravity.db`/`adlists.list`)
|
||||
- **Gravity DB:** `/etc/pihole/gravity.db` (~5.5 MB) holds adlists/domainlists/clients/groups
|
||||
- **Web admin:** `http://pfv-netboot:10002/admin/` (password `Gransyan1!`)
|
||||
- Note: query history `pihole-FTL.db` (~2.5 GB) is transient and **excluded** from replication.
|
||||
|
||||
## 4. NTP (two layers)
|
||||
|
||||
### 4a. chrony container (`tsys-ntp`) — overlay on the Tailscale IP
|
||||
- **Compose:** `/root/NTP/docker-compose.yml` (project `ntp`)
|
||||
- **Image:** `dockurr/chrony`
|
||||
- **Env:** `NTP_SERVERS=pool.ntp.org`
|
||||
- **Ports:** `100.103.64.82:123:123/udp` — bound specifically to the **Tailscale IP**
|
||||
- `restart: always`
|
||||
- chrony.conf (generated): `server pool.ntp.org iburst`, `allow all`, `rtcsync`
|
||||
- On netboot this coexists with bare-metal ntpsec because ntpsec here does **not** pre-bind the specific Tailscale-IP socket, letting Docker claim it.
|
||||
|
||||
### 4b. bare-metal `ntpsec`
|
||||
- Unit `ntpsec.service` — active, enabled; `/usr/sbin/ntpd -c /etc/ntpsec/ntp.conf -g -N -u ntpsec:ntpsec`
|
||||
- **Config** (`/etc/ntpsec/ntp.conf`):
|
||||
```
|
||||
driftfile /var/lib/ntp/ntp.drift
|
||||
leapfile /usr/share/zoneinfo/leap-seconds.list
|
||||
server pfvsvrpi.knel.net
|
||||
restrict 127.0.0.1
|
||||
restrict ::1
|
||||
```
|
||||
- Listens on all local addresses (incl. Tailscale) for UDP/123; serves LAN clients.
|
||||
|
||||
## 5. Technitium DNS (currently stopped)
|
||||
|
||||
- **Not running** — no container and **no compose file** exists for it.
|
||||
- A previous deployment left an **orphaned Docker volume** `dns_tsys-dns-config`
|
||||
(mountpoint `/var/lib/docker/volumes/dns_tsys-dns-config/_data`) whose contents
|
||||
are intact (last activity 2025-06-23). A second typo'd volume
|
||||
`dns_tyss-dns-config` is empty.
|
||||
- Config files are **binary** (Technitium's own serialization), but copy verbatim:
|
||||
`dns.config`, `auth.config`, `log.config`, `scopes/Default.scope`,
|
||||
`self-signed-cert.pfx`, `cache.bin`, `zones/`, `stats/`, `logs/`.
|
||||
- **Zones present** (12 reverse + 1 forward):
|
||||
- `knel.net.zone` — forward zone; SOA `dns.knel.net. hostadmin.knel.net.` (serial `2025062313`). A-records for the internal fleet, including: `tsys1`, `rr-middleware`, `pfv-netboot`, `pfv-k8s-cnode1`…`cnode5`, `pfv-k8s-wnode3`, `tsys-k8scloud-netcup-1`, `tsys-kali-vptechops`, `tsys-kali-dev`; NS `dns.knel.net`.
|
||||
- Reverse zones for Tailscale CGNAT ranges (`100.x.in-addr.arpa`): `199.86`, `145.105`, `181.103`, `184.108`, `194.67`, `2.108`, `211.114`, `46.96`, `64.103`, `75.110`, `97.82`, `119.127`.
|
||||
- **Auth:** `auth.config` defines user `admin` (Administrators group) with a stored password hash; the plaintext password is whatever was set on the original Technitium instance.
|
||||
- The compose project name historically was `dns` (network `dns_default` still exists).
|
||||
|
||||
## 6. Firewall / misc
|
||||
|
||||
- nftables/iptables: mostly Docker + Tailscale chains (`ts-input`, `ts-forward`,
|
||||
`DOCKER`, `DOCKER-FORWARD`); default `INPUT ACCEPT`, `FORWARD DROP`,
|
||||
`OUTPUT ACCEPT`. No UFW / firewalld.
|
||||
- Also runs (out of scope for this replication): Samba (137/138/139, 445), NFS
|
||||
(2049), rpcbind (111), Postfix (25), Cockpit (9090), Beszel agent, webmin/
|
||||
usermin (10000/10002/20000), Tailscale (41641).
|
||||
- SELinux absent; AppArmor default docker profile.
|
||||
|
||||
## 7. How it was audited (no changes made)
|
||||
|
||||
```bash
|
||||
ssh localuser@pfv-netboot 'bash -s' < audit-netboot.sh # broad read-only sweep
|
||||
ssh localuser@pfv-netboot 'bash -s' < deep-audit-netboot.sh # docker inspect + compose
|
||||
ssh localuser@pfv-netboot 'bash -s' < gather-configs.sh # pihole.toml + technitium
|
||||
```
|
||||
|
||||
Artifacts: `netboot-audit.txt`, `netboot-deep-audit.txt`, `netboot-configs.txt`.
|
||||
|
||||
@@ -1,10 +1,220 @@
|
||||
# netinfra/pfv-netinfra-setup.md
|
||||
# pfv-netinfra-01 / pfv-netinfra-02 — Network Services Setup
|
||||
|
||||
> **Documentation moved to Discourse — the canonical source of truth.**
|
||||
>
|
||||
> **pfv-netinfra-01/02 initial setup**
|
||||
>
|
||||
> **Read it here:** https://community.turnsys.com/t/306
|
||||
>
|
||||
> *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.*
|
||||
These two nodes replicate the network-infrastructure services of **pfv-netboot**
|
||||
(Pi-hole, Technitium DNS, NTP). They were deployed by `setup-netinfra.sh`, which
|
||||
reads config from pfv-netboot (read-only) and relays it to each target.
|
||||
|
||||
## 1. Nodes
|
||||
|
||||
| | pfv-netinfra-01 | pfv-netinfra-02 |
|
||||
|---|---|---|
|
||||
| OS | Debian 13 (trixie), kernel 6.12.96+deb13 | Debian 13 (trixie) |
|
||||
| LAN | `ens18` `192.168.3.252/24` | `ens18` `192.168.3.253/24` |
|
||||
| Tailscale | `100.70.181.72` | `100.93.194.82` |
|
||||
| RAM / Disk | 1.9 GiB / 30 GB (27 GB free) | 3.7 GiB / 30 GB (27 GB free) |
|
||||
| Resolver | Tailscale MagicDNS (`100.100.100.100`) | same |
|
||||
| Docker | 29.6.2 (pre-installed, enabled) | 29.6.2 |
|
||||
| Access | `ssh localuser@pfv-netinfra-0X`, passwordless sudo; `localuser` **not** in docker group → use `sudo docker` | same |
|
||||
|
||||
## 2. Service layout
|
||||
|
||||
All services live under `/home/localuser/services/<svc>/` (owned by `localuser`
|
||||
so the compose files are directly editable; data dirs keep container uids):
|
||||
|
||||
```
|
||||
/home/localuser/services/
|
||||
├── pihole/
|
||||
│ ├── docker-compose.yml
|
||||
│ └── etc-pihole/ # copied from netboot /root/pihole/etc-pihole
|
||||
│ ├── pihole.toml # Pi-hole v6 config (upstreams, etc.)
|
||||
│ ├── gravity.db # adlists / domainlists / clients / groups
|
||||
│ ├── adlists.list
|
||||
│ ├── dnsmasq.conf
|
||||
│ ├── tls.{crt,pem,crt_ca}
|
||||
│ └── versions
|
||||
├── ntp/
|
||||
│ └── docker-compose.yml # chrony container (see §5 — not used; host ntpsec serves)
|
||||
└── technitium/
|
||||
├── docker-compose.yml
|
||||
└── config/ # copied from netboot orphaned volume dns_tsys-dns-config/_data
|
||||
├── dns.config
|
||||
├── auth.config
|
||||
├── scopes/Default.scope
|
||||
├── self-signed-cert.pfx
|
||||
└── zones/ # knel.net.zone + 12 Tailscale reverse zones
|
||||
```
|
||||
|
||||
## 3. Pi-hole (container `pihole`)
|
||||
|
||||
Image `pihole/pihole:latest`; `restart: always`; `cap_add: [SYS_NICE]`.
|
||||
|
||||
| Host port | Container | Purpose |
|
||||
|---|---|---|
|
||||
| `53/tcp`, `53/udp` | 53 | DNS (the LAN/Tailscale recursive resolver) |
|
||||
| `10002/tcp` | 80 | Web admin (HTTP) |
|
||||
| `10003/tcp` | 443 | Web admin (HTTPS) |
|
||||
|
||||
`docker-compose.yml`:
|
||||
```yaml
|
||||
services:
|
||||
pihole:
|
||||
container_name: pihole
|
||||
image: pihole/pihole:latest
|
||||
hostname: pihole
|
||||
ports:
|
||||
- "53:53/tcp"
|
||||
- "53:53/udp"
|
||||
- "10002:80/tcp"
|
||||
- "10003:443/tcp"
|
||||
environment:
|
||||
TZ: 'America/Chicago'
|
||||
FTLCONF_webserver_api_password: 'Gransyan1!'
|
||||
FTLCONF_dns_listeningMode: 'all'
|
||||
volumes:
|
||||
- './etc-pihole:/etc/pihole'
|
||||
cap_add:
|
||||
- SYS_NICE
|
||||
restart: always
|
||||
```
|
||||
- Upstream DNS (from copied `pihole.toml`): `192.168.3.16`, `8.8.8.8`, `2001:4860:4860::8888`.
|
||||
- Adlist: `https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts`.
|
||||
- `pihole.toml` `interface` was adapted from netboot's `eth0` to the target's `ens18`.
|
||||
- Web admin: `http://<node>:10002/admin/` — password **`Gransyan1!`** (same as netboot).
|
||||
- Web UI URL per node: `http://100.70.181.72:10002/admin/` (-01), `http://100.93.194.82:10002/admin/` (-02).
|
||||
|
||||
## 4. Technitium DNS (container `tsys-dns`)
|
||||
|
||||
Image `technitium/dns-server`; `restart: always`. Authoritative DNS for
|
||||
`knel.net` (and Tailscale reverse zones), config copied verbatim from netboot's
|
||||
orphaned `dns_tsys-dns-config` volume.
|
||||
|
||||
| Host port | Container | Purpose |
|
||||
|---|---|---|
|
||||
| `5300/tcp`, `5300/udp` | 53 | DNS (remapped — see note) |
|
||||
| `5380/tcp` | 5380 | Web console (HTTP) |
|
||||
| `53443/tcp` | 53443 | Web console (HTTPS) |
|
||||
|
||||
`docker-compose.yml`:
|
||||
```yaml
|
||||
services:
|
||||
technitium:
|
||||
image: technitium/dns-server
|
||||
container_name: tsys-dns
|
||||
ports:
|
||||
- "5300:53/tcp"
|
||||
- "5300:53/udp"
|
||||
- "5380:5380/tcp"
|
||||
- "53443:53443/tcp"
|
||||
volumes:
|
||||
- './config:/etc/dns'
|
||||
restart: always
|
||||
```
|
||||
- Zones loaded (verified): `knel.net` SOA → `dns.knel.net. hostadmin.knel.net. 2025062313 900 300 604800 900`, plus 12 Tailscale reverse zones.
|
||||
- Web console: `http://<node>:5380/` → user **`admin`** + the original Technitium
|
||||
password (carried over via `auth.config`). If the password is unknown, reset it
|
||||
from the console or by removing `config/auth.config` and recreating the container.
|
||||
- **Port note:** Technitium's native DNS port (53) is remapped to host **5300**
|
||||
because Pi-hole already owns host :53 (they cannot both bind 0.0.0.0:53). To
|
||||
query the authoritative server: `dig -p 5300 @<node> knel.net SOA`. To make
|
||||
Pi-hole resolve `knel.net` via Technitium, add a conditional/local upstream in
|
||||
Pi-hole pointing to the container (e.g. `127.0.0.1#5300` is not host-reachable
|
||||
from Pi-hole's netns — use the docker bridge IP of `tsys-dns`, or add
|
||||
`knel.net` A-records directly in Pi-hole's Local DNS).
|
||||
|
||||
## 5. NTP (host `ntpsec`, not a container)
|
||||
|
||||
Both targets **already run a bare-metal `ntpsec` daemon** (active, enabled) that
|
||||
serves NTP on every local address — including the Tailscale IP — and keeps the
|
||||
system clock synced. This is the **same daemon family as netboot's own bare-metal
|
||||
ntpsec**.
|
||||
|
||||
- **Why no chrony container?** netboot's chrony container (`tsys-ntp`) binds the
|
||||
Tailscale IP `100.103.64.82:123`; on netboot that works only because its ntpsec
|
||||
does **not** pre-bind the specific Tailscale-IP socket. On these targets ntpsec
|
||||
**does** bind the Tailscale IP, so the container cannot claim it (`address
|
||||
already in use`) and would be a non-functional duplicate (verified: the
|
||||
container started but never synced — Stratum 0). It is therefore intentionally
|
||||
**omitted**; host ntpsec provides NTP. `setup-netinfra.sh` detects an active
|
||||
host NTP unit and removes any stale `tsys-ntp` container.
|
||||
- ntpsec config (`/etc/ntpsec/ntp.conf`): Debian NTP pool (`0-3.debian.pool.ntp.org`),
|
||||
`restrict default kod nomodify noquery limited` (serves time, blocks mgmt queries).
|
||||
- Verified sync: -01 stratum 2 (~2 ms offset), -02 stratum 3 (~0.2 ms offset),
|
||||
leap normal.
|
||||
|
||||
The `ntp/docker-compose.yml` is still written on each node for parity/reference
|
||||
(and in case the host NTP is ever disabled — then `sudo docker compose -f
|
||||
/home/localuser/services/ntp/docker-compose.yml up -d` brings up chrony).
|
||||
|
||||
## 6. Verification results (2026-07-28)
|
||||
|
||||
| Check | pfv-netinfra-01 | pfv-netinfra-02 |
|
||||
|---|---|---|
|
||||
| `pihole` health | healthy | healthy |
|
||||
| `dig @127.0.0.1:53 pi.hole` | `172.18.0.2` | `172.18.0.2` |
|
||||
| Pi-hole web `:10002` | HTTP 302 (→login) | HTTP 302 |
|
||||
| `dig @127.0.0.1:5300 knel.net SOA` | SOA answered | SOA answered |
|
||||
| Technitium web `:5380` | HTTP 200 | HTTP 200 |
|
||||
| NTP daemon | ntpsec, stratum 2, synced | ntpsec, stratum 3, synced |
|
||||
|
||||
## 7. Operating the services
|
||||
|
||||
```bash
|
||||
# status
|
||||
sudo docker ps
|
||||
|
||||
# Pi-hole
|
||||
sudo docker compose -f /home/localuser/services/pihole/docker-compose.yml ps
|
||||
sudo docker compose -f /home/localuser/services/pihole/docker-compose.yml logs -f
|
||||
sudo docker exec pihole pihole -v # version
|
||||
sudo docker exec pihole pihole -g # rebuild gravity
|
||||
sudo docker exec pihole pihole -a -p # set/change web password
|
||||
|
||||
# Technitium
|
||||
sudo docker compose -f /home/localuser/services/technitium/docker-compose.yml logs -f
|
||||
sudo docker exec tsys-dns sh # explore /etc/dns
|
||||
|
||||
# NTP (host)
|
||||
systemctl status ntpsec
|
||||
ntpq -pn
|
||||
```
|
||||
|
||||
## 8. Differences from pfv-netboot (intentional)
|
||||
|
||||
1. **Layout** under `/home/localuser/services/` instead of `/root` (so `localuser`
|
||||
can manage compose files); Pi-hole data dir still owned by `localuser`, as on netboot.
|
||||
2. **Pi-hole `interface`** set to `ens18` (targets' NIC) instead of netboot's `eth0`.
|
||||
3. **NTP:** host `ntpsec` (Debian pool) used instead of netboot's chrony container
|
||||
(the container cannot bind the Tailscale IP here; see §5).
|
||||
4. **Technitium DNS** host port remapped `53 → 5300` to avoid clashing with Pi-hole
|
||||
on `:53`. The `knel.net` zone and all reverse zones are identical to netboot's.
|
||||
5. Pi-hole query logs (`pihole-FTL.db*`) and regenerable caches/backups are not
|
||||
copied (transient); gravity DB and all configuration are.
|
||||
|
||||
## 9. Re-running / reproducing
|
||||
|
||||
`setup-netinfra.sh` is **idempotent** — it skips re-copying config if already
|
||||
present and uses `docker compose up -d` (no-ops when unchanged). It reads
|
||||
pfv-netboot read-only and never mutates it.
|
||||
|
||||
```bash
|
||||
./setup-netinfra.sh # deploy to both nodes
|
||||
./setup-netinfra.sh pfv-netinfra-01 # deploy one node
|
||||
./setup-netinfra.sh pfv-netinfra-01 verify # verify only
|
||||
```
|
||||
|
||||
Prerequisites: SSH key access to all three hosts as `localuser` with passwordless
|
||||
sudo; the targets reach `192.168.3.16`/`8.8.8.8` for Pi-hole upstream and the
|
||||
internet for image pulls.
|
||||
|
||||
## 10. Files in this directory
|
||||
|
||||
| File | Purpose |
|
||||
|---|---|
|
||||
| `setup-netinfra.sh` | orchestrator: deploys + verifies the clone on -01/-02 |
|
||||
| `audit-netboot.sh` | broad read-only audit of pfv-netboot |
|
||||
| `deep-audit-netboot.sh` | docker inspect / compose / volume deep audit (read-only) |
|
||||
| `gather-configs.sh` | targeted config pull (pihole.toml, technitium) (read-only) |
|
||||
| `baseline.sh` | read-only baseline of a target node |
|
||||
| `netboot-audit.txt`, `netboot-deep-audit.txt`, `netboot-configs.txt` | audit output |
|
||||
| `pfv-netboot-setup.md` | reference-node documentation |
|
||||
| `pfv-netinfra-setup.md` | this document |
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
# Pi-hole web UI password. NEVER commit the real .env — only this template.
|
||||
# Copy to .env and set the value before `docker compose up -d`.
|
||||
PIHOLE_WEB_PASSWORD=changeme
|
||||
@@ -1,10 +0,0 @@
|
||||
# netinfra/pihole/README.md
|
||||
|
||||
> **Documentation moved to Discourse — the canonical source of truth.**
|
||||
>
|
||||
> **Pi-hole recursive DNS hardening**
|
||||
>
|
||||
> **Read it here:** https://community.turnsys.com/t/306
|
||||
>
|
||||
> *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.*
|
||||
@@ -1,55 +0,0 @@
|
||||
services:
|
||||
pihole:
|
||||
container_name: pihole
|
||||
# Root cause of the 2026-08 gravity.db corruption: default /dev/shm (64M)
|
||||
# was too small for FTL's shared-memory metrics. 1024M has been stable.
|
||||
shm_size: '1024M'
|
||||
image: pihole/pihole:2026.07.0
|
||||
hostname: pihole
|
||||
entrypoint: ["/usr/local/bin/gravity-validate.sh"]
|
||||
ports:
|
||||
- "53:53/tcp"
|
||||
- "53:53/udp"
|
||||
- "10002:80/tcp"
|
||||
- "10003:443/tcp"
|
||||
environment:
|
||||
TZ: 'America/Chicago'
|
||||
FTLCONF_webserver_api_password: '${PIHOLE_WEB_PASSWORD}'
|
||||
FTLCONF_dns_listeningMode: 'all'
|
||||
# Rate-limiting disabled (count=0). Uptime Kuma on Cloudron VPS sends
|
||||
# high-volume DNS queries for monitoring; default 1000/60s limit was
|
||||
# causing intermittent REFUSED responses → Uptime Kuma flapping.
|
||||
FTLCONF_dns_rateLimit_count: '0'
|
||||
FTLCONF_dns_rateLimit_interval: '0'
|
||||
FTLCONF_dns_upstreams: '["8.8.8.8"]'
|
||||
volumes:
|
||||
- './etc-pihole:/etc/pihole'
|
||||
- './etc-dnsmasq.d:/etc/dnsmasq.d'
|
||||
- './gravity-validate.sh:/usr/local/bin/gravity-validate.sh:ro'
|
||||
cap_add:
|
||||
- SYS_NICE
|
||||
restart: always
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "dig +short +norecurse @127.0.0.1 pi.hole >/dev/null 2>&1 && test -s /etc/pihole/gravity.db || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 60s
|
||||
labels:
|
||||
autoheal: "true"
|
||||
networks:
|
||||
- default
|
||||
- dnsnet
|
||||
autoheal:
|
||||
container_name: autoheal
|
||||
image: willfarrell/autoheal:1.2.0
|
||||
environment:
|
||||
AUTOHEAL_CONTAINER_LABEL: autoheal
|
||||
AUTOHEAL_INTERVAL: 30
|
||||
AUTOHEAL_START_PERIOD: 60
|
||||
volumes:
|
||||
- '/var/run/docker.sock:/var/run/docker.sock:ro'
|
||||
restart: always
|
||||
networks:
|
||||
dnsnet:
|
||||
external: true
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user