mirror of
https://github.com/mulgadc/spinifex.git
synced 2026-10-23 04:36:40 +00:00
d8128ec2f3
Macvlan was a single-NIC SSH-safe hack that predated veth mode and was never selected by any active config path. Deletion is Phase 0 item 1 of the network layer redesign (mulga-siv-125.1). Removed: - BridgeModeMacvlan const + setMacvlanMAC + ifaceIsMacvlan probe - Macvlan MAC alignment block in launchService (only fired on dead path) - Macvlan branch in detectBridgeMode + matching tests - Macvlan duplicates of IGW/AddNAT centralized-NAT tests (veth versions already cover the same path) - --macvlan flag + macvlan case branch in setup-ovn.sh Refactored: - useCentralizedNAT() now == BridgeModeVeth (was != BridgeModeDirect); verifyBridgeMode already rejects empty/unknown at startup - Three IGW-pool tests now explicitly WithBridgeMode(BridgeModeVeth) (previously relied on empty-mode defaulting to centralized) Kept intentionally: - Deprecation notes in docs/compute/vpc-networking/README.md - Legacy macvlan-interface cleanup in reset-dev-env.sh + ansible teardown role (migration for operator systems with stale spx-ext-* interfaces from pre-veth installs)
844 lines
39 KiB
Markdown
844 lines
39 KiB
Markdown
# 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 5f: Security Group Enforcement (egress ACL)
|
||
Proves vpcd programs OVN ACLs that actually drop traffic, not just nominal SG records. Tests egress only — in single-node `dev_networking` mode, runner→VM SSH bypasses OVN via the hostfwd NIC, so ingress isn't a faithful probe. Egress goes through the OVN tap NIC (default route).
|
||
- Discover VM's OVN gateway from inside the VM (`ip route show default`)
|
||
- Test 5f-1 (baseline): ICMP from VM to gateway works under default SG (allow-all egress)
|
||
- Skipped gracefully if env blocks ICMP regardless of SG (no false fail)
|
||
- Test 5f-2 (revoke): `revoke-security-group-egress` strips the allow-all rule → ICMP must drop
|
||
- Test 5f-3 (re-authorize): `authorize-security-group-egress` restores the rule → ICMP must work again
|
||
- Restore the default egress rule on any failure path so subsequent phases (6, 7, 8) keep their SSH probes working
|
||
|
||
### 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 8b: VPC Public/Private Subnet E2E
|
||
Gated on `external_mode = "pool"` (skipped when only `dev_networking` is configured). Validates AWS-parity public/private subnet semantics end-to-end through OVN.
|
||
- Step 1: Discover default VPC + default subnet (`MapPublicIpOnLaunch=true`)
|
||
- Step 2: Public subnet instance
|
||
- `run-instances` into default subnet → auto-assigned `PublicIpAddress`
|
||
- `ovn-nbctl lr-nat-list` shows per-instance `dnat_and_snat` rule for `PUB_PRIVATE_IP`
|
||
- Asserts NO blanket VPC CIDR SNAT (mulga-754 AWS-parity regression)
|
||
- SSH via public IP succeeds (when bridge reachable)
|
||
- Outbound internet from inside the VM (via OVN SNAT)
|
||
- Step 2b: In-guest TLS trust — cloud-init injects Spinifex CA into the guest, so `curl https://awsgw` and `https://predastore-s3` work without `--insecure`
|
||
- Step 3: Private subnet isolation
|
||
- `create-subnet` 172.31.16.0/20 with `MapPublicIpOnLaunch=false`
|
||
- `run-instances` into private subnet → no public IP, no internet access from inside the VM
|
||
|
||
### Phase 8c: Route Table Validation
|
||
- Step 1: Default VPC main route table — local route + 0.0.0.0/0 → IGW present
|
||
- Step 2: Custom route table CRUD lifecycle — create, list local route, `create-route`, `associate-route-table`, `disassociate-route-table`, `delete-route`, `delete-route-table`
|
||
- Step 3: Error paths — cannot delete the local route; cannot delete the main route table
|
||
|
||
### Phase 8d: NAT Gateway E2E (mulga-763)
|
||
Reuses `PUB_INSTANCE_ID` (bastion) + `PRIV_INSTANCE_ID` from Phase 8b. End-to-end proof that NAT GW creation/teardown flips private-instance outbound connectivity.
|
||
- Step 1: Bastion → private SSH ready (via `ProxyJump` through PUB_IP)
|
||
- Step 2: Baseline — private VM cannot reach internet
|
||
- Step 3: `allocate-address` + `create-nat-gateway` + `create-route` (0.0.0.0/0 → NAT GW); verify OVN SNAT rule appears
|
||
- Step 4: Private VM CAN now reach internet (retried, NAT propagation can take a few seconds)
|
||
- Step 5: `delete-nat-gateway` + cleanup route + `release-address`; verify private VM loses internet again
|
||
|
||
### Phase 8e: Security Group Enforcement (SG-to-SG datapath)
|
||
End-to-end proof that the `RunInstances → ENI → vpcd → OVN port group + ACL → datapath` chain actually drops unauthorized traffic. This is the deepest SG enforcement test in the suite and supersedes Phase 5f for SG-to-SG semantics.
|
||
- **Capacity note**: Phase 8b's instance termination (PUB+PRIV) runs *before* Phase 8e on single-node to free 2 nano slots. Without that, 5 simultaneous nanos (main + PUB + PRIV + client + target) exhaust the runner and the gateway masks `InsufficientInstanceCapacity` as `InvalidInstanceType` via `isKnownInstanceType`.
|
||
- Step 1: Create `sge-client` and `sge-target` SGs; authorize tcp/22 on client-sg from 0.0.0.0/0; authorize tcp/8080 on target-sg from **client-sg** (SG-to-SG via `UserIdGroupPairs`)
|
||
- Step 2: Launch `client-vm` (sge-client) and `target-vm` (sge-target) in default subnet; target VM's HTTP server started via cloud-init `user-data` (port 8080) — target-sg has no SSH ingress, so we never SSH into it
|
||
- Step 3: `ovn-nbctl` port_group membership inspection — both client + target LSPs are in their port groups; verifies the SG-to-SG match expression resolves
|
||
- Step 4: Allowed — `curl client → target:8080` succeeds (SG-to-SG allow)
|
||
- Step 5: Denied — `nc client → target:22` blocked (target-sg has no SSH ingress; default-deny ACL drops)
|
||
- Step 6: Revoke target-sg's tcp/8080 rule and retest immediately — confirms vpcd's sync `RequestEvent` contract (no eventually-consistent gap)
|
||
- Step 7: Cleanup — terminate both VMs, delete both SGs
|
||
|
||
### 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) — generates join token, writes to `~/node1/config/join-token`
|
||
- CA certificate trust
|
||
- Read join token from node1 config
|
||
- `spx admin join` with `--token` (node2, node3 — concurrent)
|
||
- Start all 3 node services concurrently
|
||
|
||
### 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)
|
||
- `create-security-group` + 2× `authorize-security-group-ingress` (tcp/22 + icmp from 0.0.0.0/0)
|
||
- All subsequent run-instances use this SG so VMs are reachable. The default SG is egress-only, so without this every Phase 5 SSH probe would be dropped by OVN ACL.
|
||
|
||
### 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.
|
||
|
||
> **Go port:** every phase below has a `TestMultinode*` counterpart in `tests/e2e/multinode/` (phase N -> `phaseN_*` helper). CI runs the Go suite via the `e2e_go_multinode` job in `.github/workflows/e2e-go.yml`; the bash driver stays in place as the executable spec until the Go run has a soak-tested cadence on `ci-multi`.
|
||
|
||
### 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 (generates join token), read token via SSH, `spx admin join --token` on secondaries
|
||
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)
|
||
- `authorize-security-group-ingress` (tcp/22 on default VPC's default SG, 0.0.0.0/0)
|
||
- Required because the default SG is egress-only and the runner's IP is not a member; without this Phase 4 SSH would be dropped by OVN port-group ACL.
|
||
- Idempotent (tolerates `InvalidPermission.Duplicate` on re-runs).
|
||
- `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)
|
||
|
||
---
|
||
|
||
## Consolidated LB Data Plane (`run-lb-e2e.sh`)
|
||
|
||
Consolidated LB data plane test covering all 4 variants: ALB internet-facing,
|
||
ALB internal, NLB internet-facing, NLB internal. Shares a single VPC, subnet,
|
||
and set of dual-purpose app instances (HTTP:80 + TCP:9000) across all suites.
|
||
Requires pool mode with external IPAM (NOT dev_networking).
|
||
|
||
### Phase 0: Prerequisites
|
||
- Dev-mode gate (skip if dev_networking enabled)
|
||
- SSH to peer node (if `--peer` provided)
|
||
- Discover instance types (nano), AMI (non-Alpine)
|
||
- Create key pair (lb-e2e-key)
|
||
|
||
### Phase 1: Shared VPC + Subnet
|
||
- `create-vpc` (10.200.0.0/16)
|
||
- `create-internet-gateway` + attach
|
||
- `create-subnet` (10.200.1.0/24) + MapPublicIpOnLaunch
|
||
|
||
### Phase 2: Launch App Instances
|
||
- `run-instances` x2 with dual-purpose cloud-init (HTTP on 80, TCP echo on 9000)
|
||
- Poll instances to running state, collect private IPs
|
||
- Poll app ports for readiness (replaces `sleep 100`)
|
||
|
||
### Phase 3a: ALB Internet-Facing [skip if no --peer]
|
||
- Create HTTP target group, register 2 targets
|
||
- `create-load-balancer` (scheme=internet-facing), verify scheme
|
||
- Verify ENI has public IP, DNS has no `internal-` prefix
|
||
- `create-listener` (HTTP:80)
|
||
- Wait for ALB active (agent heartbeat)
|
||
- Host HTTP traffic test (curl 20x, verify round-robin)
|
||
- Peer validation (curl from --peer node, 20x)
|
||
- Cleanup: delete listener, LB, verify ENI removal, delete TG
|
||
|
||
### Phase 3b: ALB Internal
|
||
- Create HTTP target group, register 2 targets
|
||
- `create-load-balancer` (scheme=internal), verify scheme
|
||
- Verify ENI has NO public IP, DNS has `internal-` prefix
|
||
- `create-listener` (HTTP:80)
|
||
- Wait for ALB active (mgmt NIC heartbeat)
|
||
- Wait for targets healthy
|
||
- Launch client VM (cloud-init curls ALB private IP, serves results)
|
||
- Poll client status.txt, fetch results, verify round-robin
|
||
- Terminate client VM
|
||
- Cleanup: delete LB, verify ENI removal, delete TG
|
||
|
||
### Phase 3c: NLB Internet-Facing [skip if no --peer]
|
||
- Create TCP target group (port 9000), verify protocol=TCP
|
||
- Register 2 targets
|
||
- `create-load-balancer` (type=network), verify type, ARN `/net/`
|
||
- `create-listener` (TCP:9000)
|
||
- Wait for NLB active (agent heartbeat)
|
||
- Verify ENI has public IP
|
||
- Wait for targets healthy (TCP health check)
|
||
- Host TCP traffic test (nc 20x, verify round-robin)
|
||
- Peer validation (nc from --peer node)
|
||
- Deregister target, verify draining state
|
||
- Cleanup: delete listener, LB, verify ENI removal, delete TG
|
||
|
||
### Phase 3d: NLB Internal
|
||
- Create TCP target group (port 9000), register 2 targets
|
||
- `create-load-balancer` (type=network, scheme=internal)
|
||
- Verify type=network, scheme=internal, ARN `/net/`
|
||
- Verify ENI has NO public IP, DNS has `internal-` prefix
|
||
- `create-listener` (TCP:9000)
|
||
- Wait for NLB active (mgmt NIC heartbeat)
|
||
- Wait for targets healthy (TCP health check)
|
||
- Launch client VM (cloud-init nc's NLB private IP, serves results)
|
||
- Poll client status.txt, fetch results, verify TCP responses
|
||
- Terminate client VM
|
||
- Cleanup: delete LB, verify ENI removal, delete TG
|
||
|
||
### Phase 4: Cleanup (trap)
|
||
- Terminate app instances, wait for terminated
|
||
- Delete key pair, subnet, IGW, VPC
|
||
|
||
## DDIL (`tests/e2e/ddil/`)
|
||
|
||
Go-based harness and scenarios that exercise the Denied / Disrupted /
|
||
Intermittent / Limited failure modes called out in the DDIL vulnerability
|
||
assessment. Design and rationale live in
|
||
[`docs/development/improvements/ddil-e2e-test-harness.md`](../../../docs/development/improvements/ddil-e2e-test-harness.md);
|
||
the Go helper API lives in
|
||
[`tests/e2e/ddil/harness/`](./ddil/harness/) with the authoritative link
|
||
profile values in [`harness/profiles.go`](./ddil/harness/profiles.go).
|
||
|
||
Scenarios begin as `t.Skip("requires <dep>")` and are flipped to real
|
||
assertions by the hardening epics they cover (daemon-local-autonomy,
|
||
predastore-ddil-hardening). The status column below is the authoritative
|
||
live state — update it in the same PR that changes the test body.
|
||
`TestCoverageDrift` enforces the letter column against the
|
||
`TestScenario<L>_...` functions in `ddil/scenarios/`.
|
||
|
||
### Scenarios
|
||
|
||
| Letter | Scenario | DDIL finding closed | Status |
|
||
|--------|----------|---------------------|--------|
|
||
| A | NATS-only kill (daemon stays up, standalone mode) | Finding 1 | ENABLED |
|
||
| B | Daemon restart without NATS (recovers from local state) | Finding 1 | ENABLED |
|
||
| C | Clean cluster-network partition (majority + isolated both progress) | Finding 1/2 | SKIPPED (needs predastore §2 — see notes) |
|
||
| D | Degraded link under SATCOM profile (fan-out + Raft) | Finding 3 | SKIPPED |
|
||
| E | Predastore write under partition (repair journal drains on heal) | Finding 2 | SKIPPED |
|
||
| F | Raft under SATCOM latency (≤1 election over 5 min) | Finding 3 | SKIPPED |
|
||
|
||
**Scenario C quarantine note:** the witness VM's block I/O is backed by
|
||
distributed predastore, so an iptables peer-partition (PartitionNode's
|
||
OUTPUT DROP rules) also severs the witness disk writes. The result is
|
||
`BLOCK_IO_ERROR` → QEMU power-down → daemon termination → ssh handshake
|
||
EOF on the next `AssertProgressed`. Needs
|
||
[predastore-ddil-hardening §2](../../../docs/development/feature/predastore-ddil-hardening.md)
|
||
(write-quorum with deferred parity repair) before C can assert without
|
||
flapping; flip the row back to ENABLED in the same PR that lands §2.
|
||
|
||
### Link profile validation
|
||
|
||
Values in `harness/profiles.go` are unvalidated approximations derived
|
||
from public specifications — real-hardware measurement is deferred until
|
||
SATCOM/HF access is available. Update the last column when a measurement
|
||
lands.
|
||
|
||
| Profile | Specification source | Validation status | Last measured against real hardware |
|
||
|---------|----------------------|-------------------|-------------------------------------|
|
||
| LAN | baseline | unvalidated | — |
|
||
| WAN | public site-to-site norms | unvalidated | — |
|
||
| LTEDegraded | public congested-cellular norms | unvalidated | — |
|
||
| SATCOM | published GEO SATCOM figures | unvalidated | — |
|
||
| HFData | HF modem specifications | unvalidated | — |
|
||
| Flapping | synthetic (10s up / 5s down) | unvalidated | — |
|
||
|
||
### Known gaps
|
||
|
||
- Security group enforcement breadth — Phase 5f covers egress
|
||
authorize/revoke for the default SG (dev_networking single-node);
|
||
Phase 8e covers SG-to-SG ingress + revoke under external pool mode.
|
||
Not yet covered: `ModifyInstanceAttribute --groups` swap (verify port
|
||
group membership changes mid-flight), reconciler convergence after a
|
||
vpcd restart with drift (kill vpcd, delete an OVN port group, restart
|
||
vpcd, assert it converges), and per-port-range allow/deny combinations
|
||
on a single SG. Follow-up: mulga-js-79 (in progress).
|
||
- Chaos framework (randomised fault injection, long-running stress) —
|
||
follow-up plan TBD.
|
||
- Multi-day partition — requires long-running CI infrastructure;
|
||
follow-up plan TBD.
|
||
- Real-hardware SATCOM/HF validation — follow-up plan TBD.
|
||
- Clock skew injection — no hardening epic claims Finding 7 yet;
|
||
revisit when one lands.
|
||
- Dual-NIC cluster network — Scenario C uses peer-IP `iptables` rules on
|
||
the single-NIC tofu-cluster today; dedicated cluster NIC is deferred
|
||
to `docs/development/improvements/e2e-multinode-dual-nic.md` (TBC).
|
||
- Dockerised per-node pseudo-multinode isolation — candidate under
|
||
[`docs/development/improvements/docker-e2e-local.md`](../../../docs/development/improvements/docker-e2e-local.md).
|
||
- Full Go migration of the rest of the E2E suite — TBD follow-up to the
|
||
DDIL harness pilot.
|