Use this matrix to prove VM provisioning guardrails before trusting automation with real vSphere creates.
The point is not to make every test pass green. The point is to prove each unsafe condition fails at the correct layer.
Baseline Commands
Generate a plan and plan JSON:
terraform init
terraform plan -out=tfplan
terraform show -json tfplan > tfplan.json
Run preflight:
./scripts/preflight-vm-guardrails.sh --plan-json tfplan.json
Confirm clean post-apply state when a test intentionally creates a disposable VM:
terraform apply tfplan
terraform plan -detailed-exitcode
Confirm teardown hygiene when the disposable VM is no longer needed:
terraform plan -destroy -out=destroy.tfplan
terraform show -json destroy.tfplan \
| jq -r '.resource_changes[]? | [.address, .type, (.change.actions | join(","))] | @tsv'
terraform apply destroy.tfplan
1. Clean New VM
Test:
Plan one new VM name and one unused IP address.
Expected:
terraform plan succeeds
preflight passes
NetBox resources are planned when enabled
terraform apply succeeds for disposable test target
follow-up plan shows no changes
Proof to capture:
- VM name.
- IP and prefix.
- DNS name.
- planned NetBox VM/interface/IP/primary-IP resources.
- clean
terraform plan -detailed-exitcodeafter apply.
2. Duplicate IP Inside Terraform Config
Test:
Set two planned VMs to the same ipv4_address.
Command:
terraform plan -out=/tmp/duplicate-ip-test.tfplan
Expected failure:
VM IPv4 addresses must be unique within var.vms.
Layer responsible:
Terraform variable validation
3. Duplicate VM Name Inside Terraform Config
Test:
Set two planned VMs to the same name.
Command:
terraform plan -out=/tmp/duplicate-name-test.tfplan
Expected failure:
VM names must be unique within var.vms.
Layer responsible:
Terraform variable validation
4. IP Already Exists In NetBox
Test:
Use an IP address that already exists in NetBox.
Command:
./scripts/preflight-vm-guardrails.sh --plan-json tfplan.json
Expected failure:
NetBox already has IP '192.0.2.10' (192.0.2.10/24 status=active dns=cluster-a-worker-01.example.com).
Layer responsible:
Preflight NetBox check
NetBox provider resource on apply
5. VM Name Already Exists In NetBox
Test:
Use a VM name that already exists in NetBox.
Command:
./scripts/preflight-vm-guardrails.sh --plan-json tfplan.json
Expected failure:
NetBox already has VM 'cluster-a-worker-01'.
Layer responsible:
Preflight NetBox check
NetBox provider resource on apply
6. VM Exists In vCenter But Not NetBox
Test:
Use a VM name that exists in vCenter but does not exist in NetBox.
Command:
./scripts/preflight-vm-guardrails.sh \
--plan-json tfplan.json \
--skip-netbox \
--skip-dns \
--skip-nmap
Expected failure:
vCenter already has VM 'cluster-a-worker-01': /DC-Site-A/vm/K8s-Cluster/Prod/cluster-a-worker-01.
Also test missing govc configuration:
env -u GOVC_URL -u GOVC_USERNAME -u GOVC_PASSWORD \
./scripts/preflight-vm-guardrails.sh \
--plan-json tfplan.json \
--skip-netbox \
--skip-dns \
--skip-nmap
Expected failure:
vCenter checks require a working govc configuration.
Layer responsible:
Preflight vCenter check
7. IP Active On Network But Not In NetBox
Test:
Use an IP that responds to nmap -sn -n but has no NetBox record.
Command:
./scripts/preflight-vm-guardrails.sh \
--plan-json tfplan.json \
--skip-netbox \
--skip-govc \
--skip-dns
Expected failure:
Network scan indicates planned IP '192.0.2.10' is already active.
Layer responsible:
Preflight nmap check
Note: if the plan includes already-managed active VMs, the scan may flag those too. That is expected if the script scans all planned static IPs instead of only create/update actions.
8. Reverse DNS Exists
Test:
Use an IP with an existing PTR record.
Candidate checks:
dig +short -x '192.0.2.10'
getent hosts '192.0.2.10'
Preflight command:
./scripts/preflight-vm-guardrails.sh \
--plan-json tfplan.json \
--skip-netbox \
--skip-govc \
--skip-nmap
Expected failure:
Reverse DNS/getent already resolves planned IP '192.0.2.10'.
Layer responsible:
Preflight reverse DNS check
9. Forward DNS Exists For VM Name
Test:
Use a VM name that already resolves in DNS.
Candidate check:
getent hosts 'cluster-a-worker-01'
getent hosts 'cluster-a-worker-01.example.com'
Preflight command:
./scripts/preflight-vm-guardrails.sh \
--plan-json tfplan.json \
--skip-netbox \
--skip-govc \
--skip-nmap
Expected failure:
DNS already resolves planned VM name 'cluster-a-worker-01'.
Layer responsible:
Preflight forward DNS check
10. NetBox Enabled Without Provider Credentials
Test:
Set netbox_enabled = true and remove NETBOX_SERVER_URL / NETBOX_API_TOKEN.
Command:
unset NETBOX_SERVER_URL
unset NETBOX_API_TOKEN
terraform plan -out=/tmp/missing-netbox-creds.tfplan
Expected failure:
Error: Missing required argument
The argument "server_url" is required
Error: Missing required argument
The argument "api_token" is required
Layer responsible:
Terraform provider configuration
11. Preflight Without NetBox Credentials
Test:
Run preflight without NetBox credentials.
Command:
unset NETBOX_SERVER_URL
unset NETBOX_API_TOKEN
./scripts/preflight-vm-guardrails.sh --plan-json tfplan.json --skip-govc
Expected warning, if degraded mode is intentional:
Warnings:
- Skipping NetBox checks because NetBox URL/token environment variables or --netbox-url/--netbox-token were not provided.
Expected behavior:
DNS and nmap checks continue after the NetBox warning.
Layer responsible:
Preflight degraded-mode handling
12. NetBox Cluster Missing
Test:
Set netbox_enabled = true and use a non-existent netbox_cluster_name.
Command:
terraform plan -out=tfplan \
-var='netbox_cluster_name=missing-cluster-test'
Expected failure:
Error: no result
with module.vm_group.data.netbox_cluster.cluster[0]
Layer responsible:
Terraform NetBox data lookup
13. NetBox Unreachable
Test:
Point the NetBox provider at an unreachable endpoint.
Command:
NETBOX_SERVER_URL="https://netbox-unreachable.invalid" \
NETBOX_API_TOKEN="dummy" \
NETBOX_SKIP_VERSION_CHECK=true \
terraform plan -out=/tmp/netbox-unreachable-test.tfplan
Expected failure:
Planning failed.
Error: Get "https://netbox-unreachable.invalid/api/..."
Layer responsible:
Terraform NetBox provider/data lookup
Desired outcome:
NetBox unreachable -> Terraform plan fails -> vSphere VM is not created
14. Destroy Removes NetBox And vSphere Objects
Test:
Destroy a disposable Terraform-managed VM and verify Terraform removes its NetBox and vSphere records.
Confirm current state:
terraform state list
Expected state includes:
module.vm_group.netbox_interface.vm["test-1"]
module.vm_group.netbox_ip_address.vm["test-1"]
module.vm_group.netbox_primary_ip.vm["test-1"]
module.vm_group.netbox_virtual_machine.vm["test-1"]
module.vm_group.vsphere_virtual_machine.vm["test-1"]
Generate and inspect the destroy plan:
terraform plan -destroy -out=destroy.tfplan
terraform show -json destroy.tfplan \
| jq -r '.resource_changes[]? | [.address, .type, (.change.actions | join(","))] | @tsv'
Expected planned deletes:
module.vm_group.netbox_primary_ip.vm["test-1"] netbox_primary_ip delete
module.vm_group.netbox_ip_address.vm["test-1"] netbox_ip_address delete
module.vm_group.netbox_interface.vm["test-1"] netbox_interface delete
module.vm_group.netbox_virtual_machine.vm["test-1"] netbox_virtual_machine delete
module.vm_group.vsphere_virtual_machine.vm["test-1"] vsphere_virtual_machine delete
Apply the destroy plan:
terraform apply destroy.tfplan
Verify Terraform state no longer lists the managed resources:
terraform state list
Verify the NetBox VM record is gone:
curl -s \
-H "Authorization: Token $NETBOX_API_TOKEN" \
-H "Accept: application/json" \
"$NETBOX_SERVER_URL/api/virtualization/virtual-machines/?name=cluster-a-test-01" \
| jq '.count'
Expected:
0
Verify the NetBox IP address record is gone:
curl -s \
-H "Authorization: Token $NETBOX_API_TOKEN" \
-H "Accept: application/json" \
"$NETBOX_SERVER_URL/api/ipam/ip-addresses/?q=192.0.2.10" \
| jq '.count'
Expected:
0
Verify the vSphere VM is gone:
govc find / -type m -name 'cluster-a-test-01'
Expected: no output.
Layer responsible:
Terraform destroy plus post-destroy NetBox and vCenter verification
Successful result:
create -> manage -> verify idempotency -> destroy -> cleanup NetBox and vSphere
Temporary Test Cleanup
For each destructive or collision test:
- add only one temporary collision at a time.
- run the test.
- save the exact output.
- revert the temporary config immediately.
- confirm no diff remains.
Cleanup checks:
git status --short -- path/to/test/env
git diff -- path/to/test/env
Operating Rule
A VM guardrail is not proven until every unsafe path fails where you expect it to fail.
Document the test, the command, the observed output, and the cleanup state.