Closes #22174: Include DNS names for primary/OOB IPs and NAT data for VDCs (#22553)

This commit is contained in:
Martin Hauser 2026-06-26 18:34:30 +02:00 committed by GitHub
parent 68c1153325
commit 021b7c5f5b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 180 additions and 12 deletions

View File

@ -62,25 +62,25 @@ class DeviceSerializer(PrimaryModelSerializer):
nested=True,
read_only=True,
allow_null=True,
fields=[*IPAddressSerializer.Meta.brief_fields, 'nat_inside', 'nat_outside'],
fields=[*IPAddressSerializer.Meta.brief_fields, 'dns_name', 'nat_inside', 'nat_outside'],
)
primary_ip4 = IPAddressSerializer(
nested=True,
required=False,
allow_null=True,
fields=[*IPAddressSerializer.Meta.brief_fields, 'nat_inside', 'nat_outside'],
fields=[*IPAddressSerializer.Meta.brief_fields, 'dns_name', 'nat_inside', 'nat_outside'],
)
primary_ip6 = IPAddressSerializer(
nested=True,
required=False,
allow_null=True,
fields=[*IPAddressSerializer.Meta.brief_fields, 'nat_inside', 'nat_outside'],
fields=[*IPAddressSerializer.Meta.brief_fields, 'dns_name', 'nat_inside', 'nat_outside'],
)
oob_ip = IPAddressSerializer(
nested=True,
required=False,
allow_null=True,
fields=[*IPAddressSerializer.Meta.brief_fields, 'nat_inside', 'nat_outside'],
fields=[*IPAddressSerializer.Meta.brief_fields, 'dns_name', 'nat_inside', 'nat_outside'],
)
parent_device = serializers.SerializerMethodField()
cluster = ClusterSerializer(nested=True, required=False, allow_null=True)
@ -148,9 +148,24 @@ class VirtualDeviceContextSerializer(PrimaryModelSerializer):
device = DeviceSerializer(nested=True)
identifier = serializers.IntegerField(allow_null=True, max_value=32767, min_value=0, required=False, default=None)
tenant = TenantSerializer(nested=True, required=False, allow_null=True, default=None)
primary_ip = IPAddressSerializer(nested=True, read_only=True, allow_null=True)
primary_ip4 = IPAddressSerializer(nested=True, required=False, allow_null=True)
primary_ip6 = IPAddressSerializer(nested=True, required=False, allow_null=True)
primary_ip = IPAddressSerializer(
nested=True,
read_only=True,
allow_null=True,
fields=[*IPAddressSerializer.Meta.brief_fields, 'dns_name', 'nat_inside', 'nat_outside'],
)
primary_ip4 = IPAddressSerializer(
nested=True,
required=False,
allow_null=True,
fields=[*IPAddressSerializer.Meta.brief_fields, 'dns_name', 'nat_inside', 'nat_outside'],
)
primary_ip6 = IPAddressSerializer(
nested=True,
required=False,
allow_null=True,
fields=[*IPAddressSerializer.Meta.brief_fields, 'dns_name', 'nat_inside', 'nat_outside'],
)
status = ChoiceField(choices=VirtualDeviceContextStatusChoices)
# Related object counts

View File

@ -12,7 +12,7 @@ from dcim.constants import *
from dcim.models import *
from extras.models import ConfigTemplate, Tag
from ipam.choices import VLANQinQRoleChoices
from ipam.models import ASN, RIR, VLAN, VRF
from ipam.models import ASN, RIR, VLAN, VRF, IPAddress
from netbox.api.serializers import GenericObjectSerializer
from tenancy.models import Tenant
from users.constants import TOKEN_PREFIX
@ -2081,6 +2081,58 @@ class DeviceTestCase(APIViewTestCases.APIViewTestCase):
self.assertEqual(response.data['oob_ip']['nat_inside']['address'], str(real_ip.address))
self.assertEqual(response.data['oob_ip']['nat_outside'], [])
def test_get_object_includes_dns_name_on_primary_ip(self):
device = create_test_device('dns-device')
interfaces = (
Interface.objects.create(device=device, name='eth0', type='other'),
Interface.objects.create(device=device, name='eth1', type='other'),
)
ip4 = IPAddress(address='192.0.2.10/32', dns_name='device4.example.com')
ip4.assigned_object = interfaces[0]
ip4.save()
ip6 = IPAddress(address='2001:db8::10/128', dns_name='device6.example.com')
ip6.assigned_object = interfaces[1]
ip6.save()
device.primary_ip4 = ip4
device.primary_ip6 = ip6
device.save()
self.add_permissions('dcim.view_device', 'ipam.view_ipaddress')
response = self.client.get(
f'{self._get_detail_url(device)}?exclude=config_context',
**self.header,
)
self.assertHttpStatus(response, status.HTTP_200_OK)
self.assertEqual(response.data['primary_ip4']['dns_name'], 'device4.example.com')
self.assertEqual(response.data['primary_ip6']['dns_name'], 'device6.example.com')
self.assertIn(
response.data['primary_ip']['dns_name'],
('device4.example.com', 'device6.example.com'),
)
def test_get_object_includes_dns_name_on_oob_ip(self):
device = create_test_device('dns-oob-device')
interface = Interface.objects.create(device=device, name='oob0', type='other')
ip = IPAddress(address='192.0.2.20/32', dns_name='oob.example.com')
ip.assigned_object = interface
ip.save()
device.oob_ip = ip
device.save()
self.add_permissions('dcim.view_device', 'ipam.view_ipaddress')
response = self.client.get(
f'{self._get_detail_url(device)}?exclude=config_context',
**self.header,
)
self.assertHttpStatus(response, status.HTTP_200_OK)
self.assertEqual(response.data['oob_ip']['dns_name'], 'oob.example.com')
def test_render_config_with_config_template_id(self):
default_template = ConfigTemplate.objects.create(
name='Default Template',
@ -3848,6 +3900,75 @@ class VirtualDeviceContextTestCase(APIViewTestCases.APIViewTestCase):
},
]
def test_get_object_includes_nat_on_primary_ip(self):
device = create_test_device('vdc-nat-device')
interfaces = (
Interface.objects.create(device=device, name='eth0', type='other'),
Interface.objects.create(device=device, name='eth1', type='other'),
)
real_ip4, nat_ip4 = create_test_nat_ip_pair(
real_address='10.0.2.10/32',
nat_address='198.51.100.30/32',
inside_interface=interfaces[0],
)
real_ip6, nat_ip6 = create_test_nat_ip_pair(
real_address='2001:db8:2::10/128',
nat_address='2001:db8:2::20/128',
inside_interface=interfaces[1],
)
vdc = VirtualDeviceContext.objects.create(
name='VDC NAT', identifier=98, device=device, status='active'
)
vdc.primary_ip4 = nat_ip4
vdc.primary_ip6 = real_ip6
vdc.save()
self.add_permissions('dcim.view_virtualdevicecontext', 'ipam.view_ipaddress')
response = self.client.get(self._get_detail_url(vdc), **self.header)
self.assertHttpStatus(response, status.HTTP_200_OK)
self.assertEqual(response.data['primary_ip4']['nat_inside']['address'], str(real_ip4.address))
self.assertEqual(response.data['primary_ip4']['nat_outside'], [])
self.assertIsNone(response.data['primary_ip6']['nat_inside'])
self.assertCountEqual(
[ip['address'] for ip in response.data['primary_ip6']['nat_outside']],
[str(nat_ip6.address)],
)
def test_get_object_includes_dns_name_on_primary_ip(self):
device = create_test_device('vdc-dns-device')
interfaces = (
Interface.objects.create(device=device, name='eth0', type='other'),
Interface.objects.create(device=device, name='eth1', type='other'),
)
ip4 = IPAddress(address='192.0.2.30/32', dns_name='vdc4.example.com')
ip4.assigned_object = interfaces[0]
ip4.save()
ip6 = IPAddress(address='2001:db8::30/128', dns_name='vdc6.example.com')
ip6.assigned_object = interfaces[1]
ip6.save()
vdc = VirtualDeviceContext.objects.create(
name='VDC DNS', identifier=99, device=device, status='active'
)
vdc.primary_ip4 = ip4
vdc.primary_ip6 = ip6
vdc.save()
self.add_permissions('dcim.view_virtualdevicecontext', 'ipam.view_ipaddress')
response = self.client.get(self._get_detail_url(vdc), **self.header)
self.assertHttpStatus(response, status.HTTP_200_OK)
self.assertEqual(response.data['primary_ip4']['dns_name'], 'vdc4.example.com')
self.assertEqual(response.data['primary_ip6']['dns_name'], 'vdc6.example.com')
self.assertIn(
response.data['primary_ip']['dns_name'],
('vdc4.example.com', 'vdc6.example.com'),
)
class MACAddressTestCase(APIViewTestCases.APIViewTestCase):
model = MACAddress

View File

@ -61,19 +61,19 @@ class VirtualMachineSerializer(PrimaryModelSerializer):
nested=True,
read_only=True,
allow_null=True,
fields=[*IPAddressSerializer.Meta.brief_fields, 'nat_inside', 'nat_outside'],
fields=[*IPAddressSerializer.Meta.brief_fields, 'dns_name', 'nat_inside', 'nat_outside'],
)
primary_ip4 = IPAddressSerializer(
nested=True,
required=False,
allow_null=True,
fields=[*IPAddressSerializer.Meta.brief_fields, 'nat_inside', 'nat_outside'],
fields=[*IPAddressSerializer.Meta.brief_fields, 'dns_name', 'nat_inside', 'nat_outside'],
)
primary_ip6 = IPAddressSerializer(
nested=True,
required=False,
allow_null=True,
fields=[*IPAddressSerializer.Meta.brief_fields, 'nat_inside', 'nat_outside'],
fields=[*IPAddressSerializer.Meta.brief_fields, 'dns_name', 'nat_inside', 'nat_outside'],
)
config_template = ConfigTemplateSerializer(nested=True, required=False, allow_null=True, default=None)

View File

@ -11,7 +11,7 @@ from dcim.models import Platform, Site
from extras.choices import CustomFieldTypeChoices
from extras.models import ConfigTemplate, CustomField
from ipam.choices import VLANQinQRoleChoices
from ipam.models import VLAN, VRF, Prefix
from ipam.models import VLAN, VRF, IPAddress, Prefix
from users.constants import TOKEN_PREFIX
from users.models import Token
from utilities.testing import (
@ -557,6 +557,38 @@ class VirtualMachineTestCase(APIViewTestCases.APIViewTestCase):
[str(nat_ip.address)],
)
def test_get_object_includes_dns_name_on_primary_ip(self):
virtualmachine = create_test_virtualmachine('dns-vm')
interfaces = (
VMInterface.objects.create(virtual_machine=virtualmachine, name='eth0'),
VMInterface.objects.create(virtual_machine=virtualmachine, name='eth1'),
)
ip4 = IPAddress(address='192.0.2.40/32', dns_name='vm4.example.com')
ip4.assigned_object = interfaces[0]
ip4.save()
ip6 = IPAddress(address='2001:db8::40/128', dns_name='vm6.example.com')
ip6.assigned_object = interfaces[1]
ip6.save()
virtualmachine.primary_ip4 = ip4
virtualmachine.primary_ip6 = ip6
virtualmachine.save()
self.add_permissions('virtualization.view_virtualmachine', 'ipam.view_ipaddress')
response = self.client.get(
f'{self._get_detail_url(virtualmachine)}?exclude=config_context',
**self.header,
)
self.assertHttpStatus(response, status.HTTP_200_OK)
self.assertEqual(response.data['primary_ip4']['dns_name'], 'vm4.example.com')
self.assertEqual(response.data['primary_ip6']['dns_name'], 'vm6.example.com')
self.assertIn(
response.data['primary_ip']['dns_name'],
('vm4.example.com', 'vm6.example.com'),
)
def test_render_config_with_config_template_id(self):
default_template = ConfigTemplate.objects.create(
name='Default Template',