diff --git a/CLAUDE.md b/CLAUDE.md index 7ae9d9f..b195a3d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -20,6 +20,7 @@ incu-contrib/ │ ├── incusos-proxmox # Declarative Proxmox VM deployment + lab lifecycle │ ├── lab-test # Guided lab validation (12 test phases) │ ├── 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 │ └── examples/ # Example seed + Proxmox YAML files └── notes/ # Research notes and reference material @@ -88,15 +89,20 @@ incu-contrib/ etc.) only define VM specs. Merge priority: `proxmox.yaml` base → lab config `proxmox:` overlay → CLI flags (`--host`, `--method`). Auto-discovery looks 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 (`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.Config.Network VM.Config.CDROM VM.Config.Options VM.Config.HWType 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//status`). It requires a separate ACL on `/nodes/` + since the pool-scoped ACL doesn't cover node-level endpoints. - **Required VM settings** (getting any wrong causes IncusOS install failure): - `bios=ovmf`, `machine=q35` -- UEFI boot required - `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 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 deployment status (Proxmox state, install status, IP, port 8443, incus diff --git a/incusos/incusos-proxmox b/incusos/incusos-proxmox index aed8d8a..e60965d 100755 --- a/incusos/incusos-proxmox +++ b/incusos/incusos-proxmox @@ -586,7 +586,7 @@ run_doctor() { # operations-center CLI if command -v operations-center &>/dev/null; then 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}" else 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" if [[ -d "$cache_base" ]]; then 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 local cached_name cached_name=$(basename "$cached_iso" .iso | sed 's/^IncusOS_//') @@ -3511,8 +3511,13 @@ for v in data: local config_output config_output=$(pve_api GET "/nodes/${PVE_NODE}/qemu/${vmid}/config" 2>/dev/null) || continue - local desc name mem_mib disk_gib status - read -r desc name mem_mib disk_gib < <(python3 -c " + local name="" mem_mib="" disk_gib="" desc="" status + { + read -r name + read -r mem_mib + read -r disk_gib + read -r desc + } < <(python3 -c " import json data = json.loads('''${config_output}''').get('data', {}) desc = data.get('description', '') @@ -3526,8 +3531,11 @@ for part in scsi0.split(','): sz = part.strip().split('=')[1] if sz.endswith('G'): disk_g = int(sz[:-1]) -print(f'{desc}|||{name}|||{mem}|||{disk_g}') -" 2>/dev/null | tr '|||' ' ' || true) +print(name) +print(mem) +print(disk_g) +print(desc) +" 2>/dev/null || true) # Check for managed marker if [[ "$desc" != *"$MANAGED_MARKER"* ]]; then @@ -3537,7 +3545,7 @@ print(f'{desc}|||{name}|||{mem}|||{disk_g}') # Extract config= tag local config_tag="unknown" - if [[ "$desc" =~ config=([^\ \]]+) ]]; then + if [[ "$desc" =~ config=([^\ ]+) ]]; then config_tag="${BASH_REMATCH[1]}" fi @@ -3556,7 +3564,9 @@ print(f'{desc}|||{name}|||{mem}|||{disk_g}') return 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 ram_mib="${lab_ram[$config]}" local disk_gib="${lab_disk[$config]}" @@ -3592,6 +3602,7 @@ print(f'{desc}|||{name}|||{mem}|||{disk_g}') done [[ $unmanaged -gt 0 ]] && detail "${unmanaged} unmanaged VMs in pool" + return 0 } # --------------------------------------------------------------------------- @@ -4374,7 +4385,7 @@ main() { [[ "$RESOURCES" == true ]] && do_resources [[ "$LIST_LABS" == true ]] && do_list_labs - return + return 0 fi # Handle cleanup-all without a config file (only needs proxmox.yaml) @@ -4463,7 +4474,7 @@ main() { if [[ "$RESOURCES" == true ]] || [[ "$LIST_LABS" == true ]]; then [[ "$RESOURCES" == true ]] && do_resources [[ "$LIST_LABS" == true ]] && do_list_labs - return + return 0 fi # Reconcile existing deployments (interactive menu on re-runs)