netbox/netbox/dcim/tests/test_forms.py
Jonathan Senecal c111c08315 Add dynamic parent resolution for cable CSV imports
Replace device-specific fields with generic parent fields to support
circuits, power panels, and other cable termination types.
2025-11-20 12:15:56 +00:00

616 lines
24 KiB
Python

from django.test import TestCase
from circuits.models import Circuit, CircuitTermination, CircuitType, Provider, ProviderNetwork
from dcim.choices import (
CableTypeChoices, DeviceFaceChoices, DeviceStatusChoices, InterfaceModeChoices, InterfaceTypeChoices,
PortTypeChoices, PowerOutletStatusChoices,
)
from dcim.forms import *
from dcim.models import *
from ipam.models import VLAN
from utilities.testing import create_test_device
from virtualization.models import Cluster, ClusterGroup, ClusterType
def get_id(model, slug):
return model.objects.get(slug=slug).id
class PowerOutletFormTestCase(TestCase):
@classmethod
def setUpTestData(cls):
cls.site = site = Site.objects.create(name='Site 1', slug='site-1')
cls.manufacturer = manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
cls.role = role = DeviceRole.objects.create(
name='Device Role 1', slug='device-role-1', color='ff0000'
)
cls.device_type = device_type = DeviceType.objects.create(
manufacturer=manufacturer, model='Device Type 1', slug='device-type-1', u_height=1
)
cls.rack = rack = Rack.objects.create(name='Rack 1', site=site)
cls.device = Device.objects.create(
name='Device 1', device_type=device_type, role=role, site=site, rack=rack, position=1
)
def test_status_is_required(self):
form = PowerOutletForm(data={
'device': self.device,
'module': None,
'name': 'New Enabled Outlet',
})
self.assertFalse(form.is_valid())
self.assertIn('status', form.errors)
def test_status_must_be_defined_choice(self):
form = PowerOutletForm(data={
'device': self.device,
'module': None,
'name': 'New Enabled Outlet',
'status': 'this isn\'t a defined choice',
})
self.assertFalse(form.is_valid())
self.assertIn('status', form.errors)
self.assertTrue(form.errors['status'][-1].startswith('Select a valid choice.'))
def test_status_recognizes_choices(self):
for index, choice in enumerate(PowerOutletStatusChoices.CHOICES):
form = PowerOutletForm(data={
'device': self.device,
'module': None,
'name': f'New Enabled Outlet {index + 1}',
'status': choice[0],
})
self.assertEqual({}, form.errors)
self.assertTrue(form.is_valid())
instance = form.save()
self.assertEqual(instance.status, choice[0])
class DeviceTestCase(TestCase):
@classmethod
def setUpTestData(cls):
site = Site.objects.create(name='Site 1', slug='site-1')
rack = Rack.objects.create(name='Rack 1', site=site)
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
device_type = DeviceType.objects.create(
manufacturer=manufacturer, model='Device Type 1', slug='device-type-1', u_height=1
)
role = DeviceRole.objects.create(
name='Device Role 1', slug='device-role-1', color='ff0000'
)
Platform.objects.create(name='Platform 1', slug='platform-1')
Device.objects.create(
name='Device 1', device_type=device_type, role=role, site=site, rack=rack, position=1
)
cluster_type = ClusterType.objects.create(name='Cluster Type 1', slug='cluster-type-1')
cluster_group = ClusterGroup.objects.create(name='Cluster Group 1', slug='cluster-group-1')
Cluster.objects.create(name='Cluster 1', type=cluster_type, group=cluster_group)
def test_racked_device(self):
form = DeviceForm(data={
'name': 'New Device',
'role': DeviceRole.objects.first().pk,
'tenant': None,
'manufacturer': Manufacturer.objects.first().pk,
'device_type': DeviceType.objects.first().pk,
'site': Site.objects.first().pk,
'rack': Rack.objects.first().pk,
'face': DeviceFaceChoices.FACE_FRONT,
'position': 2,
'platform': Platform.objects.first().pk,
'status': DeviceStatusChoices.STATUS_ACTIVE,
})
self.assertTrue(form.is_valid())
self.assertTrue(form.save())
def test_racked_device_occupied(self):
form = DeviceForm(data={
'name': 'test',
'role': DeviceRole.objects.first().pk,
'tenant': None,
'manufacturer': Manufacturer.objects.first().pk,
'device_type': DeviceType.objects.first().pk,
'site': Site.objects.first().pk,
'rack': Rack.objects.first().pk,
'face': DeviceFaceChoices.FACE_FRONT,
'position': 1,
'platform': Platform.objects.first().pk,
'status': DeviceStatusChoices.STATUS_ACTIVE,
})
self.assertFalse(form.is_valid())
self.assertIn('position', form.errors)
def test_non_racked_device(self):
form = DeviceForm(data={
'name': 'New Device',
'role': DeviceRole.objects.first().pk,
'tenant': None,
'manufacturer': Manufacturer.objects.first().pk,
'device_type': DeviceType.objects.first().pk,
'site': Site.objects.first().pk,
'rack': None,
'face': None,
'position': None,
'platform': Platform.objects.first().pk,
'status': DeviceStatusChoices.STATUS_ACTIVE,
})
self.assertTrue(form.is_valid())
self.assertTrue(form.save())
def test_non_racked_device_with_face(self):
form = DeviceForm(data={
'name': 'New Device',
'role': DeviceRole.objects.first().pk,
'tenant': None,
'manufacturer': Manufacturer.objects.first().pk,
'device_type': DeviceType.objects.first().pk,
'site': Site.objects.first().pk,
'rack': None,
'face': DeviceFaceChoices.FACE_REAR,
'platform': None,
'status': DeviceStatusChoices.STATUS_ACTIVE,
})
self.assertFalse(form.is_valid())
self.assertIn('face', form.errors)
def test_non_racked_device_with_position(self):
form = DeviceForm(data={
'name': 'New Device',
'role': DeviceRole.objects.first().pk,
'tenant': None,
'manufacturer': Manufacturer.objects.first().pk,
'device_type': DeviceType.objects.first().pk,
'site': Site.objects.first().pk,
'rack': None,
'position': 10,
'platform': None,
'status': DeviceStatusChoices.STATUS_ACTIVE,
})
self.assertFalse(form.is_valid())
self.assertIn('position', form.errors)
class FrontPortTestCase(TestCase):
@classmethod
def setUpTestData(cls):
cls.device = create_test_device('Panel Device 1')
cls.rear_ports = (
RearPort(name='RearPort1', device=cls.device, type=PortTypeChoices.TYPE_8P8C),
RearPort(name='RearPort2', device=cls.device, type=PortTypeChoices.TYPE_8P8C),
RearPort(name='RearPort3', device=cls.device, type=PortTypeChoices.TYPE_8P8C),
RearPort(name='RearPort4', device=cls.device, type=PortTypeChoices.TYPE_8P8C),
)
RearPort.objects.bulk_create(cls.rear_ports)
def test_front_port_label_count_valid(self):
"""
Test that generating an equal number of names and labels passes form validation.
"""
front_port_data = {
'device': self.device.pk,
'name': 'FrontPort[1-4]',
'label': 'Port[1-4]',
'type': PortTypeChoices.TYPE_8P8C,
'rear_port': [f'{rear_port.pk}:1' for rear_port in self.rear_ports],
}
form = FrontPortCreateForm(front_port_data)
self.assertTrue(form.is_valid())
def test_front_port_label_count_mismatch(self):
"""
Check that attempting to generate a differing number of names and labels results in a validation error.
"""
bad_front_port_data = {
'device': self.device.pk,
'name': 'FrontPort[1-4]',
'label': 'Port[1-2]',
'type': PortTypeChoices.TYPE_8P8C,
'rear_port': [f'{rear_port.pk}:1' for rear_port in self.rear_ports],
}
form = FrontPortCreateForm(bad_front_port_data)
self.assertFalse(form.is_valid())
self.assertIn('label', form.errors)
class InterfaceTestCase(TestCase):
@classmethod
def setUpTestData(cls):
cls.device = create_test_device('Device 1')
cls.vlans = (
VLAN(name='VLAN 1', vid=1),
VLAN(name='VLAN 2', vid=2),
VLAN(name='VLAN 3', vid=3),
)
VLAN.objects.bulk_create(cls.vlans)
cls.interface = Interface.objects.create(
device=cls.device,
name='Interface 1',
type=InterfaceTypeChoices.TYPE_1GE_GBIC,
mode=InterfaceModeChoices.MODE_TAGGED,
)
def test_interface_label_count_valid(self):
"""
Test that generating an equal number of names and labels passes form validation.
"""
interface_data = {
'device': self.device.pk,
'name': 'eth[0-9]',
'label': 'Interface[0-9]',
'type': InterfaceTypeChoices.TYPE_1GE_GBIC,
}
form = InterfaceCreateForm(interface_data)
self.assertTrue(form.is_valid())
def test_interface_label_count_mismatch(self):
"""
Check that attempting to generate a differing number of names and labels results in a validation error.
"""
bad_interface_data = {
'device': self.device.pk,
'name': 'eth[0-9]',
'label': 'Interface[0-1]',
'type': InterfaceTypeChoices.TYPE_1GE_GBIC,
}
form = InterfaceCreateForm(bad_interface_data)
self.assertFalse(form.is_valid())
self.assertIn('label', form.errors)
def test_create_interface_mode_valid_data(self):
"""
Test that saving valid interface mode and tagged/untagged vlans works properly
"""
# Validate access mode
data = {
'device': self.device.pk,
'name': 'ethernet1/1',
'type': InterfaceTypeChoices.TYPE_1GE_GBIC,
'mode': InterfaceModeChoices.MODE_ACCESS,
'untagged_vlan': self.vlans[0].pk
}
form = InterfaceCreateForm(data)
self.assertTrue(form.is_valid())
# Validate tagged vlans
data = {
'device': self.device.pk,
'name': 'ethernet1/2',
'type': InterfaceTypeChoices.TYPE_1GE_GBIC,
'mode': InterfaceModeChoices.MODE_TAGGED,
'untagged_vlan': self.vlans[0].pk,
'tagged_vlans': [self.vlans[1].pk, self.vlans[2].pk]
}
form = InterfaceCreateForm(data)
self.assertTrue(form.is_valid())
# Validate tagged vlans
data = {
'device': self.device.pk,
'name': 'ethernet1/3',
'type': InterfaceTypeChoices.TYPE_1GE_GBIC,
'mode': InterfaceModeChoices.MODE_TAGGED_ALL,
'untagged_vlan': self.vlans[0].pk,
}
form = InterfaceCreateForm(data)
self.assertTrue(form.is_valid())
def test_create_interface_mode_access_invalid_data(self):
"""
Test that saving invalid interface mode and tagged/untagged vlans works properly
"""
data = {
'device': self.device.pk,
'name': 'ethernet1/4',
'type': InterfaceTypeChoices.TYPE_1GE_GBIC,
'mode': InterfaceModeChoices.MODE_ACCESS,
'untagged_vlan': self.vlans[0].pk,
'tagged_vlans': [self.vlans[1].pk, self.vlans[2].pk]
}
form = InterfaceCreateForm(data)
self.assertTrue(form.is_valid())
self.assertIn('untagged_vlan', form.cleaned_data.keys())
self.assertNotIn('tagged_vlans', form.cleaned_data.keys())
self.assertNotIn('qinq_svlan', form.cleaned_data.keys())
def test_edit_interface_mode_access_invalid_data(self):
"""
Test that saving invalid interface mode and tagged/untagged vlans works properly
"""
data = {
'device': self.device.pk,
'name': 'Ethernet 1/5',
'type': InterfaceTypeChoices.TYPE_1GE_GBIC,
'mode': InterfaceModeChoices.MODE_ACCESS,
'tagged_vlans': [self.vlans[0].pk, self.vlans[1].pk, self.vlans[2].pk]
}
form = InterfaceForm(data, instance=self.interface)
self.assertTrue(form.is_valid())
self.assertIn('untagged_vlan', form.cleaned_data.keys())
self.assertNotIn('tagged_vlans', form.cleaned_data.keys())
self.assertNotIn('qinq_svlan', form.cleaned_data.keys())
def test_create_interface_mode_tagged_all_invalid_data(self):
"""
Test that saving invalid interface mode and tagged/untagged vlans works properly
"""
data = {
'device': self.device.pk,
'name': 'ethernet1/6',
'type': InterfaceTypeChoices.TYPE_1GE_GBIC,
'mode': InterfaceModeChoices.MODE_TAGGED_ALL,
'tagged_vlans': [self.vlans[0].pk, self.vlans[1].pk, self.vlans[2].pk]
}
form = InterfaceCreateForm(data)
self.assertTrue(form.is_valid())
self.assertIn('untagged_vlan', form.cleaned_data.keys())
self.assertNotIn('tagged_vlans', form.cleaned_data.keys())
self.assertNotIn('qinq_svlan', form.cleaned_data.keys())
def test_edit_interface_mode_tagged_all_invalid_data(self):
"""
Test that saving invalid interface mode and tagged/untagged vlans works properly
"""
data = {
'device': self.device.pk,
'name': 'Ethernet 1/7',
'type': InterfaceTypeChoices.TYPE_1GE_GBIC,
'mode': InterfaceModeChoices.MODE_TAGGED_ALL,
'tagged_vlans': [self.vlans[0].pk, self.vlans[1].pk, self.vlans[2].pk]
}
form = InterfaceForm(data)
self.assertTrue(form.is_valid())
self.assertIn('untagged_vlan', form.cleaned_data.keys())
self.assertNotIn('tagged_vlans', form.cleaned_data.keys())
self.assertNotIn('qinq_svlan', form.cleaned_data.keys())
def test_create_interface_mode_routed_invalid_data(self):
"""
Test that saving invalid interface mode (routed) and tagged/untagged vlans works properly
"""
data = {
'device': self.device.pk,
'name': 'ethernet1/6',
'type': InterfaceTypeChoices.TYPE_1GE_GBIC,
'mode': None,
'untagged_vlan': self.vlans[0].pk,
'tagged_vlans': [self.vlans[0].pk, self.vlans[1].pk, self.vlans[2].pk]
}
form = InterfaceCreateForm(data)
self.assertTrue(form.is_valid())
self.assertNotIn('untagged_vlan', form.cleaned_data.keys())
self.assertNotIn('tagged_vlans', form.cleaned_data.keys())
self.assertNotIn('qinq_svlan', form.cleaned_data.keys())
def test_edit_interface_mode_routed_invalid_data(self):
"""
Test that saving invalid interface mode (routed) and tagged/untagged vlans works properly
"""
data = {
'device': self.device.pk,
'name': 'Ethernet 1/7',
'type': InterfaceTypeChoices.TYPE_1GE_GBIC,
'mode': None,
'untagged_vlan': self.vlans[0].pk,
'tagged_vlans': [self.vlans[0].pk, self.vlans[1].pk, self.vlans[2].pk]
}
form = InterfaceForm(data)
self.assertTrue(form.is_valid())
self.assertNotIn('untagged_vlan', form.cleaned_data.keys())
self.assertNotIn('tagged_vlans', form.cleaned_data.keys())
self.assertNotIn('qinq_svlan', form.cleaned_data.keys())
class CableImportFormTestCase(TestCase):
"""
Test cases for CableImportForm error handling and edge cases.
Note: Happy path scenarios (successful cable creation) are covered by
dcim.tests.test_views.CableTestCase which tests the bulk import view.
These tests focus on validation errors and edge cases not covered by the view tests.
"""
@classmethod
def setUpTestData(cls):
# Create sites
cls.site_a = Site.objects.create(name='Site A', slug='site-a')
cls.site_b = Site.objects.create(name='Site B', slug='site-b')
# Create manufacturer and device type
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
device_type = DeviceType.objects.create(
manufacturer=manufacturer,
model='Device Type 1',
slug='device-type-1',
)
role = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1', color='ff0000')
# Create devices
cls.device_a1 = Device.objects.create(
name='Device-A1',
device_type=device_type,
role=role,
site=cls.site_a,
)
cls.device_a2 = Device.objects.create(
name='Device-A2',
device_type=device_type,
role=role,
site=cls.site_a,
)
# Device with same name in different site
cls.device_b_duplicate = Device.objects.create(
name='Device-A1', # Same name as device_a1
device_type=device_type,
role=role,
site=cls.site_b,
)
# Create interfaces
cls.interface_a1_eth0 = Interface.objects.create(
device=cls.device_a1,
name='eth0',
type=InterfaceTypeChoices.TYPE_1GE_FIXED,
)
cls.interface_a2_eth0 = Interface.objects.create(
device=cls.device_a2,
name='eth0',
type=InterfaceTypeChoices.TYPE_1GE_FIXED,
)
# Create circuit for testing circuit not found error
provider = Provider.objects.create(name='Provider 1', slug='provider-1')
circuit_type = CircuitType.objects.create(name='Circuit Type 1', slug='circuit-type-1')
cls.circuit = Circuit.objects.create(
provider=provider,
type=circuit_type,
cid='CIRCUIT-001',
)
cls.circuit_term_a = CircuitTermination.objects.create(
circuit=cls.circuit,
term_side='A',
)
# Create provider network for testing provider network validation
cls.provider_network = ProviderNetwork.objects.create(
provider=provider,
name='Provider Network 1',
)
def test_device_not_found(self):
"""Test error when parent device is not found."""
form = CableImportForm(data={
'side_a_site': 'Site A',
'side_a_parent': 'NonexistentDevice',
'side_a_type': 'dcim.interface',
'side_a_name': 'eth0',
'side_b_site': 'Site A',
'side_b_parent': 'Device-A2',
'side_b_type': 'dcim.interface',
'side_b_name': 'eth0',
'type': CableTypeChoices.TYPE_CAT6,
'status': 'connected',
})
self.assertFalse(form.is_valid())
self.assertIn('Side A: Device not found: NonexistentDevice', str(form.errors))
def test_circuit_not_found(self):
"""Test error when circuit is not found."""
form = CableImportForm(data={
'side_a_site': None,
'side_a_parent': 'NONEXISTENT-CID',
'side_a_type': 'circuits.circuittermination',
'side_a_name': 'A',
'side_b_site': 'Site A',
'side_b_parent': 'Device-A1',
'side_b_type': 'dcim.interface',
'side_b_name': 'eth0',
'type': CableTypeChoices.TYPE_MMF_OM4,
'status': 'connected',
})
self.assertFalse(form.is_valid())
self.assertIn('Side A: Circuit not found: NONEXISTENT-CID', str(form.errors))
def test_termination_not_found(self):
"""Test error when termination is not found on parent."""
form = CableImportForm(data={
'side_a_site': 'Site A',
'side_a_parent': 'Device-A1',
'side_a_type': 'dcim.interface',
'side_a_name': 'eth999', # Nonexistent interface
'side_b_site': 'Site A',
'side_b_parent': 'Device-A2',
'side_b_type': 'dcim.interface',
'side_b_name': 'eth0',
'type': CableTypeChoices.TYPE_CAT6,
'status': 'connected',
})
self.assertFalse(form.is_valid())
self.assertIn('Side A: Interface not found', str(form.errors))
def test_termination_already_cabled(self):
"""Test error when termination is already connected to a cable."""
# Create an existing cable
existing_cable = Cable.objects.create(type=CableTypeChoices.TYPE_CAT6, status='connected')
self.interface_a1_eth0.cable = existing_cable
self.interface_a1_eth0.save()
form = CableImportForm(data={
'side_a_site': 'Site A',
'side_a_parent': 'Device-A1',
'side_a_type': 'dcim.interface',
'side_a_name': 'eth0',
'side_b_site': 'Site A',
'side_b_parent': 'Device-A2',
'side_b_type': 'dcim.interface',
'side_b_name': 'eth0',
'type': CableTypeChoices.TYPE_CAT6,
'status': 'connected',
})
self.assertFalse(form.is_valid())
self.assertIn('already connected', str(form.errors))
def test_circuit_termination_with_provider_network(self):
"""Test error when circuit termination is already connected to a provider network."""
from django.contrib.contenttypes.models import ContentType
# Connect circuit termination to provider network
circuit_term = CircuitTermination.objects.get(pk=self.circuit_term_a.pk)
pn_ct = ContentType.objects.get_for_model(ProviderNetwork)
circuit_term.termination_type = pn_ct
circuit_term.termination_id = self.provider_network.pk
circuit_term.save()
try:
form = CableImportForm(data={
'side_a_site': None,
'side_a_parent': 'CIRCUIT-001',
'side_a_type': 'circuits.circuittermination',
'side_a_name': 'A',
'side_b_site': 'Site A',
'side_b_parent': 'Device-A1',
'side_b_type': 'dcim.interface',
'side_b_name': 'eth0',
'type': CableTypeChoices.TYPE_MMF_OM4,
'status': 'connected',
})
self.assertFalse(form.is_valid())
self.assertIn('already connected to a provider network', str(form.errors))
finally:
# Clean up: remove provider network connection
circuit_term.termination_type = None
circuit_term.termination_id = None
circuit_term.save()
def test_multiple_parents_without_site(self):
"""Test error when multiple parent objects are found without site scoping."""
# Device-A1 exists in both site_a and site_b
# Try to find device without specifying site
form = CableImportForm(data={
'side_a_site': '', # Empty site - should cause multiple matches
'side_a_parent': 'Device-A1',
'side_a_type': 'dcim.interface',
'side_a_name': 'eth0',
'side_b_site': 'Site A',
'side_b_parent': 'Device-A2',
'side_b_type': 'dcim.interface',
'side_b_name': 'eth0',
'type': CableTypeChoices.TYPE_CAT6,
'status': 'connected',
})
self.assertFalse(form.is_valid())
self.assertIn('Multiple Device objects found', str(form.errors))