Files
spinifex/scripts/ansible/roles/vpc/tasks/present.yml
T
Josh Siviour ae6fcfe7a1 Ansible dev extensions (#254)
* chore(ansible): add dev-status playbook (read-only health doctor)

New `dev-status` role + playbook + Makefile target. Read-only snapshot of
spinifex.target, failed units, port listeners, OVN/OVS state, gateway
round-trip, AMI/keypair/volume/instance counts, and build-vs-install drift
for spx + nbdkit plugin + microvm artifacts. Pairs with dev-preflight as
the post-install / running-system diagnostic.

Plan: docs/development/improvements/ansible-dev-extensions.md (mulga).
Bead: mulga-siv-86.1.

* chore(ansible): add dev-logs playbook (bug-report bundle)

New `logs` role + playbook + Makefile target. Captures journalctl for
spinifex-* / ovn-* / openvswitch-switch / qemu / nbdkit, copies
/var/log/spinifex/, snapshots systemctl status + networking (ip/ss/ovs/
ovn show) + spx/plugin/microvm version manifest + dmesg, then bundles
into /tmp/spinifex-logs-<host>-<ts>.tar.gz owned by the invoking user.

Tail mode deliberately omitted — devs should `journalctl -u 'spinifex-*' -f`
directly; ansible captures stdout and is the wrong tool for follow.

Overrides: -e logs_since=10min, -e logs_output_dir=/tmp,
-e logs_include_dmesg=false.

Plan: docs/development/improvements/ansible-dev-extensions.md (mulga).
Bead: mulga-siv-86.2.

* chore(ansible): add dev-snapshot / dev-restore (state checkpoint)

New snapshot + restore roles + playbooks + Makefile targets.

dev-snapshot quiesces the full spinifex+OVN+OVS stack in dependency
order (spinifex.target → ovn-controller → ovn-central →
openvswitch-switch), kills lingering QEMU VMs, then tars /etc/spinifex,
/var/lib/spinifex (viperblock volumes included), /var/lib/ovn (NB+SB
DBs), and /etc/openvswitch/conf.db into /var/lib/spinifex-snapshots/
<name>.tar.gz with a sibling .manifest, restarts services in reverse,
and waits for the AWS gateway to come back.

dev-restore inverses: stop services, wipe the same path set, untar the
named bundle, copy /etc/spinifex/ca.pem back into the system trust
store and rebuild the CA bundle (the snapshot doesn't include
/usr/local/share/ca-certificates/spinifex-ca.crt), daemon-reload,
restart in reverse order.

Both refuse to run without -e snapshot_name=<name>, validate the name
matches [A-Za-z0-9._-]+, and produce honest summaries when the gateway
fails to come back (does not mask other ongoing issues).

Tested on dev box: snapshot/restore of 2.4 GiB state (large viperblock
volumes) completes cleanly with no data drift; services all return to
active.

Plan: docs/development/improvements/ansible-dev-extensions.md (mulga).
Bead: mulga-siv-86.3.

* fix(ansible): wrap aws/spx calls in sg spinifex to fix group inheritance

Adds scripts/ansible/scripts/aws-as-spinifex.sh and spx-as-spinifex.sh
helpers that re-enter the command under `sg spinifex -c '...'` with
AWS_PROFILE=spinifex preset. Swaps every AWS / spx admin task across
the status, init, deploy, smoketest, snapshot, and restore roles to
use these via spinifex_aws_cli / spinifex_spx_cli vars.

Root cause (mulga-siv-92): ansible's `become_user` is a no-op when the
target user equals the calling user, so sudo never runs and
supplementary groups are inherited from the operator's invoking shell
rather than re-initialised via initgroups(3). On shells that predate
setup.sh's spinifex group add, the spinifex group is missing and
/etc/spinifex/ca.pem (drwxr-x--- root:spinifex, the AWS CLI's
ca_bundle) is unreadable — aws cli fails with SSL Errno 13. The
helpers re-enter the command in a shell that explicitly assumes the
spinifex group via sg(1), bypassing the issue regardless of how
ansible-playbook is invoked.

Side benefit: drops 50 lines of repeated `become: true / become_user /
environment: AWS_PROFILE` boilerplate across the roles.

Closes mulga-siv-92.

* chore(ansible): add dev-version playbook (build vs install drift)

New `version` role + playbook + Makefile target. Compares sha256 of
built artifacts (bin/spx, viperblock/lib/nbdkit-viperblock-plugin.so,
build/microvm/{vmlinuz,initramfs.cpio.gz}) against their installed
counterparts (/usr/local/bin/spx, plugindir, /usr/share/spinifex/
microvm/*). Prints per-artifact drift verdict plus sub-repo HEAD
short-revs (spinifex/viperblock/predastore) for context. Closes with
either "DRIFT detected — run: make ansible-dev-deploy" or "No drift".

Pure read-only. Faster than dev-status (no AWS/OVN/port probing).

Plan: docs/development/improvements/ansible-dev-extensions.md (mulga).
Bead: mulga-siv-86.9.

* chore(ansible): add missing roles/deploy/meta/main.yml

All other roles have meta/main.yml; deploy was the lone outlier.
Consistency fix. Closes mulga-siv-86.10.

* chore(ansible): add dev-vpc playbook (idempotent dev VPC fixture)

New `vpc` role + playbook + Makefile target. Idempotent
present/absent dispatch creates a deterministic spinifex-dev-vpc with
public subnet (10.99.1.0/24, map-public-ip-on-launch), internet
gateway, route table, and default route. Hooks into smoketest's launch
path — smoketest's existing
Name=map-public-ip-on-launch,Values=true filter now picks it up
instead of falling through to the "no subnet → skip launch" branch.

Idempotency: each step describe-then-create, looking up by Name tag
where possible. Re-running on a converged VPC is changed=0, skipped=5
(the create tasks).

Workaround for mulga-siv-94 (gateway silently drops
--tag-specifications and no-ops create-tags for route tables): role
falls back to "RT associated with our subnet" lookup so tag absence
doesn't break idempotency. create-tags task is kept as a no-op-now
forward-looking step for when the gateway gap is closed.

Teardown (-e dev_vpc_state=absent) enumerates ALL non-main route
tables in the VPC (not just the tagged one), so orphans from prior
runs that landed before the fallback was added get cleaned up too.

Tested end-to-end against josh-banksia dev gateway: create / re-run
(idempotent) / teardown all converge cleanly.

Plan: docs/development/improvements/ansible-dev-extensions.md (mulga).
Bead: mulga-siv-86.4.
2026-05-26 16:14:11 +10:00

210 lines
7.8 KiB
YAML

---
# Idempotent create. Every step describes-then-creates: re-running on an
# already-converged VPC produces zero changes. Tag everything with
# Name={{ dev_vpc_name }} so a single tag query finds the whole graph.
- name: vpc — find existing VPC by Name tag
ansible.builtin.command: >-
{{ spinifex_aws_cli }} ec2 describe-vpcs
--filters Name=tag:Name,Values={{ dev_vpc_name }}
--query Vpcs[0].VpcId --output text
register: vpc_existing
changed_when: false
- name: vpc — create VPC if missing
ansible.builtin.command: >-
{{ spinifex_aws_cli }} ec2 create-vpc
--cidr-block {{ dev_vpc_cidr }}
--tag-specifications
ResourceType=vpc,Tags=[{Key=Name,Value={{ dev_vpc_name }}}]
--query Vpc.VpcId --output text
register: vpc_created
when: vpc_existing.stdout | trim in ['', 'None']
changed_when: true
- name: vpc — set VPC id fact
ansible.builtin.set_fact:
dev_vpc_id: >-
{{ vpc_created.stdout | default('') | trim
if (vpc_existing.stdout | trim in ['', 'None'])
else vpc_existing.stdout | trim }}
- name: vpc — assert VPC id resolved
ansible.builtin.assert:
that: dev_vpc_id | length > 0
fail_msg: "VPC id could not be resolved after describe/create"
- name: vpc — find existing IGW attached to this VPC
ansible.builtin.command: >-
{{ spinifex_aws_cli }} ec2 describe-internet-gateways
--filters Name=attachment.vpc-id,Values={{ dev_vpc_id }}
--query InternetGateways[0].InternetGatewayId --output text
register: vpc_igw_existing
changed_when: false
- name: vpc — create IGW if missing
ansible.builtin.command: >-
{{ spinifex_aws_cli }} ec2 create-internet-gateway
--tag-specifications
ResourceType=internet-gateway,Tags=[{Key=Name,Value={{ dev_vpc_name }}-igw}]
--query InternetGateway.InternetGatewayId --output text
register: vpc_igw_created
when: vpc_igw_existing.stdout | trim in ['', 'None']
changed_when: true
- name: vpc — set IGW id fact
ansible.builtin.set_fact:
dev_vpc_igw_id: >-
{{ vpc_igw_created.stdout | default('') | trim
if (vpc_igw_existing.stdout | trim in ['', 'None'])
else vpc_igw_existing.stdout | trim }}
- name: vpc — attach IGW to VPC if needed
ansible.builtin.command: >-
{{ spinifex_aws_cli }} ec2 attach-internet-gateway
--internet-gateway-id {{ dev_vpc_igw_id }}
--vpc-id {{ dev_vpc_id }}
register: vpc_igw_attach
when: vpc_igw_existing.stdout | trim in ['', 'None']
changed_when: true
failed_when:
- vpc_igw_attach.rc | default(0) != 0
- "'Resource.AlreadyAssociated' not in (vpc_igw_attach.stderr | default(''))"
- name: vpc — find existing subnet by Name tag
ansible.builtin.command: >-
{{ spinifex_aws_cli }} ec2 describe-subnets
--filters Name=vpc-id,Values={{ dev_vpc_id }}
Name=tag:Name,Values={{ dev_vpc_name }}-public
--query Subnets[0].SubnetId --output text
register: vpc_subnet_existing
changed_when: false
- name: vpc — create subnet if missing
ansible.builtin.command: >-
{{ spinifex_aws_cli }} ec2 create-subnet
--vpc-id {{ dev_vpc_id }}
--cidr-block {{ dev_vpc_subnet_cidr }}
--availability-zone {{ dev_vpc_subnet_az }}
--tag-specifications
ResourceType=subnet,Tags=[{Key=Name,Value={{ dev_vpc_name }}-public}]
--query Subnet.SubnetId --output text
register: vpc_subnet_created
when: vpc_subnet_existing.stdout | trim in ['', 'None']
changed_when: true
- name: vpc — set subnet id fact
ansible.builtin.set_fact:
dev_vpc_subnet_id: >-
{{ vpc_subnet_created.stdout | default('') | trim
if (vpc_subnet_existing.stdout | trim in ['', 'None'])
else vpc_subnet_existing.stdout | trim }}
- name: vpc — ensure subnet has map-public-ip-on-launch
ansible.builtin.command: >-
{{ spinifex_aws_cli }} ec2 modify-subnet-attribute
--subnet-id {{ dev_vpc_subnet_id }}
--map-public-ip-on-launch
register: vpc_subnet_mapip
changed_when: vpc_subnet_existing.stdout | trim in ['', 'None']
failed_when: false
# Try tag-based lookup first. If the gateway didn't tag a previously
# created RT (spinifex gateway silently drops --tag-specifications on
# create-route-table AND no-ops create-tags for route tables — see
# mulga-siv-94; works correctly for subnets), fall back to "RT
# associated with our subnet" which doesn't need tags.
- name: vpc — find existing route table by Name tag
ansible.builtin.command: >-
{{ spinifex_aws_cli }} ec2 describe-route-tables
--filters Name=vpc-id,Values={{ dev_vpc_id }}
Name=tag:Name,Values={{ dev_vpc_name }}-public
--query RouteTables[0].RouteTableId --output text
register: vpc_rt_existing
changed_when: false
- name: vpc — fallback lookup by subnet association
ansible.builtin.command: >-
{{ spinifex_aws_cli }} ec2 describe-route-tables
--filters Name=association.subnet-id,Values={{ dev_vpc_subnet_id }}
--query RouteTables[0].RouteTableId --output text
register: vpc_rt_byassoc
changed_when: false
when: vpc_rt_existing.stdout | trim in ['', 'None']
- name: vpc — create route table if neither lookup found one
ansible.builtin.command: >-
{{ spinifex_aws_cli }} ec2 create-route-table
--vpc-id {{ dev_vpc_id }}
--query RouteTable.RouteTableId --output text
register: vpc_rt_created
when:
- vpc_rt_existing.stdout | trim in ['', 'None']
- (vpc_rt_byassoc.stdout | default('') | trim) in ['', 'None']
changed_when: true
- name: vpc — set route-table id fact
ansible.builtin.set_fact:
dev_vpc_rt_id: >-
{%- if (vpc_rt_existing.stdout | trim) not in ['', 'None'] -%}
{{ vpc_rt_existing.stdout | trim }}
{%- elif (vpc_rt_byassoc.stdout | default('') | trim) not in ['', 'None'] -%}
{{ vpc_rt_byassoc.stdout | default('') | trim }}
{%- else -%}
{{ vpc_rt_created.stdout | default('') | trim }}
{%- endif -%}
- name: vpc — assert route-table id resolved
ansible.builtin.assert:
that: dev_vpc_rt_id | length > 0
fail_msg: "Route table id could not be resolved after describe/create"
# Spinifex gateway dropped --tag-specifications on the create call;
# apply via create-tags so re-runs can find the RT via tag filter.
- name: vpc — tag route table (workaround for create-tag-specifications gap)
ansible.builtin.command: >-
{{ spinifex_aws_cli }} ec2 create-tags
--resources {{ dev_vpc_rt_id }}
--tags Key=Name,Value={{ dev_vpc_name }}-public
changed_when: false
failed_when: false
- name: vpc — add default route via IGW (ignore RouteAlreadyExists)
ansible.builtin.command: >-
{{ spinifex_aws_cli }} ec2 create-route
--route-table-id {{ dev_vpc_rt_id }}
--destination-cidr-block 0.0.0.0/0
--gateway-id {{ dev_vpc_igw_id }}
register: vpc_route_add
changed_when:
- vpc_route_add.rc == 0
- "'RouteAlreadyExists' not in (vpc_route_add.stderr | default(''))"
failed_when:
- vpc_route_add.rc != 0
- "'RouteAlreadyExists' not in (vpc_route_add.stderr | default(''))"
- name: vpc — associate subnet with route table (ignore already-associated)
ansible.builtin.command: >-
{{ spinifex_aws_cli }} ec2 associate-route-table
--route-table-id {{ dev_vpc_rt_id }}
--subnet-id {{ dev_vpc_subnet_id }}
register: vpc_assoc
changed_when:
- vpc_assoc.rc == 0
- "'Resource.AlreadyAssociated' not in (vpc_assoc.stderr | default(''))"
failed_when:
- vpc_assoc.rc != 0
- "'Resource.AlreadyAssociated' not in (vpc_assoc.stderr | default(''))"
- name: vpc — summary (present)
ansible.builtin.debug:
msg: |
Dev VPC ready:
VPC : {{ dev_vpc_id }} ({{ dev_vpc_cidr }})
IGW : {{ dev_vpc_igw_id }}
Subnet : {{ dev_vpc_subnet_id }} ({{ dev_vpc_subnet_cidr }} @ {{ dev_vpc_subnet_az }}, public)
Route table : {{ dev_vpc_rt_id }}
Smoketest now finds this subnet via its existing
Name=map-public-ip-on-launch,Values=true filter.