Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ff94fe70bb | ||
|
|
2167819157 | ||
|
|
708989c40f | ||
|
|
8fa9e8aad9 | ||
|
|
1fa3e55b9c | ||
|
|
c3a581af95 | ||
|
|
5928c3fe75 |
+174
-102
@@ -1,113 +1,185 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(go version *)",
|
||||
"Bash(go mod *)",
|
||||
"Read(//Users/jan.novak/.claude/skills/go-project-bootstrap/references/**)",
|
||||
"Bash(ls -la /Users/jan.novak/.claude/skills/go-project-bootstrap/assets/ /Users/jan.novak/.claude/skills/go-project-bootstrap/assets/commands/ /Users/jan.novak/.claude/skills/go-project-bootstrap/assets/agents/ 2>&1)",
|
||||
"Read(//Users/jan.novak/.claude/skills/go-project-bootstrap/assets/**)",
|
||||
"Read(//Users/jan.novak/.claude/skills/go-project-bootstrap/assets/agents/**)",
|
||||
"Read(//Users/jan.novak/.claude/skills/go-project-bootstrap/assets/commands/**)",
|
||||
"Bash(mkdir -p /Users/jan.novak/srv/go/egress-proxies-operator/docs/plans && touch /Users/jan.novak/srv/go/egress-proxies-operator/docs/plans/.gitkeep && mkdir -p /Users/jan.novak/srv/go/egress-proxies-operator/.claude/commands /Users/jan.novak/srv/go/egress-proxies-operator/.claude/agents && ls -la /Users/jan.novak/srv/go/egress-proxies-operator)",
|
||||
"Bash(git status)",
|
||||
"Bash(git status:*)",
|
||||
"Bash(git diff:*)",
|
||||
"Bash(git log:*)",
|
||||
"Bash(git branch:*)",
|
||||
"Bash(git add:*)",
|
||||
"Bash(git checkout:*)",
|
||||
"Bash(git pull:*)",
|
||||
"Bash(git fetch:*)",
|
||||
"Bash(date:*)",
|
||||
"Read(//tmp/**)",
|
||||
"Read(//private/tmp/**)",
|
||||
"Bash(go build:*)",
|
||||
"Bash(go test:*)",
|
||||
"Bash(go vet:*)",
|
||||
"Bash(go mod:*)",
|
||||
"Bash(go run:*)",
|
||||
"Bash(go version:*)",
|
||||
"Bash(go get:*)",
|
||||
"Bash(golangci-lint run:*)",
|
||||
"Bash(golangci-lint --version)",
|
||||
"Bash(gofumpt:*)",
|
||||
"Bash(tea pr create:*)",
|
||||
"Bash(curl -sL https://go.dev/dl/?mode=json)",
|
||||
"Bash(find / -iname \"docs-conventions.md\" -maxdepth 6)",
|
||||
"Read(//Users/jan.novak/.claude/**)",
|
||||
"Read(//Users/jan.novak/.claude/skills/**)",
|
||||
"Bash(mkdir -p docs/plans && date \"+%Y-%m-%d-%H%M\")",
|
||||
"Bash(go install *)",
|
||||
"Bash(go env *)",
|
||||
"Bash(git rm *)",
|
||||
"Bash(mkdir -p /Users/jan.novak/srv/go/egress-proxies-operator/docs/plans-executions)",
|
||||
"Bash(cd /Users/jan.novak/srv/go/egress-proxies-operator *)",
|
||||
"Bash(echo \"build: $?\")",
|
||||
"Bash(echo \"vet: $?\")",
|
||||
"Bash(perl -i -pe 's{^\\\\t\\\\t\\\\t\\\\t\\\\t// TODO\\\\\\(user\\\\\\): Specify other spec details if needed\\\\.\\\\n}{\\\\t\\\\t\\\\t\\\\t\\\\t// A minimal, schema-valid spec so this placeholder test survives the\\\\n\\\\t\\\\t\\\\t\\\\t\\\\t// CEL/CRD validation added in Step 1. Rewritten wholesale in Step 4\\\\n\\\\t\\\\t\\\\t\\\\t\\\\t// alongside the real reconciler and envtest suite.\\\\n\\\\t\\\\t\\\\t\\\\t\\\\tSpec: crawlv1alpha1.ProxySpec{\\\\n\\\\t\\\\t\\\\t\\\\t\\\\t\\\\tMode: crawlv1alpha1.ModeExternal,\\\\n\\\\t\\\\t\\\\t\\\\t\\\\t\\\\tEndpoint: &crawlv1alpha1.EndpointSpec{Host: \"10.0.0.1\"},\\\\n\\\\t\\\\t\\\\t\\\\t\\\\t},\\\\n}' internal/controller/proxy_controller_test.go)",
|
||||
"Bash(go tool *)",
|
||||
"Bash(docker version *)",
|
||||
"Bash(curl -sI --max-time 5 https://hub.docker.com)",
|
||||
"Bash(kind get *)",
|
||||
"Bash(curl -s --max-time 5 \"https://hub.docker.com/v2/repositories/ubuntu/squid/tags?page_size=10\")",
|
||||
"Bash(python3 -c ' *)",
|
||||
"Bash(curl -s --max-time 5 \"https://hub.docker.com/v2/repositories/ubuntu/squid/tags?page_size=100\")",
|
||||
"Bash(go doc *)",
|
||||
"Bash(go list *)",
|
||||
"Bash(gofmt -w internal/provider/config.go)",
|
||||
"Bash(gofmt -l .)",
|
||||
"Bash(git restore *)",
|
||||
"Bash(make manifests *)",
|
||||
"Bash(make test *)",
|
||||
"Bash(KUBEBUILDER_ASSETS=\"/Users/jan.novak/srv/go/egress-proxies-operator/bin/k8s/1.36.2-darwin-arm64\" go test -race ./internal/controller/)",
|
||||
"Bash(grep -n 'func Channel' -A8 __CMDSUB_OUTPUT__/sigs.k8s.io/controller-runtime@v0.24.1/pkg/source/source.go)",
|
||||
"Bash(grep -n 'type GenericEvent' __CMDSUB_OUTPUT__/sigs.k8s.io/controller-runtime@v0.24.1/pkg/event/event.go)",
|
||||
"Bash(KUBEBUILDER_ASSETS=__TRACKED_VAR__/bin/k8s/1.36.2-darwin-arm64 go test -race ./...)",
|
||||
"Bash(cat >> *)",
|
||||
"Bash(make run-dev *)",
|
||||
"Bash(kubectl get *)",
|
||||
"Bash(kubectl delete *)",
|
||||
"Bash(make docker-build *)",
|
||||
"Bash(kind load *)",
|
||||
"Bash(make deploy *)",
|
||||
"Bash(kubectl -n egress-proxies-operator-system rollout status deploy/egress-proxies-operator-controller-manager --timeout=120s)",
|
||||
"Bash(kubectl -n egress-proxies-operator-system rollout restart deploy/egress-proxies-operator-controller-manager)",
|
||||
"Bash(git -C /Users/jan.novak/srv/go/egress-proxies-operator add docs/plans/2026-08-11-1742-gcp-provider-verbose-logging.md)",
|
||||
"Bash(git -C /Users/jan.novak/srv/go/egress-proxies-operator commit -m 'Add plan: verbose V-level logging in the GCP provider *)",
|
||||
"Bash(echo \"exit: $?\")",
|
||||
"Bash(echo \"tests exit: $?\")",
|
||||
"Bash(./bin/manager --version)",
|
||||
"Bash(./bin/manager-stamped --version)",
|
||||
"Bash(./bin/manager-pkg --version)",
|
||||
"Bash(docker run *)",
|
||||
"Bash(kubectl -n egress-proxies-operator-system get pods -o wide)",
|
||||
"Bash(kubectl -n egress-proxies-operator-system get deploy egress-proxies-operator-controller-manager -o jsonpath='{.spec.template.spec.containers[0].args}')",
|
||||
"Bash(kubectl -n egress-proxies-operator-system logs deploy/egress-proxies-operator-controller-manager)",
|
||||
"Bash(python3 -c \"import json; d=json.load\\(open\\('docs/deploy/sa_key.json'\\)\\); print\\(d.get\\('type'\\), d.get\\('client_email'\\)\\)\")",
|
||||
"Bash(tea pr *)",
|
||||
"Bash(git worktree *)",
|
||||
"Bash(python3 -c \"import yaml; yaml.safe_load\\(open\\('.gitea/workflows/build.yaml'\\)\\); print\\('YAML OK'\\)\")",
|
||||
"Bash(ruby -ryaml -e \"YAML.load_file\\('.gitea/workflows/build.yaml'\\); puts 'YAML OK'\")",
|
||||
"Bash(git -C /Users/jan.novak/srv/go/egress-proxies-operator status --short --branch)",
|
||||
"Bash(git -C /Users/jan.novak/srv/go/egress-proxies-operator log --oneline -1)",
|
||||
"Bash(git -C /Users/jan.novak/srv/go/egress-proxies-operator tag 0.01)",
|
||||
"Bash(git -C /Users/jan.novak/srv/go/egress-proxies-operator push origin 0.01)",
|
||||
"Bash(chmod +x docs/demo/run-demo.sh)",
|
||||
"Bash(bash -n docs/demo/run-demo.sh)",
|
||||
"Bash(git add *)",
|
||||
"Bash(git commit -q -m 'Add plan: KVM/libvirt provider *)",
|
||||
"Bash(go get *)",
|
||||
"Bash(go build *)",
|
||||
"Bash(go test *)",
|
||||
"Bash(git commit *)"
|
||||
"Bash(go vet *)",
|
||||
"Bash(go run *)",
|
||||
"Bash(go mod *)",
|
||||
"Bash(go get *)",
|
||||
"Bash(go install *)",
|
||||
"Bash(go env *)",
|
||||
"Bash(go tool *)",
|
||||
"Bash(go doc *)",
|
||||
"Bash(go list *)",
|
||||
"Bash(go version *)",
|
||||
"Bash(gofmt *)",
|
||||
"Bash(gofumpt *)",
|
||||
"Bash(golangci-lint run *)",
|
||||
"Bash(golangci-lint --version)",
|
||||
"Bash(./bin/manager* --version)",
|
||||
"Bash(make -n *)",
|
||||
"Bash(make manifests *)",
|
||||
"Bash(make test *)",
|
||||
"Bash(make test-integration-kvm *)",
|
||||
"Bash(make run-dev *)",
|
||||
"Bash(make docker-build *)",
|
||||
"Bash(make deploy *)",
|
||||
"Bash(make kvm-ssh-secret *)",
|
||||
"Bash(git status *)",
|
||||
"Bash(git diff *)",
|
||||
"Bash(git log *)",
|
||||
"Bash(git branch)",
|
||||
"Bash(git branch -a)",
|
||||
"Bash(git branch --show-current)",
|
||||
"Bash(git add *)",
|
||||
"Bash(git commit *)",
|
||||
"Bash(git fetch *)",
|
||||
"Bash(git pull *)",
|
||||
"Bash(git worktree list *)",
|
||||
"Bash(git config --get *)",
|
||||
"Bash(git config --list *)",
|
||||
"Bash(git merge-tree *)",
|
||||
"Bash(git ls-remote *)",
|
||||
"Bash(git -c credential.helper='!f\\(\\) { echo username=kacerr; echo \"password=$\\(tr -d \"[:space:]\" < ~/.config/gitea-token\\)\"; }; f' push origin feat/kvm-provider)",
|
||||
"Bash(git -c credential.helper='!f\\(\\) { echo username=kacerr; echo \"password=$\\(tr -d \"[:space:]\" < ~/.config/gitea-token\\)\"; }; f' push -q origin feat/kvm-provider)",
|
||||
"Bash(git -c credential.helper='!f\\(\\) { echo username=kacerr; echo \"password=$\\(tr -d \"[:space:]\" < ~/.config/gitea-token\\)\"; }; f' push -u origin feat/kvm-provider)",
|
||||
"Bash(tea pr create *)",
|
||||
"Bash(tea pr list *)",
|
||||
"Bash(curl -sL https://go.dev/dl/?mode=json)",
|
||||
"Bash(curl -s --max-time 10 https://gitea.home.hrajfrisbee.cz/api/v1/repos/kacerr/egress-proxies-operator/*)",
|
||||
"Bash(curl -s --max-time 10 'https://gitea.home.hrajfrisbee.cz/api/v1/repos/kacerr/egress-proxies-operator/*')",
|
||||
"Bash(curl -sI --max-time 10 https://gitea.home.hrajfrisbee.cz/v2/kacerr/egress-proxies-operator/*)",
|
||||
"Bash(curl -s -H \"Authorization: token $\\(tr -d '[:space:]' < ~/.config/gitea-token\\)\" https://gitea.home.hrajfrisbee.cz/api/v1/repos/kacerr/egress-proxies-operator/pulls/5)",
|
||||
"Bash(kubectl get *)",
|
||||
"Bash(kubectl wait *)",
|
||||
"Bash(kubectl kustomize *)",
|
||||
"Bash(kubectl apply *)",
|
||||
"Bash(kubectl run *)",
|
||||
"Bash(kubectl config current-context)",
|
||||
"Bash(kubectl config get-contexts *)",
|
||||
"Bash(kubectl config view *)",
|
||||
"Bash(kubectl -n egress-proxies-operator-system get *)",
|
||||
"Bash(kubectl -n egress-proxies-operator-system logs *)",
|
||||
"Bash(kubectl -n egress-proxies-operator-system rollout status *)",
|
||||
"Bash(kubectl -n egress-proxies-operator-system rollout restart *)",
|
||||
"Bash(kind get *)",
|
||||
"Bash(kind load *)",
|
||||
"Bash(kind version *)",
|
||||
"Bash(docker version *)",
|
||||
"Bash(docker info *)",
|
||||
"Bash(docker image ls *)",
|
||||
"Bash(docker image inspect *)",
|
||||
"Bash(docker manifest inspect *)",
|
||||
"Bash(docker history *)",
|
||||
"Bash(docker save *)",
|
||||
"Bash(docker run *)",
|
||||
"Bash(docker exec *)",
|
||||
"Bash(virsh -c qemu:///system list *)",
|
||||
"Bash(virsh -c qemu:///system net-list *)",
|
||||
"Bash(virsh -c qemu:///system net-dumpxml *)",
|
||||
"Bash(virsh -c qemu:///system pool-list *)",
|
||||
"Bash(virsh -c qemu:///system pool-dumpxml *)",
|
||||
"Bash(virsh -c qemu:///system vol-list *)",
|
||||
"Bash(ssh -i ~/.ssh/proxy-operator_ed25519 * novakj@beelink.tail8c69c.ts.net *)",
|
||||
"Bash(ip -br *)",
|
||||
"Bash(getent hosts *)",
|
||||
"Bash(tailscale status *)",
|
||||
"Bash(python3 *)",
|
||||
"Bash(grep *)",
|
||||
"Bash(echo \"*$?\")",
|
||||
"Bash(date *)",
|
||||
"Bash(env)",
|
||||
"Bash(command -v *)",
|
||||
"Bash(cat >> *)",
|
||||
"Bash(tar -tf *)",
|
||||
"Bash(bash -n *)",
|
||||
"Bash(mkdir -p docs/*)",
|
||||
"Read(//home/novakj/**)",
|
||||
"Read(//Users/jan.novak/.claude/**)",
|
||||
"Read(//tmp/**)",
|
||||
"Read(//private/tmp/**)",
|
||||
"Read(//etc/**)",
|
||||
"Read(//dev/**)",
|
||||
"Read(//run/libvirt/**)",
|
||||
"WebSearch",
|
||||
"WebFetch(domain:gitlab.com)",
|
||||
"WebFetch(domain:bugs.debian.org)",
|
||||
"WebFetch(domain:tracker.debian.org)",
|
||||
"WebFetch(domain:cloud.debian.org)",
|
||||
"WebFetch(domain:alioth-lists.debian.net)",
|
||||
"WebFetch(domain:www.mail-archive.com)",
|
||||
"WebFetch(domain:forum.proxmox.com)",
|
||||
"Bash(curl -s --max-time 10 'https://gitea.home.hrajfrisbee.cz/api/v1/repos/kacerr/egress-proxies-operator/pulls?state=open')",
|
||||
"Bash(python3 -c 'import json,sys; [print\\(p[\"number\"], p[\"head\"][\"ref\"], \"->\", p[\"base\"][\"ref\"], \"|\", p[\"title\"]\\) for p in json.load\\(sys.stdin\\)]')",
|
||||
"Bash(curl -s --max-time 10 'https://gitea.home.hrajfrisbee.cz/api/v1/repos/kacerr/egress-proxies-operator/pulls/5')",
|
||||
"Bash(python3 -c 'import json,sys; p=json.load\\(sys.stdin\\); print\\(p[\"state\"], \"merged=\", p[\"merged\"], p.get\\(\"merge_commit_sha\"\\)\\)')",
|
||||
"Read(//home/novakj/srv/egress-proxies-operator-simplify/origin/**)",
|
||||
"Bash(echo \"exit=$?\")",
|
||||
"Read(//home/novakj/srv/egress-proxies-operator-simplify/**)",
|
||||
"Bash(git -C /home/novakj/srv/egress-proxies-operator-simplify diff 82eeeef -- internal/controller/proxy_controller.go internal/provider/kvm/errors.go)",
|
||||
"Bash(git switch *)",
|
||||
"Bash(git -C /home/novakj/srv/egress-proxies-operator-simplify add -A)",
|
||||
"Bash(git -C /home/novakj/srv/egress-proxies-operator-simplify commit -q -m 'WIP: uncommitted simplify-pass edits preserved before worktree removal *)",
|
||||
"Bash(git -C /home/novakj/srv/egress-proxies-operator-simplify log --oneline -1)",
|
||||
"Bash(git worktree *)",
|
||||
"Bash(git branch *)",
|
||||
"Bash(git stash *)",
|
||||
"Bash(make generate *)",
|
||||
"Bash(bin/kustomize build *)",
|
||||
"Bash(grep -Ev \"^\\\\s*$\")",
|
||||
"Bash(make lint *)",
|
||||
"Bash(command -v kind docker)",
|
||||
"Bash(bin/golangci-lint version *)",
|
||||
"Bash(make test-e2e *)",
|
||||
"Bash(echo \"make test-e2e exit=$?\")",
|
||||
"Bash(ls \"$\\(go env GOMODCACHE\\)/golang.org/\")",
|
||||
"Bash(ls \"$\\(go env GOMODCACHE\\)/golang.org/toolchain@\"* -d)",
|
||||
"Bash(ps -eo pid,ppid,etime,args)",
|
||||
"Bash(kubectl config *)",
|
||||
"Bash(git commit -q -m 'Plan the provider blueprint document *)",
|
||||
"Bash(grep -nE '^#{1,3} ' docs/architecture.md; echo ---; sed -n '/Implemented providers/,/^## /p' README.md | head -30; echo ---; sed -n 140,180p config/manager/manager.yaml; echo ---; cat Dockerfile; echo ---; ls -a | grep -i golangci; echo ---; grep -nE 'Placement|placement|Region|Zone|MachineType|Image ' api/v1alpha1/proxy_types.go | head -40; echo ---; grep -nE 'Type|type' api/v1alpha1/providerhealth_types.go | head -20; echo ---; grep -n -i 'placement' docs/api.md | head; echo ---; grep -nE 'WithTimeout|Deadline|timeout' internal/controller/proxy_controller.go internal/gc/gc.go internal/providerhealth/monitor.go | head -20; echo ---; cat .gitea/workflows/build.yaml | sed -n 1,60p)",
|
||||
"WebFetch(domain:pve.proxmox.com)",
|
||||
"Bash(git -c credential.helper='!f\\(\\) { echo username=kacerr; echo \"password=$\\(tr -d \"[:space:]\" < ~/.config/gitea-token\\)\"; }; f' push -u origin feat/provider-blueprint)",
|
||||
"Bash(curl -s -H \"Authorization: token $\\(tr -d '[:space:]' < ~/.config/gitea-token\\)\" \"https://gitea.home.hrajfrisbee.cz/api/v1/repos/kacerr/egress-proxies-operator/pulls?state=open\")",
|
||||
"Bash(curl -s -H \"Authorization: token $\\(tr -d '[:space:]' < ~/.config/gitea-token\\)\" \"https://gitea.home.hrajfrisbee.cz/api/v1/repos/kacerr/egress-proxies-operator/pulls/7\")",
|
||||
"Bash(curl -s -X PATCH -H \"Authorization: token $\\(tr -d '[:space:]' < ~/.config/gitea-token\\)\" -H 'Content-Type: application/json' -d @/tmp/claude-1000/-home-novakj-srv-egress-proxies-operator/7662ab9d-4540-4676-a943-0874ac6b081d/scratchpad/pr7.json \"https://gitea.home.hrajfrisbee.cz/api/v1/repos/kacerr/egress-proxies-operator/pulls/7\")",
|
||||
"Bash(bin/golangci-lint run *)",
|
||||
"Bash(awk '/^## The image cache/,/^## [^T]/' docs/kvm-provider.md)",
|
||||
"Bash(awk '/^#+ .*Step 11/,0' docs/plans/2026-09-16-1333-kvm2-provider.md)",
|
||||
"Bash(sort -t: -k2 -u)",
|
||||
"Bash(awk '/kvm2/&&/^#/{f=1} f' docs/testing.md)",
|
||||
"Bash(awk -F'\"' '{print $2}')",
|
||||
"Bash(sed -n '/targets:/,/type: object/p' config/crd/bases/*providerhealths.yaml | sed -n 1,45p)",
|
||||
"Bash(ssh -i ~/.ssh/proxy-operator_ed25519 -o IdentitiesOnly=yes -o BatchMode=yes -o ConnectTimeout=10 -o UserKnownHostsFile=~/.ssh/kvm_known_hosts -o StrictHostKeyChecking=yes novakj@beelink 'hostname; id -un; virsh -c qemu:///system version; virsh -c qemu:///system pool-list --all; virsh -c qemu:///system list --all')",
|
||||
"Bash(awk '{print $1, $2}' ~/.ssh/kvm_known_hosts)",
|
||||
"Bash(ssh-keygen -F beelink -f ~/.ssh/kvm_known_hosts)",
|
||||
"Bash(ssh -i ~/.ssh/proxy-operator_ed25519 -o IdentitiesOnly=yes -o BatchMode=yes -o ConnectTimeout=10 -o UserKnownHostsFile=~/.ssh/kvm_known_hosts -o StrictHostKeyChecking=yes novakj@beelink.tail8c69c.ts.net 'hostname; id -un; id -Gn; virsh -c qemu:///system version; virsh -c qemu:///system pool-list --all --details; virsh -c qemu:///system list --all; command -v qemu-img')",
|
||||
"Bash(qemu-img --version)",
|
||||
"Bash(./probe)",
|
||||
"Bash(./probe -proxyrcv 8192)",
|
||||
"Bash(./probe -small)",
|
||||
"Bash(sysctl net.ipv4.tcp_wmem net.ipv4.tcp_retries2)",
|
||||
"Bash(GOFLAGS=-mod=mod GOPROXY=off go build -o probe .)",
|
||||
"Bash(./probe -clientsnd 131072 -proxyrcv 8192)",
|
||||
"Bash(truncate -s 64M gpt.raw)",
|
||||
"Bash(sfdisk -l gptshort.raw)",
|
||||
"Bash(sgdisk -v gptshort.raw)",
|
||||
"Bash(mkdir -p /tmp/claude-1000/-home-novakj-srv-egress-proxies-operator/8db56daf-938c-48b8-8a17-084f3e1dea44/scratchpad)",
|
||||
"Bash(curl -sS -m 30 -o storage_driver.c https://raw.githubusercontent.com/libvirt/libvirt/v11.6.0/src/storage/storage_driver.c)",
|
||||
"Bash(GOFLAGS=-mod=mod GOPROXY=off go vet ./...)",
|
||||
"Bash(GOFLAGS=-mod=mod GOPROXY=off go test -count=1 -run TestLeak -v . -args -n 1000 -transport unix)",
|
||||
"Bash(curl -sS -m 30 -o storage_util.c https://raw.githubusercontent.com/libvirt/libvirt/v11.6.0/src/storage/storage_util.c)",
|
||||
"Bash(awk '/^virStorageBackendVolUploadLocal/,/^}/' storage_util.c)",
|
||||
"Bash(export GOFLAGS=-mod=mod GOPROXY=off)",
|
||||
"Bash(awk '/WARNING: DATA RACE/{c++} c==1' race_ssh.log)",
|
||||
"Bash(kubectl version *)",
|
||||
"Bash(timeout 5 kubectl cluster-info)"
|
||||
],
|
||||
"additionalDirectories": [
|
||||
"/Users/jan.novak/srv/go/egress-proxies-operator/.claude",
|
||||
"/Users/jan.novak/srv/go/egress-proxies-operator/docs/plans",
|
||||
"/Users/jan.novak/srv/go/egress-proxies-operator/docs",
|
||||
"/Users/jan.novak/srv/go/egress-proxies-operator/docs/prompts",
|
||||
"/tmp",
|
||||
"/Users/jan.novak/srv/go/egress-proxies-operator/docs/plans-executions"
|
||||
"/home/novakj/srv/egress-proxies-operator",
|
||||
"/home/novakj/srv/egress-proxies-operator/config/crd/bases"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,320 +1,42 @@
|
||||
# egress-proxies-operator - AI Agent Guide
|
||||
|
||||
## Project Structure
|
||||
|
||||
**Single-group layout (default):**
|
||||
```
|
||||
cmd/main.go Manager entry (registers controllers/webhooks)
|
||||
api/<version>/*_types.go CRD schemas (+kubebuilder markers)
|
||||
api/<version>/zz_generated.* Auto-generated (DO NOT EDIT)
|
||||
internal/controller/* Reconciliation logic
|
||||
internal/webhook/* Validation/defaulting (if present)
|
||||
config/crd/bases/* Generated CRDs (DO NOT EDIT)
|
||||
config/rbac/role.yaml Generated RBAC (DO NOT EDIT)
|
||||
config/samples/* Example CRs (edit these)
|
||||
Makefile Build/test/deploy commands
|
||||
PROJECT Kubebuilder metadata Auto-generated (DO NOT EDIT)
|
||||
```
|
||||
|
||||
**Multi-group layout** (for projects with multiple API groups):
|
||||
```
|
||||
api/<group>/<version>/*_types.go CRD schemas by group
|
||||
internal/controller/<group>/* Controllers by group
|
||||
internal/webhook/<group>/<version>/* Webhooks by group and version (if present)
|
||||
```
|
||||
|
||||
Multi-group layout organizes APIs by group name (e.g., `batch`, `apps`). Check the `PROJECT` file for `multigroup: true`.
|
||||
|
||||
**To convert to multi-group layout:**
|
||||
1. Run: `kubebuilder edit --multigroup=true`
|
||||
2. Move APIs: `mkdir -p api/<group> && mv api/<version> api/<group>/`
|
||||
3. Move controllers: `mkdir -p internal/controller/<group> && mv internal/controller/*.go internal/controller/<group>/`
|
||||
4. Move webhooks (if present): `mkdir -p internal/webhook/<group> && mv internal/webhook/<version> internal/webhook/<group>/`
|
||||
5. Update import paths in all files
|
||||
6. Fix `path` in `PROJECT` file for each resource
|
||||
7. Update test suite CRD paths (add one more `..` to relative paths)
|
||||
|
||||
## Critical Rules
|
||||
|
||||
### Never Edit These (Auto-Generated)
|
||||
- `config/crd/bases/*.yaml` - from `make manifests`
|
||||
- `config/rbac/role.yaml` - from `make manifests`
|
||||
- `config/webhook/manifests.yaml` - from `make manifests`
|
||||
- `**/zz_generated.*.go` - from `make generate`
|
||||
- `PROJECT` - from `kubebuilder [OPTIONS]`
|
||||
|
||||
### Never Remove Scaffold Markers
|
||||
Do NOT delete `// +kubebuilder:scaffold:*` comments. CLI injects code at these markers.
|
||||
|
||||
### Keep Project Structure
|
||||
Do not move files around. The CLI expects files in specific locations.
|
||||
|
||||
### Always Use CLI Commands
|
||||
Always use `kubebuilder create api` and `kubebuilder create webhook` to scaffold. Do NOT create files manually.
|
||||
|
||||
### E2E Tests Require an Isolated Kind Cluster
|
||||
The e2e tests are designed to validate the solution in an isolated environment (similar to GitHub Actions CI).
|
||||
Ensure you run them against a dedicated [Kind](https://kind.sigs.k8s.io/) cluster (not your “real” dev/prod cluster).
|
||||
|
||||
## After Making Changes
|
||||
|
||||
**After editing `*_types.go` or markers:**
|
||||
```
|
||||
make manifests # Regenerate CRDs/RBAC from markers
|
||||
make generate # Regenerate DeepCopy methods
|
||||
```
|
||||
|
||||
**After editing `*.go` files:**
|
||||
```
|
||||
make lint-fix # Auto-fix code style
|
||||
make test # Run unit tests
|
||||
```
|
||||
|
||||
## CLI Commands Cheat Sheet
|
||||
|
||||
### Create API (your own types)
|
||||
```bash
|
||||
kubebuilder create api --group <group> --version <version> --kind <Kind>
|
||||
```
|
||||
|
||||
### Deploy Image Plugin (scaffold to deploy/manage ANY container image)
|
||||
|
||||
Generate a controller that deploys and manages a container image (nginx, redis, memcached, your app, etc.):
|
||||
|
||||
```bash
|
||||
# Example: deploying memcached
|
||||
kubebuilder create api --group example.com --version v1alpha1 --kind Memcached \
|
||||
--image=memcached:alpine \
|
||||
--plugins=deploy-image.go.kubebuilder.io/v1-alpha
|
||||
```
|
||||
|
||||
Scaffolds good-practice code: reconciliation logic, status conditions, finalizers, RBAC. Use as a reference implementation.
|
||||
|
||||
|
||||
### Create Webhooks
|
||||
```bash
|
||||
# Validation + defaulting
|
||||
kubebuilder create webhook --group <group> --version <version> --kind <Kind> \
|
||||
--defaulting --programmatic-validation
|
||||
|
||||
# Conversion webhook (for multi-version APIs)
|
||||
kubebuilder create webhook --group <group> --version v1 --kind <Kind> \
|
||||
--conversion --spoke v2
|
||||
```
|
||||
|
||||
### Controller for Core Kubernetes Types
|
||||
```bash
|
||||
# Watch Pods
|
||||
kubebuilder create api --group core --version v1 --kind Pod \
|
||||
--controller=true --resource=false
|
||||
|
||||
# Watch Deployments
|
||||
kubebuilder create api --group apps --version v1 --kind Deployment \
|
||||
--controller=true --resource=false
|
||||
```
|
||||
|
||||
### Controller for External Types (e.g., from other operators)
|
||||
|
||||
Watch resources from external APIs (cert-manager, Argo CD, Istio, etc.):
|
||||
|
||||
```bash
|
||||
# Example: watching cert-manager Certificate resources
|
||||
kubebuilder create api \
|
||||
--group cert-manager --version v1 --kind Certificate \
|
||||
--controller=true --resource=false \
|
||||
--external-api-path=github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1 \
|
||||
--external-api-domain=io \
|
||||
--external-api-module=github.com/cert-manager/cert-manager
|
||||
```
|
||||
|
||||
**Note:** Use `--external-api-module=<module>@<version>` only if you need a specific version. Otherwise, omit `@<version>` to use what's in go.mod.
|
||||
|
||||
### Webhook for External Types
|
||||
|
||||
```bash
|
||||
# Example: validating external resources
|
||||
kubebuilder create webhook \
|
||||
--group cert-manager --version v1 --kind Issuer \
|
||||
--defaulting \
|
||||
--external-api-path=github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1 \
|
||||
--external-api-domain=io \
|
||||
--external-api-module=github.com/cert-manager/cert-manager
|
||||
```
|
||||
|
||||
## Testing & Development
|
||||
|
||||
```bash
|
||||
make test # Run unit tests (uses envtest: real K8s API + etcd)
|
||||
make run # Run locally (uses current kubeconfig context)
|
||||
```
|
||||
|
||||
Tests use **Ginkgo + Gomega** (BDD style). Check `suite_test.go` for setup.
|
||||
|
||||
## Deployment Workflow
|
||||
|
||||
```bash
|
||||
# 1. Regenerate manifests
|
||||
make manifests generate
|
||||
|
||||
# 2. Build & deploy
|
||||
export IMG=<registry>/<project>:tag
|
||||
make docker-build docker-push IMG=$IMG # Or: kind load docker-image $IMG --name <cluster>
|
||||
make deploy IMG=$IMG
|
||||
|
||||
# 3. Test
|
||||
kubectl apply -k config/samples/
|
||||
|
||||
# 4. Debug
|
||||
kubectl logs -n <project>-system deployment/<project>-controller-manager -c manager -f
|
||||
```
|
||||
|
||||
### API Design
|
||||
|
||||
**Key markers for** `api/<version>/*_types.go`:
|
||||
|
||||
```go
|
||||
// +kubebuilder:object:root=true
|
||||
// +kubebuilder:subresource:status
|
||||
// +kubebuilder:resource:scope=Namespaced
|
||||
// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=".status.conditions[?(@.type=='Ready')].status"
|
||||
|
||||
// On fields:
|
||||
// +kubebuilder:validation:Required
|
||||
// +kubebuilder:validation:Minimum=1
|
||||
// +kubebuilder:validation:MaxLength=100
|
||||
// +kubebuilder:validation:Pattern="^[a-z]+$"
|
||||
// +kubebuilder:default="value"
|
||||
```
|
||||
|
||||
- **Use** `metav1.Condition` for status (not custom string fields)
|
||||
- **Use predefined types**: `metav1.Time` instead of `string` for dates
|
||||
- **Follow K8s API conventions**: Standard field names (`spec`, `status`, `metadata`)
|
||||
|
||||
### Controller Design
|
||||
|
||||
**RBAC markers in** `internal/controller/*_controller.go`:
|
||||
|
||||
```go
|
||||
// +kubebuilder:rbac:groups=mygroup.example.com,resources=mykinds,verbs=get;list;watch;create;update;patch;delete
|
||||
// +kubebuilder:rbac:groups=mygroup.example.com,resources=mykinds/status,verbs=get;update;patch
|
||||
// +kubebuilder:rbac:groups=mygroup.example.com,resources=mykinds/finalizers,verbs=update
|
||||
// +kubebuilder:rbac:groups=events.k8s.io,resources=events,verbs=create;patch
|
||||
// +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete
|
||||
```
|
||||
|
||||
**Implementation rules:**
|
||||
- **Idempotent reconciliation**: Safe to run multiple times
|
||||
- **Re-fetch before updates**: `r.Get(ctx, req.NamespacedName, obj)` before `r.Update` to avoid conflicts
|
||||
- **Structured logging**: `log := log.FromContext(ctx); log.Info("msg", "key", val)`
|
||||
- **Owner references**: Enable automatic garbage collection (`SetControllerReference`)
|
||||
- **Watch secondary resources**: Use `.Owns()` or `.Watches()`, not just `RequeueAfter`
|
||||
- **Finalizers**: Clean up external resources (buckets, VMs, DNS entries)
|
||||
|
||||
### Logging
|
||||
|
||||
**Follow Kubernetes logging message style guidelines:**
|
||||
|
||||
- Start from a capital letter
|
||||
- Do not end the message with a period
|
||||
- Active voice: subject present (`"Deployment could not create Pod"`) or omitted (`"Could not create Pod"`)
|
||||
- Past tense: `"Could not delete Pod"` not `"Cannot delete Pod"`
|
||||
- Specify object type: `"Deleted Pod"` not `"Deleted"`
|
||||
- Balanced key-value pairs
|
||||
|
||||
```go
|
||||
log.Info("Starting reconciliation")
|
||||
log.Info("Created Deployment", "name", deploy.Name)
|
||||
log.Error(err, "Failed to create Pod", "name", name)
|
||||
```
|
||||
|
||||
**Reference:** https://github.com/kubernetes/community/blob/master/contributors/devel/sig-instrumentation/logging.md#message-style-guidelines
|
||||
|
||||
### Webhooks
|
||||
- **Create all types together**: `--defaulting --programmatic-validation --conversion`
|
||||
- **When`--force`is used**: Backup custom logic first, then restore after scaffolding
|
||||
- **For multi-version APIs**: Use hub-and-spoke pattern (`--conversion --spoke v2`)
|
||||
- Hub version: Usually oldest stable version (v1)
|
||||
- Spoke versions: Newer versions that convert to/from hub (v2, v3)
|
||||
- Example: `--group crew --version v1 --kind Captain --conversion --spoke v2` (v1 is hub, v2 is spoke)
|
||||
|
||||
### Learning from Examples
|
||||
|
||||
The **deploy-image plugin** scaffolds a complete controller following good practices. Use it as a reference implementation:
|
||||
|
||||
```bash
|
||||
kubebuilder create api --group example --version v1alpha1 --kind MyApp \
|
||||
--image=<your-image> --plugins=deploy-image.go.kubebuilder.io/v1-alpha
|
||||
```
|
||||
|
||||
Generated code includes: status conditions (`metav1.Condition`), finalizers, owner references, events, idempotent reconciliation.
|
||||
|
||||
## Distribution Options
|
||||
|
||||
### Option 1: YAML Bundle (Kustomize)
|
||||
|
||||
```bash
|
||||
# Generate dist/install.yaml from Kustomize manifests
|
||||
make build-installer IMG=<registry>/<project>:tag
|
||||
```
|
||||
|
||||
**Key points:**
|
||||
- The `dist/install.yaml` is generated from Kustomize manifests (CRDs, RBAC, Deployment)
|
||||
- Commit this file to your repository for easy distribution
|
||||
- Users only need `kubectl` to install (no additional tools required)
|
||||
|
||||
**Example:** Users install with a single command:
|
||||
```bash
|
||||
kubectl apply -f https://raw.githubusercontent.com/<org>/<repo>/<tag>/dist/install.yaml
|
||||
```
|
||||
|
||||
### Option 2: Helm Chart
|
||||
|
||||
```bash
|
||||
kubebuilder edit --plugins=helm/v2-alpha # Generates dist/chart/ (default)
|
||||
kubebuilder edit --plugins=helm/v2-alpha --output-dir=charts # Generates charts/chart/
|
||||
```
|
||||
|
||||
**For development:**
|
||||
```bash
|
||||
make helm-deploy IMG=<registry>/<project>:<tag> # Deploy manager via Helm
|
||||
make helm-deploy IMG=$IMG HELM_EXTRA_ARGS="--set ..." # Deploy with custom values
|
||||
make helm-status # Show release status
|
||||
make helm-uninstall # Remove release
|
||||
make helm-history # View release history
|
||||
make helm-rollback # Rollback to previous version
|
||||
```
|
||||
|
||||
**For end users/production:**
|
||||
```bash
|
||||
helm install my-release ./<output-dir>/chart/ --namespace <ns> --create-namespace
|
||||
```
|
||||
|
||||
**Important:** If you add webhooks or modify manifests after initial chart generation:
|
||||
1. Backup any customizations in `<output-dir>/chart/values.yaml` and `<output-dir>/chart/manager/manager.yaml`
|
||||
2. Re-run: `kubebuilder edit --plugins=helm/v2-alpha --force` (use same `--output-dir` if customized)
|
||||
3. Manually restore your custom values from the backup
|
||||
|
||||
### Publish Container Image
|
||||
|
||||
```bash
|
||||
export IMG=<registry>/<project>:<version>
|
||||
make docker-build docker-push IMG=$IMG
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
### Essential Reading
|
||||
- **Kubebuilder Book**: https://book.kubebuilder.io (comprehensive guide)
|
||||
- **controller-runtime FAQ**: https://github.com/kubernetes-sigs/controller-runtime/blob/main/FAQ.md (common patterns and questions)
|
||||
- **Good Practices**: https://book.kubebuilder.io/reference/good-practices.html (why reconciliation is idempotent, status conditions, etc.)
|
||||
- **Logging Conventions**: https://github.com/kubernetes/community/blob/master/contributors/devel/sig-instrumentation/logging.md#message-style-guidelines (message style, verbosity levels)
|
||||
|
||||
### API Design & Implementation
|
||||
- **API Conventions**: https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md
|
||||
- **Operator Pattern**: https://kubernetes.io/docs/concepts/extend-kubernetes/operator/
|
||||
- **Markers Reference**: https://book.kubebuilder.io/reference/markers.html
|
||||
|
||||
### Tools & Libraries
|
||||
- **controller-runtime**: https://github.com/kubernetes-sigs/controller-runtime
|
||||
- **controller-tools**: https://github.com/kubernetes-sigs/controller-tools
|
||||
- **Kubebuilder Repo**: https://github.com/kubernetes-sigs/kubebuilder
|
||||
# egress-proxies-operator
|
||||
|
||||
## Sources and architecture
|
||||
|
||||
- Trust `go.mod`, `Makefile`, and current code over version tables or bootstrap prose. `CLAUDE.md`'s opening architecture/commands sections are stale; this is a working operator, not an empty scaffold.
|
||||
- `cmd/main.go` assembles the provider registry, reconciler, proxy health engine, in-memory lease store, discovery HTTP API, orphan GC, and provider-health monitor on one controller-runtime manager.
|
||||
- `api/v1alpha1` contains namespaced `Proxy` and cluster-scoped `ProviderHealth` resources (`crawl.example.com`). Validation/defaulting uses CRD markers/CEL; there are no admission webhooks.
|
||||
- `internal/provider` defines the backend contract independently of CRD types. Constructors are registered in `cmd/main.go`; `spec.provider` selects a configured **instance name**, not a provider type. `kvm` and `kvm2` are separate implementations and do not adopt each other's VMs.
|
||||
- Before changing a provider, read `docs/provider-contract.md`; use `docs/provider-spec-template.md` for a new one. Callers rely on self-contained provider IDs, truthful `ErrNotFound`, idempotent create/delete, and persisted UID/creation time for orphan GC. See `docs/providers-existing.md` for implementation-specific exceptions.
|
||||
- `docs/architecture.md` explains component ownership and state transitions; `docs/api.md` documents discovery/leases; `docs/testing.md` covers test boundaries and real-infrastructure setup.
|
||||
|
||||
## Commands and verification
|
||||
|
||||
- Go version comes from `go.mod` (currently 1.26.0). Make installs development tools into `bin/`.
|
||||
- After Go changes: `make lint-fix`, then `make test`. The latter regenerates manifests/DeepCopy, formats, vets, sets up envtest, then runs non-e2e tests with `-race` and writes `cover.out`.
|
||||
- Fast checks: `go test -short -race ./...` skips envtest. Focused package/test: `go test -race ./internal/lease/` or `go test -race -run TestAcquire ./internal/lease/`.
|
||||
- Envtest needs local API-server/etcd binaries; use `make setup-envtest` before targeted runs. For the controller suite with the current Kubernetes minor:
|
||||
```sh
|
||||
KUBEBUILDER_ASSETS="$(./bin/setup-envtest use 1.36 --bin-dir "$PWD/bin" -p path)" go test -race ./internal/controller/ -run TestControllers -ginkgo.focus='<spec regex>'
|
||||
```
|
||||
The Makefile derives the envtest **minor** from `go.mod`; do not replace it with a dependency patch version that may have no published binary bundle.
|
||||
- Tests are mostly stdlib `testing`; controller envtest and e2e use Ginkgo/Gomega. Fake clients do not execute CEL or schema defaults: test those with envtest, and use the API's `*OrDefault` helpers in unit tests. Envtest has no kubelet, so it cannot prove Squid starts or serves traffic.
|
||||
- `make lint` / `make lint-fix` builds a custom golangci-lint with the `logcheck` plugin from `.custom-gcl.yml`; a stock global binary is insufficient. `.golangci.yml` forbids `sort` (use `slices`). Logging uses controller-runtime/logr, with Kubernetes-style messages.
|
||||
- Real-host tests are opt-in: `make test-integration-kvm` / `make test-integration-kvm2` require the corresponding `KVM_TEST_*` / `KVM2_TEST_*` variables from `docs/testing.md`. After provider refactors, compile-check these with `go vet -tags=integration ./...`.
|
||||
- E2E requires Docker, kind, kubectl, and an isolated cluster. `make test-e2e` uses `KIND_CLUSTER=egress-proxies-operator-test-e2e`; ensure kubectl targets that cluster, especially if it already exists. On failure, run `make cleanup-test-e2e` explicitly. Compile-check with `go vet -tags=e2e ./...`. Tracing specs additionally require both `TEMPO_URL` and `OTLP_ENDPOINT`.
|
||||
- `.gitea/workflows/build.yaml` runs only on tags/manual dispatch; its gate is `go vet ./...`, `go build ./...`, `go test -short ./...`. It does not replace local lint, race, or envtest verification.
|
||||
|
||||
## Code generation
|
||||
|
||||
- After API types or Kubebuilder markers change, run `make manifests generate`. Edit source markers, never generated `config/crd/bases/*.yaml`, `config/rbac/role.yaml`, or `**/zz_generated.*.go`.
|
||||
- Keep `+kubebuilder:rbac` markers in standalone package-level comment blocks, outside function/type documentation; otherwise controller-gen can silently ignore them. Inspect `git diff config/rbac/role.yaml` after generation.
|
||||
- Preserve `+kubebuilder:scaffold:*` markers and the single-group layout. Use `kubebuilder create api` / `kubebuilder create webhook` for new scaffolding; `PROJECT` is CLI-managed.
|
||||
|
||||
## Behaviour and operational gotchas
|
||||
|
||||
- Managed proxies are replace-on-change: `internal/controller/spechash.go` hashes placement, resolved cloud-init content, and effective port. Do not hash all of `ProxySpec`: attributes, lease limits, and health settings must not churn VMs.
|
||||
- Cloud-init Secrets must carry `crawl.example.com/cloud-init: "true"`; the manager's Secret cache excludes unlabelled Secrets. Rotating labelled Secret content triggers VM replacement.
|
||||
- Proxy health checks fetch **through** the proxy. The health engine supplies snapshots/events and the reconciler writes status; status-affecting transitions drive writes, so `lastHealthCheckTime` is not a last-probe heartbeat.
|
||||
- Leases/cooldowns are in-memory and disappear on restart. Discovery is not leader-elected; the deployment uses one replica. Adding replicas alone does not provide shared lease capacity.
|
||||
- Use `make run-dev` locally: plain `make run` omits the required `--providers-config`. Host-side probes generally cannot route to kind pod IPs; deploy in-cluster using the README quickstart to verify kubernetes-provider health.
|
||||
- `make deploy IMG=...` and `make build-installer IMG=...` modify `config/manager/kustomization.yaml` in place. Check that diff before including it in a change.
|
||||
- Namespace-restricted mode disables orphan GC by default. `--gc-allow-namespaced` bypasses that guard and can classify instances owned outside the visible namespace as orphans.
|
||||
|
||||
@@ -1,5 +1,26 @@
|
||||
# Changelog
|
||||
|
||||
## 2026-09-18 07:52 UTC — docs/operating.md, starting with the libvirt-RPC log query
|
||||
|
||||
New home for day-to-day helpers against a running operator, seeded with the `jq` query
|
||||
that pulls timestamp + rpc out of the manager's JSON logs (both libvirt providers log the
|
||||
same `"libvirt call"` msg, so one filter covers `kvm` and `kvm2`), plus the `fromjson?`
|
||||
variant for streams carrying non-JSON lines. Linked from the README's doc index.
|
||||
|
||||
## 2026-09-18 07:44 UTC — a kvm2 crawl-pool sample
|
||||
|
||||
Until now the only kvm2 manifest was the one-VM `config/samples/proxy_kvm2.yaml`, which
|
||||
left the multi-proxy shape — the one a crawler actually leases from — undocumented.
|
||||
|
||||
- Added `config/samples/proxy_kvm2_crawl_pool.yaml`: five Managed Proxies on the `kvm2-home`
|
||||
entry, four sharing one labelled cloud-init Secret and one inline (it runs squid on 8080,
|
||||
so it cannot share a Secret that pins `http_port 3128`). Covers the `tier` attribute the
|
||||
discovery API selects on, a `medium`/`ubuntu-24.04` member with a raised `maxLeases`, and
|
||||
a member with a tightened `healthCheck`.
|
||||
- Kept out of `config/samples/kustomization.yaml` on purpose so `apply -k` doesn't boot a
|
||||
whole pool; the exclusion is commented there next to the existing `providers-config.yaml`
|
||||
one. Validated with `kubectl apply --dry-run=server`.
|
||||
|
||||
## 2026-09-17 20:36 UTC — kvm2: the four high findings from the code review, and its first run on a real hypervisor
|
||||
|
||||
A scoped review of the kvm2 provider (`review/REPORT.md`: 52 findings, 4 high, 20 medium,
|
||||
|
||||
@@ -310,4 +310,6 @@ and the manual kind verification procedure — is in
|
||||
|
||||
Project layout, reconcile-loop diagrams, and the decision log are in
|
||||
[docs/architecture.md](docs/architecture.md); the build history is in
|
||||
[docs/plans-executions/](docs/plans-executions/).
|
||||
[docs/plans-executions/](docs/plans-executions/). Day-to-day helpers for a
|
||||
running operator — log queries and one-off inspections — are collected in
|
||||
[docs/operating.md](docs/operating.md).
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
## Append samples of your project ##
|
||||
# providers-config.yaml is deliberately absent: it is a sample
|
||||
# --providers-config file, not a Kubernetes manifest.
|
||||
# proxy_kvm2_crawl_pool.yaml is deliberately absent too: it is a whole pool
|
||||
# of VMs, so it stays an explicit `kubectl apply -f` rather than something
|
||||
# `apply -k` boots along with the one-per-provider samples.
|
||||
resources:
|
||||
- proxy_kubernetes.yaml
|
||||
- proxy_gcp.yaml
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
# A pool of Managed crawl proxies on the kvm2 provider — the multi-proxy
|
||||
# counterpart to the single proxy_kvm2.yaml sample. Everything here boots on
|
||||
# the one host the kvm2 entry configures (beelink); the proxies differ in the
|
||||
# attributes crawlers select on, in size, and in image.
|
||||
#
|
||||
# Requires the same prerequisites as proxy_kvm2.yaml: a providers-config
|
||||
# entry named "kvm2-home" and the kvm-ssh Secret that the kvm and kvm2
|
||||
# providers share (docs/kvm2-provider.md).
|
||||
#
|
||||
# Deliberately NOT in kustomization.yaml: `kubectl apply -k config/samples`
|
||||
# would boot every VM in here at once. Apply it on its own when you want the
|
||||
# pool:
|
||||
#
|
||||
# kubectl apply -f config/samples/proxy_kvm2_crawl_pool.yaml
|
||||
#
|
||||
# Crawlers reach these through the discovery API by attribute, e.g.
|
||||
# `GET /v1/proxies?attr.purpose=crawl&healthy=true`, or by leasing one with
|
||||
# the same selector. Attribute matching is exact equality, so keep the keys
|
||||
# consistent across the pool and vary only the values you want to select on.
|
||||
#
|
||||
# The first Proxy for an image on the host waits for that image to download;
|
||||
# the rest reuse the cached copy. debian-13 and ubuntu-24.04 are separate
|
||||
# downloads, so the ubuntu member below is the slow one on a cold host.
|
||||
---
|
||||
# One cloud-init Secret shared by the whole pool: the proxies below point at
|
||||
# it with secretRef instead of each carrying its own copy of the squid
|
||||
# config. The crawl.example.com/cloud-init label is mandatory — the manager's
|
||||
# cache only holds Secrets that carry it, so an unlabelled Secret is
|
||||
# invisible to the operator and the Proxy never resolves.
|
||||
#
|
||||
# Editing this Secret changes the resolved cloud-init of every Proxy that
|
||||
# references it, and that replaces each of their VMs. That is the trade for
|
||||
# sharing one definition: convenient for a config rollout, disruptive if you
|
||||
# only meant to touch one member. Give a member its own Secret (or an inline
|
||||
# block) when it needs to diverge.
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: crawl-squid-cloud-init
|
||||
labels:
|
||||
crawl.example.com/cloud-init: "true"
|
||||
type: Opaque
|
||||
stringData:
|
||||
# The default key name; SecretKeySelector.key defaults to user-data, so
|
||||
# the proxies below omit it.
|
||||
user-data: |
|
||||
#cloud-config
|
||||
packages:
|
||||
- squid
|
||||
write_files:
|
||||
- path: /etc/squid/conf.d/proxy-operator.conf
|
||||
content: |
|
||||
http_port 3128
|
||||
http_access allow all
|
||||
via off
|
||||
forwarded_for off
|
||||
runcmd:
|
||||
- systemctl restart squid
|
||||
# Optional: log in with `ssh debian@<ip>` (or `ubuntu@<ip>` on the
|
||||
# ubuntu-24.04 member) to look around; `virsh console <name>` on the
|
||||
# host works too.
|
||||
# ssh_authorized_keys:
|
||||
# - ssh-ed25519 AAAA... you@laptop
|
||||
---
|
||||
apiVersion: crawl.example.com/v1alpha1
|
||||
kind: Proxy
|
||||
metadata:
|
||||
name: crawl-home-1
|
||||
spec:
|
||||
mode: Managed
|
||||
provider: kvm2-home
|
||||
placement:
|
||||
zone: beelink
|
||||
machineType: small
|
||||
image: debian-13
|
||||
port: 3128
|
||||
cloudInit:
|
||||
secretRef:
|
||||
name: crawl-squid-cloud-init
|
||||
attributes:
|
||||
geo: home
|
||||
purpose: crawl
|
||||
tier: general
|
||||
# ---
|
||||
# apiVersion: crawl.example.com/v1alpha1
|
||||
# kind: Proxy
|
||||
# metadata:
|
||||
# name: crawl-home-2
|
||||
# spec:
|
||||
# mode: Managed
|
||||
# provider: kvm2-home
|
||||
# placement:
|
||||
# zone: beelink
|
||||
# machineType: small
|
||||
# image: debian-13
|
||||
# port: 3128
|
||||
# cloudInit:
|
||||
# secretRef:
|
||||
# name: crawl-squid-cloud-init
|
||||
# attributes:
|
||||
# geo: home
|
||||
# purpose: crawl
|
||||
# tier: general
|
||||
@@ -0,0 +1,190 @@
|
||||
# A pool of Managed crawl proxies on the kvm2 provider — the multi-proxy
|
||||
# counterpart to the single proxy_kvm2.yaml sample. Everything here boots on
|
||||
# the one host the kvm2 entry configures (beelink); the proxies differ in the
|
||||
# attributes crawlers select on, in size, and in image.
|
||||
#
|
||||
# Requires the same prerequisites as proxy_kvm2.yaml: a providers-config
|
||||
# entry named "kvm2-home" and the kvm-ssh Secret that the kvm and kvm2
|
||||
# providers share (docs/kvm2-provider.md).
|
||||
#
|
||||
# Deliberately NOT in kustomization.yaml: `kubectl apply -k config/samples`
|
||||
# would boot every VM in here at once. Apply it on its own when you want the
|
||||
# pool:
|
||||
#
|
||||
# kubectl apply -f config/samples/proxy_kvm2_crawl_pool.yaml
|
||||
#
|
||||
# Crawlers reach these through the discovery API by attribute, e.g.
|
||||
# `GET /v1/proxies?attr.purpose=crawl&healthy=true`, or by leasing one with
|
||||
# the same selector. Attribute matching is exact equality, so keep the keys
|
||||
# consistent across the pool and vary only the values you want to select on.
|
||||
#
|
||||
# The first Proxy for an image on the host waits for that image to download;
|
||||
# the rest reuse the cached copy. debian-13 and ubuntu-24.04 are separate
|
||||
# downloads, so the ubuntu member below is the slow one on a cold host.
|
||||
---
|
||||
# One cloud-init Secret shared by the whole pool: the proxies below point at
|
||||
# it with secretRef instead of each carrying its own copy of the squid
|
||||
# config. The crawl.example.com/cloud-init label is mandatory — the manager's
|
||||
# cache only holds Secrets that carry it, so an unlabelled Secret is
|
||||
# invisible to the operator and the Proxy never resolves.
|
||||
#
|
||||
# Editing this Secret changes the resolved cloud-init of every Proxy that
|
||||
# references it, and that replaces each of their VMs. That is the trade for
|
||||
# sharing one definition: convenient for a config rollout, disruptive if you
|
||||
# only meant to touch one member. Give a member its own Secret (or an inline
|
||||
# block) when it needs to diverge.
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: crawl-squid-cloud-init
|
||||
labels:
|
||||
crawl.example.com/cloud-init: "true"
|
||||
type: Opaque
|
||||
stringData:
|
||||
# The default key name; SecretKeySelector.key defaults to user-data, so
|
||||
# the proxies below omit it.
|
||||
user-data: |
|
||||
#cloud-config
|
||||
packages:
|
||||
- squid
|
||||
write_files:
|
||||
- path: /etc/squid/conf.d/proxy-operator.conf
|
||||
content: |
|
||||
http_port 3128
|
||||
http_access allow all
|
||||
via off
|
||||
forwarded_for off
|
||||
runcmd:
|
||||
- systemctl restart squid
|
||||
# Optional: log in with `ssh debian@<ip>` (or `ubuntu@<ip>` on the
|
||||
# ubuntu-24.04 member) to look around; `virsh console <name>` on the
|
||||
# host works too.
|
||||
# ssh_authorized_keys:
|
||||
# - ssh-ed25519 AAAA... you@laptop
|
||||
---
|
||||
apiVersion: crawl.example.com/v1alpha1
|
||||
kind: Proxy
|
||||
metadata:
|
||||
name: crawl-home-1
|
||||
spec:
|
||||
mode: Managed
|
||||
provider: kvm2-home
|
||||
placement:
|
||||
zone: beelink
|
||||
machineType: small
|
||||
image: debian-13
|
||||
port: 3128
|
||||
cloudInit:
|
||||
secretRef:
|
||||
name: crawl-squid-cloud-init
|
||||
attributes:
|
||||
geo: home
|
||||
purpose: crawl
|
||||
tier: general
|
||||
---
|
||||
apiVersion: crawl.example.com/v1alpha1
|
||||
kind: Proxy
|
||||
metadata:
|
||||
name: crawl-home-2
|
||||
spec:
|
||||
mode: Managed
|
||||
provider: kvm2-home
|
||||
placement:
|
||||
zone: beelink
|
||||
machineType: small
|
||||
image: debian-13
|
||||
port: 3128
|
||||
cloudInit:
|
||||
secretRef:
|
||||
name: crawl-squid-cloud-init
|
||||
attributes:
|
||||
geo: home
|
||||
purpose: crawl
|
||||
tier: general
|
||||
---
|
||||
apiVersion: crawl.example.com/v1alpha1
|
||||
kind: Proxy
|
||||
metadata:
|
||||
name: crawl-home-3
|
||||
spec:
|
||||
mode: Managed
|
||||
provider: kvm2-home
|
||||
placement:
|
||||
zone: beelink
|
||||
machineType: small
|
||||
image: debian-13
|
||||
port: 3128
|
||||
cloudInit:
|
||||
secretRef:
|
||||
name: crawl-squid-cloud-init
|
||||
attributes:
|
||||
geo: home
|
||||
purpose: crawl
|
||||
tier: general
|
||||
---
|
||||
# A heavier member: two vCPUs and 2 GiB from the medium machine type, a
|
||||
# different base image, and room for more concurrent leases. tier=heavy is
|
||||
# what a crawler selects on to land here rather than on the general members.
|
||||
apiVersion: crawl.example.com/v1alpha1
|
||||
kind: Proxy
|
||||
metadata:
|
||||
name: crawl-home-heavy-1
|
||||
spec:
|
||||
mode: Managed
|
||||
provider: kvm2-home
|
||||
placement:
|
||||
zone: beelink
|
||||
machineType: medium
|
||||
image: ubuntu-24.04
|
||||
port: 3128
|
||||
cloudInit:
|
||||
secretRef:
|
||||
name: crawl-squid-cloud-init
|
||||
attributes:
|
||||
geo: home
|
||||
purpose: crawl
|
||||
tier: heavy
|
||||
maxLeases: 10
|
||||
---
|
||||
# A member that probes harder than the pool default (30s interval, 3 failures
|
||||
# to go unhealthy): a tighter loop takes a flapping proxy out of rotation
|
||||
# sooner, at the cost of more probe traffic. Everything under healthCheck is
|
||||
# defaulted, so set only what you want to change.
|
||||
#
|
||||
# This one also runs squid on a non-default port, which is why it carries its
|
||||
# own inline cloud-init: spec.port and the http_port in the config have to
|
||||
# agree, and the shared Secret above pins 3128.
|
||||
apiVersion: crawl.example.com/v1alpha1
|
||||
kind: Proxy
|
||||
metadata:
|
||||
name: crawl-home-probe-fast-1
|
||||
spec:
|
||||
mode: Managed
|
||||
provider: kvm2-home
|
||||
placement:
|
||||
zone: beelink
|
||||
machineType: small
|
||||
image: debian-13
|
||||
port: 8080
|
||||
cloudInit:
|
||||
inline: |
|
||||
#cloud-config
|
||||
packages:
|
||||
- squid
|
||||
write_files:
|
||||
- path: /etc/squid/conf.d/proxy-operator.conf
|
||||
content: |
|
||||
http_port 8080
|
||||
http_access allow all
|
||||
via off
|
||||
forwarded_for off
|
||||
runcmd:
|
||||
- systemctl restart squid
|
||||
healthCheck:
|
||||
intervalSeconds: 10
|
||||
timeoutSeconds: 3
|
||||
failureThreshold: 2
|
||||
attributes:
|
||||
geo: home
|
||||
purpose: crawl
|
||||
tier: general
|
||||
@@ -0,0 +1,58 @@
|
||||
# Operating the operator
|
||||
|
||||
Helpers for running the operator day to day: log queries, one-off
|
||||
inspections, and the small recipes that are easier to look up than to
|
||||
reconstruct. Not a runbook — it assumes you already know what you are
|
||||
looking for.
|
||||
|
||||
## Log queries
|
||||
|
||||
The manager logs one JSON object per line, so `jq` is the tool. The
|
||||
`.msg` field is the discriminator: it is a fixed string per event, and
|
||||
the variable parts live in their own fields rather than being
|
||||
interpolated into the message.
|
||||
|
||||
### Libvirt RPCs, with timestamps
|
||||
|
||||
Both libvirt providers log one `"libvirt call"` per RPC, so this catches
|
||||
`kvm` and `kvm2` alike:
|
||||
|
||||
```sh
|
||||
jq -r 'select(.msg == "libvirt call") | "\(.ts) \(.rpc)"'
|
||||
```
|
||||
|
||||
```text
|
||||
2026-09-18T07:48:29Z virStoragePoolGetInfo
|
||||
2026-09-18T07:48:29Z ConnectGetLibVersion
|
||||
2026-09-18T07:48:29Z ConnectGetHostname
|
||||
2026-09-18T07:48:29Z StoragePoolLookupByName
|
||||
2026-09-18T07:48:29Z StoragePoolGetInfo
|
||||
```
|
||||
|
||||
These lines are debug level, which is the binary's default
|
||||
(`zap.Options{Development: true}` in `cmd/main.go`) — nothing to turn on.
|
||||
They disappear if the Deployment has been given `--zap-log-level=info`,
|
||||
which is the switch for outcomes only.
|
||||
|
||||
Piping from a live manager:
|
||||
|
||||
```sh
|
||||
kubectl logs -n egress-proxies-operator-system \
|
||||
deploy/egress-proxies-operator-controller-manager -f \
|
||||
| jq -r 'select(.msg == "libvirt call") | "\(.ts) \(.rpc)"'
|
||||
```
|
||||
|
||||
`jq` aborts on the first line that is not JSON, which happens when
|
||||
something bypasses the structured logger — a panic trace, or output from
|
||||
a tool wrapping the manager. Reading line-by-line with `fromjson?` skips
|
||||
those instead:
|
||||
|
||||
```sh
|
||||
jq -rR 'fromjson? | select(.msg == "libvirt call") | [.ts, .rpc] | @tsv'
|
||||
```
|
||||
|
||||
Note that the connection-level events use their own `.msg` values
|
||||
(`"libvirt host connected"`, `"kvm2 host checked"`), so they drop out of
|
||||
the filters above. Widen to `select(.msg | startswith("libvirt"))` to
|
||||
keep the connect lines — usually what you want when chasing a latency
|
||||
spike rather than a call count.
|
||||
@@ -0,0 +1,282 @@
|
||||
# Execution: proxmox, a Proxmox VE provider built from the provider contract
|
||||
|
||||
Pairs with [docs/plans/2026-09-18-1502-proxmox-provider.md](../plans/2026-09-18-1502-proxmox-provider.md).
|
||||
|
||||
## Status
|
||||
|
||||
- [x] Phase 0 — Plan file, branch, host recon, fixtures
|
||||
- [ ] Phase 1 — The spec (`docs/proxmox-provider-spec.md`)
|
||||
- [ ] Phase 2 — Config block, registration, package skeleton
|
||||
- [ ] Phase 3 — Extract `nocloud` and `keyedlock` from kvm2
|
||||
- [ ] Phase 4 — Seam, HTTP client, decorator, fake
|
||||
- [ ] Phase 5 — Get, ListByTag, task helpers
|
||||
- [ ] Phase 6 — Create
|
||||
- [ ] Phase 7 — Delete, Checker, CRD comment
|
||||
- [ ] Phase 8 — Contract and observability sweep, integration test, testing.md
|
||||
- [ ] Phase 9 — Deployment and documentation
|
||||
- [ ] Phase 10 — Review, real-host validation, changelog, MR
|
||||
|
||||
## Phase 0 — Plan file, branch, host recon, fixtures
|
||||
|
||||
The plan was committed alone on `main` on 2026-09-18 (`708989c`, then
|
||||
`2167819` retargeting it at PVE 9.2). `origin/main` had not moved in the
|
||||
meantime, so the branch was cut straight from local `main`:
|
||||
|
||||
```bash
|
||||
git fetch -q origin && git rev-list --left-right --count origin/main...main # 0 2
|
||||
git switch -c feat/proxmox-provider
|
||||
```
|
||||
|
||||
The host is **pv1-1**, a single node (no cluster peers), `pve-manager/9.2.20`,
|
||||
kernel `7.0.14-17-pve`, QEMU 11.0. The user prepared the token
|
||||
(`~/.config/proxmox-token`, privsep off) and the CA (`~/.config/proxmox-ca.pem`)
|
||||
before the session.
|
||||
|
||||
**TLS: 192.168.2.8 is not in the node certificate.** The first probe failed
|
||||
with curl exit 60. The CA is right (`openssl s_client -CAfile` verifies the
|
||||
chain), but the leaf's SANs are `127.0.0.1, ::1, localhost, 192.168.0.8,
|
||||
pv1-1, pv1-1.home.lab`, and neither name resolves on the workstation.
|
||||
192.168.0.8 and 192.168.2.8 present the same certificate fingerprint: the
|
||||
node sits on `vmbr0` (192.168.0.8/24, untagged) with VLAN interfaces
|
||||
`vmbr0.2` (192.168.2.8/24) and `vmbr0.4` (192.168.4.8/24). All recon kept
|
||||
full verification by connecting to 192.168.2.8 while verifying the
|
||||
certificate as `pv1-1.home.lab`:
|
||||
|
||||
```bash
|
||||
curl -s --resolve pv1-1.home.lab:8006:192.168.2.8 --cacert ~/.config/proxmox-ca.pem \
|
||||
-H @<(printf 'Authorization: PVEAPIToken=%s\n' "$(tr -d '[:space:]' < ~/.config/proxmox-token)") \
|
||||
https://pv1-1.home.lab:8006/api2/json/version
|
||||
# {"data":{"release":"9.2","version":"9.2.20","repoid":"49318c671b82f31e"}}
|
||||
```
|
||||
|
||||
Every capture went through a scratch helper wrapping exactly that call. It
|
||||
passes the token through a file descriptor so it never reaches argv or
|
||||
output, and writes `{"status","reason","body"}` to
|
||||
`internal/provider/proxmox/testdata/<name>.json`, refusing to save a body
|
||||
that contains `PVEAPIToken`.
|
||||
|
||||
A real NoCloud seed was used instead of the plan's 1 KiB placeholder, so the
|
||||
agent could be installed and F21 captured from a live guest. No ISO tool is
|
||||
installed locally, so a throwaway module in the scratchpad reused the
|
||||
repository's `github.com/kdomanski/iso9660 v0.4.0` from the module cache and
|
||||
copied kvm2's `guestAgentScript` verbatim:
|
||||
|
||||
```bash
|
||||
GOFLAGS=-mod=mod GOPROXY=off GOSUMDB=off go run . proxy-reconaaaaaaaaaaa proxy-reconaaaaaaaaaaa-seed.iso
|
||||
# 45056 bytes, ISO 9660 'cidata'
|
||||
```
|
||||
|
||||
Write experiments ran against VMID 199999 (`proxy-reconaaaaaaaaaaa`), a
|
||||
clone of template 9001. Sequence: validation error; clone; resources, config
|
||||
write and start while the clone lock was held; status and agent while
|
||||
running bare; stop; config read; clone onto the taken VMID; tag tests;
|
||||
upload twice plus a bad name; attach `ide2`, set cores and memory; three
|
||||
resizes; start and agent poll; suspend and resume; a clone onto a storage
|
||||
without image support; stop; destroy with `purge=1&destroy-unreferenced-disks=1`;
|
||||
destroy again; ISO delete.
|
||||
|
||||
### Facts F1 to F23 (cited as Evidence by the spec)
|
||||
|
||||
- **F1.** Errors carry the text twice: in the HTTP reason phrase and as the
|
||||
body's `message`, which ends in `\n`. A 400 adds `errors`, a map from
|
||||
parameter to message. A 401 has the reason phrase only and an empty body.
|
||||
The client should use the trimmed body `message` and fall back to the
|
||||
reason phrase. Headers are ordinary CRLF.
|
||||
- **F2.** Missing VM, identical for `GET config`, `GET status/current` and
|
||||
`DELETE`: `500 Configuration file 'nodes/pv1-1/qemu-server/999999.conf' does not exist`.
|
||||
- **F3.** A start issued while the VM is clone-locked is **accepted** (200
|
||||
with a UPID). The task log reads `trying to acquire lock... OK` and the VM
|
||||
starts once the clone finishes. Setting a lock through the API was not
|
||||
tried.
|
||||
- **F4.** Clone onto a taken VMID, synchronous:
|
||||
`500 unable to create VM 199999: config file already exists`.
|
||||
- **F5.** Agent enabled but not running: `500 QEMU guest agent is not running`.
|
||||
Agent not enabled in the config: `500 No QEMU guest agent configured`.
|
||||
Stopped VM with `agent: 1`: `500 VM 199999 is not running`.
|
||||
- **F6.** Not produced. `local-lvm` is thin and overprovisions, so
|
||||
`quota: absent`.
|
||||
- **F7.** Config write while clone-locked: `500 VM is locked (clone)`.
|
||||
- **F8.** `/cluster/resources?type=vm` entries carry `vmid, name, node,
|
||||
status, type, template, tags, maxcpu, maxmem, maxdisk, uptime` and more;
|
||||
`lock` appears only when set. Status values seen: `running`, `stopped`,
|
||||
and `unknown` during a clone. **Mid-clone the entry has no `name`**: only
|
||||
`type, vmid, id, node, lock: clone, status: unknown`.
|
||||
- **F9.** Hyphenated tags are **accepted** and stored as given. Uppercase is
|
||||
folded (`Proxy_Operator` → `proxy_operator`), the separator is `;`, and
|
||||
characters such as a space or `!` give
|
||||
`400 … tags: invalid format - invalid characters in tag`. The datacenter
|
||||
has no `tag-style` or `user-tag-access` set.
|
||||
- **F10.** `description` on clone is accepted and survives verbatim,
|
||||
newlines included. The 22-character `proxy-<base32>` name is accepted.
|
||||
- **F11.** The clone's `meta` is copied from the template
|
||||
(`creation-qemu=11.0.3,ctime=1790145676`, identical to 9001), so it says
|
||||
nothing about when the clone was made. `written-at-create` is required.
|
||||
- **F12.** Upload: multipart with field `content=iso` and the file part named
|
||||
`filename`, carrying the target file name. Returns a UPID for an
|
||||
`imgcopy` task. A name without `.iso` gives
|
||||
`400 … filename: wrong file extension`. **Re-uploading an existing name
|
||||
silently overwrites it** (the task log shows a plain `cp`).
|
||||
`Datastore.AllocateTemplate` suffices.
|
||||
- **F13.** Deleting a volume needs `Datastore.Allocate` on
|
||||
`/storage/<id>`; without it the call gives
|
||||
`403 Permission check failed (/storage/local, Datastore.Allocate)`. With it
|
||||
(on `proxy-seeds`), the delete **runs as an `imgdel` task** and returns a
|
||||
UPID. Deleting a volume that no longer exists is also accepted and its
|
||||
task still ends `OK`; the log reads `disk image '…' does not exist`, then
|
||||
`Removed volume`. Seed cleanup is idempotent with no message matching.
|
||||
- **F14.** Stopped: `status: stopped, qmpstatus: stopped`. Paused:
|
||||
`status: running, qmpstatus: paused` in `status/current`, while
|
||||
`/cluster/resources` shows plain `running`.
|
||||
- **F15.** A clone onto a storage without image support is rejected up
|
||||
front (`400 … storage 'local' does not support vm images`) and leaves no
|
||||
VM. A clone failing mid-task was not observed.
|
||||
- **F16.** Task status fields: `status` (`running`, `stopped`), `exitstatus`
|
||||
(`OK` or the error text), `type, id, node, user, tokenid, upid, starttime,
|
||||
pid, pstart`. A full clone of the 3 GiB template onto `local-lvm` finished
|
||||
within about two seconds.
|
||||
- **F17.** The planned role, granted on `/`, covered clone, config, resize,
|
||||
start, stop, suspend, agent read, upload and destroy. **Missing:
|
||||
`Datastore.Allocate` for deleting the seed ISO.** `VM.Config.Cloudinit` is
|
||||
not needed for the `ide2` write. `SDN.Use` was held but never exercised,
|
||||
because no call rewrote `net0`.
|
||||
- **F18.** HTTP/1.1 only: `curl --http2` negotiates `HTTP/1.1`.
|
||||
- **F19.** Template 9001 (`debian-13-template`): `scsi0` on `local-lvm`,
|
||||
`boot: order=scsi0`, `ide2` free, `agent: 1`, OVMF, q35, `net0` untagged on
|
||||
`vmbr0`. **Attaching `ide2` makes Proxmox append it to the boot order**
|
||||
(`order=scsi0;ide2`). Template 9000 (Ubuntu) is unsuitable: it has a
|
||||
Proxmox cloud-init drive on `ide2` and VLAN tag 4 on `net0`.
|
||||
- **F20.** Resize **runs as a task** (returns a UPID). The same size is an
|
||||
`OK` no-op; shrinking fails inside the task with `exitstatus`
|
||||
`shrinking disks is not supported`.
|
||||
- **F21.** Agent response: `result[]` of `{name, ip-addresses[{ip-address,
|
||||
ip-address-type}]}`. The live guest reported `lo` and `ens18` with
|
||||
`192.168.0.150`, 50 seconds after start, via DHCP on untagged `vmbr0`.
|
||||
This proves the NoCloud-seed-on-`ide2` route end to end on this host.
|
||||
- **F22.** A wrong secret and an unknown token id both give
|
||||
`401 Authentication failed!`; no credentials gives `401 No ticket`.
|
||||
- **F23.** Uploads are staged in `/var/tmp/pveupload-*` and then copied to
|
||||
`/var/lib/vz/template/iso/`, so the tmpfs `/tmp` is not involved.
|
||||
|
||||
### Corrections to the plan that Phase 1 must fold into the spec
|
||||
|
||||
1. **Check `lock` before the name.** A mid-clone VM has no name in
|
||||
`/cluster/resources` (F8), so name-first logic turns a clone in progress
|
||||
into NotFound. Get, Delete and Create's lookup must treat a locked or
|
||||
nameless entry at our VMID as in-progress (Transient or wait), never
|
||||
absent.
|
||||
2. **Inspect a taken VMID before probing forward.** After a crash mid-clone,
|
||||
the retry's name lookup misses the nameless VM, the clone onto the
|
||||
deterministic VMID fails with F4, and forward probing would clone a
|
||||
duplicate. On F4, read that VMID: `lock: clone` means wait for it, and our
|
||||
UID in the description means adopt it. Probe forward only for a VM that
|
||||
is demonstrably someone else's.
|
||||
3. **A start queues behind a lock** instead of failing (F3).
|
||||
4. **Resize is asynchronous** (F20); wait on its task.
|
||||
5. **Drop delete-before-upload** for the seed: upload overwrites (F12).
|
||||
**Seed deletion is a task** (F13): wait on it. A repeat delete succeeds,
|
||||
so Delete needs no absent-volume matcher.
|
||||
6. **Get reads `status/current`**, not only `/cluster/resources`, to tell
|
||||
paused from running (F14).
|
||||
7. **Tag rationale**: hyphens are accepted on 9.2.20 (F9). `proxy_operator`
|
||||
stays, since it is valid everywhere, but the claim that hyphens are
|
||||
rejected is removed.
|
||||
8. **The boot-disk logic must skip `ide2`**, which Proxmox adds to the boot
|
||||
order (F19). The plan's rule already excludes it; add a test with
|
||||
`order=scsi0;ide2`.
|
||||
9. **Three agent messages, not one** (F5): `QEMU guest agent is not running`
|
||||
(booting, or the install failed), `No QEMU guest agent configured` (our
|
||||
`agent=1` was removed), and `VM <vmid> is not running` (stopped between
|
||||
the status read and the agent call). The spec decides each class; none
|
||||
may become NotFound.
|
||||
10. **Internet on the proxy network is a hard prerequisite**, twice over: the
|
||||
vendor-data agent install needs a package mirror, and the proxy itself
|
||||
needs egress. The guide says so, and recommends baking
|
||||
`qemu-guest-agent` into the template with `installGuestAgent: false` to
|
||||
drop the mirror dependency from IP discovery.
|
||||
|
||||
### Decisions made with the user in Phase 0
|
||||
|
||||
- **Seeds go to a dedicated directory storage**, `proxy-seeds`, with ISO
|
||||
content only. The token gets `Datastore.Allocate` there and nowhere else,
|
||||
so it can delete seeds but never backups or other ISOs on `local`. The
|
||||
sample config's `isoStorage` becomes `proxy-seeds`. The guide must say that
|
||||
an ACL on a more specific path replaces the inherited one for that user,
|
||||
so the role granted on `/storage/proxy-seeds` must repeat
|
||||
`Datastore.AllocateTemplate`, `AllocateSpace` and `Audit` alongside
|
||||
`Allocate`.
|
||||
- **Proxies land on a VLAN** set in config. `ProxmoxConfig` gains an
|
||||
optional `vlanTag` (1 to 4094), applied when Create rewrites `net0`
|
||||
together with the optional bridge override, keeping the MAC. Unset leaves
|
||||
`net0` as cloned. This is the first call that exercises `SDN.Use` (F17).
|
||||
- **The API URL is `https://192.168.0.8:8006`**, the address in the node
|
||||
certificate, so `caFile` verifies with no provider change. No
|
||||
`tlsServerName` option. The operator's cluster must be able to reach
|
||||
192.168.0.x.
|
||||
|
||||
### Seed storage and VLAN 4
|
||||
|
||||
The user created `proxy-seeds` (`dir`, ISO content only), granted the token
|
||||
`Datastore.Allocate`, `AllocateSpace`, `AllocateTemplate` and `Audit` on
|
||||
`/storage/proxy-seeds` through a role `ProxyOperatorSeeds`, and freed the
|
||||
leftover recon ISO on `local`. An upload to `proxy-seeds` and two deletes of
|
||||
a throwaway ISO there produced `content_delete_response.json`,
|
||||
`content_delete_again_response.json` and `task_status_volume_missing.json`.
|
||||
|
||||
The user gave the VLAN as "vlan 4, bridge: vmbr0.4@vmbr0". That is the
|
||||
host's own VLAN interface name as `ip link` prints it; a VM NIC cannot
|
||||
attach to it because it is not a bridge. On the VLAN-aware `vmbr0` the
|
||||
Proxmox form is `bridge=vmbr0,tag=4`, which is what template 9000 uses, so
|
||||
the provider's `vlanTag: 4` appends `tag=4` to the cloned `net0`.
|
||||
|
||||
A second clone of 9001 at VMID 199999 got its seed from `proxy-seeds` and
|
||||
`net0` rewritten from `virtio=BC:24:11:EE:48:83,bridge=vmbr0` to
|
||||
`virtio=BC:24:11:EE:48:83,bridge=vmbr0,tag=4`. **The write succeeded, so the
|
||||
token's `SDN.Use` on `/` covers a VLAN tag on `vmbr0`**
|
||||
(`set_config_vlan_response.json`).
|
||||
|
||||
**The first VLAN 4 attempt never produced an agent** in seven minutes of
|
||||
polling (every 13 seconds, 33 polls). The only signal was the agent call, so
|
||||
the cause could not be seen from the API. A debug seed added a `recon` user
|
||||
(user-data only, with a new instance id so cloud-init re-ran), and the user
|
||||
logged in on the console: DHCP worked, but VLAN 4 had no route to the
|
||||
internet because of a routing misconfiguration on the Proxmox host, so the
|
||||
vendor-data `apt-get install qemu-guest-agent` could not run. The user fixed
|
||||
the routing.
|
||||
|
||||
**Rerun from scratch after the fix**, same sequence the provider will use
|
||||
(destroy and seed delete first, then a fresh clone and seed, `instance-id`
|
||||
equal to the name):
|
||||
|
||||
| Step | Elapsed |
|
||||
|---|---|
|
||||
| clone task done | 4 s |
|
||||
| seed upload task done | 4 s |
|
||||
| config (`ide2`, `net0` with `tag=4`, tag) | 4 s |
|
||||
| start task done | 5 s |
|
||||
| agent reports `192.168.4.147` | 47 s after start |
|
||||
|
||||
The guest was reachable from the workstation (ping, TCP 22), as the contract
|
||||
requires of a proxy address. Stopping it gave the F5 stopped-VM fixture;
|
||||
destroy and seed delete then left the host with only its original VMs 100,
|
||||
9000 and 9001, no ISOs on `local` or `proxy-seeds`, and no `local-lvm`
|
||||
volumes of 199999. The capture is `agent_network_get_interfaces_vlan.json`.
|
||||
|
||||
### Worth noting
|
||||
|
||||
- The token file is mode 664; 600 would be tighter.
|
||||
- Fixtures: 48 files, all valid JSON, none containing `PVEAPIToken`.
|
||||
Several names differ from the plan's list because what they captured
|
||||
differed from what was expected: `start_while_clone_locked_response.json`
|
||||
(the start succeeded), `task_status_stopped_error.json` (from the shrink,
|
||||
not a start), `upload_overwrite_response.json` (success, not an error),
|
||||
`resize_same_size_response.json` and `resize_shrink_response.json`
|
||||
(UPIDs), `err_forbidden_volume_delete.json` (the 403 from F13).
|
||||
`err_no_space.json` was not produced (F6).
|
||||
- The host is back to its original state; see the table above.
|
||||
- The scratch seed builder (`seedgen` in the session scratchpad) took the
|
||||
user-data file and instance id as arguments by the end, for the debug
|
||||
login. It is not part of the repository; Phase 3's `nocloud` package
|
||||
replaces it.
|
||||
- Time to IP was about 50 s on both networks, with the agent installed at
|
||||
first boot. Budgets in the plan (running wait 60 s for the VM state, the
|
||||
agent discovered by `Get` polling) are consistent with this.
|
||||
@@ -0,0 +1,875 @@
|
||||
# Plan: proxmox, a Proxmox VE provider built from the provider contract
|
||||
|
||||
**Created:** 2026-09-18 15:02
|
||||
|
||||
## Context
|
||||
|
||||
The provider contract (`docs/provider-contract.md`), the spec template
|
||||
(`docs/provider-spec-template.md`) and the kvm2 build proved the workflow:
|
||||
spec first, then implementation against the contract, one commit per step.
|
||||
This plan adds a third VM-style backend, `proxmox`, on the Proxmox VE 9.x
|
||||
REST API at `https://<host>:8006/api2/json`. The user has a real host at
|
||||
192.168.2.8 running **9.2.20** (Debian 13 Trixie, QEMU 11) and prepares the
|
||||
API token and a cloud-init-capable template before Phase 0 (checklist
|
||||
below). Everything below is written against 9.2; where 8.x differed the
|
||||
difference is called out, because the appendix and the guide must name the
|
||||
version they were proven on.
|
||||
|
||||
The implementation runs in sessions whose context gets compacted, so every
|
||||
phase is self-contained: it names what to read first, what it creates or
|
||||
edits, the test names it must add, a `Verify:` clause, a "Done when" line
|
||||
and a commit subject. A fresh session reading only this plan, the spec
|
||||
(`docs/proxmox-provider-spec.md`, from Phase 1 on) and the execution log's
|
||||
checklist (`docs/plans-executions/2026-09-18-1502-proxmox-provider.md`) must be able to
|
||||
pick up where the last one stopped.
|
||||
|
||||
**Resumption protocol, run at the start of every phase.** Read this plan's
|
||||
Context and the phase entry; read the spec; read the execution log's
|
||||
checklist; run `git branch --show-current` (must be `feat/proxmox-provider`
|
||||
from Phase 0 on), `git status --short` and `git log --oneline -8`. The first
|
||||
unchecked box is the phase to do. If its files already exist partially, its
|
||||
"Done when" line says whether it counts as done; finish it, never redo it. A
|
||||
phase is finished only when its "Done when" holds and its execution-log entry
|
||||
(a few sentences, exact commands, "worth noting", the box ticked) is
|
||||
committed together with the code. Follow `CLAUDE.md` for the log format.
|
||||
|
||||
## Before Phase 0: what the user prepares on 192.168.2.8
|
||||
|
||||
Run as root on the Proxmox node. **`VM.Monitor` was removed in PVE 9.0**, so
|
||||
the agent read rests on `VM.GuestAgent.Audit` alone and the role below must
|
||||
not mention `VM.Monitor`. Drop any privilege `pveum` still rejects and note
|
||||
it for F17.
|
||||
|
||||
```bash
|
||||
pveum user add proxy-operator@pve --comment "egress-proxies-operator"
|
||||
pveum role add ProxyOperator -privs "VM.Allocate VM.Clone VM.Audit VM.Config.CDROM VM.Config.CPU VM.Config.Memory VM.Config.Disk VM.Config.Network VM.Config.Options VM.PowerMgmt VM.GuestAgent.Audit Datastore.Audit Datastore.AllocateSpace Datastore.AllocateTemplate Sys.Audit SDN.Use"
|
||||
pveum aclmod / -user proxy-operator@pve -role ProxyOperator
|
||||
pveum user token add proxy-operator@pve operator --privsep 0
|
||||
# The secret is printed once. Save it on the workstation as one line:
|
||||
# proxy-operator@pve!operator=<secret>
|
||||
# in ~/.config/proxmox-token (mode 0600). The same file becomes the
|
||||
# Kubernetes Secret later.
|
||||
cp /etc/pve/pve-root-ca.pem /tmp/ # copy to the workstation as ~/.config/proxmox-ca.pem, or use insecureSkipTLSVerify
|
||||
```
|
||||
|
||||
A cloud-init template without a Proxmox cloud-init drive (the provider puts
|
||||
its own NoCloud ISO on `ide2`). Debian 13's BIOS boot path did not boot
|
||||
under kvm2, so the template boots UEFI:
|
||||
|
||||
```bash
|
||||
wget https://cloud.debian.org/images/cloud/trixie/latest/debian-13-generic-amd64.qcow2
|
||||
qm create 9000 --name debian-13-template --memory 2048 --cores 2 --ostype l26 \
|
||||
--machine q35 --bios ovmf --efidisk0 local-lvm:1,efitype=4m,pre-enrolled-keys=0 \
|
||||
--scsihw virtio-scsi-pci --net0 virtio,bridge=vmbr0 \
|
||||
--serial0 socket --vga serial0 --agent 1
|
||||
qm set 9000 --scsi0 local-lvm:0,import-from=/root/debian-13-generic-amd64.qcow2
|
||||
qm set 9000 --boot order=scsi0
|
||||
qm template 9000
|
||||
```
|
||||
|
||||
Facts the user hands over at Phase 0: the token file path, the CA file path
|
||||
(or "insecure"), the node name, the template VMID, the ISO-content storage
|
||||
id (usually `local`), the VM disk storage (`local-lvm`), the bridge, and
|
||||
whether the operator's cluster can route to that bridge's LAN. Everything
|
||||
else Phase 0 reads from the API.
|
||||
|
||||
## Design decisions made with the user (not re-opened)
|
||||
|
||||
- **Backend and client.** Proxmox VE 9.x REST API, JSON, HTTP/1.1 only
|
||||
(pveproxy offers no HTTP/2 on any release; only Proxmox Backup Server
|
||||
does).
|
||||
Thin client on `net/http` (`client-kind: thin-client`), no SDK, no new
|
||||
dependency. Auth header
|
||||
`Authorization: PVEAPIToken=<user>@<realm>!<tokenid>=<secret>`, read from a
|
||||
mounted token file on every request (rotation free). TLS: `caFile` or
|
||||
`insecureSkipTLSVerify` (warned at startup). Child spans from a parent-gated
|
||||
otelhttp transport: `tracing.RestConfigWrapper()(base)` returns a plain
|
||||
`http.RoundTripper` and works as is (`child-spans: transport-wrapper`).
|
||||
- **Image source.** Clone a prepared template. Config catalogue
|
||||
`templates: {<key>: {vmid: 9000}}`; `Placement.Image` is a key.
|
||||
`Placement.Zone` is a node name from `nodes: [..]` (optional with one node).
|
||||
`Placement.MachineType` is a key of `machineTypes: {<key>: {cores, memoryMiB,
|
||||
diskGiB}}`; `diskGiB` (optional) grows the boot disk via `PUT .../resize`.
|
||||
`Placement.Region` is ignored (documented).
|
||||
- **Identity.** `primary-key: allocated-id`. VMID derived deterministically
|
||||
from the Proxy UID (FNV-1a 64 into `vmidRange: {min, max}`, default
|
||||
100000 to 199999) with forward probing on collision; a clone whose `newid`
|
||||
is taken fails with a 500, which is the allocate-then-verify step. Create
|
||||
first looks the name up in `GET /cluster/resources?type=vm`; an existing VM
|
||||
with that name must carry our mark and a matching or empty UID, else
|
||||
Permanent. providerID is location-free: `vms/<vmid>/<name>` (VMs migrate;
|
||||
the node is resolved from `/cluster/resources` on every call). Get and
|
||||
Delete verify the VM at that VMID has that name and our mark; a different
|
||||
name means our VM is gone and the VMID was reused: Get returns NotFound,
|
||||
Delete returns nil without touching it.
|
||||
- **Cloud-init.** NoCloud seed ISO built in-process by reusing kvm2's seed
|
||||
code, extracted into a shared package `internal/provider/nocloud` (its own
|
||||
phase). Uploaded to `isoStorage` (default `local`) on the target node via
|
||||
`POST /nodes/{node}/storage/{storage}/upload` (multipart, `content=iso`,
|
||||
`filename=<name>-seed.iso`, streamed through `io.Pipe`, returns a UPID),
|
||||
attached as `ide2: <storage>:iso/<name>-seed.iso,media=cdrom`. The template
|
||||
must have no Proxmox cloudinit drive on ide2 (documented prerequisite).
|
||||
Vendor-data installs qemu-guest-agent like kvm2 (`installGuestAgent`,
|
||||
`vendorData`).
|
||||
- **GC mark.** `gc-mark: written-metadata` in the VM `description`, which the
|
||||
clone call accepts (`tags` is not a clone parameter, so tags would be a
|
||||
mark window). Content, one per line: `proxy-operator-managed=true`,
|
||||
`proxy-operator-uid=<uid>`, `proxy-operator-created-at=<RFC3339>`.
|
||||
`created-at: written-at-create`. The config step after the clone also sets
|
||||
tag `proxy_operator` for UI filtering only. **Underscore, not hyphen:**
|
||||
current PVE tag validation accepts only lowercase letters, digits and
|
||||
underscore, and a hyphen is rejected; the underscore form is valid under
|
||||
both the strict and the permissive reading, so it is used regardless of
|
||||
what F9 finds. ListByTag: one
|
||||
`GET /cluster/resources?type=vm`; candidates are entries with tag
|
||||
`proxy_operator` or name prefix `proxy-`; a `GET /nodes/{node}/qemu/{vmid}/config`
|
||||
per candidate for the description; unmarked skipped; one unreadable VM
|
||||
skipped with a V(1) line unless ctx ended; the resources call failing fails
|
||||
the whole list (`list-partial: fail-whole`).
|
||||
- **Create** (`create-return: blocking`, `provisioning-state: create-blocks`,
|
||||
`async-ops: task-handle` and `backend-lock`), every step idempotent, in
|
||||
this order: validate placement (Permanent before any call); resources
|
||||
lookup by name (adopt or allocate); `POST /nodes/{tmplnode}/qemu/{tmplvmid}/clone`
|
||||
`{newid, name, description, full=1, target?, storage?}`; wait the task
|
||||
(`exitstatus != OK` is Transient with the message; a VM with `lock=clone`
|
||||
on retry is waited on); upload the seed (always rebuilt, delete then
|
||||
upload; before the config step because `ide2` may refuse a volume that
|
||||
does not exist yet); config (`cores`, `memory`, `net0` bridge override if
|
||||
configured, `agent=1`, `tags`, `ide2` seed); optional resize;
|
||||
`POST .../status/start` and wait; poll `status/current` until `running`
|
||||
(capped); return `vms/<vmid>/<name>`. Retry: name found running returns
|
||||
the ID; found stopped with our mark continues from the seed step; found
|
||||
locked waits.
|
||||
- **Get.** Resources lookup by vmid: absent is NotFound; name mismatch or no
|
||||
mark is NotFound (logged); `lock` set is Transient; `running` reads
|
||||
`GET /nodes/{node}/qemu/{vmid}/agent/network-get-interfaces`, first
|
||||
routable IPv4 excluding loopback and link-local gives Running, else
|
||||
Provisioning (the agent not running is a 500 "QEMU guest agent is not
|
||||
running", which is Provisioning, not an error); `stopped` is Stopped;
|
||||
`unknown` is Transient; `qmpstatus=paused` is Stopped; anything else is
|
||||
Stopped. UID and CreatedAt come from the description on every call, so a
|
||||
Get is three calls: resources, config, agent.
|
||||
- **Delete** (`delete-return: waits`, `secondary-resources: explicit`):
|
||||
resolve by vmid via resources; absent or foreign is nil (logged); running
|
||||
gets `POST status/stop` and a task wait; then
|
||||
`DELETE /nodes/{node}/qemu/{vmid}?purge=1&destroy-unreferenced-disks=1` and
|
||||
a task wait; then delete the seed ISO
|
||||
`DELETE /nodes/{node}/storage/{storage}/content/{storage}:iso/<name>-seed.iso`
|
||||
tolerating absence; a locked VM is Transient. Called again mid-delete it
|
||||
repeats the same tolerant steps. GC blind spot: seed ISOs without a VM
|
||||
(documented; the ISO name carries the instance name).
|
||||
- **Errors**, one `classify(op, err)` over `*apiError{Status, Message, Errors}`:
|
||||
401, 403, 400 and 404 are Permanent (404 means the path never exists;
|
||||
NotFound comes only from the list-and-miss on resources and from the 500
|
||||
"does not exist" message in Get and Delete, matched against captured
|
||||
fixtures); 500 "does not exist" is NotFound (Transient inside Create); 500
|
||||
"VM is locked" is Transient; 500 "already exists" is handled by the
|
||||
allocator, otherwise Transient; 500 "not enough free space" or "no space
|
||||
left" is QuotaExceeded once a real message is captured, else `quota:
|
||||
absent`; other 5xx, 595, 596, network and ctx errors are Transient;
|
||||
anything not an apiError is Transient. Error text: Proxmox puts most of it
|
||||
in the HTTP status reason phrase (`HTTP/1.1 500 Configuration file '...'
|
||||
does not exist`) and newer releases also in the body's `message`; the
|
||||
client reads both, body first.
|
||||
- **Checker.** One target per configured node, in config order, concurrently,
|
||||
on the same http client: `GET /version` (Details `pveVersion`),
|
||||
`GET /nodes/{node}/status` (Details `kernel`),
|
||||
`GET /nodes/{node}/storage/{isoStorage}/status` (Details `isoStorage`,
|
||||
`isoStorageAvailGiB`), template existence from `GET /cluster/resources?type=vm`
|
||||
(the `template` flag, any node; Details `templates` as `<found>/<configured>`).
|
||||
Reasons: the shared ones plus `TemplateNotFound` and `StorageNotFound`.
|
||||
The `status.type` comment in `api/v1alpha1/providerhealth_types.go` gains
|
||||
`proxmox`; `make manifests`.
|
||||
- **Timeouts** (`timeouts: per-call`): request timeout 30s
|
||||
(`requestTimeoutSeconds`); task-wait caps clone 10m, upload 2m, start and
|
||||
stop 2m, destroy 5m, running wait 60s; all in a `budgets` struct
|
||||
overridable in tests. A cancelled ctx cancels that request only; the
|
||||
connection pool is unaffected. Per-name keyed lock for Create and Delete
|
||||
(`internal/provider/keyedlock`, extracted from kvm2); Get and ListByTag
|
||||
lock-free.
|
||||
- **Logging** (`logging: tiered`): `logf.FromContext(ctx).WithName("proxmox").WithValues("provider", name)`,
|
||||
`vmid` and `name` on instance lines; V(1) outcomes and one
|
||||
`proxmox api call` line per seam call from the `observed` decorator; V(2)
|
||||
request params and response summaries; never the token, never cloud-init,
|
||||
never the multipart body, never `description` at V(1); no V(5). The
|
||||
decorator logs, the transport traces: no `proxmox.<Method>` spans, so
|
||||
nothing is double-counted.
|
||||
- **Config writes** use `PUT .../config` (synchronous; the VM is stopped at
|
||||
that point). `resize` is skipped when the boot disk's current `size=` is
|
||||
at or above the target. Boot disk: first disk key in `boot: order=...`
|
||||
that is not `net*` and not the ide2 cdrom, else `bootdisk`, else the first
|
||||
of `scsi0`, `virtio0`, `sata0`, `ide0`; none is Permanent.
|
||||
- **A UPID names its node** (`UPID:<node>:...`); task polling uses the node
|
||||
parsed from the UPID.
|
||||
- **Two VMs with our name** (Proxmox does not enforce unique names) make
|
||||
Create and Get return Permanent naming both VMIDs; an admin resolves it.
|
||||
- **Seam.** `api` interface with one method per endpoint (table below),
|
||||
`httpAPI` production adapter, `fakeAPI` in `fake_test.go` keeping VMs, tasks
|
||||
and ISOs in memory with lock simulation, task polls, per-method errors and
|
||||
block channels, and a call log.
|
||||
- **Integration test** `proxmox_integration_test.go` behind
|
||||
`//go:build integration`, `PROXMOX_TEST_URL`, `PROXMOX_TEST_TOKEN_FILE`,
|
||||
`PROXMOX_TEST_NODE`, `PROXMOX_TEST_TEMPLATE_VMID`, `PROXMOX_TEST_ISO_STORAGE`,
|
||||
optional `_CA_FILE`, `_INSECURE`, `_BRIDGE`, `_STORAGE`, `_VMID_MIN`,
|
||||
`_VMID_MAX`, `_TIMEOUT`, `_KEEP`, `_LOG_LEVEL`; `make test-integration-proxmox`.
|
||||
- **Deployment.** Secret `proxmox-token` mounted optional at
|
||||
`/etc/proxy-operator/proxmox/token`, `make proxmox-token-secret`, commented
|
||||
entries in `config/manager/providers_config.yaml` and
|
||||
`config/samples/providers-config.yaml`, `config/samples/proxy_proxmox.yaml`,
|
||||
`docs/proxmox-provider.md`, `providers-existing.md` column and section,
|
||||
`architecture.md` rows, README row, `testing.md` section, CHANGELOG after
|
||||
confirmation. Branch `feat/proxmox-provider`, MR via the Gitea API (no `tea`).
|
||||
|
||||
## Backend facts Phase 0 must verify on 192.168.2.8 (numbered for citation)
|
||||
|
||||
- F1. Where error text lives: status reason phrase vs. JSON `message`; the
|
||||
`errors` map shape on a 400.
|
||||
- F2. Exact 500 message for a missing VM (`GET /nodes/{node}/qemu/999999/config`).
|
||||
- F3. Exact 500 message for a locked VM (`POST status/start` on a VM with
|
||||
`lock` set), and whether `PUT config` with `lock=backup` sets the lock and
|
||||
`delete=lock` clears it through the API, or `qm unlock` is needed.
|
||||
- F4. Exact 500 message when `newid` is in use on clone.
|
||||
- F5. Exact 500 message for the agent not running, and for the agent call
|
||||
on a stopped VM.
|
||||
- F6. Exact message for no space, if it can be produced cheaply; otherwise
|
||||
`quota: absent`.
|
||||
- F7. Whether `PUT .../config` is refused while `lock=clone`, and with which
|
||||
message.
|
||||
- F8. `/cluster/resources?type=vm` fields present: `vmid`, `name`, `node`,
|
||||
`status` values (`running`, `stopped`, `unknown`), `type` (`qemu`, `lxc`),
|
||||
`lock`, `tags` (separator), `template`; whether `lock` appears there during
|
||||
a clone.
|
||||
- F9. Tag rules on 9.2: confirm a hyphen is rejected and `proxy_operator`
|
||||
accepted, plus case folding, length, and the `tags` separator on
|
||||
`PUT config` and in `/cluster/resources`. The underscore form stays either
|
||||
way; this fact only decides what the guide tells users about their own
|
||||
tags.
|
||||
- F10. `description` on `POST .../clone`: accepted, survives verbatim
|
||||
(newlines), max length; whether `name` accepts the 22-character
|
||||
`proxy-<base32>` form.
|
||||
- F11. `meta` in the cloned config: is `ctime` the clone time (evidence only;
|
||||
`written-at-create` stays the profile).
|
||||
- F12. Upload endpoint: field names (`content`, `filename`, file part name
|
||||
`filename`), the filename rule (`.iso` suffix, no slashes), the response
|
||||
(UPID), the privilege it needs, and whether re-uploading an existing name
|
||||
is refused (then delete-then-upload) or overwrites. On 9.x `content`
|
||||
accepts `iso`, `vztmpl` or `import` and still refuses `snippets`, which is
|
||||
why the seed travels as an ISO; confirm `iso` is what an `.iso` needs.
|
||||
- F13. `DELETE /nodes/{node}/storage/{storage}/content/{volid}`: response
|
||||
shape (UPID or null) and the message for a missing volume.
|
||||
- F14. `status/current` on a stopped VM: is `qmpstatus` present; on a paused
|
||||
VM: `status=running`, `qmpstatus=paused`.
|
||||
- F15. `POST .../clone`: whether a failed clone destroys the partial VM by
|
||||
itself, and what a VM interrupted mid-clone looks like (config present,
|
||||
`lock=clone`).
|
||||
- F16. Task status shape: `status` (`running`, `stopped`), `exitstatus`
|
||||
(`OK` or the error string), and how long a full clone of the template
|
||||
takes.
|
||||
- F17. Privileges: which of the prepared list the token really needs, in
|
||||
particular the exact ACL path that satisfies `SDN.Use` for the `net0`
|
||||
write (the 9.x docs describe it as "access SDN vnets and local network
|
||||
bridges" without spelling out the path for a plain bridge), and
|
||||
`VM.GuestAgent.Audit` for the agent read. Also whether the config step
|
||||
needs `VM.Config.Cloudinit` even though only `ide2` is written.
|
||||
- F18. HTTP/2 is not offered by pveproxy on any release (documented), so the
|
||||
client disables it; confirm with `curl --http2 -I` that the connection
|
||||
falls back to HTTP/1.1.
|
||||
- F19. The template's disk key (`scsi0` or `virtio0`), its `boot` line, that
|
||||
ide2 is free and no `cloudinit` drive exists.
|
||||
- F20. `resize` on a stopped VM: request shape, the message when the target
|
||||
is smaller or equal, and that it is synchronous.
|
||||
- F21. Agent response shape: `result[].name`, `result[]["ip-addresses"][]`
|
||||
with `ip-address`, `ip-address-type`.
|
||||
- F22. The 401 body and reason phrase on 9.2. PVE 9.2 unified the
|
||||
authentication-failure message so a failed login no longer leaks whether
|
||||
the user exists, so any 401 text remembered from 8.x is wrong. Capture it
|
||||
fresh into `err_unauthorized.json`.
|
||||
- F23. Where the upload endpoint stages the request body, given that PVE 9
|
||||
moved the node's `/tmp` to a tmpfs capped at half of RAM and cleans `/tmp`
|
||||
and `/var/tmp` periodically. A seed ISO is tens of KiB so this cannot
|
||||
bite in practice, but the observation belongs in the guide next to the
|
||||
size limit.
|
||||
|
||||
## Proxmox endpoint map for the seam
|
||||
|
||||
| `api` method | HTTP |
|
||||
|---|---|
|
||||
| `Version(ctx)` | `GET /version` |
|
||||
| `NodeStatus(ctx, node)` | `GET /nodes/{node}/status` |
|
||||
| `StorageStatus(ctx, node, storage)` | `GET /nodes/{node}/storage/{storage}/status` |
|
||||
| `ClusterResources(ctx)` | `GET /cluster/resources?type=vm` |
|
||||
| `VMConfig(ctx, node, vmid)` | `GET /nodes/{node}/qemu/{vmid}/config` |
|
||||
| `SetVMConfig(ctx, node, vmid, params)` | `PUT /nodes/{node}/qemu/{vmid}/config` |
|
||||
| `Clone(ctx, node, vmid, cloneParams)` | `POST /nodes/{node}/qemu/{vmid}/clone` |
|
||||
| `Resize(ctx, node, vmid, disk, size)` | `PUT /nodes/{node}/qemu/{vmid}/resize` |
|
||||
| `Start(ctx, node, vmid)` | `POST /nodes/{node}/qemu/{vmid}/status/start` |
|
||||
| `Stop(ctx, node, vmid)` | `POST /nodes/{node}/qemu/{vmid}/status/stop` |
|
||||
| `VMStatus(ctx, node, vmid)` | `GET /nodes/{node}/qemu/{vmid}/status/current` |
|
||||
| `Destroy(ctx, node, vmid)` | `DELETE /nodes/{node}/qemu/{vmid}?purge=1&destroy-unreferenced-disks=1` |
|
||||
| `TaskStatus(ctx, node, upid)` | `GET /nodes/{node}/tasks/{upid}/status` |
|
||||
| `AgentNetworkInterfaces(ctx, node, vmid)` | `GET /nodes/{node}/qemu/{vmid}/agent/network-get-interfaces` |
|
||||
| `UploadISO(ctx, node, storage, filename, r, size)` | `POST /nodes/{node}/storage/{storage}/upload` (multipart) |
|
||||
| `DeleteVolume(ctx, node, storage, volid)` | `DELETE /nodes/{node}/storage/{storage}/content/{volid}` |
|
||||
|
||||
## Package layout (`internal/provider/proxmox/`)
|
||||
|
||||
`proxmox.go` (`Provider`, `New`, `newProvider`, `withDefaults`, `logger`,
|
||||
`node`, `wrapErr`, `lockName`, `providerOption`, `budgets`), `api.go` (the
|
||||
`api` interface and DTOs `vmResource`, `vmConfig`, `vmStatus`, `taskStatus`,
|
||||
`agentInterface`, `storageStatus`, `nodeStatus`, `versionInfo`,
|
||||
`cloneParams`), `client.go` (`httpAPI`, `newHTTPAPI`, `do`, `readToken`,
|
||||
`decodeConfig`), `errors.go` (`apiError`, `classify`, `permanentf`,
|
||||
`transientf`, `isNotExist`, `isLocked`, `isAlreadyExists`,
|
||||
`isAgentNotRunning`, `isNoSpace`), `id.go` (`formatProviderID`,
|
||||
`parseProviderID`, `vmidFor`), `mark.go` (`instanceMark`,
|
||||
`formatDescription`, `parseDescription`), `state.go` (`mapState`,
|
||||
`firstIPv4`), `observe.go` (`observed`, `observe`), `task.go` (`parseUPID`,
|
||||
`waitTask`, `waitUnlocked`, `waitRunning`), `seed.go` (`seedISOName`,
|
||||
`seedVolID`, `uploadSeed`), `create.go`, `get.go`, `delete.go`, `list.go`,
|
||||
`check.go`, plus `fake_test.go`, `provider_test.go`,
|
||||
`proxmox_integration_test.go`, `testdata/*.json`. Shared:
|
||||
`internal/provider/config_proxmox.go`, `internal/provider/nocloud/`,
|
||||
`internal/provider/keyedlock/`.
|
||||
|
||||
Reused as-is: `provider.NameFromUID`, `provider.Wrap`, the `Err*` sentinels,
|
||||
`provider.Label*` constants, `provider.Reason*` constants,
|
||||
`tracing.RestConfigWrapper`, `tracing.WithTracerProvider`, kvm2's test
|
||||
idioms (`fixture`, `assertProviderError`, `logCapture`).
|
||||
|
||||
## Steps (one commit per phase, on `feat/proxmox-provider` off `main`)
|
||||
|
||||
- **Phase 0 (orchestrator with the user): plan file, branch, host recon,
|
||||
fixtures.** Read first: contract §5, §4.9, §7;
|
||||
`docs/plans-executions/2026-09-16-1333-kvm2-provider.md` Step 0 entry.
|
||||
Do, in this order: `git switch main && git pull --ff-only`; copy this plan
|
||||
to `docs/plans/2026-09-18-1502-proxmox-provider.md` with the real timestamp
|
||||
(`date "+%Y-%m-%d-%H%M"`), commit it alone on `main`
|
||||
(`docs: plan the proxmox provider`); create `feat/proxmox-provider`. Ask
|
||||
the user for the hand-over facts listed above. Then, with the user's
|
||||
go-ahead (each `curl` is a permission prompt; read-only ones first, one at
|
||||
a time), capture F1 to F21 with
|
||||
`curl -s --cacert "$CA" -D /tmp/h.txt -o /tmp/b.json -w '%{http_code}\n' -H "Authorization: PVEAPIToken=$(cat "$TOKEN_FILE")" https://192.168.2.8:8006/api2/json/<path>`,
|
||||
storing each response as `internal/provider/proxmox/testdata/<name>.json`
|
||||
in the shape `{"status": <code>, "reason": "<status line after the
|
||||
code>", "body": "<raw body>"}`: `version.json`, `node_status.json`,
|
||||
`storage_status.json`, `cluster_resources.json`,
|
||||
`vm_config_template.json`, `vm_config_clone.json`,
|
||||
`status_current_running.json`, `status_current_stopped.json`,
|
||||
`agent_network_get_interfaces.json`, `task_status_running.json`,
|
||||
`task_status_stopped_ok.json`, `task_status_stopped_error.json`,
|
||||
`upload_response.json`, `content_delete_response.json`,
|
||||
`err_vm_missing.json`, `err_locked.json`, `err_newid_in_use.json`,
|
||||
`err_agent_not_running.json`, `err_agent_vm_stopped.json`,
|
||||
`err_unauthorized.json` (a wrong secret), `err_forbidden.json` (a path
|
||||
the token lacks), `err_bad_param.json` (`newid=abc`), `err_no_space.json`
|
||||
(only if F6 is cheap), `err_volume_missing.json` (F13). The write
|
||||
experiments: one clone of the template into the top of the VMID range
|
||||
(`newid=199999`, a multi-line `description`, a `name` of the `proxy-`
|
||||
form), a config read of the clone (F10, F11, F19), a `PUT config` while
|
||||
it is locked if timing allows (F7), an upload of a 1 KiB
|
||||
`recon-seed.iso` (F12) and its delete (F13), a resize attempt (F20), a
|
||||
start, an agent read (F5, F21), a stop, then destroy with
|
||||
`purge=1&destroy-unreferenced-disks=1`; a final resources read shows
|
||||
nothing remains. Redact nothing but the token. Write the answers to F1 to
|
||||
F21 and the privilege outcome into the execution log's Phase 0 entry, one
|
||||
line per fact; Phase 1 cites them as Evidence. Create the execution log
|
||||
`docs/plans-executions/2026-09-18-1502-proxmox-provider.md` with the checklist (Phase
|
||||
0 to Phase 10) and this entry.
|
||||
Verify: `ls internal/provider/proxmox/testdata | wc -l` is at least 22;
|
||||
`grep -rl 'PVEAPIToken' internal/provider/proxmox/testdata` is empty;
|
||||
every fixture parses as JSON; the host shows no VM in the range and no
|
||||
`recon-seed.iso`.
|
||||
Done when: the plan commit is on `main`, the branch exists, the fixtures
|
||||
and the execution log with F1 to F21 are committed. Commit: `proxmox:
|
||||
captured API fixtures and host facts from 192.168.2.8`.
|
||||
|
||||
- **Phase 1 (orchestrator): the spec.** Read first: contract §2, §4, §5,
|
||||
§6, §11, §12; `docs/provider-spec-template.md` end to end;
|
||||
`docs/kvm2-provider-spec.md` for the level of detail; this plan's
|
||||
decisions; the Phase 0 log entry. Create `docs/proxmox-provider-spec.md`
|
||||
answering all 24 entries: 1 `vms/<vmid>/<name>`, strict parsing, the
|
||||
node-name and storage-id regexps; 2 `allocated-id`, `vmidFor(uid, rng,
|
||||
attempt)`, the two-VMs-with-our-name rule, the adoption guard; 3 the
|
||||
placement table (Region ignored); 4 the template catalogue and the "add a
|
||||
key, never edit" warning; 5 the seed ISO via `nocloud` and the ide2
|
||||
prerequisite; 6 `machineTypes` with `diskGiB`; 7 `provisioning-window`,
|
||||
`discovered` via the guest agent, the IPv4 filter, `Port` unused; 8
|
||||
`written-metadata` in `description` plus the tag, `written-at-create`, F9
|
||||
to F11 as Evidence; 9 the state table, `create-blocks`; 10 the ordered
|
||||
Create steps with what a retry finds at each, `task-handle` plus
|
||||
`backend-lock`, the budgets; 11 the Delete order, the ISO blind spot,
|
||||
node-local ISO storage after a migration; 12 `fail-whole`, the candidate
|
||||
rule, one config read per candidate; 13 the classify table with the F1 to
|
||||
F6 messages, `not-found: error-code` plus list-and-miss on resources
|
||||
(allowed because offline nodes' guests appear as `unknown`, not absent);
|
||||
14 `mounted-files` (Secret `proxmox-token`, key `token`, path
|
||||
`/etc/proxy-operator/proxmox/token`), `ca-file` or
|
||||
`insecureSkipTLSVerify`, the privilege list from F17, rotation free; 15
|
||||
the config table (Phase 2's struct); 16 the Checker; 17 `thin-client`,
|
||||
`transport-wrapper`, the seam's method list; 18 the budgets table; 19 the
|
||||
keyed lock per name; 20 the log keys; 21 the §11 list with the test names
|
||||
from Phases 2 to 8 (copy from this plan); 22 the deployment list; 23 the
|
||||
docs list; 24 the accepted limits (blocking Create up to the clone budget,
|
||||
ISO blind spot, VMID range shared with humans, node-local ISO storage, no
|
||||
adoption of foreign VMs, two-installs GC caveat, `unknown` status while a
|
||||
node is down).
|
||||
Verify: `grep -c '^\*\*Answer:\*\*$' docs/proxmox-provider-spec.md` is 0;
|
||||
every axis code used appears in contract §4.20; Evidence lines cite F
|
||||
numbers; `grep -c '—' docs/proxmox-provider-spec.md` is 0.
|
||||
Done when: the spec is committed with all 24 entries and the Phase 1 box
|
||||
ticked. Commit: `docs: proxmox provider spec`.
|
||||
|
||||
- **Phase 2 (implementer): config block, registration, package skeleton.**
|
||||
Read first: spec entries 1, 2, 3, 6, 8, 9, 13, 15; contract §10, §5, §6;
|
||||
`internal/provider/config.go`, `config_kvm.go`, `config_test.go`,
|
||||
`config_kvm_test.go`; `internal/provider/kvm2/{kvm2.go,id.go,metadata.go,state.go,errors.go}`
|
||||
and their tests as the pattern; `cmd/main.go`; `testdata/err_*.json`.
|
||||
Create `internal/provider/config_proxmox.go`: `ProxmoxConfig{URL,
|
||||
TokenFile, CAFile string; InsecureSkipTLSVerify bool; Nodes []string;
|
||||
ISOStorage, Storage, Bridge string; VMIDRange *ProxmoxVMIDRange; Templates
|
||||
map[string]ProxmoxTemplate; MachineTypes map[string]ProxmoxMachineType;
|
||||
RequestTimeoutSeconds int; VendorData string; InstallGuestAgent *bool}`,
|
||||
`ProxmoxVMIDRange{Min, Max int}`, `ProxmoxTemplate{VMID int}`,
|
||||
`ProxmoxMachineType{Cores int32; MemoryMiB int64; DiskGiB int64}`,
|
||||
`validateProxmox(prefix string, c *ProxmoxConfig) error` (URL http(s)
|
||||
with a host; `tokenFile` non-empty; at least one node, unique, matching
|
||||
`proxmoxNodeName = ^[a-z0-9][a-z0-9-]*$`; `isoStorage` and `storage`
|
||||
matching `proxmoxStorageID = ^[A-Za-z][A-Za-z0-9_.-]*$`; `vmidRange` min
|
||||
at least 100, max at most 999999999, min below max; at least one template
|
||||
with `vmid` at least 100; at least one machine type with cores and
|
||||
memoryMiB above zero and diskGiB not negative; `requestTimeoutSeconds`
|
||||
not negative). Edit `config.go`: `Proxmox *ProxmoxConfig` with tag
|
||||
`proxmox,omitempty`, `setBlocks`, the allow-list, the dispatch. Edit
|
||||
`cmd/main.go`: `"proxmox": proxmox.New`. Create in
|
||||
`internal/provider/proxmox/`: `proxmox.go` (`Provider{name, cfg, api,
|
||||
now, budgets, nameLocks, tracing}`; until Phase 3 a local `keyedLocks`
|
||||
copy in `locks.go` that Phase 3 deletes; `New` never dials: builds the
|
||||
client, `os.Stat`s the token file and warns, warns on
|
||||
`insecureSkipTLSVerify`; `withDefaults`: `isoStorage` `local`, `vmidRange`
|
||||
100000 to 199999, `requestTimeoutSeconds` 30, `installGuestAgent` true;
|
||||
`node(zone)` mirrors kvm2's `host`; the four methods stubbed with
|
||||
`transientf("not implemented")`), `api.go` (the interface and DTOs;
|
||||
`vmConfig` is `map[string]string` with scalars rendered as strings by
|
||||
`decodeConfig`), `id.go`, `mark.go` (keys from `provider.LabelManaged`,
|
||||
`provider.LabelManagedYes`, `provider.LabelUID` and
|
||||
`proxy-operator-created-at`; `parseDescription` tolerates extra lines
|
||||
and CRLF), `state.go`, `errors.go` (`apiError{Status int; Message
|
||||
string; Errors map[string]string}` whose `Error()` never includes headers
|
||||
or URLs; `classify` per spec entry 13; matcher helpers keyed on the exact
|
||||
substrings captured in F2 to F6). Tests: `config_proxmox_test.go`
|
||||
`TestValidateProxmox_table` (missing url, non-http scheme, missing
|
||||
tokenFile, no nodes, duplicate node, node with a slash, bad isoStorage
|
||||
id, vmidRange min below 100, min above max, no templates, template vmid
|
||||
below 100, no machineTypes, cores zero, negative diskGiB, negative
|
||||
timeout, valid full, valid minimal) with a `validProxmoxYAML` const;
|
||||
`config_test.go` rows `proxmox type with kvm block`, `kvm type with
|
||||
proxmox block`, `proxmox minimal entry loads`. `id_test.go`:
|
||||
`TestFormatProviderID_roundTrip`, `TestParseProviderID_malformed`,
|
||||
`TestVMIDFor_deterministicAndInRange`, `TestVMIDFor_probesForwardAndWraps`.
|
||||
`mark_test.go`: `TestFormatDescription_keys`, `TestParseDescription_table`.
|
||||
`state_test.go`: `TestMapState_table`, `TestFirstIPv4_filters`.
|
||||
`errors_test.go`: `TestClassify_table` (every class, every matched
|
||||
message loaded from `testdata/err_*.json` through `loadFixture(t, name)
|
||||
*apiError`, `op == "create"` turning NotFound into Transient,
|
||||
`context.Canceled`, a `*net.OpError`, `errors.New("x")`,
|
||||
already-classified errors keep their class), `TestAPIError_matchers`,
|
||||
`TestPermanentfAndTransientf`. `proxmox_test.go`:
|
||||
`TestNew_doesNotDialAndWarnsOnMissingToken`,
|
||||
`TestNew_insecureSkipTLSVerifyWarns`, `TestNew_nilBlockDoesNotPanic`,
|
||||
`TestWithDefaults_table`, `TestNode_table`,
|
||||
`TestWrapErr_carriesProviderNameOpAndID`.
|
||||
Verify: `grep -c '"proxmox"' internal/provider/config.go` is 3;
|
||||
`go build ./... && go vet ./... && go test -race -count=1 ./internal/provider/... ./cmd/... && go test -short ./...`;
|
||||
the fixture loader fails loudly on a missing file.
|
||||
Done when: `go test -race ./internal/provider/proxmox/` passes with the
|
||||
stubs in place and the constructor is registered. Commit: `proxmox:
|
||||
config block, IDs, marks, state mapping, classify, registration`.
|
||||
|
||||
- **Phase 3 (implementer): extract `nocloud` and `keyedlock` from kvm2.**
|
||||
Read first: `internal/provider/kvm2/{seed.go,seed_test.go,kvm2.go,kvm2_test.go,create.go}`;
|
||||
contract §1.4, §2.1; spec entries 5 and 19;
|
||||
`grep -n 'seed.go\|keyedLocks\|buildSeedISO' docs/*.md`. Create
|
||||
`internal/provider/nocloud/seed.go` exporting `BuildSeedISO(name,
|
||||
userData, vendorData string, w io.Writer) error` (plain errors, an
|
||||
`ErrEmptyName` sentinel instead of kvm2's `permanentf`),
|
||||
`BuildVendorData(configured string, installAgent bool) (string, error)`,
|
||||
`GuestAgentScript`, `SeedLabel`; move the seed tests to
|
||||
`nocloud/seed_test.go` under their existing names. Create
|
||||
`internal/provider/keyedlock/keyedlock.go` exporting `Locks`, `New()
|
||||
*Locks`, `(*Locks).Acquire(ctx, key) (release func(), err error)`; move
|
||||
the `TestKeyedLocks_*` tests. Edit kvm2: delete `seed.go`, `seed_test.go`,
|
||||
the `keyedLocks` type and its tests; `create.go` calls
|
||||
`nocloud.BuildVendorData` and `nocloud.BuildSeedISO` and wraps their
|
||||
errors with `permanentf`; `kvm2.go` uses `*keyedlock.Locks`. Edit
|
||||
`internal/provider/proxmox/proxmox.go` to use `keyedlock.Locks` and delete
|
||||
`locks.go`. Fix the doc mentions so they name the new packages.
|
||||
Verify: `go test -race -count=1 ./internal/provider/...`; kvm2's test
|
||||
count drops by exactly the moved tests and the two new packages carry
|
||||
them; `grep -rn 'iso9660' internal/provider/kvm2/` is empty;
|
||||
`go vet -tags=integration ./...` still compiles.
|
||||
Done when: kvm2 and proxmox import both packages, all tests pass, the docs
|
||||
name the new packages. Commit: `provider: extract nocloud seed builder
|
||||
and keyedlock from kvm2`.
|
||||
|
||||
- **Phase 4 (implementer): seam, HTTP client, decorator, fake.** Read first:
|
||||
spec entries 14, 17, 18, 20; contract §4.15, §7, §9;
|
||||
`internal/tracing/transport.go`; `internal/provider/kvm2/{observe.go,observe_test.go,fake_test.go}`;
|
||||
`internal/provider/proxmox/{api.go,errors.go}`; `testdata/*.json`. Create
|
||||
`client.go`: `httpAPI{base *url.URL; http *http.Client; tokenFile
|
||||
string}`; `newHTTPAPI(cfg provider.ProxmoxConfig, opts ...tracing.Option)
|
||||
(*httpAPI, error)` builds a transport with `DialContext` timeout 10s,
|
||||
`TLSHandshakeTimeout` 10s, `ResponseHeaderTimeout` from the request
|
||||
timeout, `IdleConnTimeout` 90s, `MaxIdleConnsPerHost` 4,
|
||||
`ForceAttemptHTTP2: false`, an empty `TLSNextProto` map (F18), `RootCAs`
|
||||
from `caFile` or `InsecureSkipVerify` from the flag, wrapped with
|
||||
`tracing.RestConfigWrapper(opts...)(transport)`; `http.Client.Timeout` is
|
||||
the request timeout; `do(ctx, method, path string, query, form
|
||||
url.Values, out any) error` reads the token file on every call (missing
|
||||
file `transientf`, a file without `!` and `=` `permanentf`), sets the
|
||||
header, decodes `{"data": ...}` into `out`, turns any non-2xx into
|
||||
`*apiError` per F1 with the error body read capped at 64 KiB;
|
||||
`UploadISO` streams a multipart body through `io.Pipe` with a goroutine
|
||||
writing `content=iso`, `filename` and the file part. Create `observe.go`:
|
||||
`observed{inner api; provider string}` writing one V(1) `proxmox api
|
||||
call` line per seam call with `call`, `node`, `vmid`, `storage`, `upid`,
|
||||
`duration`, `httpStatus` and `error` on failure, V(2) adding request
|
||||
params and a response summary; `observe(inner api, providerName string)
|
||||
api`; `New` wraps `httpAPI` in it. Create `fake_test.go`: `fakeAPI{mu; vms
|
||||
map[int]*fakeVM; tasks map[string]*fakeTask; isos
|
||||
map[string]map[string][]byte; hooks; block; calls; log; taskPolls int;
|
||||
cloneFailsWith, startFailsWith string; agentNotRunning, cloneLeavesLock
|
||||
bool}`, `fakeVM{node, name, status, qmpstatus, lock string; template
|
||||
bool; config map[string]string; ifaces []agentInterface}`,
|
||||
`fakeTask{node string; polls int; exit string; onDone func()}`; `Clone`
|
||||
creates the VM with `lock=clone` and a task that clears the lock or, per
|
||||
`cloneFailsWith`, removes the VM (F15); `Start` and `Stop` flip status
|
||||
through a task; `Destroy` removes the VM through a task; `UploadISO`
|
||||
records bytes; `DeleteVolume` errors with the F13 message when absent;
|
||||
`enter(ctx, method)` for blocks and hooks like kvm2; helpers `addVM`,
|
||||
`addTemplate`, `failWith`, `blockOn`, `callCount`, `callLog`,
|
||||
`fakeErr(status int, msg string) *apiError`. Tests: `client_test.go` over
|
||||
`httptest.NewTLSServer`: `TestClient_sendsTokenHeaderFromFileOnEveryRequest`
|
||||
(rotate the file between two calls), `TestClient_missingTokenFileIsTransient`,
|
||||
`TestClient_unparsableTokenFileIsPermanent`,
|
||||
`TestClient_apiErrorFromStatusReasonAndBody` (serve each
|
||||
`testdata/err_*.json` verbatim), `TestClient_decodesDataEnvelope`,
|
||||
`TestClient_http1Only`, `TestClient_requestTimeoutIsTransient`,
|
||||
`TestClient_cancelledContextReturnsPromptly`,
|
||||
`TestClient_childSpanUnderParent`, `TestClient_noSpanWithoutParent`,
|
||||
`TestClient_tokenNeverInErrorsOrLogs` (sentinel secret echoed by the
|
||||
server in a 401 body), `TestClient_caFileTrustsPrivateCA`,
|
||||
`TestClient_insecureSkipsVerification`, `TestClient_uploadStreamsMultipart`
|
||||
(the server parses the multipart; the client never `io.ReadAll`s the
|
||||
reader), `TestClient_destroyQueryFlags`. `observe_test.go`:
|
||||
`TestObserve_everySeamMethodLogsOneLine` (reflection over `api`),
|
||||
`TestObserve_detailOnlyAtV2`, `TestObserve_uploadBodyNeverLogged`,
|
||||
`TestObserve_errorFieldsOnFailure`, `TestObserve_v0Silent`.
|
||||
Verify: `go test -race -count=2 ./internal/provider/proxmox/`;
|
||||
`grep -n 'ReadAll' internal/provider/proxmox/client.go` shows only the
|
||||
bounded error-body read; `grep -n 'context.Background()' internal/provider/proxmox/*.go`
|
||||
is empty outside tests.
|
||||
Done when: the client passes its tests against the fixtures and the fake
|
||||
compiles against the full `api` interface. Commit: `proxmox: thin HTTP
|
||||
client, seam, log decorator and in-memory fake`.
|
||||
|
||||
- **Phase 5 (implementer): Get, ListByTag, task helpers.** Read first: spec
|
||||
entries 7, 8, 9, 12, 13; contract §2.2, §2.4, §4.7, §4.12;
|
||||
`internal/provider/kvm2/{get.go,get_test.go,list.go,list_test.go}`; the
|
||||
resources, config, status, agent and task fixtures. Create `task.go`:
|
||||
`parseUPID(upid) (node string, err error)` (Permanent when malformed),
|
||||
`waitTask(ctx, log, api, upid, budget) error` polling `TaskStatus` every
|
||||
`budgets.taskPoll` (2s; tests shrink it) until `status == "stopped"`,
|
||||
`exitstatus != "OK"` giving `transientf` with the message, the budget
|
||||
giving `transientf`, ctx giving Transient; `waitUnlocked(ctx, api, node,
|
||||
vmid, budget)` polling `VMConfig` until no `lock`. Create `get.go`:
|
||||
`resolve(ctx, api, vmid) (vmResource, bool, error)` (one resources call,
|
||||
`Type == "qemu"` and the vmid), the name check, `VMConfig` for the mark,
|
||||
`lock` Transient, `unknown` Transient, `running` reads the agent
|
||||
(`isAgentNotRunning` gives no IP, other errors Transient), `mapState`.
|
||||
Create `list.go` per spec entry 12; `Instance` carries ID, UID, CreatedAt
|
||||
only. Replace the two stubs. Tests: `task_test.go` `TestParseUPID_table`,
|
||||
`TestWaitTask_table` (ok, error exitstatus, budget exceeded, ctx
|
||||
cancelled, poll count), `TestWaitUnlocked_table`. `get_test.go`:
|
||||
`TestGet_missingVMIsNotFound`, `TestGet_nameMismatchIsNotFound`,
|
||||
`TestGet_unmarkedIsNotFound`, `TestGet_runningWithoutIPIsProvisioning`,
|
||||
`TestGet_runningWithIPIsRunning`, `TestGet_agentNotRunningIsProvisioning`,
|
||||
`TestGet_agentOtherErrorIsTransient`, `TestGet_ipFilterSkipsLoopbackAndLinkLocal`,
|
||||
`TestGet_stoppedIsStopped`, `TestGet_pausedIsStopped`,
|
||||
`TestGet_unknownStatusIsTransient`, `TestGet_lockedIsTransientNeverNotFound`,
|
||||
`TestGet_unknownStateIsStopped`, `TestGet_readsUIDAndCreatedAtFromBackend`,
|
||||
`TestGet_unmanagedDescriptionHasEmptyUID`, `TestGet_malformedIDIsPermanent`,
|
||||
`TestGet_resourcesFailureIsTransient`, `TestGet_twoVMsWithOurNameIsPermanent`.
|
||||
`list_test.go`: `TestListByTag_excludesUnmarkedVMs`,
|
||||
`TestListByTag_emptyUIDStillListed`, `TestListByTag_candidatesByTagOrNamePrefix`,
|
||||
`TestListByTag_skipsLXCAndTemplates`, `TestListByTag_resourcesFailureFailsWhole`,
|
||||
`TestListByTag_unreadableVMSkipped`, `TestListByTag_unreadableVMWithEndedContextFails`,
|
||||
`TestListByTag_unknownStatusStillAttempted`, `TestListByTag_doesNotReadStatusOrAgent`,
|
||||
`TestListByTag_idsAcceptedByParse`.
|
||||
Verify: `go test -race -count=2 ./internal/provider/proxmox/`;
|
||||
`grep -n 'not implemented' internal/provider/proxmox/*.go` lists only
|
||||
Create and Delete.
|
||||
Done when: Get and ListByTag are real and every listed test passes.
|
||||
Commit: `proxmox: Get, ListByTag and task polling`.
|
||||
|
||||
- **Phase 6 (implementer): Create.** Read first: spec entries 2, 3, 4, 5, 6,
|
||||
10, 19; contract §2.1, §4.2, §4.10, §4.11, §5;
|
||||
`internal/provider/kvm2/{create.go,create_test.go}`;
|
||||
`internal/provider/nocloud`; F4, F7, F10, F12, F15, F16, F19, F20 in the
|
||||
Phase 0 log. Create `seed.go`: `seedISOName(name)` is `<name>-seed.iso`,
|
||||
`seedVolID(storage, name)` is `<storage>:iso/<name>-seed.iso`,
|
||||
`uploadSeed(ctx, log, api, node, storage, req, cfg)` builds vendor-data
|
||||
and the ISO into a `bytes.Buffer` (tens of KiB), deletes an existing
|
||||
volume tolerating absence, uploads with the buffer's length, waits the
|
||||
task with `budgets.upload`, logs `cloudInitBytes` only. Create `create.go`
|
||||
implementing the ordered steps: placement validation (`node`,
|
||||
`MachineTypes`, `Templates`, all Permanent naming field and value), the
|
||||
per-name lock, `ClusterResources` once for both the name lookup (adoption
|
||||
guard, then `waitUnlocked` if locked, then continue from the seed step if
|
||||
stopped, return if running) and the template's node (absent template is
|
||||
Permanent); allocation loop `for attempt := 0; attempt < maxProbes (32)`
|
||||
calling `Clone` with `vmidFor(req.UID, rng, attempt)`, `isAlreadyExists`
|
||||
moving to the next attempt, exhaustion Transient; `waitTask` with
|
||||
`budgets.clone`; `uploadSeed`; `SetVMConfig` with `cores`, `memory`,
|
||||
`agent=1`, `tags=proxy_operator`, `ide2=<volid>,media=cdrom`, and `net0`
|
||||
rewritten from the cloned config with `bridge=<cfg.Bridge>` only when
|
||||
configured (MAC kept); `bootDisk(cfg)` and `Resize` when `diskGiB` is
|
||||
above the current `size=`; `Start` and `waitTask` with `budgets.start`;
|
||||
`waitRunning` on `VMStatus` with `budgets.running`; return the ID.
|
||||
`isNotExist` inside Create is Transient (classify with `op == "create"`).
|
||||
Tests `create_test.go`: `TestCreate_repeatReturnsSameIDAndCreatesNothing`,
|
||||
`TestCreate_resumesAfterEachPartialStep` (after clone task, after seed
|
||||
upload, after config, after resize, after start, after running),
|
||||
`TestCreate_returnsOnlyWhenRunning`, `TestCreate_runningWaitCapIsTransient`,
|
||||
`TestCreate_rejectsUnknownPlacement`, `TestCreate_zoneOptionalWithOneNode`,
|
||||
`TestCreate_regionIgnored`, `TestCreate_adoptionWithForeignUIDIsPermanent`,
|
||||
`TestCreate_adoptsOnlyMarkedVMs`, `TestCreate_duplicateNameIsPermanent`,
|
||||
`TestCreate_vmidCollisionProbesForward`, `TestCreate_vmidRangeExhaustedIsTransient`,
|
||||
`TestCreate_waitsForCloneLockOnRetry`, `TestCreate_cloneTaskFailureIsTransient`,
|
||||
`TestCreate_cloneTaskTimeoutIsTransient`, `TestCreate_markWrittenInCloneCall`,
|
||||
`TestCreate_tagSetInConfigStep`, `TestCreate_seedAlwaysRebuilt`,
|
||||
`TestCreate_seedUploadedBeforeConfig`, `TestCreate_configSetsCoresMemoryAgentAndSeed`,
|
||||
`TestCreate_bridgeOverrideKeepsMAC`, `TestCreate_noBridgeOverrideLeavesNet0`,
|
||||
`TestCreate_resizeOnlyWhenLarger`, `TestBootDisk_table`,
|
||||
`TestCreate_startTaskFailureIsTransient`, `TestCreate_templateMissingIsPermanent`,
|
||||
`TestCreate_notExistInsideCreateIsTransientNotNotFound`,
|
||||
`TestCreate_cloudInitSentinelNeverLoggedOrInErrors` (V(2) capture, every
|
||||
error path), `TestCreate_concurrentSameNameSerialised`,
|
||||
`TestCreate_cancelledMidWaitReturnsPromptly`.
|
||||
Verify: `grep -n 'CloudInit' internal/provider/proxmox/create.go internal/provider/proxmox/seed.go`
|
||||
shows only `len(req.CloudInit)` and the pass-through into
|
||||
`nocloud.BuildSeedISO`; `go test -race -count=2 ./internal/provider/proxmox/`;
|
||||
`grep -n 'not implemented'` lists only Delete.
|
||||
Done when: Create is real and every listed test passes. Commit:
|
||||
`proxmox: Create by cloning a template, seed upload, config, start`.
|
||||
|
||||
- **Phase 7 (implementer): Delete, Checker, CRD comment.** Read first: spec
|
||||
entries 11, 16; contract §2.3, §4.3, §4.13, §8;
|
||||
`internal/provider/kvm2/{delete.go,delete_test.go,check.go,check_test.go}`;
|
||||
`internal/provider/check.go`; `api/v1alpha1/providerhealth_types.go`; F13,
|
||||
F17 in the Phase 0 log. Create `delete.go`: parse the ID (Permanent), the
|
||||
per-name lock, `resolve`; absent is a logged no-op; name mismatch or
|
||||
unmarked is a logged no-op; `lock` Transient; `unknown` Transient;
|
||||
`running` gets `Stop` and `waitTask` (`budgets.stop`); `Destroy` and
|
||||
`waitTask` (`budgets.destroy`), `isNotExist` from `Destroy` being
|
||||
success; `DeleteVolume` of the seed on the VM's node tolerating the F13
|
||||
absent message; nil. Create `check.go`: `var _ provider.Checker =
|
||||
(*Provider)(nil)`; `reasonTemplateNotFound = "TemplateNotFound"`,
|
||||
`reasonStorageNotFound = "StorageNotFound"`; `Check` fans out one
|
||||
goroutine per node in config order (`wg.Go`), each `checkNode` running
|
||||
`Version`, `NodeStatus`, `StorageStatus` (a 500 "does not exist" is
|
||||
`StorageNotFound`; a storage whose `content` lacks `iso` is
|
||||
`StorageNotFound` with a message saying so), `ClusterResources` for the
|
||||
templates; `checkReason(err)`: ctx ended `Timeout`, 401 `Unauthorized`,
|
||||
403 `Forbidden`, other `*apiError` `APIError`, network `Unreachable`;
|
||||
Details `pveVersion`, `kernel`, `isoStorage`, `isoStorageAvailGiB`,
|
||||
`templates`; `Name` is the node, `Address` the base URL. Edit
|
||||
`api/v1alpha1/providerhealth_types.go` (`kubernetes, gcp, kvm, kvm2 or
|
||||
proxmox`) and run `make manifests`. Tests `delete_test.go`:
|
||||
`TestDelete_malformedIDIsPermanent`, `TestDelete_twiceSucceeds`,
|
||||
`TestDelete_absentIsNoOp`, `TestDelete_foreignNameIsNoOp`,
|
||||
`TestDelete_unmarkedIsNoOp`, `TestDelete_runningVMStoppedFirst`,
|
||||
`TestDelete_stoppedVMSkipsStop`, `TestDelete_lockedIsTransient`,
|
||||
`TestDelete_unknownStatusIsTransient`, `TestDelete_removesSeedISO`,
|
||||
`TestDelete_toleratesMissingISO`, `TestDelete_destroyTaskFailureIsTransient`,
|
||||
`TestDelete_usesPurgeAndDestroyUnreferencedDisks`,
|
||||
`TestDelete_concurrentSameNameSafe`, `TestDelete_noOpLinesAreSilentAtV0`;
|
||||
in `get_test.go`: `TestGet_afterDeleteReturnsNotFound`,
|
||||
`TestGet_duringDeleteStillReturnsInstance` (block the fake's `Destroy`).
|
||||
`check_test.go`: `TestCheck_oneEntryPerNodeInConfigOrder`,
|
||||
`TestCheck_nodesCheckedConcurrently`, `TestCheck_expiredContextReturnsPromptly`,
|
||||
`TestCheck_reasonsTable`, `TestCheck_detailsKeys`, `TestCheck_logsVerdictPerNode`,
|
||||
`TestCheckReason_table`.
|
||||
Verify: `go test -race -count=2 ./internal/provider/proxmox/ ./internal/providerhealth/`;
|
||||
`git diff --stat config/` shows only the CRD description line;
|
||||
`grep -rn 'not implemented' internal/provider/proxmox/` is empty.
|
||||
Done when: all four contract methods and `Check` are real. Commit:
|
||||
`proxmox: Delete with seed cleanup, health Checker, CRD type comment`.
|
||||
|
||||
- **Phase 8 (implementer): contract sweep, observability sweep, integration
|
||||
test, Makefile, testing.md.** Read first: contract §11 and §9; spec
|
||||
entries 17, 18, 20, 21; `internal/provider/kvm2/{provider_test.go,kvm2_integration_test.go}`;
|
||||
`internal/provider/tracing_test.go`; `Makefile` integration targets;
|
||||
`docs/testing.md` kvm2 rows and section. Create `provider_test.go`: the
|
||||
shared `fixture` (fake, log capture, budgets shrunk), `assertProviderError`,
|
||||
`TestProvider_cancelledContextReturnsPromptly` (each method blocked on
|
||||
its first seam call), `TestProvider_everyErrorIsWrappedWithConfiguredName`,
|
||||
`TestLogging_v0SilentV1OutcomeV2Detail` (V(1) lines `proxmox vm ensured
|
||||
running`, `proxmox vm fetched`, `proxmox vm deleted`, `proxmox vms
|
||||
listed`, `proxmox node checked`, every line with `provider`, instance
|
||||
lines with `vmid` and `name`; V(2) has `cloudInitBytes` and never the
|
||||
text), `TestTracing_httpSpansUnderOperationSpan` (a real `httpAPI`
|
||||
against an `httptest` server playing the fixtures for a Get, wrapped in
|
||||
`provider.WithTracing`, with a span recorder: every HTTP span is a child
|
||||
of `provider.get` and no attribute carries the token). Create
|
||||
`proxmox_integration_test.go` (`//go:build integration`):
|
||||
`loadIntegrationEnv` with the `PROXMOX_TEST_*` variables, `providerConfig()`,
|
||||
the `integrationLog` pattern from kvm2; `TestProxmox_lifecycle` asserting
|
||||
in order: `New` returns within 100 ms without a request; `Check` Ready
|
||||
with `pveVersion`, `isoStorageAvailGiB` and `templates`; `Create` returns
|
||||
`vms/<vmid>/<name>` with the vmid in range; a second `Create` returns the
|
||||
same ID and the resources count is unchanged; `Get` reaches Running with
|
||||
an IPv4 within the budget, UID and CreatedAt as written; `ListByTag`
|
||||
contains it; a TCP connect to the guest's port is logged only; `Delete`
|
||||
nil; `Get` NotFound; `Delete` again nil; the ISO storage has no
|
||||
`<name>-seed.iso` and resources has no VM with the name; an HTTP span is
|
||||
a child of `provider.create` and no attribute carries the cloud-init
|
||||
text; `PROXMOX_TEST_KEEP` skips the teardown and prints the `qm destroy`
|
||||
and ISO delete commands. `TestProxmoxCheck_realHost` creates nothing. Edit
|
||||
`Makefile`: `test-integration-proxmox` (`go test -race -tags=integration
|
||||
-count=1 -v -timeout 30m -run TestProxmox ./internal/provider/proxmox/`).
|
||||
Edit `docs/testing.md`: rows for the package in the suites table and a
|
||||
"The proxmox integration test" section. Update spec entry 21's tick list
|
||||
with the final test names.
|
||||
Verify: `go test -race -count=2 ./internal/provider/proxmox/`;
|
||||
`go vet -tags=integration ./... && go vet -tags=e2e ./...`;
|
||||
`make -n test-integration-proxmox`; every §11 item has a test name in
|
||||
spec entry 21 that exists.
|
||||
Done when: the package covers every §11 item by name, the integration
|
||||
test compiles, the target exists. Commit: `proxmox: contract and
|
||||
observability tests, integration test, testing docs`.
|
||||
|
||||
- **Phase 9 (implementer): deployment and documentation.** Read first:
|
||||
contract §12 items 11 to 13; spec entries 22, 23, 24;
|
||||
`config/manager/manager.yaml`, `config/manager/providers_config.yaml`,
|
||||
`config/samples/{providers-config.yaml,proxy_kvm2.yaml,kustomization.yaml}`;
|
||||
`Makefile` `kvm-ssh-secret`; `docs/kvm2-provider.md` headings;
|
||||
`docs/providers-existing.md`; `docs/architecture.md` lines mentioning
|
||||
kvm2; `README.md`; `docs/providers.md`; `CLAUDE.md` Gotchas. Edit
|
||||
`config/manager/manager.yaml`: an optional Secret mount `proxmox-token` at
|
||||
`/etc/proxy-operator/proxmox` (key `token`). Edit `Makefile`:
|
||||
`PROXMOX_TOKEN_FILE ?= $(HOME)/.config/proxmox-token`,
|
||||
`proxmox-token-secret` creating `proxmox-token` from `--from-file=token=`.
|
||||
Edit both providers-config files: a commented `proxmox-home` entry (`url:
|
||||
https://192.168.2.8:8006`, `tokenFile`, `caFile` or
|
||||
`insecureSkipTLSVerify`, `nodes`, `isoStorage`, `storage`, `bridge`,
|
||||
`vmidRange`, `templates`, `machineTypes`). Create
|
||||
`config/samples/proxy_proxmox.yaml` (provider `proxmox-home`, zone the
|
||||
node, machine type `small`, image the template key, the Squid cloud-init
|
||||
from `proxy_kvm2.yaml`) and list it in `config/samples/kustomization.yaml`.
|
||||
Create `docs/proxmox-provider.md` with sections: Proxmox prerequisites
|
||||
(the template commands from this plan, no cloudinit drive on ide2, the
|
||||
guest agent or outbound access, the bridge, the privilege list from F17,
|
||||
reserving the VMID range), Cluster-side setup (the Secret, the config
|
||||
entry), Config reference (spec entry 15's table), Placement (Region
|
||||
ignored, catalogue-edit warning), What lands on Proxmox (VMID scheme,
|
||||
description mark, tag, seed ISO, config keys, `qm unlock` for a stuck
|
||||
clone), Logging, Tracing, Timeouts, Health check, Error classes, Known
|
||||
gaps (spec entry 24), Testing. Edit `docs/providers-existing.md`: a fifth
|
||||
column in the three tables and a proxmox section mirroring kvm2's. Edit
|
||||
`docs/architecture.md`: components row, a proxmox block in §5, the span
|
||||
note in §10, a §11 row (and the "All four providers" count), Decisions
|
||||
bullets. Edit `README.md`, `docs/providers.md`, `CLAUDE.md` Gotchas
|
||||
(`make test-integration-proxmox`).
|
||||
Verify: `bin/kustomize build config/default >/dev/null && bin/kustomize build config/samples >/dev/null`;
|
||||
a test `TestLoadConfig_proxmoxSampleEntry` loads the uncommented block
|
||||
from `config/samples/providers-config.yaml` through `provider.LoadConfig`;
|
||||
a link sweep over the edited docs finds no missing target;
|
||||
`grep -c '—' docs/proxmox-provider.md` is 0.
|
||||
Done when: manifests build, the sample loads, docs are linked and
|
||||
link-clean. Commit: `proxmox: deployment files, sample, user guide and
|
||||
doc rows`.
|
||||
|
||||
- **Phase 10 (orchestrator with the user): review, real-host validation,
|
||||
changelog, MR.** Read first: `docs/plans-executions/2026-09-17-1515-kvm2-review-fixes.md`;
|
||||
`.claude/agents/go-operator-reviewer.md`; the latest report under
|
||||
`docs/reviews/` for the shape; the spec; this plan's Risks. Do: run
|
||||
`go-operator-reviewer` on the branch with the spec and the contract as
|
||||
inputs, then `/code-review high`, `make lint` (if the local toolchain
|
||||
panics in staticcheck as it did during kvm2, record it and run
|
||||
`bin/golangci-lint run ./internal/provider/proxmox/...` with staticcheck
|
||||
disabled), `make test`. Fix accepted findings with regression tests in
|
||||
one commit; record rejected ones under `docs/reviews/<date>-proxmox-provider-review/`.
|
||||
Hand the user the host checklist (template ready, no leftover VMs in the
|
||||
range, ISO storage has space, the token's privileges per F17) and the
|
||||
`PROXMOX_TEST_*` export lines; with the go-ahead run
|
||||
`make test-integration-proxmox` and record the clone duration and time to
|
||||
IP. Optionally the in-cluster pass on kind: build and load the image,
|
||||
`make proxmox-token-secret`, a scratch ConfigMap with the `proxmox-home`
|
||||
entry, apply `proxy_proxmox.yaml`, wait for `Ready`, `kubectl get
|
||||
providerhealth proxmox-home -o yaml`, a `curl -x` through the proxy,
|
||||
delete and confirm the VM and ISO are gone; scratch overrides are not
|
||||
committed. After the user confirms: CHANGELOG entry
|
||||
(`date "+%Y-%m-%d %H:%M %Z"`), push with `-u` using the token helper from
|
||||
memory (`~/.config/gitea-token`), open the MR with the Gitea API
|
||||
(`POST /api/v1/repos/kacerr/egress-proxies-operator/pulls` with `head:
|
||||
feat/proxmox-provider`, `base: main`, the body ending with the
|
||||
attribution line; `pr.json` in the scratchpad, never committed). Never
|
||||
merge.
|
||||
Verify: the integration run's ordered assertions pass on 192.168.2.8;
|
||||
`make test` green; the review report lists every finding as accepted or
|
||||
rejected.
|
||||
Done when: the MR URL is printed and the execution log's last box is
|
||||
ticked. Commit: `proxmox: review fixes, real-host validation record,
|
||||
changelog`.
|
||||
|
||||
## Verification
|
||||
|
||||
- Per phase: the `Verify:` clause, plus `go build ./... && go test -short ./...`
|
||||
before every commit and `git diff --stat` reviewed for files outside the
|
||||
phase's list.
|
||||
- Contract mapping: after Phase 8 every rule of contract §2 and §8 has a
|
||||
named test in the package, and spec entry 21 maps the §11 list 1:1 to
|
||||
existing test names.
|
||||
- Fixture discipline: every message `classify` or a matcher helper compares
|
||||
against is loaded from `testdata/` in `TestClassify_table` or
|
||||
`TestAPIError_matchers`; a string literal in `errors.go` without a fixture
|
||||
is a review finding.
|
||||
- Secrets: `TestClient_tokenNeverInErrorsOrLogs` and
|
||||
`TestCreate_cloudInitSentinelNeverLoggedOrInErrors` run at V(2), and the
|
||||
integration test asserts no span attribute carries the cloud-init text.
|
||||
- End to end: the integration run (Phase 8's assertions) on 192.168.2.8 and
|
||||
the optional kind pass in Phase 10.
|
||||
|
||||
## Risks
|
||||
|
||||
- **Error text location and exact wording (F1 to F6)** decide the whole
|
||||
NotFound path; if messages vary by storage type or PVE minor version, the
|
||||
matchers get one fixture per variant and the guide names the tested
|
||||
version. This is not hypothetical: 9.2 rewrote the authentication-failure
|
||||
message (F22), so fixtures captured from an 8.x host would already be
|
||||
wrong. Every matched string comes from 192.168.2.8 at 9.2.20, and the
|
||||
guide says so.
|
||||
- **Clone cleanup (F15).** A failed or interrupted clone may leave a VM with
|
||||
`lock=clone` that never clears; Create then waits out `budgets.clone` and
|
||||
returns Transient every time. The guide documents `qm unlock`; the
|
||||
reconciler keeps retrying rather than latching.
|
||||
- **`SDN.Use` (F17).** PVE 9 refuses `net0` writes to non-root users without
|
||||
it; a missing privilege is a 403 Permanent that latches the Proxy until
|
||||
its spec changes. The guide's privilege list is the mitigation.
|
||||
- **The ide2 seed and a template with a cloudinit drive** both claim
|
||||
cloud-init; the Proxy's user-data would be ignored. Documented
|
||||
prerequisite; the Checker cannot see it.
|
||||
- **VMID range shared with humans.** A human VM inside the range costs a
|
||||
probe; a human VM carrying our name is Permanent. The guide says to
|
||||
reserve the range.
|
||||
- **Node-local ISO storage after a migration** leaves the seed on the
|
||||
original node; Delete tolerates its absence and the ISO is a documented
|
||||
blind spot. Shared ISO storage avoids it.
|
||||
- **Blocking Create** holds a reconcile worker for up to the clone budget;
|
||||
the guide sets expectations.
|
||||
- **The 30s request timeout on the seed upload** is fine for tens of KiB; a
|
||||
multi-megabyte `vendorData` would trip it. Documented.
|
||||
- **Two operator installs** on one Proxmox cluster delete each other's VMs
|
||||
(contract §13); documented in entry 24.
|
||||
- **kvm2 edits in Phase 3** touch a merged provider; the moved tests are the
|
||||
guard, and `go vet -tags=integration` keeps its integration test compiling.
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"status": 200,
|
||||
"reason": "OK",
|
||||
"body": "{\"data\":{\"result\":[{\"statistics\":{\"tx-errs\":0,\"rx-dropped\":0,\"rx-bytes\":7101,\"rx-packets\":86,\"rx-errs\":0,\"tx-dropped\":0,\"tx-packets\":86,\"tx-bytes\":7101},\"name\":\"lo\",\"ip-addresses\":[{\"ip-address\":\"127.0.0.1\",\"ip-address-type\":\"ipv4\",\"prefix\":8},{\"ip-address\":\"::1\",\"ip-address-type\":\"ipv6\",\"prefix\":128}],\"hardware-address\":\"00:00:00:00:00:00\"},{\"hardware-address\":\"bc:24:11:48:11:46\",\"ip-addresses\":[{\"ip-address\":\"192.168.0.150\",\"ip-address-type\":\"ipv4\",\"prefix\":24},{\"ip-address\":\"fe80::be24:11ff:fe48:1146\",\"ip-address-type\":\"ipv6\",\"prefix\":64}],\"name\":\"ens18\",\"statistics\":{\"rx-dropped\":0,\"rx-bytes\":29688403,\"rx-packets\":6744,\"tx-errs\":0,\"tx-bytes\":221044,\"tx-dropped\":0,\"tx-packets\":2970,\"rx-errs\":0}}]}}"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"status": 200,
|
||||
"reason": "OK",
|
||||
"body": "{\"data\":{\"result\":[{\"ip-addresses\":[{\"prefix\":8,\"ip-address-type\":\"ipv4\",\"ip-address\":\"127.0.0.1\"},{\"prefix\":128,\"ip-address-type\":\"ipv6\",\"ip-address\":\"::1\"}],\"hardware-address\":\"00:00:00:00:00:00\",\"statistics\":{\"tx-errs\":0,\"rx-packets\":86,\"rx-bytes\":7101,\"rx-dropped\":0,\"rx-errs\":0,\"tx-dropped\":0,\"tx-packets\":86,\"tx-bytes\":7101},\"name\":\"lo\"},{\"ip-addresses\":[{\"ip-address-type\":\"ipv4\",\"ip-address\":\"192.168.4.147\",\"prefix\":24},{\"ip-address\":\"fe80::be24:11ff:fea9:b4eb\",\"ip-address-type\":\"ipv6\",\"prefix\":64}],\"hardware-address\":\"bc:24:11:a9:b4:eb\",\"statistics\":{\"rx-errs\":0,\"tx-bytes\":319460,\"tx-dropped\":0,\"tx-packets\":4544,\"tx-errs\":0,\"rx-bytes\":29576554,\"rx-dropped\":0,\"rx-packets\":6027},\"name\":\"ens18\"}]}}"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"status": 200,
|
||||
"reason": "OK",
|
||||
"body": "{\"data\":\"UPID:pv1-1:0010F9DC:02801DA1:6AB37871:qmclone:9001:proxy-operator@pve!operator:\"}"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"status": 200,
|
||||
"reason": "OK",
|
||||
"body": "{\"data\":[{\"maxmem\":2147483648,\"netin\":0,\"uptime\":0,\"disk\":0,\"netout\":0,\"mem\":0,\"maxdisk\":34359738368,\"maxcpu\":2,\"type\":\"qemu\",\"memhost\":0,\"template\":0,\"id\":\"qemu/100\",\"cpu\":0,\"name\":\"ubuntu-vm-1\",\"vmid\":100,\"status\":\"stopped\",\"diskwrite\":0,\"diskread\":0,\"node\":\"pv1-1\"},{\"node\":\"pv1-1\",\"diskread\":0,\"diskwrite\":0,\"status\":\"stopped\",\"vmid\":9000,\"cpu\":0,\"name\":\"ubuntu-2604-template\",\"template\":1,\"id\":\"qemu/9000\",\"memhost\":0,\"type\":\"qemu\",\"maxcpu\":2,\"maxdisk\":34359738368,\"mem\":0,\"disk\":0,\"netout\":0,\"uptime\":0,\"netin\":0,\"maxmem\":2147483648},{\"uptime\":0,\"netout\":0,\"disk\":0,\"netin\":0,\"maxmem\":2147483648,\"maxdisk\":3221225472,\"mem\":0,\"id\":\"qemu/9001\",\"template\":1,\"name\":\"debian-13-template\",\"cpu\":0,\"type\":\"qemu\",\"maxcpu\":2,\"memhost\":0,\"diskwrite\":0,\"diskread\":0,\"node\":\"pv1-1\",\"vmid\":9001,\"status\":\"stopped\"}]}"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"status": 200,
|
||||
"reason": "OK",
|
||||
"body": "{\"data\":[{\"template\":0,\"id\":\"qemu/100\",\"cpu\":0,\"name\":\"ubuntu-vm-1\",\"type\":\"qemu\",\"maxcpu\":2,\"memhost\":0,\"diskread\":0,\"diskwrite\":0,\"node\":\"pv1-1\",\"vmid\":100,\"status\":\"stopped\",\"uptime\":0,\"disk\":0,\"netout\":0,\"netin\":0,\"maxmem\":2147483648,\"maxdisk\":34359738368,\"mem\":0},{\"type\":\"qemu\",\"vmid\":199999,\"lock\":\"clone\",\"status\":\"unknown\",\"id\":\"qemu/199999\",\"node\":\"pv1-1\"},{\"diskwrite\":0,\"diskread\":0,\"node\":\"pv1-1\",\"vmid\":9000,\"status\":\"stopped\",\"id\":\"qemu/9000\",\"template\":1,\"name\":\"ubuntu-2604-template\",\"cpu\":0,\"maxcpu\":2,\"type\":\"qemu\",\"memhost\":0,\"maxdisk\":34359738368,\"mem\":0,\"uptime\":0,\"netout\":0,\"disk\":0,\"netin\":0,\"maxmem\":2147483648},{\"type\":\"qemu\",\"maxcpu\":2,\"memhost\":0,\"template\":1,\"id\":\"qemu/9001\",\"cpu\":0,\"name\":\"debian-13-template\",\"vmid\":9001,\"status\":\"stopped\",\"diskwrite\":0,\"diskread\":0,\"node\":\"pv1-1\",\"netin\":0,\"maxmem\":2147483648,\"uptime\":0,\"disk\":0,\"netout\":0,\"mem\":0,\"maxdisk\":3221225472}]}"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"status": 200,
|
||||
"reason": "OK",
|
||||
"body": "{\"data\":[{\"id\":\"qemu/100\",\"template\":0,\"name\":\"ubuntu-vm-1\",\"cpu\":0,\"type\":\"qemu\",\"maxcpu\":2,\"memhost\":0,\"diskwrite\":0,\"diskread\":0,\"node\":\"pv1-1\",\"vmid\":100,\"status\":\"stopped\",\"uptime\":0,\"netout\":0,\"disk\":0,\"maxmem\":2147483648,\"netin\":0,\"maxdisk\":34359738368,\"mem\":0},{\"tags\":\"proxy_operator\",\"mem\":0,\"maxdisk\":4294967296,\"netin\":0,\"maxmem\":1073741824,\"uptime\":0,\"netout\":0,\"disk\":0,\"vmid\":199999,\"status\":\"stopped\",\"diskread\":0,\"diskwrite\":0,\"node\":\"pv1-1\",\"maxcpu\":1,\"type\":\"qemu\",\"memhost\":0,\"id\":\"qemu/199999\",\"template\":0,\"name\":\"proxy-reconaaaaaaaaaaa\",\"cpu\":0},{\"uptime\":0,\"netout\":0,\"disk\":0,\"netin\":0,\"maxmem\":2147483648,\"maxdisk\":34359738368,\"mem\":0,\"id\":\"qemu/9000\",\"template\":1,\"name\":\"ubuntu-2604-template\",\"cpu\":0,\"maxcpu\":2,\"type\":\"qemu\",\"memhost\":0,\"diskwrite\":0,\"diskread\":0,\"node\":\"pv1-1\",\"vmid\":9000,\"status\":\"stopped\"},{\"mem\":0,\"maxdisk\":3221225472,\"maxmem\":2147483648,\"netin\":0,\"uptime\":0,\"disk\":0,\"netout\":0,\"vmid\":9001,\"status\":\"stopped\",\"diskwrite\":0,\"diskread\":0,\"node\":\"pv1-1\",\"maxcpu\":2,\"type\":\"qemu\",\"memhost\":0,\"template\":1,\"id\":\"qemu/9001\",\"cpu\":0,\"name\":\"debian-13-template\"}]}"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"status": 200,
|
||||
"reason": "OK",
|
||||
"body": "{\"data\":{\"meta\":\"creation-qemu=11.0.3,ctime=1790145676\",\"description\":\"proxy-operator-managed=true\\nproxy-operator-uid=00000000-0000-0000-0000-00000000recon\\nproxy-operator-created-at=2026-09-23T07:00:00Z\",\"ide2\":\"local:iso/proxy-reconaaaaaaaaaaa-seed.iso,media=cdrom,size=44K\",\"cores\":1,\"memory\":\"1024\",\"scsihw\":\"virtio-scsi-pci\",\"net0\":\"virtio=BC:24:11:48:11:46,bridge=vmbr0\",\"efidisk0\":\"local-lvm:vm-199999-disk-0,efitype=4m,pre-enrolled-keys=0,size=4M\",\"scsi0\":\"local-lvm:vm-199999-disk-1,size=4G\",\"boot\":\"order=scsi0;ide2\",\"vmgenid\":\"2dd339a4-d7fb-4d47-96fd-5a61a25f808d\",\"digest\":\"98a4fff988b5508a7e1f6aa77c34768812d2382c\",\"name\":\"proxy-reconaaaaaaaaaaa\",\"vga\":\"serial0\",\"serial0\":\"socket\",\"smbios1\":\"uuid=f2ade202-ffdb-4a89-9867-3d419384471d\",\"machine\":\"q35\",\"bios\":\"ovmf\",\"tags\":\"proxy_operator\",\"agent\":\"1\",\"ostype\":\"l26\"}}"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"status": 200,
|
||||
"reason": "OK",
|
||||
"body": "{\"data\":\"UPID:pv1-1:00111FF7:02853E62:6AB38591:imgdel:proxy-seeds:proxy-operator@pve!operator:\"}"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"status": 200,
|
||||
"reason": "OK",
|
||||
"body": "{\"data\":\"UPID:pv1-1:00111FF3:02853D30:6AB3858E:imgdel:proxy-seeds:proxy-operator@pve!operator:\"}"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"status": 200,
|
||||
"reason": "OK",
|
||||
"body": "{\"data\":\"UPID:pv1-1:00110564:0281A657:6AB37C5E:qmdestroy:199999:proxy-operator@pve!operator:\"}"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"status": 500,
|
||||
"reason": "QEMU guest agent is not running",
|
||||
"body": "{\"message\":\"QEMU guest agent is not running\\n\",\"data\":null}"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"status": 500,
|
||||
"reason": "No QEMU guest agent configured",
|
||||
"body": "{\"message\":\"No QEMU guest agent configured\\n\",\"data\":null}"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"status": 500,
|
||||
"reason": "VM 199999 is not running",
|
||||
"body": "{\"message\":\"VM 199999 is not running\\n\",\"data\":null}"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"status": 400,
|
||||
"reason": "Parameter verification failed.",
|
||||
"body": "{\"data\":null,\"message\":\"Parameter verification failed.\\n\",\"errors\":{\"newid\":\"type check ('integer') failed - got 'abc'\"}}"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"status": 400,
|
||||
"reason": "Parameter verification failed.",
|
||||
"body": "{\"errors\":{\"storage\":\"storage 'local' does not support vm images\"},\"message\":\"Parameter verification failed.\\n\",\"data\":null}"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"status": 500,
|
||||
"reason": "VM is locked (clone)",
|
||||
"body": "{\"data\":null,\"message\":\"VM is locked (clone)\\n\"}"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"status": 500,
|
||||
"reason": "Configuration file 'nodes/pv1-1/qemu-server/199999.conf' does not exist",
|
||||
"body": "{\"data\":null,\"message\":\"Configuration file 'nodes/pv1-1/qemu-server/199999.conf' does not exist\\n\"}"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"status": 403,
|
||||
"reason": "Permission check failed (/nodes/pv1-1, Sys.Syslog)",
|
||||
"body": "{\"message\":\"Permission check failed (/nodes/pv1-1, Sys.Syslog)\\n\",\"data\":null}"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"status": 403,
|
||||
"reason": "Permission check failed (/storage/local, Datastore.Allocate)",
|
||||
"body": "{\"data\":null,\"message\":\"Permission check failed (/storage/local, Datastore.Allocate)\\n\"}"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"status": 500,
|
||||
"reason": "unable to create VM 199999: config file already exists",
|
||||
"body": "{\"data\":null,\"message\":\"unable to create VM 199999: config file already exists\\n\"}"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"status": 401,
|
||||
"reason": "No ticket",
|
||||
"body": ""
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"status": 200,
|
||||
"reason": "OK",
|
||||
"body": "{\"data\":null}"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"status": 400,
|
||||
"reason": "Parameter verification failed.",
|
||||
"body": "{\"errors\":{\"tags\":\"invalid format - invalid characters in tag\\n\"},\"data\":null,\"message\":\"Parameter verification failed.\\n\"}"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"status": 401,
|
||||
"reason": "Authentication failed!",
|
||||
"body": ""
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"status": 401,
|
||||
"reason": "Authentication failed!",
|
||||
"body": ""
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"status": 400,
|
||||
"reason": "Parameter verification failed.",
|
||||
"body": "{\"message\":\"Parameter verification failed.\\n\",\"data\":null,\"errors\":{\"filename\":\"wrong file extension\"}}"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"status": 500,
|
||||
"reason": "Configuration file 'nodes/pv1-1/qemu-server/999999.conf' does not exist",
|
||||
"body": "{\"message\":\"Configuration file 'nodes/pv1-1/qemu-server/999999.conf' does not exist\\n\",\"data\":null}"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"status": 500,
|
||||
"reason": "Configuration file 'nodes/pv1-1/qemu-server/999999.conf' does not exist",
|
||||
"body": "{\"data\":null,\"message\":\"Configuration file 'nodes/pv1-1/qemu-server/999999.conf' does not exist\\n\"}"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"status": 200,
|
||||
"reason": "OK",
|
||||
"body": "{\"data\":{\"rootfs\":{\"total\":100861726720,\"avail\":88717860864,\"used\":6973128704,\"free\":93888598016},\"pveversion\":\"pve-manager/9.2.20/49318c671b82f31e\",\"current-kernel\":{\"machine\":\"x86_64\",\"sysname\":\"Linux\",\"release\":\"7.0.14-17-pve\",\"version\":\"#1 SMP PREEMPT_DYNAMIC PMX 7.0.14-17 (2026-09-10T10:16Z)\"},\"cpu\":0,\"cpuinfo\":{\"mhz\":\"1100.947\",\"user_hz\":100,\"cpus\":16,\"sockets\":1,\"model\":\"AMD Ryzen 9 PRO 8945HS w/ Radeon 780M Graphics\",\"vendor\":\"AuthenticAMD\",\"family\":\"25\",\"hvm\":\"1\",\"flags\":\"fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ht syscall nx mmxext fxsr_opt pdpe1gb rdtscp lm constant_tsc rep_good amd_lbr_v2 nopl xtopology nonstop_tsc cpuid extd_apicid aperfmperf rapl pni pclmulqdq monitor ssse3 fma cx16 sse4_1 sse4_2 x2apic movbe popcnt aes xsave avx f16c rdrand lahf_lm cmp_legacy svm extapic cr8_legacy abm sse4a misalignsse 3dnowprefetch osvw ibs skinit wdt tce topoext perfctr_core perfctr_nb bpext perfctr_llc mwaitx cpuid_fault cpb cat_l3 cdp_l3 hw_pstate ssbd mba perfmon_v2 ibrs ibpb stibp ibrs_enhanced vmmcall fsgsbase bmi1 avx2 smep bmi2 erms invpcid cqm rdt_a avx512f avx512dq rdseed adx smap avx512ifma clflushopt clwb avx512cd sha_ni avx512bw avx512vl xsaveopt xsavec xgetbv1 xsaves cqm_llc cqm_occup_llc cqm_mbm_total cqm_mbm_local user_shstk avx512_bf16 clzero irperf xsaveerptr rdpru wbnoinvd cppc arat npt lbrv svm_lock nrip_save tsc_scale vmcb_clean flushbyasid decodeassists pausefilter pfthreshold vgif x2avic v_spec_ctrl vnmi avx512vbmi umip pku ospke avx512_vbmi2 gfni vaes vpclmulqdq avx512_vnni avx512_bitalg avx512_vpopcntdq rdpid overflow_recov succor smca fsrm flush_l1d amd_lbr_pmc_freeze\",\"cores\":8},\"kversion\":\"Linux 7.0.14-17-pve #1 SMP PREEMPT_DYNAMIC PMX 7.0.14-17 (2026-09-10T10:16Z)\",\"wait\":0,\"boot-info\":{\"mode\":\"efi\",\"secureboot\":1},\"loadavg\":[\"0.05\",\"0.12\",\"0.10\"],\"memory\":{\"used\":2054795264,\"available\":29252005888,\"total\":31306801152,\"free\":28277043200},\"idle\":0,\"uptime\":419233,\"swap\":{\"used\":0,\"total\":8589930496,\"free\":8589930496},\"ksm\":{\"shared\":0}}}"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"status": 200,
|
||||
"reason": "OK",
|
||||
"body": "{\"data\":\"UPID:pv1-1:0010FB74:02804090:6AB378CA:resize:199999:proxy-operator@pve!operator:\"}"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"status": 200,
|
||||
"reason": "OK",
|
||||
"body": "{\"data\":\"UPID:pv1-1:0010FB6F:02804085:6AB378CA:resize:199999:proxy-operator@pve!operator:\"}"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"status": 200,
|
||||
"reason": "OK",
|
||||
"body": "{\"data\":\"UPID:pv1-1:0010FB72:0280408B:6AB378CA:resize:199999:proxy-operator@pve!operator:\"}"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"status": 200,
|
||||
"reason": "OK",
|
||||
"body": "{\"data\":null}"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"status": 200,
|
||||
"reason": "OK",
|
||||
"body": "{\"data\":null}"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"status": 200,
|
||||
"reason": "OK",
|
||||
"body": "{\"data\":\"UPID:pv1-1:0010FBF1:02804D0D:6AB378EA:qmstart:199999:proxy-operator@pve!operator:\"}"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"status": 200,
|
||||
"reason": "OK",
|
||||
"body": "{\"data\":\"UPID:pv1-1:0010F9F0:02801DAF:6AB37871:qmstart:199999:proxy-operator@pve!operator:\"}"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"status": 200,
|
||||
"reason": "OK",
|
||||
"body": "{\"data\":{\"serial\":1,\"proxmox-support\":{\"pbs-dirty-bitmap-migration\":true,\"query-bitmap-info\":true,\"backup-fleecing\":true,\"pbs-masterkey\":true,\"pbs-dirty-bitmap-savevm\":true,\"backup-max-workers\":true,\"backup-access-api\":true,\"pbs-dirty-bitmap\":true,\"pbs-library-version\":\"2.0.3 (UNKNOWN)\"},\"maxdisk\":4294967296,\"vmid\":199999,\"ha\":{\"managed\":0},\"diskwrite\":373281280,\"uptime\":858,\"ballooninfo\":{\"mem_swapped_out\":0,\"mem_swapped_in\":0,\"max_mem\":1073741824,\"minor_page_faults\":266583,\"actual\":1073741824,\"last_update\":1790147647,\"major_page_faults\":599,\"free_mem\":414076928,\"total_mem\":943988736},\"running-qemu\":\"11.0.3\",\"disk\":0,\"cpus\":1,\"name\":\"proxy-reconaaaaaaaaaaa\",\"blockstat\":{\"ide2\":{\"rd_merged\":0,\"wr_total_time_ns\":0,\"failed_flush_operations\":0,\"account_failed\":true,\"account_invalid\":true,\"failed_wr_operations\":0,\"unmap_bytes\":0,\"invalid_rd_operations\":0,\"wr_highest_offset\":0,\"failed_zone_append_operations\":0,\"wr_operations\":0,\"unmap_merged\":0,\"wr_bytes\":0,\"idle_time_ns\":847849524087,\"unmap_total_time_ns\":0,\"zone_append_total_time_ns\":0,\"wr_merged\":0,\"rd_total_time_ns\":1255768,\"zone_append_bytes\":0,\"zone_append_operations\":0,\"invalid_unmap_operations\":0,\"unmap_operations\":0,\"invalid_zone_append_operations\":0,\"invalid_wr_operations\":0,\"flush_operations\":0,\"rd_operations\":61,\"invalid_flush_operations\":0,\"zone_append_merged\":0,\"failed_unmap_operations\":0,\"failed_rd_operations\":0,\"flush_total_time_ns\":0,\"rd_bytes\":174304,\"timed_stats\":[]},\"efidisk0\":{\"wr_highest_offset\":15360,\"unmap_merged\":0,\"wr_operations\":0,\"failed_zone_append_operations\":0,\"wr_bytes\":0,\"rd_merged\":0,\"failed_flush_operations\":0,\"wr_total_time_ns\":0,\"account_invalid\":true,\"account_failed\":true,\"invalid_rd_operations\":0,\"unmap_bytes\":0,\"failed_wr_operations\":0,\"invalid_wr_operations\":0,\"invalid_zone_append_operations\":0,\"flush_operations\":0,\"invalid_flush_operations\":0,\"rd_operations\":0,\"failed_rd_operations\":0,\"zone_append_merged\":0,\"failed_unmap_operations\":0,\"timed_stats\":[],\"rd_bytes\":0,\"flush_total_time_ns\":0,\"wr_merged\":0,\"zone_append_total_time_ns\":0,\"unmap_total_time_ns\":0,\"rd_total_time_ns\":0,\"zone_append_operations\":0,\"zone_append_bytes\":0,\"unmap_operations\":0,\"invalid_unmap_operations\":0},\"scsi0\":{\"unmap_operations\":81,\"invalid_unmap_operations\":0,\"zone_append_bytes\":0,\"zone_append_operations\":0,\"rd_total_time_ns\":899575874,\"wr_merged\":0,\"zone_append_total_time_ns\":0,\"idle_time_ns\":18104802903,\"unmap_total_time_ns\":326592,\"timed_stats\":[],\"rd_bytes\":196229120,\"flush_total_time_ns\":47510703,\"failed_rd_operations\":0,\"failed_unmap_operations\":0,\"zone_append_merged\":0,\"invalid_flush_operations\":0,\"rd_operations\":8843,\"invalid_wr_operations\":0,\"flush_operations\":145,\"invalid_zone_append_operations\":0,\"invalid_rd_operations\":0,\"unmap_bytes\":100188160,\"failed_wr_operations\":0,\"account_invalid\":true,\"account_failed\":true,\"failed_flush_operations\":0,\"wr_total_time_ns\":4726294270,\"rd_merged\":0,\"wr_bytes\":373281280,\"unmap_merged\":0,\"wr_operations\":1449,\"failed_zone_append_operations\":0,\"wr_highest_offset\":4294967296},\"pflash0\":{\"unmap_operations\":0,\"invalid_unmap_operations\":0,\"zone_append_operations\":0,\"zone_append_bytes\":0,\"rd_total_time_ns\":0,\"wr_merged\":0,\"zone_append_total_time_ns\":0,\"unmap_total_time_ns\":0,\"timed_stats\":[],\"rd_bytes\":0,\"flush_total_time_ns\":0,\"failed_rd_operations\":0,\"failed_unmap_operations\":0,\"zone_append_merged\":0,\"invalid_flush_operations\":0,\"rd_operations\":0,\"flush_operations\":0,\"invalid_wr_operations\":0,\"invalid_zone_append_operations\":0,\"unmap_bytes\":0,\"invalid_rd_operations\":0,\"failed_wr_operations\":0,\"account_invalid\":true,\"account_failed\":true,\"failed_flush_operations\":0,\"wr_total_time_ns\":0,\"rd_merged\":0,\"wr_bytes\":0,\"unmap_merged\":0,\"failed_zone_append_operations\":0,\"wr_operations\":0,\"wr_highest_offset\":0}},\"agent\":1,\"pid\":1113088,\"tags\":\"proxy_operator\",\"pressureiosome\":0,\"diskread\":196403424,\"pressurememoryfull\":0,\"nics\":{\"tap199999i0\":{\"netin\":30673586,\"netout\":240050}},\"qmpstatus\":\"paused\",\"status\":\"running\",\"netin\":30673586,\"balloon\":1073741824,\"netout\":240050,\"cpu\":0,\"clipboard\":null,\"pressurememorysome\":0,\"pressurecpufull\":0,\"running-machine\":\"pc-q35-11.0+pve0\",\"mem\":529911808,\"pressurecpusome\":0,\"freemem\":414076928,\"maxmem\":1073741824,\"pressureiofull\":0,\"memhost\":674607104}}"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"status": 200,
|
||||
"reason": "OK",
|
||||
"body": "{\"data\":{\"running-machine\":\"pc-q35-11.0+pve0\",\"mem\":190930944,\"pressurecpufull\":0,\"pressurememorysome\":0,\"pressureiofull\":0,\"memhost\":337240064,\"pressurecpusome\":0,\"maxmem\":2147483648,\"freemem\":1808162816,\"pressurememoryfull\":0,\"nics\":{\"tap199999i0\":{\"netout\":0,\"netin\":255}},\"diskread\":148379648,\"clipboard\":null,\"status\":\"running\",\"qmpstatus\":\"running\",\"netin\":255,\"balloon\":2147483648,\"netout\":0,\"cpu\":0,\"agent\":1,\"pid\":1112650,\"name\":\"proxy-reconaaaaaaaaaaa\",\"blockstat\":{\"scsi0\":{\"zone_append_operations\":0,\"zone_append_bytes\":0,\"invalid_unmap_operations\":0,\"unmap_operations\":6,\"idle_time_ns\":4599584011,\"unmap_total_time_ns\":24404,\"zone_append_total_time_ns\":0,\"wr_merged\":0,\"rd_total_time_ns\":466322772,\"zone_append_merged\":0,\"failed_unmap_operations\":0,\"failed_rd_operations\":0,\"flush_total_time_ns\":4612531,\"rd_bytes\":148379648,\"timed_stats\":[],\"flush_operations\":19,\"invalid_wr_operations\":0,\"invalid_zone_append_operations\":0,\"rd_operations\":4279,\"invalid_flush_operations\":0,\"account_failed\":true,\"account_invalid\":true,\"failed_wr_operations\":0,\"invalid_rd_operations\":0,\"unmap_bytes\":282624,\"rd_merged\":0,\"wr_total_time_ns\":190079426,\"failed_flush_operations\":0,\"wr_bytes\":6373888,\"wr_highest_offset\":1533730816,\"failed_zone_append_operations\":0,\"wr_operations\":220,\"unmap_merged\":0},\"efidisk0\":{\"rd_merged\":0,\"failed_flush_operations\":0,\"wr_total_time_ns\":0,\"account_invalid\":true,\"account_failed\":true,\"unmap_bytes\":0,\"invalid_rd_operations\":0,\"failed_wr_operations\":0,\"wr_highest_offset\":9216,\"unmap_merged\":0,\"failed_zone_append_operations\":0,\"wr_operations\":0,\"wr_bytes\":0,\"wr_merged\":0,\"zone_append_total_time_ns\":0,\"unmap_total_time_ns\":0,\"rd_total_time_ns\":0,\"zone_append_bytes\":0,\"zone_append_operations\":0,\"unmap_operations\":0,\"invalid_unmap_operations\":0,\"invalid_wr_operations\":0,\"invalid_zone_append_operations\":0,\"flush_operations\":0,\"invalid_flush_operations\":0,\"rd_operations\":0,\"failed_rd_operations\":0,\"zone_append_merged\":0,\"failed_unmap_operations\":0,\"timed_stats\":[],\"rd_bytes\":0,\"flush_total_time_ns\":0},\"pflash0\":{\"wr_total_time_ns\":0,\"failed_flush_operations\":0,\"rd_merged\":0,\"failed_wr_operations\":0,\"unmap_bytes\":0,\"invalid_rd_operations\":0,\"account_failed\":true,\"account_invalid\":true,\"wr_operations\":0,\"failed_zone_append_operations\":0,\"unmap_merged\":0,\"wr_highest_offset\":0,\"wr_bytes\":0,\"rd_total_time_ns\":0,\"unmap_total_time_ns\":0,\"zone_append_total_time_ns\":0,\"wr_merged\":0,\"invalid_unmap_operations\":0,\"unmap_operations\":0,\"zone_append_operations\":0,\"zone_append_bytes\":0,\"rd_operations\":0,\"invalid_flush_operations\":0,\"invalid_zone_append_operations\":0,\"invalid_wr_operations\":0,\"flush_operations\":0,\"flush_total_time_ns\":0,\"rd_bytes\":0,\"timed_stats\":[],\"failed_unmap_operations\":0,\"zone_append_merged\":0,\"failed_rd_operations\":0}},\"pressureiosome\":0.01,\"vmid\":199999,\"ha\":{\"managed\":0},\"diskwrite\":6373888,\"serial\":1,\"proxmox-support\":{\"pbs-dirty-bitmap-savevm\":true,\"pbs-masterkey\":true,\"backup-fleecing\":true,\"pbs-dirty-bitmap-migration\":true,\"query-bitmap-info\":true,\"backup-access-api\":true,\"pbs-dirty-bitmap\":true,\"pbs-library-version\":\"2.0.3 (UNKNOWN)\",\"backup-max-workers\":true},\"maxdisk\":3221225472,\"ballooninfo\":{\"actual\":2147483648,\"mem_swapped_out\":0,\"mem_swapped_in\":0,\"max_mem\":2147483648,\"minor_page_faults\":122998,\"major_page_faults\":616,\"free_mem\":1808162816,\"total_mem\":1999093760,\"last_update\":1790146701},\"uptime\":26,\"running-qemu\":\"11.0.3\",\"disk\":0,\"cpus\":2}}"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"status": 200,
|
||||
"reason": "OK",
|
||||
"body": "{\"data\":{\"name\":\"ubuntu-vm-1\",\"mem\":0,\"maxmem\":2147483648,\"memhost\":0,\"maxdisk\":34359738368,\"serial\":1,\"ha\":{\"managed\":0},\"vmid\":100,\"cpu\":0,\"cpus\":2,\"netout\":0,\"disk\":0,\"qmpstatus\":\"stopped\",\"status\":\"stopped\",\"netin\":0,\"uptime\":0,\"clipboard\":null}}"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"status": 200,
|
||||
"reason": "OK",
|
||||
"body": "{\"data\":\"UPID:pv1-1:0010FAC5:02802A52:6AB37891:qmstop:199999:proxy-operator@pve!operator:\"}"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"status": 200,
|
||||
"reason": "OK",
|
||||
"body": "{\"data\":[{\"size\":45056,\"volid\":\"local:iso/proxy-reconaaaaaaaaaaa-seed.iso\",\"format\":\"iso\",\"content\":\"iso\",\"ctime\":1790146742}]}"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"status": 200,
|
||||
"reason": "OK",
|
||||
"body": "{\"data\":{\"total\":100861726720,\"active\":1,\"enabled\":1,\"used\":6973128704,\"avail\":88717860864,\"type\":\"dir\",\"shared\":0,\"content\":\"iso,import,backup,vztmpl\"}}"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"status": 200,
|
||||
"reason": "OK",
|
||||
"body": "{\"data\":{\"upid\":\"UPID:pv1-1:0010F9DC:02801DA1:6AB37871:qmclone:9001:proxy-operator@pve!operator:\",\"exitstatus\":\"OK\",\"status\":\"stopped\",\"pid\":1112540,\"pstart\":41950625,\"user\":\"proxy-operator@pve\",\"node\":\"pv1-1\",\"type\":\"qmclone\",\"starttime\":1790146673,\"id\":\"9001\",\"tokenid\":\"operator\"}}"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"status": 200,
|
||||
"reason": "OK",
|
||||
"body": "{\"data\":{\"pstart\":41962765,\"upid\":\"UPID:pv1-1:0010FBF1:02804D0D:6AB378EA:qmstart:199999:proxy-operator@pve!operator:\",\"pid\":1113073,\"node\":\"pv1-1\",\"user\":\"proxy-operator@pve\",\"type\":\"qmstart\",\"status\":\"running\",\"tokenid\":\"operator\",\"id\":\"199999\",\"starttime\":1790146794}}"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"status": 200,
|
||||
"reason": "OK",
|
||||
"body": "{\"data\":{\"exitstatus\":\"OK\",\"status\":\"stopped\",\"upid\":\"UPID:pv1-1:0010F9F0:02801DAF:6AB37871:qmstart:199999:proxy-operator@pve!operator:\",\"type\":\"qmstart\",\"tokenid\":\"operator\",\"id\":\"199999\",\"starttime\":1790146673,\"pstart\":41950639,\"pid\":1112560,\"node\":\"pv1-1\",\"user\":\"proxy-operator@pve\"}}"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"status": 200,
|
||||
"reason": "OK",
|
||||
"body": "{\"data\":{\"type\":\"resize\",\"tokenid\":\"operator\",\"id\":\"199999\",\"starttime\":1790146762,\"pstart\":41959563,\"pid\":1112946,\"user\":\"proxy-operator@pve\",\"node\":\"pv1-1\",\"exitstatus\":\"shrinking disks is not supported\",\"status\":\"stopped\",\"upid\":\"UPID:pv1-1:0010FB72:0280408B:6AB378CA:resize:199999:proxy-operator@pve!operator:\"}}"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"status": 200,
|
||||
"reason": "OK",
|
||||
"body": "{\"data\":{\"exitstatus\":\"OK\",\"status\":\"stopped\",\"upid\":\"UPID:pv1-1:0010FAC5:02802A52:6AB37891:qmstop:199999:proxy-operator@pve!operator:\",\"type\":\"qmstop\",\"starttime\":1790146705,\"id\":\"199999\",\"tokenid\":\"operator\",\"pid\":1112773,\"pstart\":41953874,\"user\":\"proxy-operator@pve\",\"node\":\"pv1-1\"}}"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"status": 200,
|
||||
"reason": "OK",
|
||||
"body": "{\"data\":{\"user\":\"proxy-operator@pve\",\"node\":\"pv1-1\",\"pstart\":42286690,\"pid\":1122295,\"id\":\"proxy-seeds\",\"tokenid\":\"operator\",\"starttime\":1790150033,\"type\":\"imgdel\",\"upid\":\"UPID:pv1-1:00111FF7:02853E62:6AB38591:imgdel:proxy-seeds:proxy-operator@pve!operator:\",\"status\":\"stopped\",\"exitstatus\":\"OK\"}}"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"status": 200,
|
||||
"reason": "OK",
|
||||
"body": "{\"data\":\"UPID:pv1-1:0010FB2D:028038B0:6AB378B6:imgcopy::proxy-operator@pve!operator:\"}"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"status": 200,
|
||||
"reason": "OK",
|
||||
"body": "{\"data\":\"UPID:pv1-1:0010FB27:0280377E:6AB378B3:imgcopy::proxy-operator@pve!operator:\"}"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"status": 200,
|
||||
"reason": "OK",
|
||||
"body": "{\"data\":\"UPID:pv1-1:00110D02:0282B09D:6AB37F08:imgcopy::proxy-operator@pve!operator:\"}"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"status": 200,
|
||||
"reason": "OK",
|
||||
"body": "{\"data\":{\"version\":\"9.2.20\",\"repoid\":\"49318c671b82f31e\",\"release\":\"9.2\"}}"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"status": 200,
|
||||
"reason": "OK",
|
||||
"body": "{\"data\":{\"boot\":\"order=scsi0\",\"vmgenid\":\"2dd339a4-d7fb-4d47-96fd-5a61a25f808d\",\"digest\":\"e747d94a5aac26cf6528b43835600b1bc4e82dda\",\"scsi0\":\"local-lvm:vm-199999-disk-1,size=3G\",\"serial0\":\"socket\",\"name\":\"proxy-reconaaaaaaaaaaa\",\"vga\":\"serial0\",\"cores\":2,\"meta\":\"creation-qemu=11.0.3,ctime=1790145676\",\"description\":\"proxy-operator-managed=true\\nproxy-operator-uid=00000000-0000-0000-0000-00000000recon\\nproxy-operator-created-at=2026-09-23T07:00:00Z\",\"efidisk0\":\"local-lvm:vm-199999-disk-0,efitype=4m,pre-enrolled-keys=0,size=4M\",\"memory\":\"2048\",\"scsihw\":\"virtio-scsi-pci\",\"net0\":\"virtio=BC:24:11:48:11:46,bridge=vmbr0\",\"agent\":\"1\",\"ostype\":\"l26\",\"bios\":\"ovmf\",\"machine\":\"q35\",\"smbios1\":\"uuid=f2ade202-ffdb-4a89-9867-3d419384471d\"}}"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"status": 200,
|
||||
"reason": "OK",
|
||||
"body": "{\"data\":{\"ostype\":\"l26\",\"agent\":\"1\",\"smbios1\":\"uuid=5b131064-9841-4bc7-982e-5fb5b2e677eb\",\"machine\":\"q35\",\"bios\":\"ovmf\",\"scsi0\":\"local-lvm:base-9001-disk-2,size=3G\",\"digest\":\"03bdc5d457ce2165bb633505d2ff0cd94340dbc2\",\"vmgenid\":\"9a42b153-7965-4601-864d-59fc2d71ecd4\",\"boot\":\"order=scsi0\",\"vga\":\"serial0\",\"template\":1,\"name\":\"debian-13-template\",\"serial0\":\"socket\",\"meta\":\"creation-qemu=11.0.3,ctime=1790145676\",\"cores\":2,\"net0\":\"virtio=BC:24:11:93:25:82,bridge=vmbr0\",\"scsihw\":\"virtio-scsi-pci\",\"memory\":\"2048\",\"efidisk0\":\"local-lvm:base-9001-disk-0,efitype=4m,pre-enrolled-keys=0,size=4M\"}}"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"status": 200,
|
||||
"reason": "OK",
|
||||
"body": "{\"data\":{\"ciuser\":\"ubuntu\",\"smbios1\":\"uuid=6ba36ff6-9d1f-457b-8cdc-a44448f5bd35\",\"ostype\":\"l26\",\"memory\":\"2048\",\"scsihw\":\"virtio-scsi-pci\",\"net0\":\"virtio=BC:24:11:2F:97:A0,bridge=vmbr0,tag=4\",\"ide2\":\"local-lvm:vm-9000-cloudinit,media=cdrom\",\"cores\":2,\"meta\":\"creation-qemu=11.0.0,ctime=1789719724\",\"ipconfig0\":\"ip=dhcp\",\"serial0\":\"socket\",\"name\":\"ubuntu-2604-template\",\"template\":1,\"vga\":\"serial0\",\"boot\":\"order=scsi0\",\"vmgenid\":\"37d16d5c-957f-4c83-aab3-b403513e466c\",\"digest\":\"8820e9bb34a576d0f54b94cfe3e69fbfdf13a8ce\",\"scsi0\":\"local-lvm:base-9000-disk-0,size=32G\"}}"
|
||||
}
|
||||
Reference in New Issue
Block a user