Add AWX showcase blueprints: webapp, staticsite, k3scluster
Three end-to-end blueprint examples demonstrating AWX configuring real applications via Aether lifecycle hooks: - webapp: nginx+PHP web tier + MariaDB with cross-instance service discovery, credential sharing, and OVN ACL management - staticsite: horizontally scaled nginx with per-instance branding - k3scluster: K3s control plane + workers with automatic token discovery and cluster join (fixes Incus files API symlink issue where node-token symlink returns target path instead of content) Includes dispatcher playbooks, shared helpers (sibling discovery, temporary internet NIC), decommission handlers, and deploy-awx changes to push showcase playbooks and create AWX templates. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
5ee32fe08c
commit
a526c4ea9f
|
|
@ -0,0 +1,29 @@
|
||||||
|
---
|
||||||
|
# k3scluster-decommission.yml — Decommission k3scluster blueprint instances
|
||||||
|
#
|
||||||
|
# Aether sends ffsdn_instances (a list of all instances in the deployment).
|
||||||
|
# This playbook loops over each instance and routes to K3s uninstall.
|
||||||
|
|
||||||
|
- name: K3scluster decommission — graceful cleanup via Incus API
|
||||||
|
hosts: localhost
|
||||||
|
gather_facts: false
|
||||||
|
connection: local
|
||||||
|
|
||||||
|
vars:
|
||||||
|
incus_api: "https://192.168.102.140:8443"
|
||||||
|
incus_cert: "/runner/project/incus-client.crt"
|
||||||
|
incus_key: "/runner/project/incus-client.key"
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
- name: Validate required variables
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- ffsdn_instances is defined
|
||||||
|
- ffsdn_instances | length > 0
|
||||||
|
fail_msg: "ffsdn_instances must be provided by Aether"
|
||||||
|
|
||||||
|
- name: Decommission each instance
|
||||||
|
ansible.builtin.include_tasks: showcase/decommission/k3scluster-instance.yml
|
||||||
|
loop: "{{ ffsdn_instances }}"
|
||||||
|
loop_control:
|
||||||
|
loop_var: instance
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
---
|
||||||
|
# k3scluster-post-deploy.yml — Configure k3scluster blueprint instances
|
||||||
|
#
|
||||||
|
# Aether sends ffsdn_instances (a list of all instances in the deployment).
|
||||||
|
# This playbook loops over each instance and dispatches to control or worker tasks.
|
||||||
|
|
||||||
|
- name: K3scluster post-deploy — configure instances via Incus API
|
||||||
|
hosts: localhost
|
||||||
|
gather_facts: false
|
||||||
|
connection: local
|
||||||
|
|
||||||
|
vars:
|
||||||
|
incus_api: "https://192.168.102.140:8443"
|
||||||
|
incus_cert: "/runner/project/incus-client.crt"
|
||||||
|
incus_key: "/runner/project/incus-client.key"
|
||||||
|
base_packages: "curl vim htop jq"
|
||||||
|
blueprint_name: "k3scluster"
|
||||||
|
valid_components: ["control", "worker"]
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
- name: Validate required variables
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- ffsdn_instances is defined
|
||||||
|
- ffsdn_instances | length > 0
|
||||||
|
fail_msg: "ffsdn_instances must be provided by Aether"
|
||||||
|
|
||||||
|
- name: Configure each instance
|
||||||
|
ansible.builtin.include_tasks: showcase/k3scluster-deploy-instance.yml
|
||||||
|
loop: "{{ ffsdn_instances }}"
|
||||||
|
loop_control:
|
||||||
|
loop_var: instance
|
||||||
|
|
@ -0,0 +1,231 @@
|
||||||
|
---
|
||||||
|
# showcase/common.yml — Common setup tasks for all showcase instances
|
||||||
|
#
|
||||||
|
# Included by showcase-post-deploy.yml after name parsing.
|
||||||
|
# Uses vars from the parent play: incus_api, incus_cert, incus_key,
|
||||||
|
# base_packages, ffsdn_*, blueprint_name, component_name.
|
||||||
|
#
|
||||||
|
# Adds a temporary incusbr0 NIC for internet access during package
|
||||||
|
# installation (OVN SNAT may not provide outbound internet in all labs),
|
||||||
|
# then the deploy-instance playbook removes it after setup completes.
|
||||||
|
|
||||||
|
# ── Step 1: Add temporary internet NIC ──────────────────────────────
|
||||||
|
|
||||||
|
- name: Add temporary incusbr0 NIC for internet access
|
||||||
|
ansible.builtin.uri:
|
||||||
|
url: "{{ incus_api }}/1.0/instances/{{ ffsdn_instance_name }}"
|
||||||
|
method: PATCH
|
||||||
|
client_cert: "{{ incus_cert }}"
|
||||||
|
client_key: "{{ incus_key }}"
|
||||||
|
validate_certs: false
|
||||||
|
body_format: json
|
||||||
|
body:
|
||||||
|
devices:
|
||||||
|
eth-setup:
|
||||||
|
type: nic
|
||||||
|
network: incusbr0
|
||||||
|
name: eth1
|
||||||
|
status_code: [200, 202]
|
||||||
|
return_content: true
|
||||||
|
register: nic_add_result
|
||||||
|
|
||||||
|
- name: Wait for NIC addition if async
|
||||||
|
ansible.builtin.uri:
|
||||||
|
url: "{{ incus_api }}{{ nic_add_result.json.operation }}/wait?timeout=30"
|
||||||
|
method: GET
|
||||||
|
client_cert: "{{ incus_cert }}"
|
||||||
|
client_key: "{{ incus_key }}"
|
||||||
|
validate_certs: false
|
||||||
|
return_content: true
|
||||||
|
when: nic_add_result.status == 202
|
||||||
|
|
||||||
|
- name: Push NIC activation script
|
||||||
|
ansible.builtin.uri:
|
||||||
|
url: "{{ incus_api }}/1.0/instances/{{ ffsdn_instance_name }}/files?path=/tmp/activate-eth1.sh"
|
||||||
|
method: POST
|
||||||
|
client_cert: "{{ incus_cert }}"
|
||||||
|
client_key: "{{ incus_key }}"
|
||||||
|
validate_certs: false
|
||||||
|
body: |
|
||||||
|
#!/bin/bash
|
||||||
|
# Configure eth1 for DHCP via systemd-networkd
|
||||||
|
mkdir -p /etc/systemd/network
|
||||||
|
cat > /etc/systemd/network/80-eth1.network << 'EOF'
|
||||||
|
[Match]
|
||||||
|
Name=eth1
|
||||||
|
[Network]
|
||||||
|
DHCP=yes
|
||||||
|
[DHCPv4]
|
||||||
|
RouteMetric=100
|
||||||
|
EOF
|
||||||
|
ip link set eth1 up
|
||||||
|
systemctl restart systemd-networkd
|
||||||
|
# Wait up to 15s for IPv4 address
|
||||||
|
for i in $(seq 1 15); do
|
||||||
|
if ip -4 addr show eth1 | grep -q 'inet '; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
# Last resort: try dhclient
|
||||||
|
dhclient eth1 2>/dev/null || true
|
||||||
|
sleep 2
|
||||||
|
headers:
|
||||||
|
Content-Type: "application/octet-stream"
|
||||||
|
X-Incus-type: "file"
|
||||||
|
X-Incus-mode: "0755"
|
||||||
|
status_code: [200]
|
||||||
|
|
||||||
|
- name: Activate temporary NIC inside container
|
||||||
|
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/activate-eth1.sh"]
|
||||||
|
record-output: true
|
||||||
|
interactive: false
|
||||||
|
wait-for-websocket: false
|
||||||
|
status_code: [202]
|
||||||
|
return_content: true
|
||||||
|
register: nic_activate_exec
|
||||||
|
|
||||||
|
- name: Wait for NIC activation (up to 30s)
|
||||||
|
ansible.builtin.uri:
|
||||||
|
url: "{{ incus_api }}{{ nic_activate_exec.json.operation }}/wait?timeout=30"
|
||||||
|
method: GET
|
||||||
|
client_cert: "{{ incus_cert }}"
|
||||||
|
client_key: "{{ incus_key }}"
|
||||||
|
validate_certs: false
|
||||||
|
return_content: true
|
||||||
|
|
||||||
|
- name: Wait for DHCP on temporary NIC (up to 30s)
|
||||||
|
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: nic_state
|
||||||
|
until: >-
|
||||||
|
nic_state.json.metadata.network is defined and
|
||||||
|
nic_state.json.metadata.network.eth1 is defined and
|
||||||
|
nic_state.json.metadata.network.eth1.addresses | selectattr('family', 'equalto', 'inet') | list | length > 0
|
||||||
|
retries: 6
|
||||||
|
delay: 5
|
||||||
|
|
||||||
|
# ── Step 2: Run base setup with internet access ────────────────────
|
||||||
|
|
||||||
|
- name: Generate base setup script
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
base_setup_script: |
|
||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "=== Showcase base setup starting ==="
|
||||||
|
echo "Blueprint: {{ blueprint_name }}, Component: {{ component_name }}"
|
||||||
|
|
||||||
|
# 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
|
||||||
|
ln -sf /usr/share/zoneinfo/UTC /etc/localtime
|
||||||
|
echo "UTC" > /etc/timezone
|
||||||
|
echo "[OK] Timezone set to UTC"
|
||||||
|
|
||||||
|
# Write deployment metadata
|
||||||
|
cat > /etc/deploy-info << 'DEPLOYINFO'
|
||||||
|
blueprint={{ blueprint_name }}
|
||||||
|
component={{ component_name }}
|
||||||
|
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('') }}
|
||||||
|
DEPLOYINFO
|
||||||
|
echo "deployed=$(date -Iseconds)" >> /etc/deploy-info
|
||||||
|
echo "[OK] Deployment metadata written"
|
||||||
|
|
||||||
|
echo "=== Showcase base setup complete ==="
|
||||||
|
|
||||||
|
- name: Push base setup script to instance
|
||||||
|
ansible.builtin.uri:
|
||||||
|
url: "{{ incus_api }}/1.0/instances/{{ ffsdn_instance_name }}/files?path=/tmp/showcase-setup.sh"
|
||||||
|
method: POST
|
||||||
|
client_cert: "{{ incus_cert }}"
|
||||||
|
client_key: "{{ incus_key }}"
|
||||||
|
validate_certs: false
|
||||||
|
body: "{{ base_setup_script }}"
|
||||||
|
headers:
|
||||||
|
Content-Type: "application/octet-stream"
|
||||||
|
X-Incus-type: "file"
|
||||||
|
X-Incus-mode: "0755"
|
||||||
|
status_code: [200]
|
||||||
|
|
||||||
|
- name: Execute base 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/bash", "/tmp/showcase-setup.sh"]
|
||||||
|
record-output: true
|
||||||
|
interactive: false
|
||||||
|
wait-for-websocket: false
|
||||||
|
environment:
|
||||||
|
DEBIAN_FRONTEND: "noninteractive"
|
||||||
|
status_code: [202]
|
||||||
|
return_content: true
|
||||||
|
register: base_setup_exec
|
||||||
|
|
||||||
|
- name: Wait for base setup to complete (up to 3 minutes)
|
||||||
|
ansible.builtin.uri:
|
||||||
|
url: "{{ incus_api }}{{ base_setup_exec.json.operation }}/wait?timeout=180"
|
||||||
|
method: GET
|
||||||
|
client_cert: "{{ incus_cert }}"
|
||||||
|
client_key: "{{ incus_key }}"
|
||||||
|
validate_certs: false
|
||||||
|
return_content: true
|
||||||
|
timeout: 200
|
||||||
|
register: base_setup_wait
|
||||||
|
failed_when: base_setup_wait.json.metadata.status != "Success"
|
||||||
|
|
||||||
|
- name: Verify base setup exit code
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- base_setup_wait.json.metadata.metadata.return | int == 0
|
||||||
|
fail_msg: "Base setup exited with code {{ base_setup_wait.json.metadata.metadata.return }}"
|
||||||
|
success_msg: "Base setup completed successfully"
|
||||||
|
|
||||||
|
- name: Clean up base 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/showcase-setup.sh"]
|
||||||
|
record-output: false
|
||||||
|
interactive: false
|
||||||
|
wait-for-websocket: false
|
||||||
|
status_code: [202]
|
||||||
|
ignore_errors: true
|
||||||
|
|
@ -0,0 +1,87 @@
|
||||||
|
---
|
||||||
|
# showcase/decommission/k3s.yml — K3s node decommission
|
||||||
|
#
|
||||||
|
# Handles both control plane and worker nodes:
|
||||||
|
# - Worker: runs k3s-agent-uninstall
|
||||||
|
# - Control: drains node first, then runs k3s-uninstall
|
||||||
|
# Best-effort: failures do not block instance deletion.
|
||||||
|
|
||||||
|
- name: Determine K3s role from component name
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
k3s_role: "{{ component_name | default('worker') }}"
|
||||||
|
|
||||||
|
- name: Uninstall K3s agent (worker node)
|
||||||
|
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"
|
||||||
|
- "-c"
|
||||||
|
- |
|
||||||
|
echo "Uninstalling K3s agent..."
|
||||||
|
if [ -f /usr/local/bin/k3s-agent-uninstall.sh ]; then
|
||||||
|
/usr/local/bin/k3s-agent-uninstall.sh
|
||||||
|
echo "K3s agent uninstalled"
|
||||||
|
else
|
||||||
|
echo "K3s agent uninstall script not found — skipping"
|
||||||
|
fi
|
||||||
|
record-output: true
|
||||||
|
interactive: false
|
||||||
|
wait-for-websocket: false
|
||||||
|
status_code: [202]
|
||||||
|
return_content: true
|
||||||
|
register: k3s_worker_uninstall
|
||||||
|
ignore_errors: true
|
||||||
|
when: k3s_role == "worker"
|
||||||
|
|
||||||
|
- name: Uninstall K3s server (control plane)
|
||||||
|
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"
|
||||||
|
- "-c"
|
||||||
|
- |
|
||||||
|
echo "Uninstalling K3s server..."
|
||||||
|
if [ -f /usr/local/bin/k3s-uninstall.sh ]; then
|
||||||
|
/usr/local/bin/k3s-uninstall.sh
|
||||||
|
echo "K3s server uninstalled"
|
||||||
|
else
|
||||||
|
echo "K3s server uninstall script not found — skipping"
|
||||||
|
fi
|
||||||
|
record-output: true
|
||||||
|
interactive: false
|
||||||
|
wait-for-websocket: false
|
||||||
|
status_code: [202]
|
||||||
|
return_content: true
|
||||||
|
register: k3s_server_uninstall
|
||||||
|
ignore_errors: true
|
||||||
|
when: k3s_role == "control"
|
||||||
|
|
||||||
|
- name: Wait for K3s uninstall (up to 60s)
|
||||||
|
ansible.builtin.uri:
|
||||||
|
url: "{{ incus_api }}{{ (k3s_worker_uninstall.json.operation if k3s_role == 'worker' else k3s_server_uninstall.json.operation) }}/wait?timeout=60"
|
||||||
|
method: GET
|
||||||
|
client_cert: "{{ incus_cert }}"
|
||||||
|
client_key: "{{ incus_key }}"
|
||||||
|
validate_certs: false
|
||||||
|
return_content: true
|
||||||
|
timeout: 80
|
||||||
|
ignore_errors: true
|
||||||
|
when: >-
|
||||||
|
(k3s_role == 'worker' and k3s_worker_uninstall is not failed) or
|
||||||
|
(k3s_role == 'control' and k3s_server_uninstall is not failed)
|
||||||
|
|
||||||
|
- name: Report K3s decommission
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg: "K3s {{ k3s_role }} node {{ ffsdn_instance_name }} decommissioned"
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
---
|
||||||
|
# showcase/decommission/k3scluster-instance.yml — Per-instance decommission for k3scluster
|
||||||
|
#
|
||||||
|
# Called from k3scluster-decommission.yml in a loop over ffsdn_instances.
|
||||||
|
# The 'instance' loop var has: name, ip, component_name, image_alias, type
|
||||||
|
# Routes to k3s.yml for both control plane and worker nodes.
|
||||||
|
|
||||||
|
- name: Set instance facts from loop variable
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
ffsdn_instance_name: "{{ instance.name }}"
|
||||||
|
component_name: "{{ instance.component_name }}"
|
||||||
|
|
||||||
|
- name: "Check if {{ ffsdn_instance_name }} still exists"
|
||||||
|
ansible.builtin.uri:
|
||||||
|
url: "{{ incus_api }}/1.0/instances/{{ ffsdn_instance_name }}"
|
||||||
|
method: GET
|
||||||
|
client_cert: "{{ incus_cert }}"
|
||||||
|
client_key: "{{ incus_key }}"
|
||||||
|
validate_certs: false
|
||||||
|
return_content: true
|
||||||
|
status_code: [200, 404]
|
||||||
|
register: instance_check
|
||||||
|
|
||||||
|
- name: "Route to K3s decommission for {{ ffsdn_instance_name }}"
|
||||||
|
ansible.builtin.include_tasks: k3s.yml
|
||||||
|
when:
|
||||||
|
- instance_check.status == 200
|
||||||
|
- component_name in ['control', 'worker']
|
||||||
|
- instance_check.json.metadata.status | default('') == "Running"
|
||||||
|
ignore_errors: true
|
||||||
|
|
||||||
|
- name: Record decommission in ledger
|
||||||
|
ansible.builtin.lineinfile:
|
||||||
|
path: /tmp/awx-deploy-ledger.log
|
||||||
|
line: >-
|
||||||
|
{{ lookup('pipe', 'date -Iseconds') }}
|
||||||
|
SHOWCASE-DECOMMISSION instance={{ ffsdn_instance_name }}
|
||||||
|
blueprint=k3scluster component={{ component_name | default('unknown') }}
|
||||||
|
status=completed
|
||||||
|
create: true
|
||||||
|
mode: "0644"
|
||||||
|
when: instance_check.status == 200
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
---
|
||||||
|
# showcase/decommission/staticsite-instance.yml — Per-instance decommission for staticsite
|
||||||
|
#
|
||||||
|
# Called from staticsite-decommission.yml in a loop over ffsdn_instances.
|
||||||
|
# The 'instance' loop var has: name, ip, component_name, image_alias, type
|
||||||
|
# No special cleanup needed — just stop nginx gracefully.
|
||||||
|
|
||||||
|
- name: Set instance facts from loop variable
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
ffsdn_instance_name: "{{ instance.name }}"
|
||||||
|
component_name: "{{ instance.component_name }}"
|
||||||
|
|
||||||
|
- name: "Check if {{ ffsdn_instance_name }} still exists"
|
||||||
|
ansible.builtin.uri:
|
||||||
|
url: "{{ incus_api }}/1.0/instances/{{ ffsdn_instance_name }}"
|
||||||
|
method: GET
|
||||||
|
client_cert: "{{ incus_cert }}"
|
||||||
|
client_key: "{{ incus_key }}"
|
||||||
|
validate_certs: false
|
||||||
|
return_content: true
|
||||||
|
status_code: [200, 404]
|
||||||
|
register: instance_check
|
||||||
|
|
||||||
|
- name: "Stop nginx gracefully on {{ ffsdn_instance_name }}"
|
||||||
|
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"
|
||||||
|
- "-c"
|
||||||
|
- "systemctl stop nginx 2>/dev/null || true"
|
||||||
|
record-output: false
|
||||||
|
interactive: false
|
||||||
|
wait-for-websocket: false
|
||||||
|
status_code: [202]
|
||||||
|
ignore_errors: true
|
||||||
|
when:
|
||||||
|
- instance_check.status == 200
|
||||||
|
- instance_check.json.metadata.status | default('') == "Running"
|
||||||
|
|
||||||
|
- name: Record decommission in ledger
|
||||||
|
ansible.builtin.lineinfile:
|
||||||
|
path: /tmp/awx-deploy-ledger.log
|
||||||
|
line: >-
|
||||||
|
{{ lookup('pipe', 'date -Iseconds') }}
|
||||||
|
SHOWCASE-DECOMMISSION instance={{ ffsdn_instance_name }}
|
||||||
|
blueprint=staticsite component={{ component_name }}
|
||||||
|
status=completed
|
||||||
|
create: true
|
||||||
|
mode: "0644"
|
||||||
|
when: instance_check.status == 200
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
---
|
||||||
|
# showcase/decommission/webapp-db.yml — Graceful MariaDB shutdown
|
||||||
|
#
|
||||||
|
# Best-effort: failures do not block instance deletion.
|
||||||
|
|
||||||
|
- name: Stop MariaDB gracefully
|
||||||
|
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"
|
||||||
|
- "-c"
|
||||||
|
- |
|
||||||
|
echo "Stopping MariaDB gracefully..."
|
||||||
|
systemctl stop mariadb 2>/dev/null || true
|
||||||
|
echo "MariaDB stopped"
|
||||||
|
record-output: true
|
||||||
|
interactive: false
|
||||||
|
wait-for-websocket: false
|
||||||
|
status_code: [202]
|
||||||
|
return_content: true
|
||||||
|
register: db_stop_exec
|
||||||
|
ignore_errors: true
|
||||||
|
|
||||||
|
- name: Wait for MariaDB stop (up to 30s)
|
||||||
|
ansible.builtin.uri:
|
||||||
|
url: "{{ incus_api }}{{ db_stop_exec.json.operation }}/wait?timeout=30"
|
||||||
|
method: GET
|
||||||
|
client_cert: "{{ incus_cert }}"
|
||||||
|
client_key: "{{ incus_key }}"
|
||||||
|
validate_certs: false
|
||||||
|
return_content: true
|
||||||
|
ignore_errors: true
|
||||||
|
when: db_stop_exec is not failed
|
||||||
|
|
||||||
|
- name: Report MariaDB decommission
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg: "MariaDB on {{ ffsdn_instance_name }} stopped gracefully"
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
---
|
||||||
|
# showcase/decommission/webapp-instance.yml — Per-instance decommission for webapp
|
||||||
|
#
|
||||||
|
# Called from webapp-decommission.yml in a loop over ffsdn_instances.
|
||||||
|
# The 'instance' loop var has: name, ip, component_name, image_alias, type
|
||||||
|
# Routes to webapp-db.yml or webapp-web.yml based on component_name.
|
||||||
|
|
||||||
|
- name: Set instance facts from loop variable
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
ffsdn_instance_name: "{{ instance.name }}"
|
||||||
|
component_name: "{{ instance.component_name }}"
|
||||||
|
|
||||||
|
- name: "Check if {{ ffsdn_instance_name }} still exists"
|
||||||
|
ansible.builtin.uri:
|
||||||
|
url: "{{ incus_api }}/1.0/instances/{{ ffsdn_instance_name }}"
|
||||||
|
method: GET
|
||||||
|
client_cert: "{{ incus_cert }}"
|
||||||
|
client_key: "{{ incus_key }}"
|
||||||
|
validate_certs: false
|
||||||
|
return_content: true
|
||||||
|
status_code: [200, 404]
|
||||||
|
register: instance_check
|
||||||
|
|
||||||
|
- name: "Route to component-specific decommission for {{ ffsdn_instance_name }}"
|
||||||
|
ansible.builtin.include_tasks: "webapp-{{ component_name }}.yml"
|
||||||
|
when:
|
||||||
|
- instance_check.status == 200
|
||||||
|
- component_name in decommission_tasks
|
||||||
|
- instance_check.json.metadata.status | default('') == "Running"
|
||||||
|
ignore_errors: true
|
||||||
|
|
||||||
|
- name: Record decommission in ledger
|
||||||
|
ansible.builtin.lineinfile:
|
||||||
|
path: /tmp/awx-deploy-ledger.log
|
||||||
|
line: >-
|
||||||
|
{{ lookup('pipe', 'date -Iseconds') }}
|
||||||
|
SHOWCASE-DECOMMISSION instance={{ ffsdn_instance_name }}
|
||||||
|
blueprint=webapp component={{ component_name | default('unknown') }}
|
||||||
|
status=completed
|
||||||
|
create: true
|
||||||
|
mode: "0644"
|
||||||
|
when: instance_check.status == 200
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
---
|
||||||
|
# showcase/decommission/webapp-web.yml — Graceful nginx shutdown + ACL cleanup
|
||||||
|
#
|
||||||
|
# Stops nginx and removes this web instance's ACL rule from the db ACL.
|
||||||
|
# Best-effort: failures do not block instance deletion.
|
||||||
|
|
||||||
|
- name: Stop nginx gracefully
|
||||||
|
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"
|
||||||
|
- "-c"
|
||||||
|
- |
|
||||||
|
systemctl stop nginx 2>/dev/null || true
|
||||||
|
systemctl stop php*-fpm 2>/dev/null || true
|
||||||
|
record-output: false
|
||||||
|
interactive: false
|
||||||
|
wait-for-websocket: false
|
||||||
|
status_code: [202]
|
||||||
|
ignore_errors: true
|
||||||
|
|
||||||
|
# --- Clean up ACL rule from db (best-effort) ---
|
||||||
|
|
||||||
|
- name: Discover database instances for ACL cleanup
|
||||||
|
ansible.builtin.include_tasks: showcase/helpers.yml
|
||||||
|
vars:
|
||||||
|
discovery_prefix: "webapp-db-"
|
||||||
|
ignore_errors: true
|
||||||
|
|
||||||
|
- name: Remove ACL rule from db
|
||||||
|
when:
|
||||||
|
- discovered_siblings is defined
|
||||||
|
- discovered_siblings | length > 0
|
||||||
|
block:
|
||||||
|
- name: Build db ACL name for cleanup
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
db_acl_name: "52-{{ discovered_siblings[0].name }}-aether-acl"
|
||||||
|
|
||||||
|
- name: Read current db ACL for cleanup
|
||||||
|
ansible.builtin.uri:
|
||||||
|
url: "{{ incus_api }}/1.0/network-acls/{{ db_acl_name }}"
|
||||||
|
method: GET
|
||||||
|
client_cert: "{{ incus_cert }}"
|
||||||
|
client_key: "{{ incus_key }}"
|
||||||
|
validate_certs: false
|
||||||
|
return_content: true
|
||||||
|
status_code: [200, 404]
|
||||||
|
register: db_acl_cleanup
|
||||||
|
|
||||||
|
- name: Filter out this instance's ACL rules
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
cleaned_ingress: >-
|
||||||
|
{{ db_acl_cleanup.json.metadata.ingress | default([])
|
||||||
|
| rejectattr('description', 'search', ffsdn_instance_name)
|
||||||
|
| list }}
|
||||||
|
when: db_acl_cleanup.status == 200
|
||||||
|
|
||||||
|
- name: Update db ACL (remove this web instance's rule)
|
||||||
|
ansible.builtin.uri:
|
||||||
|
url: "{{ incus_api }}/1.0/network-acls/{{ db_acl_name }}"
|
||||||
|
method: PATCH
|
||||||
|
client_cert: "{{ incus_cert }}"
|
||||||
|
client_key: "{{ incus_key }}"
|
||||||
|
validate_certs: false
|
||||||
|
body_format: json
|
||||||
|
body:
|
||||||
|
ingress: "{{ cleaned_ingress }}"
|
||||||
|
status_code: [200]
|
||||||
|
when:
|
||||||
|
- db_acl_cleanup.status == 200
|
||||||
|
- cleaned_ingress is defined
|
||||||
|
ignore_errors: true
|
||||||
|
|
||||||
|
- name: Report web decommission
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg: "Web instance {{ ffsdn_instance_name }} decommissioned (nginx stopped, ACL cleaned)"
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
---
|
||||||
|
# showcase/helpers.yml — Sibling instance discovery via Incus API
|
||||||
|
#
|
||||||
|
# Required input vars (set before include_tasks):
|
||||||
|
# discovery_prefix — name prefix to match (e.g., "webapp-db-")
|
||||||
|
#
|
||||||
|
# Output facts:
|
||||||
|
# discovered_siblings — list of dicts: [{name: "...", ip: "..."}]
|
||||||
|
|
||||||
|
- name: "Query all instances from Incus API"
|
||||||
|
ansible.builtin.uri:
|
||||||
|
url: "{{ incus_api }}/1.0/instances?recursion=1"
|
||||||
|
method: GET
|
||||||
|
client_cert: "{{ incus_cert }}"
|
||||||
|
client_key: "{{ incus_key }}"
|
||||||
|
validate_certs: false
|
||||||
|
return_content: true
|
||||||
|
register: all_instances_response
|
||||||
|
|
||||||
|
- name: "Find instances matching prefix '{{ discovery_prefix }}'"
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
matching_instance_names: >-
|
||||||
|
{{ all_instances_response.json.metadata
|
||||||
|
| selectattr('name', 'match', '^' + discovery_prefix)
|
||||||
|
| map(attribute='name')
|
||||||
|
| list }}
|
||||||
|
|
||||||
|
- name: "Report discovery results"
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg: "Found {{ matching_instance_names | length }} instances matching '{{ discovery_prefix }}': {{ matching_instance_names | join(', ') }}"
|
||||||
|
|
||||||
|
- name: "Get state for each matching instance"
|
||||||
|
ansible.builtin.uri:
|
||||||
|
url: "{{ incus_api }}/1.0/instances/{{ item }}/state"
|
||||||
|
method: GET
|
||||||
|
client_cert: "{{ incus_cert }}"
|
||||||
|
client_key: "{{ incus_key }}"
|
||||||
|
validate_certs: false
|
||||||
|
return_content: true
|
||||||
|
register: sibling_states
|
||||||
|
loop: "{{ matching_instance_names }}"
|
||||||
|
when: matching_instance_names | length > 0
|
||||||
|
|
||||||
|
- name: "Initialize discovered siblings"
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
discovered_siblings: []
|
||||||
|
|
||||||
|
- name: "Build discovered siblings list with IPs"
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
discovered_siblings: >-
|
||||||
|
{{ discovered_siblings + [{
|
||||||
|
'name': item.item,
|
||||||
|
'ip': (item.json.metadata.network.eth0.addresses | default([])
|
||||||
|
| selectattr('family', 'equalto', 'inet')
|
||||||
|
| selectattr('scope', 'equalto', 'global')
|
||||||
|
| map(attribute='address') | list | first | default(''))
|
||||||
|
}] }}
|
||||||
|
loop: "{{ sibling_states.results | default([]) }}"
|
||||||
|
when:
|
||||||
|
- item.json is defined
|
||||||
|
- item.json.metadata.network is defined
|
||||||
|
|
||||||
|
- name: "Display discovered siblings"
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg: "Discovered siblings: {{ discovered_siblings }}"
|
||||||
|
|
@ -0,0 +1,142 @@
|
||||||
|
---
|
||||||
|
# showcase/k3s-control.yml — K3s server (control plane) setup
|
||||||
|
#
|
||||||
|
# Installs K3s in server mode. The join token is stored at
|
||||||
|
# /var/lib/rancher/k3s/server/token for workers to read
|
||||||
|
# via the Incus API.
|
||||||
|
#
|
||||||
|
# Note: The Aether blueprint profile should include:
|
||||||
|
# security.nesting: "true"
|
||||||
|
# for K3s to manage pods and containers properly.
|
||||||
|
|
||||||
|
- name: Generate K3s server setup script
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
k3s_server_script: |
|
||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "=== K3s server setup starting ==="
|
||||||
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
|
# Container workaround: create /dev/kmsg if missing
|
||||||
|
[ -e /dev/kmsg ] || ln -s /dev/console /dev/kmsg
|
||||||
|
|
||||||
|
# Install prerequisites
|
||||||
|
apt-get install -y -qq curl 2>&1 | tail -1
|
||||||
|
|
||||||
|
# Install K3s server
|
||||||
|
echo "Installing K3s server (this takes 1-2 minutes)..."
|
||||||
|
curl -sfL https://get.k3s.io | INSTALL_K3S_EXEC="\
|
||||||
|
--write-kubeconfig-mode=644 \
|
||||||
|
--disable=traefik \
|
||||||
|
--snapshotter=native \
|
||||||
|
--kubelet-arg=feature-gates=KubeletInUserNamespace=true \
|
||||||
|
--node-ip={{ ffsdn_instance_ip | default('127.0.0.1') }} \
|
||||||
|
--tls-san={{ ffsdn_instance_ip | default('127.0.0.1') }}" sh -
|
||||||
|
echo "[OK] K3s installed"
|
||||||
|
|
||||||
|
# Wait for K3s to be ready
|
||||||
|
echo "Waiting for K3s node to be ready..."
|
||||||
|
export KUBECONFIG=/etc/rancher/k3s/k3s.yaml
|
||||||
|
for i in $(seq 1 60); do
|
||||||
|
if kubectl get nodes 2>/dev/null | grep -q ' Ready'; then
|
||||||
|
echo "[OK] K3s node ready"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 5
|
||||||
|
done
|
||||||
|
|
||||||
|
# Show node status
|
||||||
|
kubectl get nodes -o wide 2>/dev/null || true
|
||||||
|
|
||||||
|
# Verify token file exists
|
||||||
|
if [ -f /var/lib/rancher/k3s/server/node-token ]; then
|
||||||
|
echo "[OK] Join token stored at /var/lib/rancher/k3s/server/node-token"
|
||||||
|
else
|
||||||
|
echo "[ERROR] Join token not found"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "=== K3s server setup complete ==="
|
||||||
|
|
||||||
|
- name: Push K3s server setup script
|
||||||
|
ansible.builtin.uri:
|
||||||
|
url: "{{ incus_api }}/1.0/instances/{{ ffsdn_instance_name }}/files?path=/tmp/k3s-server-setup.sh"
|
||||||
|
method: POST
|
||||||
|
client_cert: "{{ incus_cert }}"
|
||||||
|
client_key: "{{ incus_key }}"
|
||||||
|
validate_certs: false
|
||||||
|
body: "{{ k3s_server_script }}"
|
||||||
|
headers:
|
||||||
|
Content-Type: "application/octet-stream"
|
||||||
|
X-Incus-type: "file"
|
||||||
|
X-Incus-mode: "0755"
|
||||||
|
status_code: [200]
|
||||||
|
|
||||||
|
- name: Execute K3s server 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/bash", "/tmp/k3s-server-setup.sh"]
|
||||||
|
record-output: true
|
||||||
|
interactive: false
|
||||||
|
wait-for-websocket: false
|
||||||
|
environment:
|
||||||
|
DEBIAN_FRONTEND: "noninteractive"
|
||||||
|
status_code: [202]
|
||||||
|
return_content: true
|
||||||
|
register: k3s_server_exec
|
||||||
|
|
||||||
|
- name: Wait for K3s server setup to complete (up to 10 minutes)
|
||||||
|
ansible.builtin.uri:
|
||||||
|
url: "{{ incus_api }}{{ k3s_server_exec.json.operation }}/wait?timeout=600"
|
||||||
|
method: GET
|
||||||
|
client_cert: "{{ incus_cert }}"
|
||||||
|
client_key: "{{ incus_key }}"
|
||||||
|
validate_certs: false
|
||||||
|
return_content: true
|
||||||
|
timeout: 620
|
||||||
|
register: k3s_server_wait
|
||||||
|
failed_when: k3s_server_wait.json.metadata.status != "Success"
|
||||||
|
|
||||||
|
- name: Verify K3s server setup exit code
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- k3s_server_wait.json.metadata.metadata.return | int == 0
|
||||||
|
fail_msg: "K3s server setup exited with code {{ k3s_server_wait.json.metadata.metadata.return }}"
|
||||||
|
success_msg: "K3s server setup completed — control plane ready"
|
||||||
|
|
||||||
|
- name: Verify join token is accessible
|
||||||
|
ansible.builtin.uri:
|
||||||
|
url: "{{ incus_api }}/1.0/instances/{{ ffsdn_instance_name }}/files?path=/var/lib/rancher/k3s/server/token"
|
||||||
|
method: GET
|
||||||
|
client_cert: "{{ incus_cert }}"
|
||||||
|
client_key: "{{ incus_key }}"
|
||||||
|
validate_certs: false
|
||||||
|
return_content: true
|
||||||
|
register: token_check
|
||||||
|
|
||||||
|
- name: Display K3s server info
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg: "K3s control plane ready at {{ ffsdn_instance_ip | default('unknown') }}:6443 — token available for workers"
|
||||||
|
|
||||||
|
- 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/k3s-server-setup.sh"]
|
||||||
|
record-output: false
|
||||||
|
interactive: false
|
||||||
|
wait-for-websocket: false
|
||||||
|
status_code: [202]
|
||||||
|
ignore_errors: true
|
||||||
|
|
@ -0,0 +1,345 @@
|
||||||
|
---
|
||||||
|
# showcase/k3s-worker.yml — K3s agent (worker node) setup
|
||||||
|
#
|
||||||
|
# Discovers the control plane instance via Incus API, reads the join
|
||||||
|
# token from the control instance, and installs K3s agent joining
|
||||||
|
# the discovered control plane.
|
||||||
|
#
|
||||||
|
# Note: The Aether blueprint profile should include:
|
||||||
|
# security.nesting: "true"
|
||||||
|
|
||||||
|
# --- Step 1: Find the control plane from the deployment manifest ---
|
||||||
|
|
||||||
|
- name: Find control plane instances from deployment
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
control_instances: >-
|
||||||
|
{{ ffsdn_instances | selectattr('component_name', 'equalto', 'control') | list }}
|
||||||
|
|
||||||
|
- name: Validate control plane found
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- control_instances | length > 0
|
||||||
|
fail_msg: >-
|
||||||
|
No control component found in ffsdn_instances. The blueprint must
|
||||||
|
include a control component for worker nodes to join.
|
||||||
|
|
||||||
|
- name: Select control plane instance
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
control_instance:
|
||||||
|
name: "{{ control_instances[0].name }}"
|
||||||
|
ip: "{{ control_instances[0].ip }}"
|
||||||
|
|
||||||
|
- name: Display control plane target
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg: "Joining control plane: {{ control_instance.name }} at {{ control_instance.ip }}"
|
||||||
|
|
||||||
|
# --- Step 2: Wait for and read the join token ---
|
||||||
|
|
||||||
|
- name: Wait for K3s join token to be available (up to 10 minutes)
|
||||||
|
ansible.builtin.uri:
|
||||||
|
url: "{{ incus_api }}/1.0/instances/{{ control_instance.name }}/files?path=/var/lib/rancher/k3s/server/token"
|
||||||
|
method: GET
|
||||||
|
client_cert: "{{ incus_cert }}"
|
||||||
|
client_key: "{{ incus_key }}"
|
||||||
|
validate_certs: false
|
||||||
|
return_content: true
|
||||||
|
status_code: [200, 404]
|
||||||
|
register: k3s_token_response
|
||||||
|
until: k3s_token_response.status == 200
|
||||||
|
retries: 60
|
||||||
|
delay: 10
|
||||||
|
|
||||||
|
- name: Extract join token
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
k3s_server_url: "https://{{ control_instance.ip }}:6443"
|
||||||
|
k3s_token: "{{ k3s_token_response.content | trim }}"
|
||||||
|
|
||||||
|
# --- Step 2b: Add ACL rules for K3s communication ---
|
||||||
|
|
||||||
|
- name: Build control ACL name
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
control_acl_name: "52-{{ control_instance.name }}-aether-acl"
|
||||||
|
|
||||||
|
- name: Read current control ACL
|
||||||
|
ansible.builtin.uri:
|
||||||
|
url: "{{ incus_api }}/1.0/network-acls/{{ control_acl_name }}"
|
||||||
|
method: GET
|
||||||
|
client_cert: "{{ incus_cert }}"
|
||||||
|
client_key: "{{ incus_key }}"
|
||||||
|
validate_certs: false
|
||||||
|
return_content: true
|
||||||
|
status_code: [200, 404]
|
||||||
|
register: control_acl_response
|
||||||
|
|
||||||
|
- name: Build updated control ingress rules (allow worker to reach K3s API)
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
updated_control_ingress: >-
|
||||||
|
{{ (control_acl_response.json.metadata.ingress | default([])) + [
|
||||||
|
{
|
||||||
|
'action': 'allow',
|
||||||
|
'state': 'enabled',
|
||||||
|
'source': (ffsdn_instance_ip | default('')) + '/32',
|
||||||
|
'destination': control_instance.ip + '/32',
|
||||||
|
'protocol': 'tcp',
|
||||||
|
'destination_port': '6443,10250',
|
||||||
|
'description': 'Allow ' + ffsdn_instance_name + ' to control K3s TCP ports'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'action': 'allow',
|
||||||
|
'state': 'enabled',
|
||||||
|
'source': (ffsdn_instance_ip | default('')) + '/32',
|
||||||
|
'destination': control_instance.ip + '/32',
|
||||||
|
'protocol': 'udp',
|
||||||
|
'destination_port': '8472',
|
||||||
|
'description': 'Allow ' + ffsdn_instance_name + ' to control VXLAN'
|
||||||
|
}
|
||||||
|
] }}
|
||||||
|
when: control_acl_response.status == 200
|
||||||
|
|
||||||
|
- name: Update control ACL with worker access rule
|
||||||
|
ansible.builtin.uri:
|
||||||
|
url: "{{ incus_api }}/1.0/network-acls/{{ control_acl_name }}"
|
||||||
|
method: PATCH
|
||||||
|
client_cert: "{{ incus_cert }}"
|
||||||
|
client_key: "{{ incus_key }}"
|
||||||
|
validate_certs: false
|
||||||
|
body_format: json
|
||||||
|
body:
|
||||||
|
ingress: "{{ updated_control_ingress }}"
|
||||||
|
status_code: [200]
|
||||||
|
when:
|
||||||
|
- control_acl_response.status == 200
|
||||||
|
- updated_control_ingress is defined
|
||||||
|
|
||||||
|
- name: Build worker ACL name
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
worker_acl_name: "52-{{ ffsdn_instance_name }}-aether-acl"
|
||||||
|
|
||||||
|
- name: Read current worker ACL
|
||||||
|
ansible.builtin.uri:
|
||||||
|
url: "{{ incus_api }}/1.0/network-acls/{{ worker_acl_name }}"
|
||||||
|
method: GET
|
||||||
|
client_cert: "{{ incus_cert }}"
|
||||||
|
client_key: "{{ incus_key }}"
|
||||||
|
validate_certs: false
|
||||||
|
return_content: true
|
||||||
|
status_code: [200, 404]
|
||||||
|
register: worker_acl_response
|
||||||
|
|
||||||
|
- name: Build updated worker ingress rules (allow control to reach kubelet)
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
updated_worker_ingress: >-
|
||||||
|
{{ (worker_acl_response.json.metadata.ingress | default([])) + [
|
||||||
|
{
|
||||||
|
'action': 'allow',
|
||||||
|
'state': 'enabled',
|
||||||
|
'source': control_instance.ip + '/32',
|
||||||
|
'destination': (ffsdn_instance_ip | default('')) + '/32',
|
||||||
|
'protocol': 'tcp',
|
||||||
|
'destination_port': '10250',
|
||||||
|
'description': 'Allow control to reach ' + ffsdn_instance_name + ' kubelet'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'action': 'allow',
|
||||||
|
'state': 'enabled',
|
||||||
|
'source': control_instance.ip + '/32',
|
||||||
|
'destination': (ffsdn_instance_ip | default('')) + '/32',
|
||||||
|
'protocol': 'udp',
|
||||||
|
'destination_port': '8472',
|
||||||
|
'description': 'Allow control VXLAN to ' + ffsdn_instance_name
|
||||||
|
}
|
||||||
|
] }}
|
||||||
|
when: worker_acl_response.status == 200
|
||||||
|
|
||||||
|
- name: Update worker ACL with control access rule
|
||||||
|
ansible.builtin.uri:
|
||||||
|
url: "{{ incus_api }}/1.0/network-acls/{{ worker_acl_name }}"
|
||||||
|
method: PATCH
|
||||||
|
client_cert: "{{ incus_cert }}"
|
||||||
|
client_key: "{{ incus_key }}"
|
||||||
|
validate_certs: false
|
||||||
|
body_format: json
|
||||||
|
body:
|
||||||
|
ingress: "{{ updated_worker_ingress }}"
|
||||||
|
status_code: [200]
|
||||||
|
when:
|
||||||
|
- worker_acl_response.status == 200
|
||||||
|
- updated_worker_ingress is defined
|
||||||
|
|
||||||
|
- name: Report ACL updates
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg: >-
|
||||||
|
ACL rules added: {{ ffsdn_instance_name }} ↔ {{ control_instance.name }}
|
||||||
|
(K3s API 6443, kubelet 10250, VXLAN 8472)
|
||||||
|
|
||||||
|
# --- Step 3: Install K3s agent ---
|
||||||
|
|
||||||
|
- name: Generate K3s agent setup script
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
k3s_agent_script: |
|
||||||
|
#!/bin/bash
|
||||||
|
exec > >(tee /tmp/k3s-agent-setup.log) 2>&1
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "=== K3s agent setup starting ==="
|
||||||
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
|
# Container workaround: create /dev/kmsg if missing
|
||||||
|
[ -e /dev/kmsg ] || ln -s /dev/console /dev/kmsg
|
||||||
|
|
||||||
|
# Install prerequisites
|
||||||
|
apt-get install -y -qq curl iptables 2>&1 | tail -1
|
||||||
|
echo "[OK] curl + iptables installed"
|
||||||
|
|
||||||
|
# Test connectivity to control plane before installing
|
||||||
|
echo "Testing connectivity to {{ k3s_server_url }}..."
|
||||||
|
if curl -sk --connect-timeout 5 "{{ k3s_server_url }}" >/dev/null 2>&1; then
|
||||||
|
echo "[OK] Control plane reachable"
|
||||||
|
else
|
||||||
|
echo "[WARN] Control plane not reachable via curl (may still work via K3s)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Download and install K3s binary (skip service start)
|
||||||
|
echo "Downloading K3s install script..."
|
||||||
|
curl -sfL https://get.k3s.io -o /tmp/k3s-install.sh
|
||||||
|
chmod +x /tmp/k3s-install.sh
|
||||||
|
echo "[OK] Install script downloaded"
|
||||||
|
|
||||||
|
echo "Installing K3s agent binary (skip start)..."
|
||||||
|
K3S_URL="{{ k3s_server_url }}" \
|
||||||
|
K3S_TOKEN="{{ k3s_token }}" \
|
||||||
|
INSTALL_K3S_EXEC="--snapshotter=native --kubelet-arg=feature-gates=KubeletInUserNamespace=true --node-ip={{ ffsdn_instance_ip | default('127.0.0.1') }}" \
|
||||||
|
INSTALL_K3S_SKIP_START=true \
|
||||||
|
/tmp/k3s-install.sh
|
||||||
|
echo "[OK] K3s agent binary installed"
|
||||||
|
|
||||||
|
# Show resource limits
|
||||||
|
echo "--- container limits ---"
|
||||||
|
echo "Memory limit: $(cat /sys/fs/cgroup/memory.max 2>/dev/null || echo unknown)"
|
||||||
|
echo "PIDs max: $(cat /sys/fs/cgroup/pids.max 2>/dev/null || echo unknown)"
|
||||||
|
free -m 2>/dev/null || true
|
||||||
|
|
||||||
|
# Write config file (K3s re-execs itself and loses CLI args)
|
||||||
|
mkdir -p /etc/rancher/k3s
|
||||||
|
cat > /etc/rancher/k3s/config.yaml <<'CFGEOF'
|
||||||
|
server: "{{ k3s_server_url }}"
|
||||||
|
token: "{{ k3s_token }}"
|
||||||
|
snapshotter: native
|
||||||
|
kubelet-arg:
|
||||||
|
- "feature-gates=KubeletInUserNamespace=true"
|
||||||
|
node-ip: "{{ ffsdn_instance_ip | default('127.0.0.1') }}"
|
||||||
|
CFGEOF
|
||||||
|
echo "[OK] K3s agent config written to /etc/rancher/k3s/config.yaml"
|
||||||
|
|
||||||
|
# Start K3s agent directly (bypass systemd/D-Bus to avoid container crash)
|
||||||
|
echo "Starting K3s agent directly..."
|
||||||
|
nohup /usr/local/bin/k3s agent \
|
||||||
|
> /var/log/k3s-agent.log 2>&1 &
|
||||||
|
K3S_PID=$!
|
||||||
|
echo "[OK] K3s agent started (PID $K3S_PID)"
|
||||||
|
|
||||||
|
# Wait for agent to initialize (check process is still alive)
|
||||||
|
echo "Waiting for K3s agent to initialize..."
|
||||||
|
for i in $(seq 1 12); do
|
||||||
|
sleep 5
|
||||||
|
if ! kill -0 $K3S_PID 2>/dev/null; then
|
||||||
|
echo "[ERROR] K3s agent process died"
|
||||||
|
echo "--- last 30 lines of agent log ---"
|
||||||
|
tail -30 /var/log/k3s-agent.log 2>/dev/null || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo " ... alive after ${i}x5s (PID $K3S_PID)"
|
||||||
|
done
|
||||||
|
echo "[OK] K3s agent running for 60s"
|
||||||
|
|
||||||
|
# Verify node is registered with the cluster
|
||||||
|
echo "--- K3s agent log (last 10 lines) ---"
|
||||||
|
tail -10 /var/log/k3s-agent.log 2>/dev/null || true
|
||||||
|
|
||||||
|
echo "=== K3s agent setup complete ==="
|
||||||
|
|
||||||
|
- name: Push K3s agent setup script
|
||||||
|
ansible.builtin.uri:
|
||||||
|
url: "{{ incus_api }}/1.0/instances/{{ ffsdn_instance_name }}/files?path=/tmp/k3s-agent-setup.sh"
|
||||||
|
method: POST
|
||||||
|
client_cert: "{{ incus_cert }}"
|
||||||
|
client_key: "{{ incus_key }}"
|
||||||
|
validate_certs: false
|
||||||
|
body: "{{ k3s_agent_script }}"
|
||||||
|
headers:
|
||||||
|
Content-Type: "application/octet-stream"
|
||||||
|
X-Incus-type: "file"
|
||||||
|
X-Incus-mode: "0755"
|
||||||
|
status_code: [200]
|
||||||
|
|
||||||
|
- name: Execute K3s agent 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/bash", "/tmp/k3s-agent-setup.sh"]
|
||||||
|
record-output: true
|
||||||
|
interactive: false
|
||||||
|
wait-for-websocket: false
|
||||||
|
environment:
|
||||||
|
DEBIAN_FRONTEND: "noninteractive"
|
||||||
|
status_code: [202]
|
||||||
|
return_content: true
|
||||||
|
register: k3s_agent_exec
|
||||||
|
|
||||||
|
- name: Wait for K3s agent setup to complete (up to 10 minutes)
|
||||||
|
ansible.builtin.uri:
|
||||||
|
url: "{{ incus_api }}{{ k3s_agent_exec.json.operation }}/wait?timeout=600"
|
||||||
|
method: GET
|
||||||
|
client_cert: "{{ incus_cert }}"
|
||||||
|
client_key: "{{ incus_key }}"
|
||||||
|
validate_certs: false
|
||||||
|
return_content: true
|
||||||
|
timeout: 620
|
||||||
|
register: k3s_agent_wait
|
||||||
|
failed_when: false
|
||||||
|
|
||||||
|
- name: Retrieve K3s agent setup log from container
|
||||||
|
ansible.builtin.uri:
|
||||||
|
url: "{{ incus_api }}/1.0/instances/{{ ffsdn_instance_name }}/files?path=/tmp/k3s-agent-setup.log"
|
||||||
|
method: GET
|
||||||
|
client_cert: "{{ incus_cert }}"
|
||||||
|
client_key: "{{ incus_key }}"
|
||||||
|
validate_certs: false
|
||||||
|
return_content: true
|
||||||
|
status_code: [200, 404]
|
||||||
|
register: k3s_agent_log
|
||||||
|
ignore_errors: true
|
||||||
|
|
||||||
|
- name: Display K3s agent setup log
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg: "{{ k3s_agent_log.content | default('No log captured') }}"
|
||||||
|
when: k3s_agent_log is defined and k3s_agent_log.status == 200
|
||||||
|
ignore_errors: true
|
||||||
|
|
||||||
|
- name: Verify K3s agent setup exit code
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- k3s_agent_wait.json.metadata.metadata.return | int == 0
|
||||||
|
fail_msg: "K3s agent setup exited with code {{ k3s_agent_wait.json.metadata.metadata.return }}"
|
||||||
|
success_msg: "K3s agent joined control plane at {{ k3s_server_url }}"
|
||||||
|
|
||||||
|
- 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/k3s-agent-setup.sh"]
|
||||||
|
record-output: false
|
||||||
|
interactive: false
|
||||||
|
wait-for-websocket: false
|
||||||
|
status_code: [202]
|
||||||
|
ignore_errors: true
|
||||||
|
|
@ -0,0 +1,127 @@
|
||||||
|
---
|
||||||
|
# showcase/k3scluster-deploy-instance.yml — Per-instance tasks for k3scluster
|
||||||
|
#
|
||||||
|
# Called from k3scluster-post-deploy.yml in a loop over ffsdn_instances.
|
||||||
|
# The 'instance' loop var has: name, ip, component_name, image_alias, type
|
||||||
|
# Dispatches to k3s-control.yml or k3s-worker.yml based on component_name.
|
||||||
|
|
||||||
|
- name: Set instance facts from loop variable
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
ffsdn_instance_name: "{{ instance.name }}"
|
||||||
|
ffsdn_instance_ip: "{{ instance.ip | default('') }}"
|
||||||
|
component_name: "{{ instance.component_name }}"
|
||||||
|
|
||||||
|
- name: "Wait for {{ ffsdn_instance_name }} 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: Validate component
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- component_name in valid_components
|
||||||
|
fail_msg: >-
|
||||||
|
Unknown component '{{ component_name }}' for k3scluster blueprint.
|
||||||
|
Instance '{{ ffsdn_instance_name }}' component must be one of: {{ valid_components | join(', ') }}.
|
||||||
|
|
||||||
|
- name: Display dispatch info
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg: "K3scluster {{ component_name }} — configuring {{ ffsdn_instance_name }}"
|
||||||
|
|
||||||
|
- name: Enable security.nesting and raise resource limits for K3s
|
||||||
|
ansible.builtin.uri:
|
||||||
|
url: "{{ incus_api }}/1.0/instances/{{ ffsdn_instance_name }}"
|
||||||
|
method: PATCH
|
||||||
|
client_cert: "{{ incus_cert }}"
|
||||||
|
client_key: "{{ incus_key }}"
|
||||||
|
validate_certs: false
|
||||||
|
body_format: json
|
||||||
|
body:
|
||||||
|
config:
|
||||||
|
security.nesting: "true"
|
||||||
|
security.syscalls.intercept.mknod: "true"
|
||||||
|
security.syscalls.intercept.setxattr: "true"
|
||||||
|
limits.processes: "4096"
|
||||||
|
limits.memory: "4GiB"
|
||||||
|
limits.cpu: "2"
|
||||||
|
status_code: [200, 202]
|
||||||
|
return_content: true
|
||||||
|
register: nesting_result
|
||||||
|
|
||||||
|
- name: Wait for nesting config if async
|
||||||
|
ansible.builtin.uri:
|
||||||
|
url: "{{ incus_api }}{{ nesting_result.json.operation }}/wait?timeout=30"
|
||||||
|
method: GET
|
||||||
|
client_cert: "{{ incus_cert }}"
|
||||||
|
client_key: "{{ incus_key }}"
|
||||||
|
validate_certs: false
|
||||||
|
return_content: true
|
||||||
|
when: nesting_result.status == 202
|
||||||
|
|
||||||
|
- name: Restart container to apply security and resource config
|
||||||
|
ansible.builtin.uri:
|
||||||
|
url: "{{ incus_api }}/1.0/instances/{{ ffsdn_instance_name }}/state"
|
||||||
|
method: PUT
|
||||||
|
client_cert: "{{ incus_cert }}"
|
||||||
|
client_key: "{{ incus_key }}"
|
||||||
|
validate_certs: false
|
||||||
|
body_format: json
|
||||||
|
body:
|
||||||
|
action: restart
|
||||||
|
timeout: 30
|
||||||
|
status_code: [200, 202]
|
||||||
|
return_content: true
|
||||||
|
register: restart_result
|
||||||
|
|
||||||
|
- name: Wait for restart to complete
|
||||||
|
ansible.builtin.uri:
|
||||||
|
url: "{{ incus_api }}{{ restart_result.json.operation }}/wait?timeout=60"
|
||||||
|
method: GET
|
||||||
|
client_cert: "{{ incus_cert }}"
|
||||||
|
client_key: "{{ incus_key }}"
|
||||||
|
validate_certs: false
|
||||||
|
return_content: true
|
||||||
|
when: restart_result.status == 202
|
||||||
|
|
||||||
|
- name: Wait for container to be running after restart (up to 30s)
|
||||||
|
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: post_restart_state
|
||||||
|
until: post_restart_state.json.metadata.status == "Running"
|
||||||
|
retries: 6
|
||||||
|
delay: 5
|
||||||
|
|
||||||
|
- name: Run common setup tasks
|
||||||
|
ansible.builtin.include_tasks: common.yml
|
||||||
|
|
||||||
|
- name: Route to component-specific tasks
|
||||||
|
ansible.builtin.include_tasks: "k3s-{{ component_name }}.yml"
|
||||||
|
|
||||||
|
- name: Remove temporary internet NIC
|
||||||
|
ansible.builtin.include_tasks: remove-setup-nic.yml
|
||||||
|
|
||||||
|
- name: Record deployment in ledger
|
||||||
|
ansible.builtin.lineinfile:
|
||||||
|
path: /tmp/awx-deploy-ledger.log
|
||||||
|
line: >-
|
||||||
|
{{ lookup('pipe', 'date -Iseconds') }}
|
||||||
|
SHOWCASE-DEPLOY instance={{ ffsdn_instance_name }}
|
||||||
|
blueprint=k3scluster component={{ component_name }}
|
||||||
|
ip={{ ffsdn_instance_ip }}
|
||||||
|
cluster={{ ffsdn_cluster_name | default('unknown') }}
|
||||||
|
status=success
|
||||||
|
create: true
|
||||||
|
mode: "0644"
|
||||||
|
|
@ -0,0 +1,69 @@
|
||||||
|
---
|
||||||
|
# showcase/remove-setup-nic.yml — Remove temporary internet NIC
|
||||||
|
#
|
||||||
|
# Removes the eth-setup device added by common.yml after package
|
||||||
|
# installation is complete. Uses the Incus PUT API to remove the device.
|
||||||
|
# Running containers return 202 (async), so we accept that and wait.
|
||||||
|
|
||||||
|
- name: Remove temporary incusbr0 NIC
|
||||||
|
ansible.builtin.uri:
|
||||||
|
url: "{{ incus_api }}/1.0/instances/{{ ffsdn_instance_name }}"
|
||||||
|
method: GET
|
||||||
|
client_cert: "{{ incus_cert }}"
|
||||||
|
client_key: "{{ incus_key }}"
|
||||||
|
validate_certs: false
|
||||||
|
return_content: true
|
||||||
|
register: instance_config
|
||||||
|
|
||||||
|
- name: Delete eth-setup device from instance
|
||||||
|
ansible.builtin.uri:
|
||||||
|
url: "{{ incus_api }}/1.0/instances/{{ ffsdn_instance_name }}"
|
||||||
|
method: PUT
|
||||||
|
client_cert: "{{ incus_cert }}"
|
||||||
|
client_key: "{{ incus_key }}"
|
||||||
|
validate_certs: false
|
||||||
|
body_format: json
|
||||||
|
body: >-
|
||||||
|
{{
|
||||||
|
instance_config.json.metadata
|
||||||
|
| combine({
|
||||||
|
'devices': instance_config.json.metadata.devices
|
||||||
|
| dict2items
|
||||||
|
| rejectattr('key', 'equalto', 'eth-setup')
|
||||||
|
| items2dict
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
status_code: [200, 202]
|
||||||
|
return_content: true
|
||||||
|
headers:
|
||||||
|
If-Match: "{{ instance_config.headers.etag | default('') }}"
|
||||||
|
register: nic_remove_result
|
||||||
|
when: "'eth-setup' in instance_config.json.metadata.devices"
|
||||||
|
|
||||||
|
- name: Wait for device removal to complete
|
||||||
|
ansible.builtin.uri:
|
||||||
|
url: "{{ incus_api }}{{ nic_remove_result.json.operation }}/wait?timeout=30"
|
||||||
|
method: GET
|
||||||
|
client_cert: "{{ incus_cert }}"
|
||||||
|
client_key: "{{ incus_key }}"
|
||||||
|
validate_certs: false
|
||||||
|
return_content: true
|
||||||
|
when:
|
||||||
|
- nic_remove_result is defined
|
||||||
|
- nic_remove_result.status | default(200) == 202
|
||||||
|
|
||||||
|
- name: Clean up eth1 network config inside container
|
||||||
|
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", "/etc/systemd/network/80-eth1.network", "/tmp/activate-eth1.sh"]
|
||||||
|
record-output: false
|
||||||
|
interactive: false
|
||||||
|
wait-for-websocket: false
|
||||||
|
status_code: [202]
|
||||||
|
ignore_errors: true
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
---
|
||||||
|
# showcase/staticsite-deploy-instance.yml — Per-instance tasks for staticsite
|
||||||
|
#
|
||||||
|
# Called from staticsite-post-deploy.yml in a loop over ffsdn_instances.
|
||||||
|
# The 'instance' loop var has: name, ip, component_name, image_alias, type
|
||||||
|
|
||||||
|
- name: Set instance facts from loop variable
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
ffsdn_instance_name: "{{ instance.name }}"
|
||||||
|
ffsdn_instance_ip: "{{ instance.ip | default('') }}"
|
||||||
|
component_name: "{{ instance.component_name }}"
|
||||||
|
|
||||||
|
- name: "Wait for {{ ffsdn_instance_name }} 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: Display info
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg: "Staticsite — configuring {{ ffsdn_instance_name }}"
|
||||||
|
|
||||||
|
- name: Run common setup tasks
|
||||||
|
ansible.builtin.include_tasks: common.yml
|
||||||
|
|
||||||
|
- name: Configure static site
|
||||||
|
ansible.builtin.include_tasks: staticsite-site.yml
|
||||||
|
|
||||||
|
- name: Remove temporary internet NIC
|
||||||
|
ansible.builtin.include_tasks: remove-setup-nic.yml
|
||||||
|
|
||||||
|
- name: Record deployment in ledger
|
||||||
|
ansible.builtin.lineinfile:
|
||||||
|
path: /tmp/awx-deploy-ledger.log
|
||||||
|
line: >-
|
||||||
|
{{ lookup('pipe', 'date -Iseconds') }}
|
||||||
|
SHOWCASE-DEPLOY instance={{ ffsdn_instance_name }}
|
||||||
|
blueprint=staticsite component={{ component_name }}
|
||||||
|
ip={{ ffsdn_instance_ip }}
|
||||||
|
cluster={{ ffsdn_cluster_name | default('unknown') }}
|
||||||
|
status=success
|
||||||
|
create: true
|
||||||
|
mode: "0644"
|
||||||
|
|
@ -0,0 +1,161 @@
|
||||||
|
---
|
||||||
|
# showcase/staticsite-site.yml — nginx static site
|
||||||
|
#
|
||||||
|
# Installs nginx and deploys a branded HTML page showing the instance
|
||||||
|
# name, IP, node placement, and deployment timestamp. Each instance is
|
||||||
|
# independently configured — no cross-instance coordination needed.
|
||||||
|
|
||||||
|
- name: Generate static site setup script
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
site_setup_script: |
|
||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "=== Static site setup starting ==="
|
||||||
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
|
# Install nginx
|
||||||
|
apt-get install -y -qq nginx 2>&1 | tail -1
|
||||||
|
echo "[OK] nginx installed"
|
||||||
|
|
||||||
|
# Get instance metadata
|
||||||
|
MY_IP=$(hostname -I | awk '{print $1}')
|
||||||
|
DEPLOY_TIME=$(date -Iseconds)
|
||||||
|
|
||||||
|
# Deploy branded HTML page
|
||||||
|
cat > /var/www/html/index.html << SITEHTML
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>{{ ffsdn_instance_name }} — Static Site Showcase</title>
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body { font-family: -apple-system, sans-serif; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; display: flex; align-items: center; justify-content: center; }
|
||||||
|
.container { background: white; border-radius: 16px; padding: 48px; max-width: 600px; width: 90%; box-shadow: 0 20px 60px rgba(0,0,0,0.3); }
|
||||||
|
h1 { font-size: 28px; color: #1a1a2e; margin-bottom: 8px; }
|
||||||
|
.subtitle { color: #888; font-size: 14px; margin-bottom: 32px; }
|
||||||
|
.info-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
|
||||||
|
.info-card { background: #f8f9fa; border-radius: 8px; padding: 16px; }
|
||||||
|
.info-label { font-size: 12px; color: #888; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px; }
|
||||||
|
.info-value { font-size: 18px; font-weight: 600; color: #1a1a2e; word-break: break-all; }
|
||||||
|
.badge { display: inline-block; background: #667eea; color: white; padding: 4px 12px; border-radius: 12px; font-size: 13px; font-weight: 600; margin-top: 24px; }
|
||||||
|
.footer { text-align: center; margin-top: 32px; color: #aaa; font-size: 12px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>{{ ffsdn_instance_name }}</h1>
|
||||||
|
<div class="subtitle">Static Site Showcase — deployed by AWX via Aether</div>
|
||||||
|
<div class="info-grid">
|
||||||
|
<div class="info-card">
|
||||||
|
<div class="info-label">Instance</div>
|
||||||
|
<div class="info-value">{{ ffsdn_instance_name }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="info-card">
|
||||||
|
<div class="info-label">IP Address</div>
|
||||||
|
<div class="info-value">${MY_IP}</div>
|
||||||
|
</div>
|
||||||
|
<div class="info-card">
|
||||||
|
<div class="info-label">Cluster</div>
|
||||||
|
<div class="info-value">{{ ffsdn_cluster_name | default('unknown') }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="info-card">
|
||||||
|
<div class="info-label">Deployed By</div>
|
||||||
|
<div class="info-value">{{ ffsdn_deployed_by | default('unknown') }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="info-card">
|
||||||
|
<div class="info-label">Image</div>
|
||||||
|
<div class="info-value">{{ ffsdn_image_os | default('') }} {{ ffsdn_image_release | default('') }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="info-card">
|
||||||
|
<div class="info-label">Deployed At</div>
|
||||||
|
<div class="info-value">${DEPLOY_TIME}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="text-align:center"><span class="badge">staticsite blueprint</span></div>
|
||||||
|
<div class="footer">
|
||||||
|
Horizontal scaling demo — each instance independently configured
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
SITEHTML
|
||||||
|
echo "[OK] HTML page deployed"
|
||||||
|
|
||||||
|
# Remove default pages
|
||||||
|
rm -f /var/www/html/index.nginx-debian.html
|
||||||
|
|
||||||
|
# Enable and start nginx
|
||||||
|
systemctl enable nginx
|
||||||
|
systemctl restart nginx
|
||||||
|
echo "[OK] nginx started"
|
||||||
|
|
||||||
|
echo "=== Static site setup complete ==="
|
||||||
|
|
||||||
|
- name: Push static site setup script
|
||||||
|
ansible.builtin.uri:
|
||||||
|
url: "{{ incus_api }}/1.0/instances/{{ ffsdn_instance_name }}/files?path=/tmp/staticsite-setup.sh"
|
||||||
|
method: POST
|
||||||
|
client_cert: "{{ incus_cert }}"
|
||||||
|
client_key: "{{ incus_key }}"
|
||||||
|
validate_certs: false
|
||||||
|
body: "{{ site_setup_script }}"
|
||||||
|
headers:
|
||||||
|
Content-Type: "application/octet-stream"
|
||||||
|
X-Incus-type: "file"
|
||||||
|
X-Incus-mode: "0755"
|
||||||
|
status_code: [200]
|
||||||
|
|
||||||
|
- name: Execute static site 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/bash", "/tmp/staticsite-setup.sh"]
|
||||||
|
record-output: true
|
||||||
|
interactive: false
|
||||||
|
wait-for-websocket: false
|
||||||
|
environment:
|
||||||
|
DEBIAN_FRONTEND: "noninteractive"
|
||||||
|
status_code: [202]
|
||||||
|
return_content: true
|
||||||
|
register: site_setup_exec
|
||||||
|
|
||||||
|
- name: Wait for static site setup to complete (up to 3 minutes)
|
||||||
|
ansible.builtin.uri:
|
||||||
|
url: "{{ incus_api }}{{ site_setup_exec.json.operation }}/wait?timeout=180"
|
||||||
|
method: GET
|
||||||
|
client_cert: "{{ incus_cert }}"
|
||||||
|
client_key: "{{ incus_key }}"
|
||||||
|
validate_certs: false
|
||||||
|
return_content: true
|
||||||
|
timeout: 200
|
||||||
|
register: site_setup_wait
|
||||||
|
failed_when: site_setup_wait.json.metadata.status != "Success"
|
||||||
|
|
||||||
|
- name: Verify static site setup exit code
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- site_setup_wait.json.metadata.metadata.return | int == 0
|
||||||
|
fail_msg: "Static site setup exited with code {{ site_setup_wait.json.metadata.metadata.return }}"
|
||||||
|
success_msg: "Static site setup completed — nginx serving on port 80"
|
||||||
|
|
||||||
|
- 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/staticsite-setup.sh"]
|
||||||
|
record-output: false
|
||||||
|
interactive: false
|
||||||
|
wait-for-websocket: false
|
||||||
|
status_code: [202]
|
||||||
|
ignore_errors: true
|
||||||
|
|
@ -0,0 +1,148 @@
|
||||||
|
---
|
||||||
|
# showcase/webapp-db.yml — MariaDB setup for the webapp blueprint
|
||||||
|
#
|
||||||
|
# Installs MariaDB, creates webapp database and user with random password,
|
||||||
|
# configures remote access, and stores credentials in /etc/webapp-db-creds
|
||||||
|
# for the web tier to read via Incus API.
|
||||||
|
|
||||||
|
- name: Generate random database password
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
db_password: "{{ lookup('password', '/dev/null chars=ascii_letters,digits length=20') }}"
|
||||||
|
|
||||||
|
- name: Generate MariaDB setup script
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
db_setup_script: |
|
||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "=== MariaDB setup starting ==="
|
||||||
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
|
# Install MariaDB
|
||||||
|
apt-get install -y -qq mariadb-server 2>&1 | tail -3
|
||||||
|
echo "[OK] MariaDB installed"
|
||||||
|
|
||||||
|
# Start and enable
|
||||||
|
systemctl enable mariadb
|
||||||
|
systemctl start mariadb
|
||||||
|
echo "[OK] MariaDB started"
|
||||||
|
|
||||||
|
# Configure bind address for remote connections
|
||||||
|
sed -i 's/^bind-address.*=.*/bind-address = 0.0.0.0/' /etc/mysql/mariadb.conf.d/50-server.cnf
|
||||||
|
systemctl restart mariadb
|
||||||
|
echo "[OK] Bind address set to 0.0.0.0"
|
||||||
|
|
||||||
|
# Create database and user
|
||||||
|
mysql -u root << 'SQL'
|
||||||
|
CREATE DATABASE IF NOT EXISTS webapp;
|
||||||
|
CREATE USER IF NOT EXISTS 'webapp'@'%' IDENTIFIED BY '{{ db_password }}';
|
||||||
|
GRANT ALL PRIVILEGES ON webapp.* TO 'webapp'@'%';
|
||||||
|
FLUSH PRIVILEGES;
|
||||||
|
|
||||||
|
-- Create sample table for the web tier to query
|
||||||
|
USE webapp;
|
||||||
|
CREATE TABLE IF NOT EXISTS visits (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
visitor_ip VARCHAR(45),
|
||||||
|
visitor_host VARCHAR(255),
|
||||||
|
visited_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
INSERT INTO visits (visitor_ip, visitor_host) VALUES ('setup', '{{ ffsdn_instance_name }}');
|
||||||
|
SQL
|
||||||
|
echo "[OK] Database 'webapp' and user created"
|
||||||
|
|
||||||
|
# Store credentials for the web tier to read
|
||||||
|
MY_IP=$(hostname -I | awk '{print $1}')
|
||||||
|
cat > /etc/webapp-db-creds << CREDS
|
||||||
|
DB_HOST=${MY_IP}
|
||||||
|
DB_PORT=3306
|
||||||
|
DB_NAME=webapp
|
||||||
|
DB_USER=webapp
|
||||||
|
DB_PASS={{ db_password }}
|
||||||
|
CREDS
|
||||||
|
chmod 644 /etc/webapp-db-creds
|
||||||
|
echo "[OK] Credentials stored in /etc/webapp-db-creds"
|
||||||
|
|
||||||
|
echo "=== MariaDB setup complete ==="
|
||||||
|
|
||||||
|
- name: Push MariaDB setup script
|
||||||
|
ansible.builtin.uri:
|
||||||
|
url: "{{ incus_api }}/1.0/instances/{{ ffsdn_instance_name }}/files?path=/tmp/webapp-db-setup.sh"
|
||||||
|
method: POST
|
||||||
|
client_cert: "{{ incus_cert }}"
|
||||||
|
client_key: "{{ incus_key }}"
|
||||||
|
validate_certs: false
|
||||||
|
body: "{{ db_setup_script }}"
|
||||||
|
headers:
|
||||||
|
Content-Type: "application/octet-stream"
|
||||||
|
X-Incus-type: "file"
|
||||||
|
X-Incus-mode: "0755"
|
||||||
|
status_code: [200]
|
||||||
|
|
||||||
|
- name: Execute MariaDB 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/bash", "/tmp/webapp-db-setup.sh"]
|
||||||
|
record-output: true
|
||||||
|
interactive: false
|
||||||
|
wait-for-websocket: false
|
||||||
|
environment:
|
||||||
|
DEBIAN_FRONTEND: "noninteractive"
|
||||||
|
status_code: [202]
|
||||||
|
return_content: true
|
||||||
|
register: db_setup_exec
|
||||||
|
|
||||||
|
- name: Wait for MariaDB setup to complete (up to 5 minutes)
|
||||||
|
ansible.builtin.uri:
|
||||||
|
url: "{{ incus_api }}{{ db_setup_exec.json.operation }}/wait?timeout=300"
|
||||||
|
method: GET
|
||||||
|
client_cert: "{{ incus_cert }}"
|
||||||
|
client_key: "{{ incus_key }}"
|
||||||
|
validate_certs: false
|
||||||
|
return_content: true
|
||||||
|
timeout: 320
|
||||||
|
register: db_setup_wait
|
||||||
|
failed_when: db_setup_wait.json.metadata.status != "Success"
|
||||||
|
|
||||||
|
- name: Verify MariaDB setup exit code
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- db_setup_wait.json.metadata.metadata.return | int == 0
|
||||||
|
fail_msg: "MariaDB setup exited with code {{ db_setup_wait.json.metadata.metadata.return }}"
|
||||||
|
success_msg: "MariaDB setup completed successfully"
|
||||||
|
|
||||||
|
- name: Verify credentials file was written
|
||||||
|
ansible.builtin.uri:
|
||||||
|
url: "{{ incus_api }}/1.0/instances/{{ ffsdn_instance_name }}/files?path=/etc/webapp-db-creds"
|
||||||
|
method: GET
|
||||||
|
client_cert: "{{ incus_cert }}"
|
||||||
|
client_key: "{{ incus_key }}"
|
||||||
|
validate_certs: false
|
||||||
|
return_content: true
|
||||||
|
register: creds_check
|
||||||
|
|
||||||
|
- name: Display MariaDB configuration
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg: "MariaDB configured: {{ creds_check.content | trim }}"
|
||||||
|
|
||||||
|
- 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/webapp-db-setup.sh"]
|
||||||
|
record-output: false
|
||||||
|
interactive: false
|
||||||
|
wait-for-websocket: false
|
||||||
|
status_code: [202]
|
||||||
|
ignore_errors: true
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
---
|
||||||
|
# showcase/webapp-deploy-instance.yml — Per-instance tasks for webapp
|
||||||
|
#
|
||||||
|
# Called from webapp-post-deploy.yml in a loop over ffsdn_instances.
|
||||||
|
# The 'instance' loop var has: name, ip, component_name, image_alias, type
|
||||||
|
# Dispatches to webapp-db.yml or webapp-web.yml based on component_name.
|
||||||
|
|
||||||
|
- name: Set instance facts from loop variable
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
ffsdn_instance_name: "{{ instance.name }}"
|
||||||
|
ffsdn_instance_ip: "{{ instance.ip | default('') }}"
|
||||||
|
component_name: "{{ instance.component_name }}"
|
||||||
|
|
||||||
|
- name: "Wait for {{ ffsdn_instance_name }} 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: Validate component
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- component_name in valid_components
|
||||||
|
fail_msg: >-
|
||||||
|
Unknown component '{{ component_name }}' for webapp blueprint.
|
||||||
|
Instance '{{ ffsdn_instance_name }}' component must be one of: {{ valid_components | join(', ') }}.
|
||||||
|
|
||||||
|
- name: Display dispatch info
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg: "Webapp {{ component_name }} — configuring {{ ffsdn_instance_name }}"
|
||||||
|
|
||||||
|
- name: Run common setup tasks
|
||||||
|
ansible.builtin.include_tasks: common.yml
|
||||||
|
|
||||||
|
- name: Route to component-specific tasks
|
||||||
|
ansible.builtin.include_tasks: "webapp-{{ component_name }}.yml"
|
||||||
|
|
||||||
|
- name: Remove temporary internet NIC
|
||||||
|
ansible.builtin.include_tasks: remove-setup-nic.yml
|
||||||
|
|
||||||
|
- name: Record deployment in ledger
|
||||||
|
ansible.builtin.lineinfile:
|
||||||
|
path: /tmp/awx-deploy-ledger.log
|
||||||
|
line: >-
|
||||||
|
{{ lookup('pipe', 'date -Iseconds') }}
|
||||||
|
SHOWCASE-DEPLOY instance={{ ffsdn_instance_name }}
|
||||||
|
blueprint=webapp component={{ component_name }}
|
||||||
|
ip={{ ffsdn_instance_ip }}
|
||||||
|
cluster={{ ffsdn_cluster_name | default('unknown') }}
|
||||||
|
status=success
|
||||||
|
create: true
|
||||||
|
mode: "0644"
|
||||||
|
|
@ -0,0 +1,351 @@
|
||||||
|
---
|
||||||
|
# showcase/webapp-web.yml — nginx + PHP-FPM setup for the webapp blueprint
|
||||||
|
#
|
||||||
|
# Discovers the db sibling via Incus API, reads db credentials from the
|
||||||
|
# db instance, deploys a PHP app that shows DB connectivity, and adds
|
||||||
|
# an ACL rule to the db's ACL allowing this web instance on port 3306.
|
||||||
|
|
||||||
|
# --- Step 1: Find the database instance from the deployment manifest ---
|
||||||
|
|
||||||
|
- name: Find database instances from deployment
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
db_instances: >-
|
||||||
|
{{ ffsdn_instances | selectattr('component_name', 'equalto', 'db') | list }}
|
||||||
|
|
||||||
|
- name: Validate database found
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- db_instances | length > 0
|
||||||
|
fail_msg: >-
|
||||||
|
No db component found in ffsdn_instances. The blueprint must
|
||||||
|
include a db component for the web tier to connect to.
|
||||||
|
|
||||||
|
- name: Select primary database instance
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
db_instance:
|
||||||
|
name: "{{ db_instances[0].name }}"
|
||||||
|
ip: "{{ db_instances[0].ip }}"
|
||||||
|
|
||||||
|
- name: Display database target
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg: "Using database: {{ db_instance.name }} at {{ db_instance.ip }}"
|
||||||
|
|
||||||
|
# --- Step 2: Wait for and read database credentials ---
|
||||||
|
|
||||||
|
- name: Wait for db credentials to be available (up to 5 minutes)
|
||||||
|
ansible.builtin.uri:
|
||||||
|
url: "{{ incus_api }}/1.0/instances/{{ db_instance.name }}/files?path=/etc/webapp-db-creds"
|
||||||
|
method: GET
|
||||||
|
client_cert: "{{ incus_cert }}"
|
||||||
|
client_key: "{{ incus_key }}"
|
||||||
|
validate_certs: false
|
||||||
|
return_content: true
|
||||||
|
status_code: [200, 404]
|
||||||
|
register: db_creds_response
|
||||||
|
until: db_creds_response.status == 200
|
||||||
|
retries: 30
|
||||||
|
delay: 10
|
||||||
|
|
||||||
|
- name: Parse database credentials
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
db_host: "{{ db_creds_response.content | regex_search('DB_HOST=(\\S+)', '\\1') | first }}"
|
||||||
|
db_port: "{{ db_creds_response.content | regex_search('DB_PORT=(\\S+)', '\\1') | first }}"
|
||||||
|
db_name: "{{ db_creds_response.content | regex_search('DB_NAME=(\\S+)', '\\1') | first }}"
|
||||||
|
db_user: "{{ db_creds_response.content | regex_search('DB_USER=(\\S+)', '\\1') | first }}"
|
||||||
|
db_pass: "{{ db_creds_response.content | regex_search('DB_PASS=(\\S+)', '\\1') | first }}"
|
||||||
|
|
||||||
|
- name: Display parsed credentials
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg: "DB connection: {{ db_user }}@{{ db_host }}:{{ db_port }}/{{ db_name }}"
|
||||||
|
|
||||||
|
# --- Step 3: Install nginx + PHP-FPM and deploy app ---
|
||||||
|
|
||||||
|
- name: Generate web setup script
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
web_setup_script: |
|
||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "=== Web tier setup starting ==="
|
||||||
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
|
# Install nginx + PHP-FPM + MySQL extension
|
||||||
|
apt-get install -y -qq nginx php-fpm php-mysql 2>&1 | tail -3
|
||||||
|
echo "[OK] nginx + PHP-FPM + php-mysql installed"
|
||||||
|
|
||||||
|
# Find PHP-FPM socket path (varies by PHP version)
|
||||||
|
PHP_SOCK=$(find /run/php/ -name 'php*-fpm.sock' 2>/dev/null | head -1)
|
||||||
|
if [ -z "$PHP_SOCK" ]; then
|
||||||
|
PHP_VERSION=$(php -r 'echo PHP_MAJOR_VERSION.".".PHP_MINOR_VERSION;')
|
||||||
|
PHP_SOCK="/run/php/php${PHP_VERSION}-fpm.sock"
|
||||||
|
fi
|
||||||
|
echo "[OK] PHP-FPM socket: $PHP_SOCK"
|
||||||
|
|
||||||
|
# Configure nginx with PHP
|
||||||
|
cat > /etc/nginx/sites-available/default << NGINX
|
||||||
|
server {
|
||||||
|
listen 80 default_server;
|
||||||
|
root /var/www/html;
|
||||||
|
index index.php index.html;
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files \$uri \$uri/ =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ \\.php\$ {
|
||||||
|
include snippets/fastcgi-php.conf;
|
||||||
|
fastcgi_pass unix:${PHP_SOCK};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
NGINX
|
||||||
|
echo "[OK] nginx configured"
|
||||||
|
|
||||||
|
# Deploy PHP application
|
||||||
|
cat > /var/www/html/index.php << 'PHPAPP'
|
||||||
|
<?php
|
||||||
|
$config = [];
|
||||||
|
foreach (file('/etc/webapp-web-config') as $line) {
|
||||||
|
$line = trim($line);
|
||||||
|
if ($line && strpos($line, '=') !== false) {
|
||||||
|
list($key, $value) = explode('=', $line, 2);
|
||||||
|
$config[trim($key)] = trim($value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$db_host = $config['DB_HOST'] ?? 'unknown';
|
||||||
|
$db_port = $config['DB_PORT'] ?? '3306';
|
||||||
|
$db_name = $config['DB_NAME'] ?? 'webapp';
|
||||||
|
$db_user = $config['DB_USER'] ?? 'webapp';
|
||||||
|
$db_pass = $config['DB_PASS'] ?? '';
|
||||||
|
|
||||||
|
$instance = gethostname();
|
||||||
|
$ip = $config['INSTANCE_IP'] ?? 'unknown';
|
||||||
|
$db_status = 'disconnected';
|
||||||
|
$visit_count = 0;
|
||||||
|
$error = '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo = new PDO(
|
||||||
|
"mysql:host={$db_host};port={$db_port};dbname={$db_name}",
|
||||||
|
$db_user, $db_pass,
|
||||||
|
[PDO::ATTR_TIMEOUT => 5]
|
||||||
|
);
|
||||||
|
$db_status = 'connected';
|
||||||
|
|
||||||
|
// Record this visit
|
||||||
|
$stmt = $pdo->prepare('INSERT INTO visits (visitor_ip, visitor_host) VALUES (?, ?)');
|
||||||
|
$stmt->execute([$_SERVER['REMOTE_ADDR'] ?? 'direct', $instance]);
|
||||||
|
|
||||||
|
// Count total visits
|
||||||
|
$visit_count = $pdo->query('SELECT COUNT(*) FROM visits')->fetchColumn();
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
$error = $e->getMessage();
|
||||||
|
$db_status = 'error';
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Webapp Showcase — <?= htmlspecialchars($instance) ?></title>
|
||||||
|
<style>
|
||||||
|
body { font-family: -apple-system, sans-serif; max-width: 700px; margin: 40px auto; padding: 0 20px; background: #f5f5f5; }
|
||||||
|
.card { background: white; border-radius: 8px; padding: 24px; margin: 16px 0; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
|
||||||
|
h1 { color: #1a1a2e; }
|
||||||
|
.status { display: inline-block; padding: 4px 12px; border-radius: 12px; font-size: 14px; font-weight: 600; }
|
||||||
|
.connected { background: #d4edda; color: #155724; }
|
||||||
|
.error, .disconnected { background: #f8d7da; color: #721c24; }
|
||||||
|
table { width: 100%; border-collapse: collapse; }
|
||||||
|
td { padding: 8px; border-bottom: 1px solid #eee; }
|
||||||
|
td:first-child { font-weight: 600; width: 40%; color: #555; }
|
||||||
|
.footer { text-align: center; color: #888; font-size: 13px; margin-top: 24px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Webapp Showcase</h1>
|
||||||
|
<div class="card">
|
||||||
|
<h2>Instance Info</h2>
|
||||||
|
<table>
|
||||||
|
<tr><td>Instance</td><td><?= htmlspecialchars($instance) ?></td></tr>
|
||||||
|
<tr><td>IP Address</td><td><?= htmlspecialchars($ip) ?></td></tr>
|
||||||
|
<tr><td>Component</td><td>web</td></tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h2>Database Connection <span class="status <?= $db_status ?>"><?= $db_status ?></span></h2>
|
||||||
|
<table>
|
||||||
|
<tr><td>Host</td><td><?= htmlspecialchars($db_host) ?>:<?= htmlspecialchars($db_port) ?></td></tr>
|
||||||
|
<tr><td>Database</td><td><?= htmlspecialchars($db_name) ?></td></tr>
|
||||||
|
<tr><td>Total Visits</td><td><?= $visit_count ?></td></tr>
|
||||||
|
<?php if ($error): ?>
|
||||||
|
<tr><td>Error</td><td style="color:#721c24"><?= htmlspecialchars($error) ?></td></tr>
|
||||||
|
<?php endif; ?>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="footer">
|
||||||
|
Deployed by AWX via Aether — <?= date('Y-m-d H:i:s T') ?>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
PHPAPP
|
||||||
|
echo "[OK] PHP application deployed"
|
||||||
|
|
||||||
|
# Remove default HTML page
|
||||||
|
rm -f /var/www/html/index.html /var/www/html/index.nginx-debian.html
|
||||||
|
|
||||||
|
# Detect PHP-FPM service name (e.g., php8.2-fpm)
|
||||||
|
PHP_VERSION=$(php -r 'echo PHP_MAJOR_VERSION.".".PHP_MINOR_VERSION;')
|
||||||
|
PHP_FPM_SVC="php${PHP_VERSION}-fpm"
|
||||||
|
|
||||||
|
# Restart services
|
||||||
|
systemctl enable nginx "$PHP_FPM_SVC"
|
||||||
|
systemctl restart "$PHP_FPM_SVC"
|
||||||
|
systemctl restart nginx
|
||||||
|
echo "[OK] Services started (PHP-FPM: $PHP_FPM_SVC)"
|
||||||
|
|
||||||
|
echo "=== Web tier setup complete ==="
|
||||||
|
|
||||||
|
- name: Generate web config file content
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
web_config_content: |
|
||||||
|
DB_HOST={{ db_host }}
|
||||||
|
DB_PORT={{ db_port }}
|
||||||
|
DB_NAME={{ db_name }}
|
||||||
|
DB_USER={{ db_user }}
|
||||||
|
DB_PASS={{ db_pass }}
|
||||||
|
INSTANCE_IP={{ ffsdn_instance_ip | default('unknown') }}
|
||||||
|
|
||||||
|
- name: Push web config to instance
|
||||||
|
ansible.builtin.uri:
|
||||||
|
url: "{{ incus_api }}/1.0/instances/{{ ffsdn_instance_name }}/files?path=/etc/webapp-web-config"
|
||||||
|
method: POST
|
||||||
|
client_cert: "{{ incus_cert }}"
|
||||||
|
client_key: "{{ incus_key }}"
|
||||||
|
validate_certs: false
|
||||||
|
body: "{{ web_config_content }}"
|
||||||
|
headers:
|
||||||
|
Content-Type: "application/octet-stream"
|
||||||
|
X-Incus-type: "file"
|
||||||
|
X-Incus-mode: "0644"
|
||||||
|
status_code: [200]
|
||||||
|
|
||||||
|
- name: Push web setup script
|
||||||
|
ansible.builtin.uri:
|
||||||
|
url: "{{ incus_api }}/1.0/instances/{{ ffsdn_instance_name }}/files?path=/tmp/webapp-web-setup.sh"
|
||||||
|
method: POST
|
||||||
|
client_cert: "{{ incus_cert }}"
|
||||||
|
client_key: "{{ incus_key }}"
|
||||||
|
validate_certs: false
|
||||||
|
body: "{{ web_setup_script }}"
|
||||||
|
headers:
|
||||||
|
Content-Type: "application/octet-stream"
|
||||||
|
X-Incus-type: "file"
|
||||||
|
X-Incus-mode: "0755"
|
||||||
|
status_code: [200]
|
||||||
|
|
||||||
|
- name: Execute web 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/bash", "/tmp/webapp-web-setup.sh"]
|
||||||
|
record-output: true
|
||||||
|
interactive: false
|
||||||
|
wait-for-websocket: false
|
||||||
|
environment:
|
||||||
|
DEBIAN_FRONTEND: "noninteractive"
|
||||||
|
status_code: [202]
|
||||||
|
return_content: true
|
||||||
|
register: web_setup_exec
|
||||||
|
|
||||||
|
- name: Wait for web setup to complete (up to 5 minutes)
|
||||||
|
ansible.builtin.uri:
|
||||||
|
url: "{{ incus_api }}{{ web_setup_exec.json.operation }}/wait?timeout=300"
|
||||||
|
method: GET
|
||||||
|
client_cert: "{{ incus_cert }}"
|
||||||
|
client_key: "{{ incus_key }}"
|
||||||
|
validate_certs: false
|
||||||
|
return_content: true
|
||||||
|
timeout: 320
|
||||||
|
register: web_setup_wait
|
||||||
|
failed_when: web_setup_wait.json.metadata.status != "Success"
|
||||||
|
|
||||||
|
- name: Verify web setup exit code
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- web_setup_wait.json.metadata.metadata.return | int == 0
|
||||||
|
fail_msg: "Web setup exited with code {{ web_setup_wait.json.metadata.metadata.return }}"
|
||||||
|
success_msg: "Web tier setup completed successfully"
|
||||||
|
|
||||||
|
# --- Step 4: Add ACL rule to allow this web instance to reach the DB ---
|
||||||
|
|
||||||
|
- name: Build db ACL name
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
db_acl_name: "52-{{ db_instance.name }}-aether-acl"
|
||||||
|
|
||||||
|
- name: Read current db ACL
|
||||||
|
ansible.builtin.uri:
|
||||||
|
url: "{{ incus_api }}/1.0/network-acls/{{ db_acl_name }}"
|
||||||
|
method: GET
|
||||||
|
client_cert: "{{ incus_cert }}"
|
||||||
|
client_key: "{{ incus_key }}"
|
||||||
|
validate_certs: false
|
||||||
|
return_content: true
|
||||||
|
status_code: [200, 404]
|
||||||
|
register: db_acl_response
|
||||||
|
|
||||||
|
- name: Build updated ingress rules (append web access)
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
updated_ingress: >-
|
||||||
|
{{ (db_acl_response.json.metadata.ingress | default([])) + [{
|
||||||
|
'action': 'allow',
|
||||||
|
'state': 'enabled',
|
||||||
|
'source': (ffsdn_instance_ip | default('')) + '/32',
|
||||||
|
'destination': db_instance.ip + '/32',
|
||||||
|
'protocol': 'tcp',
|
||||||
|
'destination_port': '3306',
|
||||||
|
'description': 'Allow ' + ffsdn_instance_name + ' to db on 3306'
|
||||||
|
}] }}
|
||||||
|
when: db_acl_response.status == 200
|
||||||
|
|
||||||
|
- name: Update db ACL with web access rule
|
||||||
|
ansible.builtin.uri:
|
||||||
|
url: "{{ incus_api }}/1.0/network-acls/{{ db_acl_name }}"
|
||||||
|
method: PATCH
|
||||||
|
client_cert: "{{ incus_cert }}"
|
||||||
|
client_key: "{{ incus_key }}"
|
||||||
|
validate_certs: false
|
||||||
|
body_format: json
|
||||||
|
body:
|
||||||
|
ingress: "{{ updated_ingress }}"
|
||||||
|
status_code: [200]
|
||||||
|
when:
|
||||||
|
- db_acl_response.status == 200
|
||||||
|
- updated_ingress is defined
|
||||||
|
|
||||||
|
- name: Report ACL update
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg: >-
|
||||||
|
{% if db_acl_response.status == 200 %}ACL '{{ db_acl_name }}' updated:
|
||||||
|
{{ ffsdn_instance_name }} ({{ ffsdn_instance_ip | default('?') }}) → {{ db_instance.name }}:3306
|
||||||
|
{% else %}ACL '{{ db_acl_name }}' not found — skipping ACL rule (Aether may not have created it yet){% endif %}
|
||||||
|
|
||||||
|
- 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/webapp-web-setup.sh"]
|
||||||
|
record-output: false
|
||||||
|
interactive: false
|
||||||
|
wait-for-websocket: false
|
||||||
|
status_code: [202]
|
||||||
|
ignore_errors: true
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
---
|
||||||
|
# staticsite-decommission.yml — Decommission staticsite blueprint instances
|
||||||
|
#
|
||||||
|
# Aether sends ffsdn_instances (a list of all instances in the deployment).
|
||||||
|
# This playbook loops over each instance for graceful cleanup.
|
||||||
|
|
||||||
|
- name: Staticsite decommission — graceful cleanup via Incus API
|
||||||
|
hosts: localhost
|
||||||
|
gather_facts: false
|
||||||
|
connection: local
|
||||||
|
|
||||||
|
vars:
|
||||||
|
incus_api: "https://192.168.102.140:8443"
|
||||||
|
incus_cert: "/runner/project/incus-client.crt"
|
||||||
|
incus_key: "/runner/project/incus-client.key"
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
- name: Validate required variables
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- ffsdn_instances is defined
|
||||||
|
- ffsdn_instances | length > 0
|
||||||
|
fail_msg: "ffsdn_instances must be provided by Aether"
|
||||||
|
|
||||||
|
- name: Decommission each instance
|
||||||
|
ansible.builtin.include_tasks: showcase/decommission/staticsite-instance.yml
|
||||||
|
loop: "{{ ffsdn_instances }}"
|
||||||
|
loop_control:
|
||||||
|
loop_var: instance
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
---
|
||||||
|
# staticsite-post-deploy.yml — Configure staticsite blueprint instances
|
||||||
|
#
|
||||||
|
# Aether sends ffsdn_instances (a list of all instances in the deployment).
|
||||||
|
# This playbook loops over each instance and includes the per-instance tasks.
|
||||||
|
|
||||||
|
- name: Staticsite post-deploy — configure instances via Incus API
|
||||||
|
hosts: localhost
|
||||||
|
gather_facts: false
|
||||||
|
connection: local
|
||||||
|
|
||||||
|
vars:
|
||||||
|
incus_api: "https://192.168.102.140:8443"
|
||||||
|
incus_cert: "/runner/project/incus-client.crt"
|
||||||
|
incus_key: "/runner/project/incus-client.key"
|
||||||
|
base_packages: "curl vim htop jq"
|
||||||
|
blueprint_name: "staticsite"
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
- name: Validate required variables
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- ffsdn_instances is defined
|
||||||
|
- ffsdn_instances | length > 0
|
||||||
|
fail_msg: "ffsdn_instances must be provided by Aether"
|
||||||
|
|
||||||
|
- name: Configure each instance
|
||||||
|
ansible.builtin.include_tasks: showcase/staticsite-deploy-instance.yml
|
||||||
|
loop: "{{ ffsdn_instances }}"
|
||||||
|
loop_control:
|
||||||
|
loop_var: instance
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
---
|
||||||
|
# webapp-decommission.yml — Decommission webapp blueprint instances
|
||||||
|
#
|
||||||
|
# Aether sends ffsdn_instances (a list of all instances in the deployment).
|
||||||
|
# This playbook loops over each instance and routes to component-specific cleanup.
|
||||||
|
|
||||||
|
- name: Webapp decommission — graceful cleanup via Incus API
|
||||||
|
hosts: localhost
|
||||||
|
gather_facts: false
|
||||||
|
connection: local
|
||||||
|
|
||||||
|
vars:
|
||||||
|
incus_api: "https://192.168.102.140:8443"
|
||||||
|
incus_cert: "/runner/project/incus-client.crt"
|
||||||
|
incus_key: "/runner/project/incus-client.key"
|
||||||
|
blueprint_name: "webapp"
|
||||||
|
decommission_tasks:
|
||||||
|
db: "showcase/decommission/webapp-db.yml"
|
||||||
|
web: "showcase/decommission/webapp-web.yml"
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
- name: Validate required variables
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- ffsdn_instances is defined
|
||||||
|
- ffsdn_instances | length > 0
|
||||||
|
fail_msg: "ffsdn_instances must be provided by Aether"
|
||||||
|
|
||||||
|
- name: Decommission each instance
|
||||||
|
ansible.builtin.include_tasks: showcase/decommission/webapp-instance.yml
|
||||||
|
loop: "{{ ffsdn_instances }}"
|
||||||
|
loop_control:
|
||||||
|
loop_var: instance
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
---
|
||||||
|
# webapp-post-deploy.yml — Configure webapp blueprint instances
|
||||||
|
#
|
||||||
|
# Aether sends ffsdn_instances (a list of all instances in the deployment).
|
||||||
|
# This playbook loops over each instance and dispatches to db or web tasks.
|
||||||
|
|
||||||
|
- name: Webapp post-deploy — configure instances via Incus API
|
||||||
|
hosts: localhost
|
||||||
|
gather_facts: false
|
||||||
|
connection: local
|
||||||
|
|
||||||
|
vars:
|
||||||
|
incus_api: "https://192.168.102.140:8443"
|
||||||
|
incus_cert: "/runner/project/incus-client.crt"
|
||||||
|
incus_key: "/runner/project/incus-client.key"
|
||||||
|
base_packages: "curl vim htop jq"
|
||||||
|
blueprint_name: "webapp"
|
||||||
|
valid_components: ["db", "web"]
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
- name: Validate required variables
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- ffsdn_instances is defined
|
||||||
|
- ffsdn_instances | length > 0
|
||||||
|
fail_msg: "ffsdn_instances must be provided by Aether"
|
||||||
|
|
||||||
|
- name: Configure each instance
|
||||||
|
ansible.builtin.include_tasks: showcase/webapp-deploy-instance.yml
|
||||||
|
loop: "{{ ffsdn_instances }}"
|
||||||
|
loop_control:
|
||||||
|
loop_var: instance
|
||||||
|
|
@ -797,7 +797,7 @@ action_configure() {
|
||||||
if [[ -f "$incus_cert" && -f "$incus_key" ]]; then
|
if [[ -f "$incus_cert" && -f "$incus_key" ]]; then
|
||||||
local task_pod
|
local task_pod
|
||||||
task_pod=$(vm_exec kubectl -n "$AWX_NAMESPACE" get pods \
|
task_pod=$(vm_exec kubectl -n "$AWX_NAMESPACE" get pods \
|
||||||
-l app.kubernetes.io/component=awx-task -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true)
|
-l app.kubernetes.io/name=awx-task -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true)
|
||||||
if [[ -n "$task_pod" ]]; then
|
if [[ -n "$task_pod" ]]; then
|
||||||
local project_dir="/var/lib/awx/projects/incus-contrib"
|
local project_dir="/var/lib/awx/projects/incus-contrib"
|
||||||
vm_exec kubectl -n "$AWX_NAMESPACE" exec "$task_pod" -- mkdir -p "$project_dir"
|
vm_exec kubectl -n "$AWX_NAMESPACE" exec "$task_pod" -- mkdir -p "$project_dir"
|
||||||
|
|
@ -841,11 +841,17 @@ action_configure() {
|
||||||
if [[ -n "$project_id" ]]; then
|
if [[ -n "$project_id" ]]; then
|
||||||
local task_pod
|
local task_pod
|
||||||
task_pod=$(vm_exec kubectl -n "$AWX_NAMESPACE" get pods \
|
task_pod=$(vm_exec kubectl -n "$AWX_NAMESPACE" get pods \
|
||||||
-l app.kubernetes.io/component=awx-task -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true)
|
-l app.kubernetes.io/name=awx-task -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true)
|
||||||
if [[ -n "$task_pod" ]]; then
|
if [[ -n "$task_pod" ]]; then
|
||||||
local project_dir="/var/lib/awx/projects/incus-contrib"
|
local project_dir="/var/lib/awx/projects/incus-contrib"
|
||||||
vm_exec kubectl -n "$AWX_NAMESPACE" exec "$task_pod" -- mkdir -p "${project_dir}/playbooks"
|
vm_exec kubectl -n "$AWX_NAMESPACE" exec "$task_pod" -- \
|
||||||
for playbook in post-deploy.yml decommission.yml; do
|
mkdir -p "${project_dir}/playbooks" \
|
||||||
|
"${project_dir}/playbooks/showcase" \
|
||||||
|
"${project_dir}/playbooks/showcase/decommission"
|
||||||
|
for playbook in post-deploy.yml decommission.yml \
|
||||||
|
webapp-post-deploy.yml webapp-decommission.yml \
|
||||||
|
staticsite-post-deploy.yml staticsite-decommission.yml \
|
||||||
|
k3scluster-post-deploy.yml k3scluster-decommission.yml; do
|
||||||
local pb_path="${SCRIPT_DIR}/../ansible/playbooks/${playbook}"
|
local pb_path="${SCRIPT_DIR}/../ansible/playbooks/${playbook}"
|
||||||
if [[ -f "$pb_path" ]]; then
|
if [[ -f "$pb_path" ]]; then
|
||||||
local pb_b64
|
local pb_b64
|
||||||
|
|
@ -857,6 +863,32 @@ action_configure() {
|
||||||
warn "Playbook not found: ${pb_path}"
|
warn "Playbook not found: ${pb_path}"
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
# Push showcase task files
|
||||||
|
for taskfile in showcase/common.yml showcase/helpers.yml \
|
||||||
|
showcase/remove-setup-nic.yml \
|
||||||
|
showcase/webapp-db.yml showcase/webapp-web.yml \
|
||||||
|
showcase/staticsite-site.yml \
|
||||||
|
showcase/k3s-control.yml showcase/k3s-worker.yml \
|
||||||
|
showcase/staticsite-deploy-instance.yml \
|
||||||
|
showcase/webapp-deploy-instance.yml \
|
||||||
|
showcase/k3scluster-deploy-instance.yml \
|
||||||
|
showcase/decommission/webapp-db.yml \
|
||||||
|
showcase/decommission/webapp-web.yml \
|
||||||
|
showcase/decommission/k3s.yml \
|
||||||
|
showcase/decommission/staticsite-instance.yml \
|
||||||
|
showcase/decommission/webapp-instance.yml \
|
||||||
|
showcase/decommission/k3scluster-instance.yml; do
|
||||||
|
local tf_path="${SCRIPT_DIR}/../ansible/playbooks/${taskfile}"
|
||||||
|
if [[ -f "$tf_path" ]]; then
|
||||||
|
local tf_b64
|
||||||
|
tf_b64=$(base64 -w0 "$tf_path")
|
||||||
|
vm_exec kubectl -n "$AWX_NAMESPACE" exec "$task_pod" -- \
|
||||||
|
bash -c "echo '${tf_b64}' | base64 -d > ${project_dir}/playbooks/${taskfile}"
|
||||||
|
detail "Pushed ${taskfile}"
|
||||||
|
else
|
||||||
|
warn "Task file not found: ${tf_path}"
|
||||||
|
fi
|
||||||
|
done
|
||||||
success "Playbooks pushed to AWX project directory"
|
success "Playbooks pushed to AWX project directory"
|
||||||
else
|
else
|
||||||
warn "Cannot find AWX task pod — push playbooks manually"
|
warn "Cannot find AWX task pod — push playbooks manually"
|
||||||
|
|
@ -934,11 +966,56 @@ print(json.dumps(d))")
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
|
# 6. Create per-blueprint job templates
|
||||||
|
step "Creating blueprint job templates"
|
||||||
|
for tmpl_name in webapp-post-deploy webapp-decommission \
|
||||||
|
staticsite-post-deploy staticsite-decommission \
|
||||||
|
k3scluster-post-deploy k3scluster-decommission; do
|
||||||
|
local tmpl_id
|
||||||
|
tmpl_id=$(awx_auth GET "/api/v2/job_templates/?name=${tmpl_name}" 2>/dev/null \
|
||||||
|
| python3 -c "import sys,json; r=json.load(sys.stdin); print(r['results'][0]['id'] if r.get('count',0)>0 else '')" 2>/dev/null || true)
|
||||||
|
|
||||||
|
if [[ -z "$tmpl_id" ]]; then
|
||||||
|
local tmpl_payload
|
||||||
|
tmpl_payload=$(python3 -c "
|
||||||
|
import json
|
||||||
|
d = {
|
||||||
|
'name': '${tmpl_name}',
|
||||||
|
'organization': 1,
|
||||||
|
'job_type': 'run',
|
||||||
|
'ask_variables_on_launch': True,
|
||||||
|
'playbook': '${PLAYBOOK_DIR}/${tmpl_name}.yml'
|
||||||
|
}
|
||||||
|
pid = '${project_id}'
|
||||||
|
iid = '${inventory_id}'
|
||||||
|
cid = '${machine_cred_id}'
|
||||||
|
if pid: d['project'] = int(pid)
|
||||||
|
if iid: d['inventory'] = int(iid)
|
||||||
|
print(json.dumps(d))")
|
||||||
|
|
||||||
|
tmpl_id=$(awx_auth POST "/api/v2/job_templates/" -d "$tmpl_payload" 2>/dev/null \
|
||||||
|
| python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
|
||||||
|
|
||||||
|
# Associate credential
|
||||||
|
if [[ -n "$tmpl_id" && -n "$machine_cred_id" ]]; then
|
||||||
|
awx_auth POST "/api/v2/job_templates/${tmpl_id}/credentials/" \
|
||||||
|
-d "{\"id\":${machine_cred_id}}" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
[[ -n "$tmpl_id" ]] && success "Template '${tmpl_name}' created (ID: ${tmpl_id})" || warn "Failed to create template '${tmpl_name}'"
|
||||||
|
else
|
||||||
|
success "Template '${tmpl_name}' exists (ID: ${tmpl_id})"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
echo
|
echo
|
||||||
success "AWX configuration complete"
|
success "AWX configuration complete"
|
||||||
echo
|
echo
|
||||||
info "Template IDs (for Aether binding):"
|
info "Template IDs (for Aether binding):"
|
||||||
for tmpl_name in post-deploy decommission; do
|
for tmpl_name in post-deploy decommission \
|
||||||
|
webapp-post-deploy webapp-decommission \
|
||||||
|
staticsite-post-deploy staticsite-decommission \
|
||||||
|
k3scluster-post-deploy k3scluster-decommission; do
|
||||||
local tid
|
local tid
|
||||||
tid=$(awx_auth GET "/api/v2/job_templates/?name=${tmpl_name}" 2>/dev/null \
|
tid=$(awx_auth GET "/api/v2/job_templates/?name=${tmpl_name}" 2>/dev/null \
|
||||||
| python3 -c "import sys,json; r=json.load(sys.stdin); print(r['results'][0]['id'] if r.get('count',0)>0 else '')" 2>/dev/null || true)
|
| python3 -c "import sys,json; r=json.load(sys.stdin); print(r['results'][0]['id'] if r.get('count',0)>0 else '')" 2>/dev/null || true)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue