Files
spinifex/tests/e2e/TEST_COVERAGE.md
T
Ben Duncan 48012a4058 refactor: rename all documentation from hive to spinifex
- Rename .claude/rules/hive.md → spinifex.md
- Update CLAUDE.md component description
- Rename doc directories: hive-admin-cli → spinifex-admin-cli, etc.
- Rename doc files: hive-cluster-stats, hive-code-consolidation, etc.
- Bulk update all markdown files across docs/ tree
- Update internal docs: CLUSTER-INSTALL, TESTING, DEVNOTES, BRANCHES
2026-03-18 01:19:05 +00:00

28 KiB

E2E Test Coverage

Single-Node (run-e2e.sh)

Phase 1: Environment Setup

  • KVM support check
  • spx admin init (region/az/node config)
  • CA certificate trust
  • Start all services (start-dev.sh)
  • Wait for AWS gateway on localhost:9999

Phase 1b: Cluster Stats CLI

  • spx get nodes — verify node shows Ready
  • spx top nodes — verify CPU/MEM resource stats
  • spx get vms — verify "No VMs found" before any launches

Phase 2: Discovery & Metadata

  • describe-regions
  • describe-availability-zones (verify zone name and state)
  • describe-instance-types (discover available types)
  • Select nano instance type and detect architecture

Phase 2b: Serial Console Access Settings

  • get-serial-console-access-status (verify default disabled)
  • enable-serial-console-access (verify returns true, get confirms true)
  • disable-serial-console-access (verify returns false, get confirms false)

Phase 3: SSH Key Management

  • create-key-pair (test-key-1, verify private key material)
  • import-key-pair (test-key-2, from local RSA key)
  • describe-key-pairs (verify both keys exist)
  • delete-key-pair (test-key-2, verify only test-key-1 remains)

Phase 4: Image Management

  • spx admin images import (file-based, architecture-aware)
  • describe-images (verify AMI by ID)

Phase 5: Instance Lifecycle

  • run-instances (launch VM with key pair)
  • describe-instances (poll pending -> running)

Phase 5a-pre: Cluster Stats CLI (with running VM)

  • spx get vms — verify running instance appears in output

Phase 5a: Instance Metadata Validation

  • describe-instances — verify InstanceType matches requested type
  • Verify KeyName matches requested key
  • Verify ImageId matches requested AMI
  • Verify at least 1 BlockDeviceMapping present

Phase 5a-ii: SSH Connectivity & Volume Verification

  • SSH into instance via QEMU hostfwd port
  • Verify SSH connectivity (id command returns ec2-user)
  • Verify root volume size from inside VM matches API-reported size (lsblk vs describe-volumes)
  • Verify VM hostname

Phase 5a-iii: Console Output

  • get-console-output succeeds (verify InstanceId in response)

Phase 5 (cont): Root Volume

  • describe-volumes (verify root volume attached)

Phase 5b: Volume Lifecycle

  • create-volume (10GB, ap-southeast-2a)
  • modify-volume (resize to 20GB, poll to verify)
  • attach-volume (to running instance, /dev/sdf)
  • describe-volumes (verify in-use + attached state)
  • detach-volume (verify available state)
  • delete-volume (verify gone)

Phase 5b-ii: DescribeVolumeStatus

  • describe-volume-status (on root volume, verify VolumeId in response)

Phase 5c: Snapshot Lifecycle

  • Uses root volume already attached to running instance (snapshots require a mounted VB instance)
  • create-snapshot (from attached root volume, with description)
  • Verify create response fields (VolumeId, VolumeSize, State, Progress)
  • describe-snapshots (by ID, verify VolumeId/Size/Description)
  • copy-snapshot (with new description, verify distinct ID)
  • describe-snapshots (verify both original + copy visible)
  • delete-snapshot (original, verify gone while copy survives)
  • delete-snapshot (copy, cleanup)

Phase 5d: Verify Snapshot-Backed Instance Launch

  • All run-instances calls use the snapshot path (cloneAMIToVolume -> OpenFromSnapshot), so the Phase 5 instance is already snapshot-backed
  • Verify AMI snapshot exists in Predastore (snap-{amiId}/config.json)
  • Read Phase 5 root volume's config.json from Predastore
  • Verify SnapshotID and SourceVolumeName are set (proves zero-copy clone)

Phase 5e: CreateImage Lifecycle

  • create-image (from running instance, with name and description)
  • Verify returned ImageId is non-empty
  • describe-images (verify custom AMI name and state)
  • Extract backing snapshot ID from Predastore config (for cleanup before termination)

Phase 6: Tag Management

  • create-tags (3 tags on instance)
  • describe-tags (filter by resource-id)
  • create-tags (2 tags on volume)
  • describe-tags (filter by key)
  • describe-tags (filter by resource-type)
  • create-tags (overwrite existing tag value)
  • delete-tags (unconditional by key)
  • delete-tags (with wrong value — should be no-op)
  • delete-tags (with correct value)
  • Verify final tag count

Phase 7: Instance State Transitions

  • stop-instances (poll -> stopped)

Phase 7a: Attach Volume to Stopped Instance (Error Path)

  • create-volume (for attach test)
  • attach-volume to stopped instance (expect IncorrectInstanceState error)
  • delete-volume (cleanup)

Phase 7b: ModifyInstanceAttribute

  • modify-instance-attribute (change instance type from nano → xlarge while stopped)
  • describe-instances (verify type updated in KV)
  • describe-instance-types (get expected vCPU count and memory for new type)
  • start-instances (poll -> running with new type)
  • SSH: nproc — verify vCPU count matches xlarge (4 vCPUs)
  • SSH: /proc/meminfo MemTotal — verify memory matches xlarge (within 85% of expected)

Phase 7c: RunInstances with count > 1

  • run-instances --count 2 (launch 2 instances in a single call)
  • Verify 2 instances returned in response
  • Poll both to running state
  • terminate-instances (both, poll -> terminated)

Phase 8: Negative / Error Path Tests

  • run-instances with malformed AMI ID (expect InvalidAMIID.Malformed)
  • run-instances with invalid instance type (expect InvalidInstanceType)
  • attach-volume on in-use volume (expect VolumeInUse)
  • detach-volume on boot volume (expect OperationNotPermitted)
  • delete-snapshot on non-existent snapshot (expect InvalidSnapshot.NotFound)
  • Unsupported Action via raw HTTP (expect InvalidAction or error response)
  • run-instances with non-existent AMI ID (expect InvalidAMIID.NotFound)
  • run-instances with non-existent key pair (expect InvalidKeyPair.NotFound)
  • delete-volume on non-existent volume (expect InvalidVolume.NotFound)
  • create-key-pair with duplicate name (expect InvalidKeyPair.Duplicate)
  • import-key-pair with duplicate name (expect InvalidKeyPair.Duplicate)
  • import-key-pair with invalid key format (expect InvalidKey.Format)
  • describe-volumes with non-existent volume ID (expect InvalidVolume.NotFound)
  • describe-images with non-existent AMI ID (expect InvalidAMIID.NotFound)
  • create-image with duplicate name (expect InvalidAMIName.Duplicate)
  • delete-key-pair on non-existent key (expect success — idempotent, matches AWS)
  • modify-instance-attribute on running instance (expect InvalidInstanceID.NotFound — running instances not in stopped KV)

IAM Phase 1: User CRUD

  • Root auth via iam list-users (root user exists)
  • create-user (alice, bob with path)
  • Duplicate user (expect EntityAlreadyExists)
  • get-user (alice)
  • get-user non-existent (expect NoSuchEntity)
  • list-users (verify count)
  • list-users with --path-prefix filter

IAM Phase 2: Access Key Lifecycle

  • create-access-key (alice key 1, key 2)
  • Third key exceeds limit (expect LimitExceeded)
  • create-access-key for non-existent user (expect NoSuchEntity)
  • list-access-keys (alice: 2 keys, bob: 0 keys)
  • update-access-key (Inactive, verify status, reactivate)
  • delete-access-key (verify count decremented)

IAM Phase 3: User Authentication

  • Configure IAM user profile with access key
  • Deactivate key → auth rejected (InvalidClientTokenId)
  • Reactivate key
  • Wrong secret → SignatureDoesNotMatch
  • Non-existent key ID → InvalidClientTokenId
  • Multi-user simultaneous auth (root + bob)

IAM Phase 4: Policy CRUD

  • create-policy (EC2ReadOnly, FullAdmin, DenyTerminate, IAMReadOnly, EC2DescribeAll)
  • Duplicate policy (expect EntityAlreadyExists)
  • Malformed JSON (expect MalformedPolicyDocument)
  • get-policy (by ARN)
  • get-policy non-existent (expect NoSuchEntity)
  • get-policy-version (v1)
  • list-policies (verify count)

IAM Phase 5: Policy Attachment & Enforcement

  • Create charlie with access key
  • attach-user-policy (alice: EC2ReadOnly + IAMReadOnly, bob: DenyTerminate)
  • list-attached-user-policies (verify count)
  • Idempotent attach (no duplicate)
  • Attach non-existent policy (expect NoSuchEntity)
  • Attach to non-existent user (expect NoSuchEntity)
  • Default Deny: charlie (no policies) → AccessDenied on ec2 + iam
  • Explicit Allow: alice → ec2:Describe{Instances,Vpcs} allowed, ec2:DescribeKeyPairs denied, iam:ListUsers allowed, iam:CreateUser denied
  • Deny Override: bob → ec2:Describe allowed, ec2:TerminateInstances denied (explicit Deny), iam denied
  • Root Bypass: root user → all actions succeed
  • Prefix Wildcard: swap alice to EC2DescribeAll (ec2:Describe*) → Describe* allowed, non-Describe denied
  • FullAdmin: charlie with FullAdmin → all actions allowed

IAM Phase 6: Policy Lifecycle — Detach & Delete

  • detach-user-policy → user loses access
  • delete-policy while attached (expect DeleteConflict)
  • Detach then delete → get-policy returns NoSuchEntity

IAM Phase 7: IAM Cleanup

  • Delete all test users (remove keys + policies first)
  • Verify root-only remains (list-users count = 1)
  • Delete all test policies (list-policies count = 0)
  • Clean up AWS CLI profiles

Phase 8: EC2 Account Scoping

Tests that EC2 resources are properly isolated between tenant accounts (Alpha, Beta). Based on docs/development/feature/iam-phase4-e2e-test-guide.md. Skips CreateImage (mulga-612) and instance tags (mulga-613).

Step 1: Account Setup

  • spx admin account create (Team Alpha, Team Beta)
  • Configure AWS CLI profiles
  • Verify auth for both accounts

Step 2: Instance Scoping

  • Alpha + Beta each launch 1 instance
  • describe-instances isolation (each sees only own)
  • OwnerId field matches account ID
  • Cross-account stop-instances → InvalidInstanceID.NotFound
  • Cross-account terminate-instances → InvalidInstanceID.NotFound
  • Cross-account start-instances → InvalidInstanceID.NotFound
  • Cross-account modify-instance-attribute → InvalidInstanceID.NotFound
  • Cross-account get-console-output → InvalidInstanceID.NotFound

Step 3: Volume Scoping

  • Alpha + Beta each create 1 volume
  • describe-volumes isolation
  • Cross-account describe-volumes by ID → InvalidVolume.NotFound
  • Cross-account delete-volume → InvalidVolume.NotFound
  • Cross-account attach-volume → InvalidVolume.NotFound
  • Cross-account detach-volume → InvalidVolume.NotFound
  • Cross-account modify-volume → InvalidVolume.NotFound

Step 4: Key Pair Scoping

  • Alpha + Beta each create key pair
  • describe-key-pairs isolation
  • Same key name in both accounts (namespace isolation — different KeyPairIds)
  • Cross-account delete-key-pair → no effect on other account's key
  • import-key-pair → invisible to other account

Step 5: Snapshot Scoping

  • Alpha + Beta each create snapshot from own volume
  • describe-snapshots isolation + OwnerId verification
  • Cross-account delete-snapshot → UnauthorizedOperation
  • Cross-account create-snapshot from other's volume → InvalidVolume.NotFound

Step 6: VPC/Subnet Scoping

  • Alpha + Beta each create VPC (same CIDR — no conflict)
  • describe-vpcs isolation
  • Cross-account describe-vpcs by ID → InvalidVpcID.NotFound
  • Cross-account delete-vpc → InvalidVpcID.NotFound
  • Alpha + Beta each create subnet
  • describe-subnets isolation
  • Cross-account create-subnet in other's VPC → InvalidVpcID.NotFound
  • Cross-account delete-subnet → InvalidSubnetID.NotFound

Step 7: IGW + EIGW Scoping

  • Alpha + Beta each create IGW
  • describe-internet-gateways isolation
  • Cross-account describe-internet-gateways by ID → InvalidInternetGatewayID.NotFound
  • Cross-account delete-internet-gateway → InvalidInternetGatewayID.NotFound
  • Cross-account attach-internet-gateway → InvalidInternetGatewayID.NotFound
  • Cross-account detach-internet-gateway → InvalidInternetGatewayID.NotFound
  • Alpha + Beta each create EIGW
  • describe-egress-only-internet-gateways isolation
  • Cross-account EIGW delete → no effect

Step 8: Account Settings

  • Alpha enable-ebs-encryption-by-default → Beta unaffected
  • Independent toggle verification

Step 9: Global Resources

  • describe-regions identical for both accounts
  • describe-availability-zones identical
  • describe-instance-types identical

Step 10: Edge Cases

  • Empty account (Gamma) — no resources visible
  • Root isolation from tenants (root cannot see tenant instances)
  • Non-existent resource IDs → same error as cross-account (no info leakage)

Step 11: Cleanup

  • Terminate instances, delete volumes/snapshots/keys/VPCs/IGWs/EIGWs/subnets
  • Clean up AWS CLI profiles

Phase 9: Terminate and Verify Cleanup

  • delete-snapshot (CreateImage backing snapshot, so DeleteOnTermination is not blocked)
  • terminate-instances (poll -> terminated)

Phase 9a: SSH Unreachable Verification

  • Verify SSH connection is refused/unreachable after termination

Phase 9b: Volume Cleanup Verification

  • describe-volumes on root volume after termination
  • Verify root volume is deleted (DeleteOnTermination)

Pseudo Multi-Node (run-pseudo-multinode-e2e.sh)

Phase 1: Environment Setup

  • KVM support check
  • Simulated network IPs (10.11.12.{1,2,3} on loopback)
  • Simulated network IPs (no ramdisk — start-dev.sh uses disk-backed WAL/VB in CI)

Phase 2: Cluster Initialization

  • spx admin init (leader node1)
  • CA certificate trust
  • Start node1 services
  • spx admin join (node2, node3)
  • Start node2 + node3 services

Phase 3: Cluster Health Verification

  • Verify NATS cluster (3 nodes)
  • Verify Predastore cluster (3 nodes)
  • Wait for gateway on node1
  • Wait for daemon NATS readiness
  • describe-regions (gateway connectivity check)

Phase 3b: Cluster Stats CLI

  • spx get nodes — verify all 3 nodes show Ready
  • spx top nodes — verify instance type capacity table
  • spx get vms — verify empty (no instances yet)

Phase 4: Image and Key Setup

  • describe-instance-types (discover + select nano)
  • create-key-pair
  • spx admin images import (with node1 config paths)
  • describe-images (verify AMI)

Phase 4b: Multi-Node Key Pair Operations

  • import-key-pair (multinode-test-key-2, from local RSA key)
  • describe-key-pairs (verify both keys visible across cluster)
  • delete-key-pair (multinode-test-key-2, verify deletion)

Phase 5: Multi-Node Instance Tests

Test 1: Instance Distribution

  • run-instances x3 (distribute across nodes)
  • Poll all instances to running state
  • Check instance distribution across nodes
  • spx get vms — verify all instances visible

Test 1a-ii: SSH Connectivity & Volume Verification

  • SSH into all 3 instances via QEMU hostfwd port
  • Verify SSH connectivity (id command returns ec2-user)
  • Verify root volume size from inside VM matches API-reported size (lsblk vs describe-volumes)
  • Verify VM hostname

Test 1b: Volume Lifecycle

  • create-volume (10GB)
  • modify-volume (resize to 20GB)
  • attach-volume (to first instance)
  • detach-volume
  • delete-volume

Test 1c: Snapshot Lifecycle

  • Uses root volume of first instance (snapshots require a mounted VB instance)
  • create-snapshot (from attached root volume, with description)
  • Verify create response fields (VolumeId, VolumeSize, State)
  • describe-snapshots (by ID, verify fields)
  • copy-snapshot (with new description)
  • describe-snapshots (verify both exist)
  • delete-snapshot (original, verify copy survives)
  • delete-snapshot (copy, cleanup)

Test 1c-ii: Verify Snapshot-Backed Instance Launch

  • All run-instances calls use the snapshot path (cloneAMIToVolume -> OpenFromSnapshot), so the Test 1 instances are already snapshot-backed
  • Verify AMI snapshot exists in Predastore (snap-{amiId}/config.json)
  • Read first instance's root volume config.json from Predastore
  • Verify SnapshotID and SourceVolumeName are set (proves zero-copy clone)

Test 1d: Tag Management (Instances)

  • create-tags (3 tags on instance)
  • describe-tags (filter by resource-id)
  • describe-tags (filter by key)
  • describe-tags (filter by resource-type)
  • create-tags (overwrite tag value)
  • delete-tags (unconditional by key)
  • delete-tags (wrong value — no-op)
  • delete-tags (correct value)
  • Verify final tag count

Test 1d-ii: Tag Management (Volumes)

  • create-tags (2 tags on root volume)
  • describe-tags (filter by resource-id, verify count)
  • describe-tags (filter by resource-type=volume)
  • delete-tags (both tags, verify cleanup)

Test 2: DescribeInstances Aggregation

  • describe-instances (fan-out across all nodes, verify count)

Test 3: Cross-Node Operations

  • stop-instances (poll -> stopped)
  • start-instances (poll -> running)

Test 4: NATS Cluster Health (Post-Operations)

  • Verify NATS cluster still healthy after all operations

Test 5: VM Crash Recovery

  • Kill QEMU process with SIGKILL (simulate OOM kill)
  • Verify daemon detects crash (instance transitions to error/pending)
  • Wait for auto-restart (backoff starts at 5s)
  • Verify new QEMU PID differs from original
  • Verify instance reaches running state
  • Verify SSH connectivity after recovery

Test 5b: Crash Loop Prevention

  • Kill QEMU 4 times rapidly on a third instance
  • Verify crash loop is detected and restarts stop after max attempts (3 in 10-min window)
  • Verify instance reaches error state (won't restart further)

Phase 5c: IAM Accounts & Cross-Account Isolation

Step 1: Create Accounts

  • spx admin account create (Team Alpha, Team Beta)
  • Verify sequential 12-digit account IDs
  • spx admin account list (verify both accounts)

Step 2: Account Admin Auth

  • Alpha admin authenticates via ec2 + iam
  • Beta admin authenticates via ec2 + iam

Step 3: Account-Scoped Users

  • create-user alice in Alpha + Beta (same name, different accounts)
  • create-user team-member (Alpha), dev-user (Beta)
  • list-users scoped per account (verify different user lists)
  • Cross-account isolation: Alpha cannot see Beta's users and vice versa (NoSuchEntity)

Step 4: Account-Scoped Access Keys

  • create-access-key for alice in Alpha + Beta
  • Configure separate AWS CLI profiles per account user

Step 5: Account-Scoped Policies & Enforcement

  • create-policy EC2ReadOnly in Alpha (narrow: DescribeInstances + DescribeVpcs)
  • create-policy EC2ReadOnly in Beta (broad: ec2:Describe*)
  • attach-user-policy to alice in both accounts
  • Alpha alice: DescribeInstances allowed, DescribeKeyPairs denied (narrow)
  • Beta alice: DescribeInstances allowed, DescribeKeyPairs allowed (broad Describe*)
  • Both denied: CreateKeyPair

Step 6: Cross-Account Delete Isolation

  • Delete Alpha's alice (detach policy, delete key, delete user)
  • Verify Alpha alice gone (NoSuchEntity)
  • Verify Beta alice unaffected (still exists, auth still works)

Step 7: EC2 Resource Scoping

Tests that EC2 resources are properly isolated between the Alpha/Beta accounts. Skips CreateImage (mulga-612) and instance tags (mulga-613).

7a: Instance Scoping

  • Alpha + Beta each launch 1 instance
  • describe-instances isolation (each sees only own)
  • OwnerId field matches account ID
  • Cross-account stop-instances → InvalidInstanceID.NotFound
  • Cross-account terminate-instances → InvalidInstanceID.NotFound
  • Cross-account get-console-output → InvalidInstanceID.NotFound

7b: Volume Scoping

  • Alpha + Beta each create 1 volume
  • describe-volumes isolation
  • Cross-account describe-volumes by ID → InvalidVolume.NotFound
  • Cross-account delete-volume → InvalidVolume.NotFound
  • Cross-account attach-volume → InvalidVolume.NotFound
  • Cross-account detach-volume → InvalidVolume.NotFound
  • Cross-account modify-volume → InvalidVolume.NotFound

7c: Key Pair Scoping

  • Alpha + Beta each create key pair
  • describe-key-pairs isolation
  • Same key name in both accounts (namespace isolation — different KeyPairIds)
  • Cross-account delete-key-pair → no effect on other account's key
  • import-key-pair → invisible to other account

7d: Snapshot Scoping

  • Alpha + Beta each create snapshot from own volume
  • describe-snapshots isolation + OwnerId verification
  • Cross-account delete-snapshot → UnauthorizedOperation
  • Cross-account create-snapshot from other's volume → InvalidVolume.NotFound

7e: VPC/Subnet Scoping

  • Alpha + Beta each create VPC (same CIDR — no conflict)
  • describe-vpcs / describe-subnets isolation
  • Cross-account VPC/subnet operations → NotFound

7f: IGW + EIGW Scoping

  • Alpha + Beta each create IGW + EIGW
  • Describe isolation for both
  • Cross-account attach/detach/delete → NotFound

7g: Account Settings

  • enable-ebs-encryption-by-default per account — independent toggle

7h: Global Resources

  • describe-regions, describe-availability-zones, describe-instance-types identical for both accounts

Step 8: Edge Cases

  • Empty account (Gamma) — no resources visible
  • Root isolation from tenants
  • Non-existent resource IDs → same error as cross-account
  • Parallel key creation (race condition) — no cross-contamination

Step 9: EC2 + IAM Cleanup

  • Terminate instances, delete volumes/snapshots/keys/VPCs/IGWs/EIGWs/subnets
  • Delete all IAM users, policies, access keys
  • Clean up AWS CLI profiles

Phase 5d: VPC Networking

  • Step 1: create-vpc (10.100.0.0/16) + create-subnet (10.100.1.0/24)
  • Step 2: run-instances x3 with --subnet-id (launch into VPC subnet)
  • Poll all VPC instances to running state
  • Step 3: Verify PrivateIpAddress in describe-instances for each instance
  • Verify SubnetId and VpcId match requested values
  • Verify at least 1 NetworkInterface per instance
  • Verify all IPs are unique and in subnet range (10.100.1.x)
  • Step 4: SSH + ping connectivity (skipped in CI — OVN DHCP wait too slow)
  • Step 5: Stop/start IP persistence
    • stop-instances (all VPC instances, poll -> stopped)
    • Verify PrivateIpAddress persists in stopped state
    • start-instances (all VPC instances, poll -> running)
    • Verify PrivateIpAddress identical after restart
  • Step 6: Cleanup — terminate VPC instances, delete subnet, delete VPC

Phase 6: Cluster Shutdown + Restart

Test 6a: Dry-Run Shutdown

  • spx admin cluster shutdown --dry-run
  • Validate output contains all 5 phases (GATE, DRAIN, STORAGE, PERSIST, INFRA)

Test 6b: Coordinated Cluster Shutdown

  • spx admin cluster shutdown --force --timeout 60s
  • Verify all services down on all nodes (gateway, NATS, QEMU)

Test 6c: Cluster Restart + Recovery

  • Restart all 3 node services
  • Verify NATS cluster reforms (3 members)
  • Wait for gateway and daemon readiness
  • Smoke test: describe-instance-types returns valid results

Test 6d: Instance Relaunch + Terminate

  • Wait for instances to finish relaunching after restart (pending → running/error)
  • terminate-instances (all 3 instances)
  • Poll all to terminated state

Cleanup

  • Coordinated cluster shutdown (with fallback to per-node PID stops)
  • Remove simulated IPs

Real Multi-Node (run-multinode-e2e.sh)

Runs on a real 3-node libvirt cluster provisioned by OpenTofu (scripts/tofu-cluster/). Each node is a separate VM with its own OVN, NATS, Predastore, and Spinifex daemon. Bootstrap (bootstrap.sh) handles all provisioning before the test script runs.

Bootstrap (pre-test)

  1. OpenTofu provisions 3 libvirt VMs (bottlebrush, ironbark, casuarina) with cloud-init
  2. Wait for all nodes to be SSH-reachable
  3. Copy SSH key to node1 for peer access
  4. git fetch + checkout test branch on all nodes (all 3 repos)
  5. make build on all nodes (parallel)
  6. setup-ovn.sh — primary gets --management, secondaries connect to primary's OVN central
  7. spx admin init on primary, spx admin join on secondaries (formation server handshake)
  8. start-dev.sh on all nodes
  9. Wait for /health (awsgw=ok) on all nodes
  10. Install Spinifex CA certificate on all nodes
  11. import-key-pair (spinifex-key) + spx admin images import (Ubuntu 24.04)
  12. create-vpc (10.200.0.0/16) + create-subnet (10.200.1.0/24)

Phase 1: Pre-flight Validation

  • KVM support check (/dev/kvm writable)
  • SSH connectivity to all peer nodes

Phase 2: Cluster Health

  • NATS cluster: verify 2 unique route peers (3-node quorum)
  • Predastore reachable on all 3 nodes
  • AWS gateway reachable on all 3 nodes
  • Daemon readiness (describe-instance-types returns results)
  • spx get nodes — verify all 3 nodes show Ready
  • spx get vms — verify empty (no instances yet)

Phase 3: Instance Lifecycle + Distribution

  • Discover nano instance type and AMI (from bootstrap import)
  • run-instances x3 with staggered launches
  • Poll all instances to running state
  • Check instance distribution across physical nodes (QEMU process check)
  • Verify at least 2 different hosting nodes (non-deterministic, non-fatal if all on 1)
  • spx get vms — verify all 3 instances visible

Phase 4: SSH into Guest VMs

  • For each instance across all nodes:
    • Find hosting node via QEMU process scan
    • Extract SSH hostfwd port from QEMU command line on remote node
    • Wait for SSH readiness (up to 60s)
    • Verify SSH connectivity (id returns ec2-user)
    • Verify block device (lsblk)

Phase 5: Volume Lifecycle

  • create-volume (10GB)
  • attach-volume (to first instance, /dev/sdf)
  • Poll for in-use + attached state
  • detach-volume (poll for available)
  • delete-volume (verify gone)

Phase 6: Cross-Node Gateway Access

  • describe-instances via node1 gateway (baseline count)
  • describe-instances via node2 and node3 gateways
  • Verify all gateways return the same instance count (fan-out aggregation)

Phase 7: Cross-Node Operations

  • Find which node hosts instance 0
  • Pick a different node's gateway for stop
  • stop-instances via remote gateway (poll -> stopped)
  • Pick a third node's gateway for start
  • start-instances via another remote gateway (poll -> running)

Phase 8: Node Failure

  • stop-dev.sh on node2 (simulate node failure)
  • Verify node1 still serves requests (describe-instance-types)
  • Verify node3 still serves requests
  • Check NATS degraded state (1 peer instead of 2)
  • describe-instances still works from surviving nodes

Phase 9: Node Recovery

  • start-dev.sh on node2 (restart failed node)
  • Wait for NATS cluster to reform (2 peers again)
  • Verify node2 gateway is back
  • spx get nodes — verify all 3 nodes Ready again
  • Verify node2 serves requests after recovery

Phase 10: Cleanup

  • terminate-instances (all 3 instances, poll -> terminated)

VPC (run-vpc-e2e.sh)

Standalone VPC networking test suite. Runs against a running Spinifex endpoint (configurable via ENDPOINT env var). OVN topology tests are skipped when OVN is unavailable.

Phase 1: VPC CRUD

  • create-vpc (10.99.0.0/16, verify VpcId)
  • describe-vpcs (by ID, verify exactly 1 returned)

Phase 2: Subnet CRUD

  • create-subnet (10.99.1.0/24 in VPC, verify SubnetId)
  • describe-subnets (by ID, verify exactly 1 returned)

Phase 3: Internet Gateway CRUD

  • create-internet-gateway (verify InternetGatewayId)
  • describe-internet-gateways (by ID, verify exactly 1 returned)

Phase 4: Internet Gateway Attach / Detach

  • attach-internet-gateway (IGW to VPC)
  • describe-internet-gateways (verify attachment VpcId)
  • delete-internet-gateway on attached IGW (expect DependencyViolation or rejection)
  • detach-internet-gateway (IGW from VPC)
  • describe-internet-gateways (verify no attachments)

Phase 5: OVN Topology Verification (requires OVN)

  • Re-attach IGW for topology inspection
  • Verify OVN logical switch exists for subnet
  • Verify OVN logical router exists for VPC
  • Verify OVN external switch exists (IGW attached)
  • Verify SNAT rule on VPC router
  • Verify default route (0.0.0.0/0) on VPC router
  • Dump full OVN NB DB topology for debugging
  • Detach IGW
  • Skipped when ovn-nbctl is not available

Phase 6: Cleanup

  • delete-internet-gateway
  • delete-subnet
  • delete-vpc
  • Verify OVN cleanup (no VPC routers remaining, when OVN available)