chore(agent): add CHANGELOG, version the agent at 0.1.0
- CHANGELOG.md with the v0.1.0 (slice 1) entry - main: version var (0.1.0, ldflags-overridable) + --version flag; version shown in selftest header and startup log Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,55 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to **felhom-agent** are recorded here. Update on every code
|
||||||
|
change that gets pushed.
|
||||||
|
|
||||||
|
## v0.1.0 — Scaffold + `proxmox` interaction layer (slice 1) (2026-06-08)
|
||||||
|
|
||||||
|
First slice: stand up the host-agent project and its foundation — the typed
|
||||||
|
Proxmox interaction layer every other module will call. No reconcile loop, hub
|
||||||
|
client, signing, or storage/backup orchestration yet (later slices).
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **Project scaffold**: module `gitea.dooplex.hu/admin/felhom-agent`, binary
|
||||||
|
`felhom-agent` (`cmd/felhom-agent/`), Go 1.24, zero external dependencies
|
||||||
|
(pure stdlib). `--version` flag; `version` var overridable via
|
||||||
|
`-ldflags "-X main.version=<v>"`.
|
||||||
|
- **`internal/proxmox` — API backend (`Client`)**: hand-rolled REST client over
|
||||||
|
`https://<host>:8006/api2/json` with `PVEAPIToken` auth. Typed read ops
|
||||||
|
(`Version`, `Nodes`, `NodeStatus`, `ListLXC`, `GuestStatus`, `GuestConfig`,
|
||||||
|
`ListStorage`, `NodeStorage`, `StorageContent`) and async mutating ops
|
||||||
|
returning a UPID (`RestoreLXC` — the primary create path, `Vzdump`, `Snapshot`,
|
||||||
|
`Rollback`, `DeleteSnapshot`, `SetConfig`, `Start`, `Stop`).
|
||||||
|
- **`WaitTask`**: polls `GET /nodes/{node}/tasks/{upid}/status` until stopped, then
|
||||||
|
asserts `exitstatus == "OK"` (authorization can surface at task execution, not
|
||||||
|
the POST — phase1-2 §1.3). Exponential backoff (1s→5s cap), context
|
||||||
|
cancellation + timeout. `*APIError` parses the offending privilege from a 403;
|
||||||
|
`*TaskError` parses it from a failed task exitstatus + log tail.
|
||||||
|
- **`internal/proxmox` — fenced root-CLI backend (`Privileged`)**: limited to the
|
||||||
|
three proven OS-root exceptions only — `CreateGoldenLXC` (keyctl `pct create`),
|
||||||
|
`MountUSBByUUID`, `SMART`, `Sensors`; each cites why it can't be the API. Fence
|
||||||
|
is structural (Client never shells out, Privileged never makes an HTTP call) and
|
||||||
|
asserted in tests.
|
||||||
|
- **TLS trust**: SHA-256 leaf-cert pinning (the host serves a self-signed cert) or
|
||||||
|
a CA file; an explicitly-named `insecure_skip_verify` that is off by default. No
|
||||||
|
blanket verification disable.
|
||||||
|
- **`internal/config`**: JSON config file + `FELHOM_AGENT_*` env overrides; the
|
||||||
|
token secret is never logged (`Redacted()`).
|
||||||
|
- **`internal/log`**: slog setup (text, stderr, configurable level).
|
||||||
|
- **`cmd/felhom-agent --selftest`**: read-only health report against a live host
|
||||||
|
(version/nodes/status/guests/storage); `--selftest=task --vmid N` exercises
|
||||||
|
`WaitTask` on a reversible snapshot→rollback→delete op (gated; default selftest
|
||||||
|
mutates nothing).
|
||||||
|
- **Tests**: unit tests with a mock HTTP transport + mock runner (UPID parse,
|
||||||
|
`WaitTask` running→OK / failed-403 / timeout / ctx-cancel, 403→privilege error,
|
||||||
|
response decoding against shapes captured live from `demo-felhom`, config
|
||||||
|
redaction, and the API-vs-root routing fence).
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
- Types are grounded in the spike findings
|
||||||
|
(`felhom.eu/documentation/proxmox-platform.md`, `tests/phase{0,1-2,3}-findings.md`)
|
||||||
|
and the exact JSON shapes captured live from `demo-felhom` (PVE 9.2.2).
|
||||||
|
- Verified: `go build/vet/test` green on Go 1.24.4 (build server) and a live
|
||||||
|
read-only `--selftest` against the demo host with TLS fingerprint pinning.
|
||||||
|
- The 16-privilege `FelhomAgent` role + privsep token (role on **both** user and
|
||||||
|
token) is provisioned out-of-band; the agent only consumes the token.
|
||||||
@@ -22,17 +22,28 @@ import (
|
|||||||
"gitea.dooplex.hu/admin/felhom-agent/internal/proxmox"
|
"gitea.dooplex.hu/admin/felhom-agent/internal/proxmox"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// version is the agent version. Overridable at build time with
|
||||||
|
// -ldflags "-X main.version=<v>"; defaults to the in-repo CHANGELOG version.
|
||||||
|
var version = "0.1.0"
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
var (
|
var (
|
||||||
cfgPath string
|
cfgPath string
|
||||||
selftest selftestFlag
|
selftest selftestFlag
|
||||||
vmid int
|
vmid int
|
||||||
|
showVersion bool
|
||||||
)
|
)
|
||||||
flag.StringVar(&cfgPath, "config", envOr("FELHOM_AGENT_CONFIG", "/etc/felhom-agent/agent.json"), "path to the agent config file (JSON)")
|
flag.StringVar(&cfgPath, "config", envOr("FELHOM_AGENT_CONFIG", "/etc/felhom-agent/agent.json"), "path to the agent config file (JSON)")
|
||||||
flag.Var(&selftest, "selftest", "run a self-test and exit: bare/`read` = read-only queries; `task` = reversible mutating exercise (needs -vmid)")
|
flag.Var(&selftest, "selftest", "run a self-test and exit: bare/`read` = read-only queries; `task` = reversible mutating exercise (needs -vmid)")
|
||||||
flag.IntVar(&vmid, "vmid", 0, "guest VMID for --selftest=task (the reversible snapshot/rollback exercise)")
|
flag.IntVar(&vmid, "vmid", 0, "guest VMID for --selftest=task (the reversible snapshot/rollback exercise)")
|
||||||
|
flag.BoolVar(&showVersion, "version", false, "print version and exit")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
|
if showVersion {
|
||||||
|
fmt.Println("felhom-agent", version)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
cfg, err := config.Load(cfgPath)
|
cfg, err := config.Load(cfgPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// A missing default config file is fine if env provides the values; only a
|
// A missing default config file is fine if env provides the values; only a
|
||||||
@@ -48,7 +59,8 @@ func main() {
|
|||||||
switch selftest.mode {
|
switch selftest.mode {
|
||||||
case "":
|
case "":
|
||||||
// No daemon loop yet.
|
// No daemon loop yet.
|
||||||
logger.Info("felhom-agent slice-1 scaffold; no run loop yet",
|
logger.Info("felhom-agent scaffold; no run loop yet",
|
||||||
|
"version", version,
|
||||||
"hint", "use --selftest (read-only) or --selftest=task --vmid N")
|
"hint", "use --selftest (read-only) or --selftest=task --vmid N")
|
||||||
// TODO: poll loop — slice 3/4.
|
// TODO: poll loop — slice 3/4.
|
||||||
return
|
return
|
||||||
@@ -87,7 +99,7 @@ func runSelftestRead(ctx context.Context, cfg config.Config, logger *slog.Logger
|
|||||||
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
fmt.Println("=== felhom-agent selftest (read-only) ===")
|
fmt.Printf("=== felhom-agent %s selftest (read-only) ===\n", version)
|
||||||
fmt.Printf("endpoint : %s node=%s\n", cfg.Proxmox.Endpoint, cfg.Proxmox.Node)
|
fmt.Printf("endpoint : %s node=%s\n", cfg.Proxmox.Endpoint, cfg.Proxmox.Node)
|
||||||
|
|
||||||
fail := 0
|
fail := 0
|
||||||
|
|||||||
Reference in New Issue
Block a user