Read-only copy of pvginkel/Ansible
docs/runbooks/adoption.md as of 80f07c4 (2026-10-17).
The source is authoritative: change it there.
Adopting a host into Ansible management #
How to bring a host under Ansible management when it was not provisioned via the Terraform + cloud-init path. Use this for the PVE physical nodes (pve, pve1, pve2), the operator workstation (wrkdev), and any future hand-installed Linux box.
For Terraform-provisioned VMs the host key arrives in the repo at terraform apply time and bootstrap runs as part of the normal site.yml flow — adoption is not needed.
What adopt.yml does #
Two plays:
- On
localhost:ssh-keyscaneach target's existing ed25519 host key and write it intoansible/files/known_hosts.d/<basename>in the samename,name.home keytype keydataformat Terraform uses. - On each target: apply the
bootstraprole — creates theansibleautomation user (UID 900, passwordless sudo) and thepvginkeloperator user, drops their SSH keys.
After both plays succeed, append the new known_hosts file path to ansible.cfg's UserKnownHostsFile and commit. From that point on the host is reachable via site.yml.
Prerequisites #
- The target's ed25519 host key is reachable over the network (a modern Debian/Ubuntu serves one by default; verify with
ssh-keyscan -t ed25519 <host>). - The operator can SSH to the target with elevated privileges. Two patterns:
- Linux host with operator account: log in as
pvginkel(or another sudoer), elevate viasudo— invokeadopt.ymlwith-e adoption_user=pvginkel -K. - PVE node (root-only): log in as
rootdirectly using thepve-rootkey inssh-agent— invoke with-e adoption_user=root(no-K; root needs no sudo password).
- Linux host with operator account: log in as
- The host resolves under
.homefrom the workstation. The known_hosts entry that gets written includes both the short name and the FQDN.
Run #
From the ansible/ directory:
poetry run ansible-playbook playbooks/adopt.yml \
-i inventories/<inv> \
[-K] \
-e adoption_targets=<host_or_group_pattern> \
-e adoption_user=<login user> \
-e adoption_known_hosts_file=<basename>
The three -e extras are required.
| Variable | What it is |
|---|---|
adoption_targets |
What Play 2's hosts: resolves to. A group name (proxmox), a single host (wrkdev), or a comma-separated list. Anything ansible-inventory would accept. |
adoption_user |
The login user for the SSH connection. Use this, not -u — group_vars/all.yml's ansible_user: ansible outranks the -u flag, and the ansible user is exactly what bootstrap is about to create. Extra-vars do beat group_vars. |
adoption_known_hosts_file |
Basename written under files/known_hosts.d/. Pick something stable per inventory or per group — once added to ansible.cfg, the file is referenced by name forever. |
Example: adopt wrkdev (operator workstation) #
poetry run ansible-playbook playbooks/adopt.yml \
-i inventories/prd \
-K \
-e adoption_targets=wrkdev \
-e adoption_user=pvginkel \
-e adoption_known_hosts_file=dev
(adoption_known_hosts_file=dev writes files/known_hosts.d/dev — the basename is independent of inventory naming and stays as-is for backwards compatibility with ansible.cfg's UserKnownHostsFile.)
Example: adopt the three PVE nodes (prd inventory) #
poetry run ansible-playbook playbooks/adopt.yml \
-i inventories/prd \
-e adoption_targets=proxmox \
-e adoption_user=root \
-e adoption_known_hosts_file=proxmox
(No -K — root needs no sudo password. The pve-root key must be loaded in ssh-agent.)
The PVE adoption order is operator's choice; the cluster keeps working through it. Default suggestion: pick a non-master node first (pve2), confirm site.yml --check against it, then the second non-master (pve1), then the master (pve).
Wire the new file into ansible.cfg #
After adopt.yml succeeds, edit ansible/ansible.cfg's ssh_args and add the new file to UserKnownHostsFile. The option takes a space-separated list:
-o "UserKnownHostsFile=files/known_hosts.d/scratch files/known_hosts.d/<basename>"
Commit ansible.cfg and the new files/known_hosts.d/<basename> together. From that commit onward, site.yml against the host works without the override that adopt.yml itself uses.
Verify #
poetry run ansible-playbook playbooks/site.yml -i inventories/<inv> --limit <host> --check
A second run should report changed=0. If --check flags drift, that's expected on first contact — apply for real:
poetry run ansible-playbook playbooks/site.yml -i inventories/<inv> --limit <host>
One-time post-adoption step #
The bootstrap role does not set a password for pvginkel, so interactive sudo does not work yet. Set one (per ansible/roles/bootstrap/README.md):
ssh ansible@<host> sudo passwd pvginkel
When things go wrong #
ssh-keyscan returned no ed25519 host key: target serves only RSA/ECDSA. Add an ed25519 key (ssh-keygen -Aon the target generates the missing types) and rerun.- Play 2 fails with host-key verification error: the line written by Play 1 doesn't match what SSH sees on connect. Inspect
files/known_hosts.d/<basename>— the line should be<host>,<host>.home ssh-ed25519 <keydata>. Verify the target hasn't rotated its key between the keyscan and the connect. Permission deniedon Play 2: connection user can't sudo. With-u pvginkel, ensure-Kis set and the password is right. With-u root, confirmssh-add -llists the key and the agent is being forwarded.adoption_targets resolved to no hosts: pattern didn't match anything in inventory. Runansible-inventory -i inventories/<inv> --graphto confirm the host or group exists.