feat(agent): scaffold + proxmox interaction layer (slice 1)
Stand up the felhom-agent project (module gitea.dooplex.hu/admin/felhom-agent, binary felhom-agent) and the internal/proxmox package: the typed library every other agent module calls to talk to Proxmox. - API-first Client (hand-rolled REST over net/http, PVEAPIToken auth) with typed read ops (version/nodes/status/lxc/config/storage) and async mutating ops (restore/vzdump/snapshot/rollback/delete-snapshot/setconfig/start/stop), each returning a UPID. WaitTask polls task status until stopped and asserts exitstatus OK (authz can surface at task exec, not the POST — phase1-2 §1.3). - Fenced Privileged (root-CLI) backend for the THREE proven exceptions only (keyctl pct create, USB mount/fstab, SMART/sensors); each cites why it can't be the API. Fence is structural (Client never shells out, Privileged never HTTPs) and asserted in routing_test.go. - TLS: SHA-256 leaf-cert pinning or CA file; insecure mode explicit + off by default. No blanket verification disable. - 403 -> privilege-named APIError; failed task -> privilege-named TaskError. - JSON config + env overrides (token never logged); slog logging. - cmd/felhom-agent --selftest (read-only health report) + gated --selftest=task (reversible snapshot/rollback/delete exercise of WaitTask). No daemon loop yet. - Types grounded in the spike findings and exact JSON shapes captured live from demo-felhom (PVE 9.2.2). Unit tests use a mock transport + runner. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
package proxmox
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestNormalizeFingerprint(t *testing.T) {
|
||||
// 64-hex with colons (the /nodes ssl_fingerprint form) normalizes fine.
|
||||
const withColons = "BA:7C:99:7D:45:D0:67:91:E2:F2:72:74:6E:D6:9F:83:51:D1:61:E5:C3:BD:F6:A0:B8:0B:E3:D8:DB:89:5B:CF"
|
||||
got, err := normalizeFingerprint(withColons)
|
||||
if err != nil {
|
||||
t.Fatalf("normalize: %v", err)
|
||||
}
|
||||
if len(got) != 64 {
|
||||
t.Errorf("len = %d", len(got))
|
||||
}
|
||||
if got != "ba7c997d45d06791e2f272746ed69f8351d161e5c3bdf6a0b80be3d8db895bcf" {
|
||||
t.Errorf("got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeFingerprint_Bad(t *testing.T) {
|
||||
for _, c := range []string{"", "tooshort", "zz7c997d45d06791e2f272746ed69f8351d161e5c3bdf6a0b80be3d8db895bcf"} {
|
||||
if _, err := normalizeFingerprint(c); err == nil {
|
||||
t.Errorf("normalize(%q) = nil, want error", c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTLSConfig_Build(t *testing.T) {
|
||||
// Fingerprint pin produces a config with a pin verifier (and the documented
|
||||
// InsecureSkipVerify=true that the verifier overrides).
|
||||
c, err := (TLSConfig{Fingerprint: "ba7c997d45d06791e2f272746ed69f8351d161e5c3bdf6a0b80be3d8db895bcf"}).build()
|
||||
if err != nil {
|
||||
t.Fatalf("build pin: %v", err)
|
||||
}
|
||||
if c.VerifyPeerCertificate == nil {
|
||||
t.Errorf("pin config missing VerifyPeerCertificate")
|
||||
}
|
||||
// Default (no trust set) uses system roots, no skip.
|
||||
def, err := (TLSConfig{}).build()
|
||||
if err != nil {
|
||||
t.Fatalf("build default: %v", err)
|
||||
}
|
||||
if def.InsecureSkipVerify {
|
||||
t.Errorf("default must verify")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user