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-exitcode after 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.