incus-contrib/ansible/playbooks/post-deploy.yml

203 lines
7.4 KiB
YAML

---
# post-deploy.yml — Runs after Aether creates an instance
#
# Aether passes these extra vars (ffsdn_ prefix):
# ffsdn_instance_name — instance name
# ffsdn_instance_ip — IP address assigned by Aether
# ffsdn_cluster_id — numeric cluster ID
# ffsdn_cluster_name — cluster display name
# ffsdn_deployed_by — Aether username who triggered the deploy
# ffsdn_image_os — image OS (e.g., "Debian")
# ffsdn_image_release — image release (e.g., "bookworm")
# ffsdn_image_alias — image alias (may be empty)
#
# This playbook configures new instances via the Incus REST API
# (file push + exec), which works regardless of network topology.
# AWX does NOT need SSH access to the container — it talks to the
# Incus cluster API at the node level.
#
# Requirements:
# - Incus client cert/key pushed to AWX task pod at
# /var/lib/awx/projects/incus-contrib/ (mounted as /runner/project/
# inside the EE container during job execution)
# - Cluster API reachable from AWX (e.g., https://192.168.102.140:8443)
- name: Post-deploy — configure instance via Incus API
hosts: localhost
gather_facts: false
connection: local
vars:
# Incus cluster API (any cluster member works)
incus_api: "https://10.10.0.111:8443"
# AWX runner mounts project at /runner/project/ during job execution
incus_cert: "/runner/project/incus-client.crt"
incus_key: "/runner/project/incus-client.key"
base_packages: "curl vim htop jq tmux rsync unattended-upgrades"
tasks:
- name: Validate required variables
ansible.builtin.assert:
that:
- ffsdn_instance_name is defined
- ffsdn_instance_name | length > 0
fail_msg: "ffsdn_instance_name must be provided by Aether"
- name: Wait for instance to be running (up to 60s)
ansible.builtin.uri:
url: "{{ incus_api }}/1.0/instances/{{ ffsdn_instance_name }}/state"
method: GET
client_cert: "{{ incus_cert }}"
client_key: "{{ incus_key }}"
validate_certs: false
return_content: true
register: instance_state
until: instance_state.json.metadata.status == "Running"
retries: 12
delay: 5
- name: Generate setup script
ansible.builtin.set_fact:
setup_script: |
#!/bin/bash
set -e
echo "=== Post-deploy configuration starting ==="
# Set hostname
hostname {{ ffsdn_instance_name }}
echo "{{ ffsdn_instance_name }}" > /etc/hostname
sed -i '/^127\.0\.1\.1/d' /etc/hosts
echo "127.0.1.1 {{ ffsdn_instance_name }}" >> /etc/hosts
echo "[OK] Hostname set to {{ ffsdn_instance_name }}"
# Update package cache and install base packages
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq
apt-get install -y -qq {{ base_packages }} 2>&1 | tail -1
echo "[OK] Base packages installed"
# Set timezone
if command -v timedatectl >/dev/null 2>&1; then
timedatectl set-timezone UTC
else
ln -sf /usr/share/zoneinfo/UTC /etc/localtime
echo "UTC" > /etc/timezone
fi
echo "[OK] Timezone set to UTC"
# Configure unattended upgrades
cat > /etc/apt/apt.conf.d/20auto-upgrades << 'AUTOUPGRADE'
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";
AUTOUPGRADE
echo "[OK] Unattended upgrades configured"
# Write deployment metadata
cat > /etc/deploy-info << DEPLOYINFO
hostname={{ ffsdn_instance_name }}
ip={{ ffsdn_instance_ip | default('unknown') }}
cluster={{ ffsdn_cluster_name | default('unknown') }}
deployed_by={{ ffsdn_deployed_by | default('unknown') }}
image={{ ffsdn_image_os | default('') }}/{{ ffsdn_image_release | default('') }}
deployed=$(date -Iseconds)
DEPLOYINFO
echo "[OK] Deployment metadata written to /etc/deploy-info"
echo "=== Post-deploy configuration complete ==="
- name: Push setup script to instance
ansible.builtin.uri:
url: "{{ incus_api }}/1.0/instances/{{ ffsdn_instance_name }}/files?path=/tmp/post-deploy-setup.sh"
method: POST
client_cert: "{{ incus_cert }}"
client_key: "{{ incus_key }}"
validate_certs: false
body: "{{ setup_script }}"
headers:
Content-Type: "application/octet-stream"
X-Incus-type: "file"
X-Incus-mode: "0755"
status_code: [200]
- name: Execute setup script via Incus API
ansible.builtin.uri:
url: "{{ incus_api }}/1.0/instances/{{ ffsdn_instance_name }}/exec"
method: POST
client_cert: "{{ incus_cert }}"
client_key: "{{ incus_key }}"
validate_certs: false
body_format: json
body:
command: ["/bin/bash", "/tmp/post-deploy-setup.sh"]
record-output: true
interactive: false
wait-for-websocket: false
environment:
DEBIAN_FRONTEND: "noninteractive"
status_code: [202]
return_content: true
register: exec_result
- name: Wait for setup script to complete (up to 5 minutes)
ansible.builtin.uri:
url: "{{ incus_api }}{{ exec_result.json.operation }}/wait?timeout=300"
method: GET
client_cert: "{{ incus_cert }}"
client_key: "{{ incus_key }}"
validate_certs: false
return_content: true
register: exec_wait
failed_when: exec_wait.json.metadata.status != "Success"
- name: Verify exit code
ansible.builtin.assert:
that:
- exec_wait.json.metadata.metadata.return | int == 0
fail_msg: "Setup script exited with code {{ exec_wait.json.metadata.metadata.return }}"
success_msg: "Setup completed successfully (exit code 0)"
- name: Verify deployment metadata was written
ansible.builtin.uri:
url: "{{ incus_api }}/1.0/instances/{{ ffsdn_instance_name }}/files?path=/etc/deploy-info"
method: GET
client_cert: "{{ incus_cert }}"
client_key: "{{ incus_key }}"
validate_certs: false
return_content: true
register: deploy_info
- name: Display deployment metadata
ansible.builtin.debug:
msg: "{{ deploy_info.content }}"
- name: Clean up setup script
ansible.builtin.uri:
url: "{{ incus_api }}/1.0/instances/{{ ffsdn_instance_name }}/exec"
method: POST
client_cert: "{{ incus_cert }}"
client_key: "{{ incus_key }}"
validate_certs: false
body_format: json
body:
command: ["/bin/rm", "-f", "/tmp/post-deploy-setup.sh"]
record-output: false
interactive: false
wait-for-websocket: false
status_code: [202]
ignore_errors: true
- name: Record deployment in ledger
ansible.builtin.lineinfile:
path: /tmp/awx-deploy-ledger.log
line: >-
{{ lookup('pipe', 'date -Iseconds') }}
DEPLOY instance={{ ffsdn_instance_name | default('unknown') }}
ip={{ ffsdn_instance_ip | default('') }}
cluster={{ ffsdn_cluster_name | default('unknown') }}
deployed_by={{ ffsdn_deployed_by | default('unknown') }}
image={{ ffsdn_image_os | default('') }}/{{ ffsdn_image_release | default('') }}
status=success
create: true
mode: "0644"