Fix exit code bugs, doctor output, and labs parsing from live testing

Tested all new commands (--resources, --labs, --lab-down, --lab-up,
--doctor, --status) against live Proxmox with 4 running IncusOS VMs.

Fixes:
- return → return 0 in 4 early-exit paths (pipefail + [[ ]] false
  propagated non-zero exit codes)
- do_list_labs: broken ERE regex [^\ \]]+ → [^\ ]+ for config tag
- do_list_labs: line-based Python output instead of delimiter-based
  tr (descriptions with spaces broke read parsing)
- do_list_labs: unsafe echo|tr|sort pipeline → printf|sort
- run_doctor: operations-center version → --version (shows 0.2.2)
- run_doctor: ls glob pipeline crash under pipefail (|| true)

CLAUDE.md:
- Document env file (PROXMOX_TOKEN_SECRET) and source workflow
- Add Sys.Audit to minimum API privileges (needed for --resources)
- Correct role name to IncusOSDeployer
- Add /nodes/pve ACL requirement for node-level endpoints

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Maarten 2026-02-21 00:08:29 +01:00
parent f812e04746
commit 37d63c35a6
2 changed files with 31 additions and 13 deletions

View File

@ -20,6 +20,7 @@ incu-contrib/
│ ├── incusos-proxmox # Declarative Proxmox VM deployment + lab lifecycle │ ├── incusos-proxmox # Declarative Proxmox VM deployment + lab lifecycle
│ ├── lab-test # Guided lab validation (12 test phases) │ ├── lab-test # Guided lab validation (12 test phases)
│ ├── proxmox.yaml # Proxmox connection config (gitignored, contains credentials) │ ├── proxmox.yaml # Proxmox connection config (gitignored, contains credentials)
│ ├── ../env # PROXMOX_TOKEN_SECRET (gitignored); source with: source env
│ ├── TESTING.md # Testing guide for incusos-proxmox and lab-test │ ├── TESTING.md # Testing guide for incusos-proxmox and lab-test
│ └── examples/ # Example seed + Proxmox YAML files │ └── examples/ # Example seed + Proxmox YAML files
└── notes/ # Research notes and reference material └── notes/ # Research notes and reference material
@ -88,15 +89,20 @@ incu-contrib/
etc.) only define VM specs. Merge priority: `proxmox.yaml` base → lab config etc.) only define VM specs. Merge priority: `proxmox.yaml` base → lab config
`proxmox:` overlay → CLI flags (`--host`, `--method`). Auto-discovery looks `proxmox:` overlay → CLI flags (`--host`, `--method`). Auto-discovery looks
for `proxmox.yaml` in script directory then cwd; override with `--proxmox FILE`. for `proxmox.yaml` in script directory then cwd; override with `--proxmox FILE`.
- **API token secret**: stored in `env` file at the repo root (gitignored).
Source it before running scripts: `source env` (exports `PROXMOX_TOKEN_SECRET`).
- **Connection methods**: SSH (default, `ssh root@host qm ...`) or API - **Connection methods**: SSH (default, `ssh root@host qm ...`) or API
(`curl -k https://host:8006/api2/json/...` with `PVEAPIToken` header). (`curl -k https://host:8006/api2/json/...` with `PVEAPIToken` header).
- **Minimum API privileges** for token-based access: - **Minimum API privileges** for token-based access (role: `IncusOSDeployer`):
``` ```
VM.Allocate VM.Config.Disk VM.Config.CPU VM.Config.Memory VM.Allocate VM.Config.Disk VM.Config.CPU VM.Config.Memory
VM.Config.Network VM.Config.CDROM VM.Config.Options VM.Config.HWType VM.Config.Network VM.Config.CDROM VM.Config.Options VM.Config.HWType
VM.PowerMgmt VM.Audit Datastore.AllocateSpace Datastore.AllocateTemplate VM.PowerMgmt VM.Audit Datastore.AllocateSpace Datastore.AllocateTemplate
Datastore.Audit SDN.Use Datastore.Audit SDN.Use Sys.Audit
``` ```
`Sys.Audit` is needed for `--resources` (host RAM/CPU/uptime via
`/nodes/<node>/status`). It requires a separate ACL on `/nodes/<node>`
since the pool-scoped ACL doesn't cover node-level endpoints.
- **Required VM settings** (getting any wrong causes IncusOS install failure): - **Required VM settings** (getting any wrong causes IncusOS install failure):
- `bios=ovmf`, `machine=q35` -- UEFI boot required - `bios=ovmf`, `machine=q35` -- UEFI boot required
- `efidisk0`: `pre-enrolled-keys=0` -- IncusOS enrolls its own Secure Boot keys - `efidisk0`: `pre-enrolled-keys=0` -- IncusOS enrolls its own Secure Boot keys
@ -134,7 +140,8 @@ incu-contrib/
``` ```
pveum pool add IncusLab --comment "IncusOS Lab VMs" pveum pool add IncusLab --comment "IncusOS Lab VMs"
pveum pool modify IncusLab --storage local-lvm,local pveum pool modify IncusLab --storage local-lvm,local
pveum aclmod /pool/IncusLab -user automation@pve -role IncusOSDeploy pveum aclmod /pool/IncusLab -user automation@pve -role IncusOSDeployer
pveum aclmod /nodes/pve -user automation@pve -role IncusOSDeployer -propagate 0
``` ```
- **`--status` command**: `incusos-proxmox --status config.yaml` shows per-VM - **`--status` command**: `incusos-proxmox --status config.yaml` shows per-VM
deployment status (Proxmox state, install status, IP, port 8443, incus deployment status (Proxmox state, install status, IP, port 8443, incus

View File

@ -586,7 +586,7 @@ run_doctor() {
# operations-center CLI # operations-center CLI
if command -v operations-center &>/dev/null; then if command -v operations-center &>/dev/null; then
local oc_ver local oc_ver
oc_ver=$(operations-center version 2>/dev/null | head -1 || echo "installed") oc_ver=$(operations-center --version 2>/dev/null | head -1 || echo "installed")
echo -e " operations-center CLI: ${GREEN}${oc_ver} ✓${RESET}" echo -e " operations-center CLI: ${GREEN}${oc_ver} ✓${RESET}"
else else
echo -e " operations-center CLI: ${DIM}not installed (optional)${RESET}" echo -e " operations-center CLI: ${DIM}not installed (optional)${RESET}"
@ -670,7 +670,7 @@ if versions:
local cache_base="${XDG_CACHE_HOME:-${HOME}/.cache}/${SCRIPT_NAME}/iso-cache" local cache_base="${XDG_CACHE_HOME:-${HOME}/.cache}/${SCRIPT_NAME}/iso-cache"
if [[ -d "$cache_base" ]]; then if [[ -d "$cache_base" ]]; then
local cached_iso local cached_iso
cached_iso=$(ls -1 "${cache_base}"/IncusOS_*.iso 2>/dev/null | sort | tail -1) cached_iso=$(ls -1 "${cache_base}"/IncusOS_*.iso 2>/dev/null | sort | tail -1 || true)
if [[ -n "$cached_iso" ]]; then if [[ -n "$cached_iso" ]]; then
local cached_name local cached_name
cached_name=$(basename "$cached_iso" .iso | sed 's/^IncusOS_//') cached_name=$(basename "$cached_iso" .iso | sed 's/^IncusOS_//')
@ -3511,8 +3511,13 @@ for v in data:
local config_output local config_output
config_output=$(pve_api GET "/nodes/${PVE_NODE}/qemu/${vmid}/config" 2>/dev/null) || continue config_output=$(pve_api GET "/nodes/${PVE_NODE}/qemu/${vmid}/config" 2>/dev/null) || continue
local desc name mem_mib disk_gib status local name="" mem_mib="" disk_gib="" desc="" status
read -r desc name mem_mib disk_gib < <(python3 -c " {
read -r name
read -r mem_mib
read -r disk_gib
read -r desc
} < <(python3 -c "
import json import json
data = json.loads('''${config_output}''').get('data', {}) data = json.loads('''${config_output}''').get('data', {})
desc = data.get('description', '') desc = data.get('description', '')
@ -3526,8 +3531,11 @@ for part in scsi0.split(','):
sz = part.strip().split('=')[1] sz = part.strip().split('=')[1]
if sz.endswith('G'): if sz.endswith('G'):
disk_g = int(sz[:-1]) disk_g = int(sz[:-1])
print(f'{desc}|||{name}|||{mem}|||{disk_g}') print(name)
" 2>/dev/null | tr '|||' ' ' || true) print(mem)
print(disk_g)
print(desc)
" 2>/dev/null || true)
# Check for managed marker # Check for managed marker
if [[ "$desc" != *"$MANAGED_MARKER"* ]]; then if [[ "$desc" != *"$MANAGED_MARKER"* ]]; then
@ -3537,7 +3545,7 @@ print(f'{desc}|||{name}|||{mem}|||{disk_g}')
# Extract config= tag # Extract config= tag
local config_tag="unknown" local config_tag="unknown"
if [[ "$desc" =~ config=([^\ \]]+) ]]; then if [[ "$desc" =~ config=([^\ ]+) ]]; then
config_tag="${BASH_REMATCH[1]}" config_tag="${BASH_REMATCH[1]}"
fi fi
@ -3556,7 +3564,9 @@ print(f'{desc}|||{name}|||{mem}|||{disk_g}')
return return
fi fi
for config in $(echo "${!lab_configs[@]}" | tr ' ' '\n' | sort); do local sorted_configs
sorted_configs=$(printf '%s\n' "${!lab_configs[@]}" | sort)
for config in $sorted_configs; do
local vms="${lab_configs[$config]}" local vms="${lab_configs[$config]}"
local ram_mib="${lab_ram[$config]}" local ram_mib="${lab_ram[$config]}"
local disk_gib="${lab_disk[$config]}" local disk_gib="${lab_disk[$config]}"
@ -3592,6 +3602,7 @@ print(f'{desc}|||{name}|||{mem}|||{disk_g}')
done done
[[ $unmanaged -gt 0 ]] && detail "${unmanaged} unmanaged VMs in pool" [[ $unmanaged -gt 0 ]] && detail "${unmanaged} unmanaged VMs in pool"
return 0
} }
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@ -4374,7 +4385,7 @@ main() {
[[ "$RESOURCES" == true ]] && do_resources [[ "$RESOURCES" == true ]] && do_resources
[[ "$LIST_LABS" == true ]] && do_list_labs [[ "$LIST_LABS" == true ]] && do_list_labs
return return 0
fi fi
# Handle cleanup-all without a config file (only needs proxmox.yaml) # Handle cleanup-all without a config file (only needs proxmox.yaml)
@ -4463,7 +4474,7 @@ main() {
if [[ "$RESOURCES" == true ]] || [[ "$LIST_LABS" == true ]]; then if [[ "$RESOURCES" == true ]] || [[ "$LIST_LABS" == true ]]; then
[[ "$RESOURCES" == true ]] && do_resources [[ "$RESOURCES" == true ]] && do_resources
[[ "$LIST_LABS" == true ]] && do_list_labs [[ "$LIST_LABS" == true ]] && do_list_labs
return return 0
fi fi
# Reconcile existing deployments (interactive menu on re-runs) # Reconcile existing deployments (interactive menu on re-runs)