diff --git a/e2e/conftest.py b/e2e/conftest.py index d833a4b..8e8c5bb 100644 --- a/e2e/conftest.py +++ b/e2e/conftest.py @@ -59,6 +59,7 @@ def run_inventory(stack): source: str = "", filter_name: str = "", filter_value: str = "", + ip_version: str = "", args: tuple[str, ...] = ("--list",), token: str | None = None, ): @@ -71,6 +72,7 @@ def run_inventory(stack): "NETBOX_INVENTORY_SOURCE": source or "ip-addresses", "NETBOX_INVENTORY_FILTER": filter_name, "NETBOX_INVENTORY_FILTER_VALUE": filter_value, + "NETBOX_INVENTORY_IP_VERSION": ip_version or "both", } result = subprocess.run( [SCRIPT, *args], capture_output=True, text=True, timeout=120, env=env diff --git a/e2e/seed.py b/e2e/seed.py index 2fc387d..b94a351 100644 --- a/e2e/seed.py +++ b/e2e/seed.py @@ -192,13 +192,13 @@ def seed(url: str, admin_token: str) -> dict[str, str]: }, ) - # give vm01 a primary IP: interface -> assigned address -> primary_ip4 + # give vm01 primary IPs: interface -> assigned addresses -> primary_ip4/6 eth0 = api.ensure( "virtualization/interfaces", {"virtual_machine_id": vm01["id"], "name": "eth0"}, {"virtual_machine": vm01["id"], "name": "eth0"}, ) - vm01_ip = api.ensure( + vm01_ip4 = api.ensure( "ipam/ip-addresses", {"address": "192.0.2.20/24"}, { @@ -208,12 +208,27 @@ def seed(url: str, admin_token: str) -> dict[str, str]: "assigned_object_id": eth0["id"], }, ) - api.patch("virtualization/virtual-machines", vm01["id"], {"primary_ip4": vm01_ip["id"]}) + vm01_ip6 = api.ensure( + "ipam/ip-addresses", + {"address": "2001:db8::20/64"}, + { + "address": "2001:db8::20/64", + "dns_name": "vm01-v6.example.com", + "assigned_object_type": "virtualization.vminterface", + "assigned_object_id": eth0["id"], + }, + ) + api.patch( + "virtualization/virtual-machines", + vm01["id"], + {"primary_ip4": vm01_ip4["id"], "primary_ip6": vm01_ip6["id"]}, + ) standalone_addresses = [ {"address": "192.0.2.10/24", "dns_name": "web.example.com", "tenant": customer1["id"]}, {"address": "192.0.2.11/24", "vrf": internal["id"]}, {"address": "192.0.2.12/24", "dns_name": "web.example.com"}, + {"address": "2001:db8::10/64", "dns_name": "v6.example.com"}, { "address": "192.0.2.13/24", "dns_name": "db.other.org", diff --git a/e2e/test_e2e.py b/e2e/test_e2e.py index 9643aad..defc6aa 100644 --- a/e2e/test_e2e.py +++ b/e2e/test_e2e.py @@ -5,11 +5,12 @@ End-to-end tests: every source/filter scenario against a live NetBox. The dataset these expectations rest on is created by seed.py: tenants customer1/customer2, sites milan/turin, clusters cluster1 (milan) and -cluster2 (turin), VRF internal, VMs vm01 (cluster1, customer1, primary IP -192.0.2.20 named vm01.example.com), vm02 (cluster1, customer2, no primary IP) -and vm03.example.com (cluster2), plus standalone addresses 192.0.2.10 -(web.example.com, customer1), 192.0.2.11 (unnamed, internal), 192.0.2.12 -(web.example.com duplicate) and 192.0.2.13 (db.other.org, customer2, internal). +cluster2 (turin), VRF internal, VMs vm01 (cluster1, customer1, primary IPs +192.0.2.20 vm01.example.com and 2001:db8::20 vm01-v6.example.com), vm02 +(cluster1, customer2, no primary IP) and vm03.example.com (cluster2), plus +standalone addresses 192.0.2.10 (web.example.com, customer1), 192.0.2.11 +(unnamed, internal), 192.0.2.12 (web.example.com duplicate), 2001:db8::10 +(v6.example.com) and 192.0.2.13 (db.other.org, customer2, internal). """ @@ -32,6 +33,8 @@ class TestIpAddressSource: "192.0.2.12", "db.other.org", "vm01.example.com", + "v6.example.com", + "vm01-v6.example.com", } web = hostvars(result)["web.example.com"] assert web["ansible_host"] == "192.0.2.10" @@ -48,7 +51,13 @@ class TestIpAddressSource: def test_named_filter_deduplicates(self, run_inventory): result = run_inventory(source="ip-addresses", filter_name="named") assert result.returncode == 0, result.stderr - assert hosts(result) == {"web.example.com", "db.other.org", "vm01.example.com"} + assert hosts(result) == { + "web.example.com", + "db.other.org", + "vm01.example.com", + "v6.example.com", + "vm01-v6.example.com", + } def test_named_false_keeps_unnamed(self, run_inventory): result = run_inventory(source="ip-addresses", filter_name="named", filter_value="false") @@ -65,7 +74,13 @@ class TestIpAddressSource: source="ip-addresses", filter_name="domain", filter_value="example.com" ) assert result.returncode == 0, result.stderr - assert hosts(result) == {"web.example.com", "192.0.2.12", "vm01.example.com"} + assert hosts(result) == { + "web.example.com", + "192.0.2.12", + "vm01.example.com", + "v6.example.com", + "vm01-v6.example.com", + } def test_domain_filter_other_domain(self, run_inventory): result = run_inventory( @@ -74,6 +89,33 @@ class TestIpAddressSource: assert result.returncode == 0, result.stderr assert hosts(result) == {"db.other.org"} + def test_ip_version_v4(self, run_inventory): + result = run_inventory(source="ip-addresses", ip_version="v4") + assert result.returncode == 0, result.stderr + assert hosts(result) == { + "web.example.com", + "192.0.2.11", + "192.0.2.12", + "db.other.org", + "vm01.example.com", + } + + def test_ip_version_v6(self, run_inventory): + result = run_inventory(source="ip-addresses", ip_version="v6") + assert result.returncode == 0, result.stderr + assert hosts(result) == {"v6.example.com", "vm01-v6.example.com"} + assert hostvars(result)["v6.example.com"]["ansible_host"] == "2001:db8::10" + + def test_ip_version_combines_with_filters(self, run_inventory): + result = run_inventory( + source="ip-addresses", + filter_name="domain", + filter_value="example.com", + ip_version="v4", + ) + assert result.returncode == 0, result.stderr + assert hosts(result) == {"web.example.com", "192.0.2.12", "vm01.example.com"} + class TestVirtualMachineSource: def test_all_virtual_machines(self, run_inventory): @@ -81,7 +123,8 @@ class TestVirtualMachineSource: assert result.returncode == 0, result.stderr assert hosts(result) == {"vm01", "vm02", "vm03.example.com"} vm01 = hostvars(result)["vm01"] - assert vm01["ansible_host"] == "192.0.2.20" + # with both families set, NetBox's primary_ip prefers IPv6 by default + assert vm01["ansible_host"] == "2001:db8::20" assert vm01["netbox_cluster"] == "cluster1" assert vm01["netbox_site"] == "milan" assert vm01["netbox_tenant"] == "customer1" @@ -115,6 +158,20 @@ class TestVirtualMachineSource: assert result.returncode == 0, result.stderr assert hosts(result) == {"vm03.example.com"} + def test_ip_version_v4_uses_primary_ip4(self, run_inventory): + result = run_inventory(source="virtual-machines", ip_version="v4") + assert result.returncode == 0, result.stderr + assert hosts(result) == {"vm01", "vm02", "vm03.example.com"} + assert hostvars(result)["vm01"]["ansible_host"] == "192.0.2.20" + + def test_ip_version_v6_uses_primary_ip6(self, run_inventory): + result = run_inventory(source="virtual-machines", ip_version="v6") + assert result.returncode == 0, result.stderr + assert hostvars(result)["vm01"]["ansible_host"] == "2001:db8::20" + # VMs without a primary IP of that family keep no ansible_host + assert "ansible_host" not in hostvars(result)["vm02"] + assert "ansible_host" not in hostvars(result)["vm03.example.com"] + class TestErrorScenarios: def test_unknown_tenant_name(self, run_inventory): @@ -144,6 +201,11 @@ class TestErrorScenarios: assert result.returncode == 1 assert "requires a value" in result.stderr + def test_invalid_ip_version(self, run_inventory): + result = run_inventory(source="ip-addresses", ip_version="ipv4") + assert result.returncode == 1 + assert "Unknown ip_version" in result.stderr + def test_invalid_token(self, run_inventory): result = run_inventory(source="ip-addresses", token="wrong-token") assert result.returncode == 1