--- # 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://192.168.102.140: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"