# Execution log: idempotent management cluster (Ansible + cluster-api-operator) Pairs with [docs/plans/2026-09-27-0058-mgmt-cluster-ansible.md](../plans/2026-09-27-0058-mgmt-cluster-ansible.md). ## Status - [x] Step 0 — Initial import commit + plan commit - [x] Step 1 — Scaffold `ansible/`, `.gitignore` exception, syntax/lint checks - [ ] Step 2 — Run the playbook tag by tag against the lab (owner) - [ ] Step 3 — Idempotency check (second full run `changed=0`) - [ ] Step 4 — Docs, versions, decisions, changelog ## Step 0 — Initial import + plan The repo had no commits, so the existing tree went in first as `5fd998c` (after a grep for token/secret/key material found only placeholders), then the plan alone as `ac7d5a4`. ## Step 1 — Scaffold `ansible/` (2026-09-27 01:02 CEST) Wrote `ansible/{ansible.cfg,inventory.yml,group_vars/all.yml,secrets.yml.example,mgmt.yml}` and four templates as planned. `ansible/secrets.yml` was already ignored by `*secret*`; added `!ansible/secrets.yml.example`. `ansible-core` is not installed on the laptop, so checks ran through `uvx` (cached, nothing installed globally), pinned to ansible-core 2.18 (resolved 2.18.19): ```bash cd ansible uvx --from 'ansible-core>=2.18,<2.19' ansible-playbook mgmt.yml --syntax-check uvx --with 'ansible-core>=2.18,<2.19' ansible-lint --profile production mgmt.yml ``` First lint run: 6× `name[casing]`, 5× `no-changed-when`. The commands flagged are the create/resize/start/install tasks that only run behind a `when:` guard, so they got `changed_when: true` rather than a fake condition. Second run: production profile passed. Rendered all templates with dummy values (`-e mgmt_ip=192.0.2.10`, secret `DUMMY`) through a throwaway local playbook: provider manifest parses as 11 documents (5 Namespaces, Secret, 5 provider CRs), k3s config and kubeconfig rewrite (`server` → VM IP, every `default` name → `lab-mgmt`) come out as intended. Connectivity to the PVE host: ```bash uvx --from 'ansible-core>=2.18,<2.19' ansible pve -m ping -o # pv1-1 | UNREACHABLE!: ... root@192.168.0.8: Permission denied (publickey,password). ``` **Worth noting:** - The plan's assumption "root SSH from the laptop to `pv1-1` works" does not hold yet; the host answers but rejects the laptop's keys. Needed before Step 2. - Condition-based waits: the playbook waits for `Available` Deployments in each provider namespace instead of a condition on the provider CRs, so it doesn't depend on the operator's condition naming (plan left that open). - VM params (`mgmt_vmid`, `mgmt_ip`, `mgmt_gateway`, `mgmt_dns`) are empty in `group_vars/all.yml`; Play 1 asserts on them. - `clusterctl` is not installed on the laptop either (needed for phase-1 Step 3, not for this playbook). ## Step 2 — Playbook runs (in progress) ### `--tags vm`, first attempt (2026-09-27 09:30 CEST, owner) Owner authorised the laptop key on `pv1-1`, installed ansible locally and filled in VM params (`mgmt_vmid: 201`, `mgmt_ip: 192.168.4.21/24`, gateway `192.168.4.8`). ```bash ansible-playbook mgmt.yml --tags vm # TASK [Create the VM] # "cmd": [..., "--nameserver", "8.8.8.8,", "2.2.2.2"] # stderr: 400 too many arguments ``` `mgmt_dns` was the string `"8.8.8.8, 2.2.2.2"`; the command module splits on whitespace, so `qm` got two positional leftovers. Fix: `mgmt_dns` is now a list and the task passes `--nameserver '{{ mgmt_dns | join(' ') }}'` as one argument (space-separated is PVE's format for the cloud-init nameserver list). Image download and key staging had already succeeded and are idempotent on re-run; `qm status 201` confirmed no half-created VM was left behind.