mgmt_dns is a list, passed to qm create as one space-separated argument. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3.6 KiB
Execution log: idempotent management cluster (Ansible + cluster-api-operator)
Pairs with docs/plans/2026-09-27-0058-mgmt-cluster-ansible.md.
Status
- Step 0 — Initial import commit + plan commit
- Step 1 — Scaffold
ansible/,.gitignoreexception, 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):
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:
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-1works" does not hold yet; the host answers but rejects the laptop's keys. Needed before Step 2. - Condition-based waits: the playbook waits for
AvailableDeployments 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 ingroup_vars/all.yml; Play 1 asserts on them. clusterctlis 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).
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.