mirror of
https://github.com/netbox-community/netbox.git
synced 2026-01-19 10:08:44 -06:00
Compare commits
3 Commits
f016a47bea
...
b64fb69fa7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b64fb69fa7 | ||
|
|
ad29402b87 | ||
|
|
c111c08315 |
@@ -5,6 +5,7 @@ from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from circuits.models import Circuit
|
||||
from dcim.choices import *
|
||||
from dcim.constants import *
|
||||
from dcim.models import *
|
||||
@@ -1414,19 +1415,52 @@ class MACAddressImportForm(NetBoxModelImportForm):
|
||||
#
|
||||
|
||||
class CableImportForm(NetBoxModelImportForm):
|
||||
"""
|
||||
CSV bulk import form for cables.
|
||||
|
||||
Supports dynamic parent model resolution - terminations are identified by their parent
|
||||
object (device, circuit, or power panel) and termination name.
|
||||
|
||||
The parent field resolves to different models based on the termination type
|
||||
See CABLE_PARENT_MAPPING for supported termination types.
|
||||
"""
|
||||
|
||||
# Map cable termination content types to their parent model and lookup field.
|
||||
#
|
||||
# This mapping enables dynamic parent model resolution during cable CSV imports.
|
||||
# Each entry maps a termination type to a tuple of (parent_content_type, accessor):
|
||||
#
|
||||
# Format: 'app.model': ('parent_app.ParentModel', 'accessor')
|
||||
#
|
||||
CABLE_PARENT_MAPPING = {
|
||||
'dcim.interface': ('dcim.Device', 'name'),
|
||||
'dcim.consoleport': ('dcim.Device', 'name'),
|
||||
'dcim.consoleserverport': ('dcim.Device', 'name'),
|
||||
'dcim.powerport': ('dcim.Device', 'name'),
|
||||
'dcim.poweroutlet': ('dcim.Device', 'name'),
|
||||
'dcim.frontport': ('dcim.Device', 'name'),
|
||||
'dcim.rearport': ('dcim.Device', 'name'),
|
||||
'circuits.circuittermination': ('circuits.Circuit', 'cid'),
|
||||
'dcim.powerfeed': ('dcim.PowerPanel', 'name'),
|
||||
}
|
||||
# Map parent model name to (parent_field_name, termination_name_field, value_transform)
|
||||
TERMINATION_FIELDS = {
|
||||
'Circuit': ('circuit', 'term_side', str.upper),
|
||||
'Device': ('device', 'name', None),
|
||||
'PowerPanel': ('power_panel', 'name', None),
|
||||
}
|
||||
|
||||
# Termination A
|
||||
side_a_site = CSVModelChoiceField(
|
||||
label=_('Side A site'),
|
||||
queryset=Site.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text=_('Site of parent device A (if any)'),
|
||||
help_text=_('Site of parent A (if any)')
|
||||
)
|
||||
side_a_device = CSVModelChoiceField(
|
||||
label=_('Side A device'),
|
||||
queryset=Device.objects.all(),
|
||||
to_field_name='name',
|
||||
help_text=_('Device name')
|
||||
side_a_parent = forms.CharField(
|
||||
label=_('Side A parent'),
|
||||
help_text=_('Device name, Circuit CID, or Power Panel name')
|
||||
)
|
||||
side_a_type = CSVContentTypeField(
|
||||
label=_('Side A type'),
|
||||
@@ -1445,13 +1479,11 @@ class CableImportForm(NetBoxModelImportForm):
|
||||
queryset=Site.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text=_('Site of parent device B (if any)'),
|
||||
help_text=_('Site of parent B (if any)')
|
||||
)
|
||||
side_b_device = CSVModelChoiceField(
|
||||
label=_('Side B device'),
|
||||
queryset=Device.objects.all(),
|
||||
to_field_name='name',
|
||||
help_text=_('Device name')
|
||||
side_b_parent = forms.CharField(
|
||||
label=_('Side B parent'),
|
||||
help_text=_('Device name, Circuit CID, or Power Panel name')
|
||||
)
|
||||
side_b_type = CSVContentTypeField(
|
||||
label=_('Side B type'),
|
||||
@@ -1500,7 +1532,7 @@ class CableImportForm(NetBoxModelImportForm):
|
||||
class Meta:
|
||||
model = Cable
|
||||
fields = [
|
||||
'side_a_site', 'side_a_device', 'side_a_type', 'side_a_name', 'side_b_site', 'side_b_device', 'side_b_type',
|
||||
'side_a_site', 'side_a_parent', 'side_a_type', 'side_a_name', 'side_b_site', 'side_b_parent', 'side_b_type',
|
||||
'side_b_name', 'type', 'status', 'tenant', 'label', 'color', 'length', 'length_unit', 'description',
|
||||
'comments', 'tags',
|
||||
]
|
||||
@@ -1508,21 +1540,6 @@ class CableImportForm(NetBoxModelImportForm):
|
||||
def __init__(self, data=None, *args, **kwargs):
|
||||
super().__init__(data, *args, **kwargs)
|
||||
|
||||
if data:
|
||||
# Limit choices for side_a_device to the assigned side_a_site
|
||||
if side_a_site := data.get('side_a_site'):
|
||||
side_a_device_params = {f'site__{self.fields["side_a_site"].to_field_name}': side_a_site}
|
||||
self.fields['side_a_device'].queryset = self.fields['side_a_device'].queryset.filter(
|
||||
**side_a_device_params
|
||||
)
|
||||
|
||||
# Limit choices for side_b_device to the assigned side_b_site
|
||||
if side_b_site := data.get('side_b_site'):
|
||||
side_b_device_params = {f'site__{self.fields["side_b_site"].to_field_name}': side_b_site}
|
||||
self.fields['side_b_device'].queryset = self.fields['side_b_device'].queryset.filter(
|
||||
**side_b_device_params
|
||||
)
|
||||
|
||||
def _clean_side(self, side):
|
||||
"""
|
||||
Derive a Cable's A/B termination objects.
|
||||
@@ -1531,31 +1548,118 @@ class CableImportForm(NetBoxModelImportForm):
|
||||
"""
|
||||
assert side in 'ab', f"Invalid side designation: {side}"
|
||||
|
||||
device = self.cleaned_data.get(f'side_{side}_device')
|
||||
content_type = self.cleaned_data.get(f'side_{side}_type')
|
||||
site = self.cleaned_data.get(f'side_{side}_site')
|
||||
parent_value = self.cleaned_data.get(f'side_{side}_parent')
|
||||
name = self.cleaned_data.get(f'side_{side}_name')
|
||||
if not device or not content_type or not name:
|
||||
|
||||
if not parent_value or not content_type or not name: # pragma: no cover
|
||||
return None
|
||||
|
||||
model = content_type.model_class()
|
||||
# Get the parent model mapping from the submitted content_type
|
||||
parent_map = self.CABLE_PARENT_MAPPING.get(f'{content_type.app_label}.{content_type.model}')
|
||||
# This should never happen
|
||||
assert parent_map, (
|
||||
'Unknown cable termination content type parent mapping: '
|
||||
f'{content_type.app_label}.{content_type.model}'
|
||||
)
|
||||
|
||||
parent_content_type, parent_accessor = parent_map
|
||||
parent_app_label, parent_model_name = parent_content_type.split('.')
|
||||
|
||||
# Get the parent model class
|
||||
try:
|
||||
if device.virtual_chassis and device.virtual_chassis.master == device and \
|
||||
model.objects.filter(device=device, name=name).count() == 0:
|
||||
termination_object = model.objects.get(device__in=device.virtual_chassis.members.all(), name=name)
|
||||
else:
|
||||
termination_object = model.objects.get(device=device, name=name)
|
||||
if termination_object.cable is not None and termination_object.cable != self.instance:
|
||||
raise forms.ValidationError(
|
||||
_("Side {side_upper}: {device} {termination_object} is already connected").format(
|
||||
side_upper=side.upper(), device=device, termination_object=termination_object
|
||||
)
|
||||
)
|
||||
except ObjectDoesNotExist:
|
||||
parent_ct = ContentType.objects.get(app_label=parent_app_label.lower(), model=parent_model_name.lower())
|
||||
parent_model: Device | PowerPanel | Circuit = parent_ct.model_class()
|
||||
except ContentType.DoesNotExist: # pragma: no cover
|
||||
# This should never happen
|
||||
raise AssertionError(f'Unknown cable termination parent content type: {parent_content_type}')
|
||||
|
||||
# Build query for parent lookup
|
||||
parent_query = {parent_accessor: parent_value}
|
||||
# Add site to query if provided
|
||||
if site:
|
||||
parent_query['site'] = site
|
||||
|
||||
# Look up the parent object
|
||||
try:
|
||||
parent_object = parent_model.objects.get(**parent_query)
|
||||
except parent_model.DoesNotExist:
|
||||
raise forms.ValidationError(
|
||||
_("{side_upper} side termination not found: {device} {name}").format(
|
||||
side_upper=side.upper(), device=device, name=name
|
||||
_('Side {side_upper}: {model_name} not found: {value}').format(
|
||||
side_upper=side.upper(), model_name=parent_model.__name__, value=parent_value
|
||||
)
|
||||
)
|
||||
except parent_model.MultipleObjectsReturned:
|
||||
raise forms.ValidationError(
|
||||
_('Side {side_upper}: Multiple {model_name} objects found: {value}').format(
|
||||
side_upper=side.upper(), model_name=parent_model.__name__, value=parent_value
|
||||
)
|
||||
)
|
||||
|
||||
# Get the termination model class
|
||||
termination_model = content_type.model_class()
|
||||
|
||||
# Build the query to find the termination object
|
||||
field_mapping = self.TERMINATION_FIELDS.get(parent_model.__name__)
|
||||
if not field_mapping: # pragma: no cover
|
||||
return None
|
||||
|
||||
parent_field, name_field, value_transform = field_mapping
|
||||
query = {parent_field: parent_object}
|
||||
|
||||
if value_transform:
|
||||
name = value_transform(name)
|
||||
|
||||
if name:
|
||||
query[name_field] = name
|
||||
|
||||
# Add site to query if provided (for site-scoped parents)
|
||||
if site and parent_field in ('device', 'power_panel'):
|
||||
query[f'{parent_field}__site'] = site
|
||||
|
||||
# Look up the termination object
|
||||
try:
|
||||
# Handle virtual chassis for device-based terminations
|
||||
if (parent_field == 'device' and
|
||||
parent_object.virtual_chassis and
|
||||
parent_object.virtual_chassis.master == parent_object and
|
||||
termination_model.objects.filter(**query).count() == 0):
|
||||
query[f'{parent_field}__in'] = parent_object.virtual_chassis.members.all()
|
||||
query.pop(parent_field, None)
|
||||
termination_object = termination_model.objects.get(**query)
|
||||
else:
|
||||
termination_object = termination_model.objects.get(**query)
|
||||
|
||||
# Check if already connected to a cable
|
||||
if termination_object.cable is not None and termination_object.cable != self.instance:
|
||||
raise forms.ValidationError(
|
||||
_('Side {side_upper}: {parent} {termination} is already connected').format(
|
||||
side_upper=side.upper(), parent=parent_object, termination=termination_object
|
||||
)
|
||||
)
|
||||
|
||||
# Circuit terminations can also be connected to provider networks
|
||||
if (name_field == 'term_side' and
|
||||
hasattr(termination_object, '_provider_network') and
|
||||
termination_object._provider_network is not None):
|
||||
raise forms.ValidationError(
|
||||
_('Side {side_upper}: {parent} {termination} is already connected to a provider network').format(
|
||||
side_upper=side.upper(), parent=parent_object, termination=termination_object
|
||||
)
|
||||
)
|
||||
except termination_model.DoesNotExist:
|
||||
raise forms.ValidationError(
|
||||
_('Side {side_upper}: {model_name} not found: {parent} {name}').format(
|
||||
side_upper=side.upper(),
|
||||
model_name=termination_model.__name__,
|
||||
parent=parent_object, name=name or '',
|
||||
),
|
||||
)
|
||||
except termination_model.MultipleObjectsReturned: # pragma: no cover
|
||||
# This should never happen
|
||||
raise AssertionError('Multiple termination objects returned for query: {query}'.format(query=query))
|
||||
|
||||
setattr(self.instance, f'{side}_terminations', [termination_object])
|
||||
return termination_object
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
from django.test import TestCase
|
||||
|
||||
from circuits.models import Circuit, CircuitTermination, CircuitType, Provider, ProviderNetwork
|
||||
from dcim.choices import (
|
||||
DeviceFaceChoices, DeviceStatusChoices, InterfaceModeChoices, InterfaceTypeChoices, PortTypeChoices,
|
||||
PowerOutletStatusChoices,
|
||||
CableTypeChoices, DeviceFaceChoices, DeviceStatusChoices, InterfaceModeChoices, InterfaceTypeChoices,
|
||||
PortTypeChoices, PowerOutletStatusChoices,
|
||||
)
|
||||
from dcim.forms import *
|
||||
from dcim.models import *
|
||||
@@ -411,3 +412,204 @@ class InterfaceTestCase(TestCase):
|
||||
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))
|
||||
|
||||
@@ -11,6 +11,7 @@ from core.models import ObjectType
|
||||
from dcim.choices import *
|
||||
from dcim.constants import *
|
||||
from dcim.models import *
|
||||
from circuits.models import Circuit, CircuitTermination, CircuitType, Provider
|
||||
from ipam.models import ASN, RIR, VLAN, VRF
|
||||
from netbox.choices import CSVDelimiterChoices, ImportFormatChoices, WeightUnitChoices
|
||||
from tenancy.models import Tenant
|
||||
@@ -3495,7 +3496,7 @@ class CableTestCase(
|
||||
Interface(device=devices[4], name='Interface 2', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
|
||||
Interface(device=devices[4], name='Interface 3', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
|
||||
|
||||
# Device 1, Site 2
|
||||
# Device 5, Site 2
|
||||
Interface(device=devices[5], name='Interface 1', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
|
||||
Interface(device=devices[5], name='Interface 2', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
|
||||
Interface(device=devices[5], name='Interface 3', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
|
||||
@@ -3507,6 +3508,22 @@ class CableTestCase(
|
||||
)
|
||||
Interface.objects.bulk_create(interfaces)
|
||||
|
||||
ConsolePort.objects.create(device=devices[0], name='Console 1')
|
||||
ConsoleServerPort.objects.create(device=devices[1], name='Console Server 1')
|
||||
|
||||
power_panel = PowerPanel.objects.create(site=sites[0], name='Power Panel 1')
|
||||
PowerFeed.objects.create(power_panel=power_panel, name='Feed 1')
|
||||
PowerPort.objects.create(device=devices[0], name='PSU1')
|
||||
|
||||
provider = Provider.objects.create(name='Provider 1', slug='provider-1')
|
||||
circuit_type = CircuitType.objects.create(name='Circuit Type 1', slug='circuit-type-1')
|
||||
circuit = Circuit.objects.create(provider=provider, type=circuit_type, cid='CIRCUIT-001')
|
||||
circuit_terminations = (
|
||||
CircuitTermination(circuit=circuit, term_side='A'),
|
||||
CircuitTermination(circuit=circuit, term_side='Z'),
|
||||
)
|
||||
CircuitTermination.objects.bulk_create(circuit_terminations)
|
||||
|
||||
cable1 = Cable(a_terminations=[interfaces[0]], b_terminations=[interfaces[3]], type=CableTypeChoices.TYPE_CAT6)
|
||||
cable1.save()
|
||||
cable2 = Cable(a_terminations=[interfaces[1]], b_terminations=[interfaces[4]], type=CableTypeChoices.TYPE_CAT6)
|
||||
@@ -3532,7 +3549,7 @@ class CableTestCase(
|
||||
|
||||
cls.csv_data = {
|
||||
'default': (
|
||||
"side_a_device,side_a_type,side_a_name,side_b_device,side_b_type,side_b_name",
|
||||
"side_a_parent,side_a_type,side_a_name,side_b_parent,side_b_type,side_b_name",
|
||||
"Device 4,dcim.interface,Interface 1,Device 5,dcim.interface,Interface 1",
|
||||
"Device 3,dcim.interface,Interface 2,Device 4,dcim.interface,Interface 2",
|
||||
"Device 3,dcim.interface,Interface 3,Device 4,dcim.interface,Interface 3",
|
||||
@@ -3545,12 +3562,28 @@ class CableTestCase(
|
||||
'site-filtering': (
|
||||
# Ensure that CSV bulk import supports assigning terminations from parent devices
|
||||
# that share the same device name, provided those devices belong to different sites.
|
||||
"side_a_site,side_a_device,side_a_type,side_a_name,side_b_site,side_b_device,side_b_type,side_b_name",
|
||||
"side_a_site,side_a_parent,side_a_type,side_a_name,side_b_site,side_b_parent,side_b_type,side_b_name",
|
||||
"Site 1,Device 3,dcim.interface,Interface 1,Site 2,Device 1,dcim.interface,Interface 1",
|
||||
"Site 1,Device 3,dcim.interface,Interface 2,Site 2,Device 1,dcim.interface,Interface 2",
|
||||
"Site 1,Device 3,dcim.interface,Interface 3,Site 2,Device 1,dcim.interface,Interface 3",
|
||||
"Site 1,Device 1,dcim.interface,Device 2 Interface,Site 2,Device 1,dcim.interface,Interface 4",
|
||||
"Site 1,Device 1,dcim.interface,Device 3 Interface,Site 2,Device 1,dcim.interface,Interface 5",
|
||||
),
|
||||
'circuits': (
|
||||
# Test circuit termination to interface cables
|
||||
"side_a_parent,side_a_type,side_a_name,side_b_site,side_b_parent,side_b_type,side_b_name",
|
||||
"CIRCUIT-001,circuits.circuittermination,A,Site 1,Device 4,dcim.interface,Interface 2",
|
||||
"CIRCUIT-001,circuits.circuittermination,z,Site 2,Device 5,dcim.interface,Interface 2",
|
||||
),
|
||||
'power': (
|
||||
# Test power feed to power port cables
|
||||
"side_a_site,side_a_parent,side_a_type,side_a_name,side_b_site,side_b_parent,side_b_type,side_b_name",
|
||||
"Site 1,Power Panel 1,dcim.powerfeed,Feed 1,Site 1,Device 1,dcim.powerport,PSU1",
|
||||
),
|
||||
'console': (
|
||||
# Test console port to console server port cables
|
||||
"side_a_site,side_a_parent,side_a_type,side_a_name,side_b_site,side_b_parent,side_b_type,side_b_name",
|
||||
"Site 1,Device 1,dcim.consoleport,Console 1,Site 1,Device 2,dcim.consoleserverport,Console Server 1",
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-12-12 05:02+0000\n"
|
||||
"POT-Creation-Date: 2025-12-13 05:01+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@@ -1441,7 +1441,7 @@ msgstr ""
|
||||
#: netbox/dcim/models/device_components.py:517
|
||||
#: netbox/dcim/models/device_components.py:1063
|
||||
#: netbox/dcim/models/device_components.py:1134
|
||||
#: netbox/dcim/models/device_components.py:1280
|
||||
#: netbox/dcim/models/device_components.py:1282
|
||||
#: netbox/dcim/models/devices.py:382 netbox/dcim/models/racks.py:227
|
||||
#: netbox/extras/models/tags.py:29
|
||||
msgid "color"
|
||||
@@ -1469,8 +1469,8 @@ msgstr ""
|
||||
#: netbox/circuits/models/virtual_circuits.py:59 netbox/core/models/data.py:52
|
||||
#: netbox/core/models/jobs.py:95 netbox/dcim/models/cables.py:51
|
||||
#: netbox/dcim/models/device_components.py:488
|
||||
#: netbox/dcim/models/device_components.py:1319
|
||||
#: netbox/dcim/models/devices.py:580 netbox/dcim/models/devices.py:1202
|
||||
#: netbox/dcim/models/device_components.py:1321
|
||||
#: netbox/dcim/models/devices.py:580 netbox/dcim/models/devices.py:1207
|
||||
#: netbox/dcim/models/modules.py:209 netbox/dcim/models/power.py:94
|
||||
#: netbox/dcim/models/racks.py:294 netbox/dcim/models/racks.py:677
|
||||
#: netbox/dcim/models/sites.py:157 netbox/dcim/models/sites.py:281
|
||||
@@ -1604,7 +1604,7 @@ msgstr ""
|
||||
#: netbox/core/models/jobs.py:56
|
||||
#: netbox/dcim/models/device_component_templates.py:44
|
||||
#: netbox/dcim/models/device_components.py:53 netbox/dcim/models/devices.py:524
|
||||
#: netbox/dcim/models/devices.py:1128 netbox/dcim/models/devices.py:1197
|
||||
#: netbox/dcim/models/devices.py:1133 netbox/dcim/models/devices.py:1202
|
||||
#: netbox/dcim/models/modules.py:31 netbox/dcim/models/power.py:38
|
||||
#: netbox/dcim/models/power.py:89 netbox/dcim/models/racks.py:263
|
||||
#: netbox/dcim/models/sites.py:145 netbox/extras/models/configs.py:36
|
||||
@@ -3817,8 +3817,8 @@ msgstr ""
|
||||
|
||||
#: netbox/dcim/filtersets.py:1197 netbox/dcim/forms/filtersets.py:855
|
||||
#: netbox/dcim/forms/filtersets.py:1483 netbox/dcim/forms/filtersets.py:1699
|
||||
#: netbox/dcim/forms/model_forms.py:1900 netbox/dcim/models/devices.py:1298
|
||||
#: netbox/dcim/models/devices.py:1318 netbox/virtualization/filtersets.py:201
|
||||
#: netbox/dcim/forms/model_forms.py:1900 netbox/dcim/models/devices.py:1303
|
||||
#: netbox/dcim/models/devices.py:1323 netbox/virtualization/filtersets.py:201
|
||||
#: netbox/virtualization/filtersets.py:273
|
||||
#: netbox/virtualization/forms/filtersets.py:178
|
||||
#: netbox/virtualization/forms/filtersets.py:231
|
||||
@@ -6277,12 +6277,12 @@ msgid ""
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/device_component_templates.py:777
|
||||
#: netbox/dcim/models/device_components.py:1340
|
||||
#: netbox/dcim/models/device_components.py:1342
|
||||
msgid "part ID"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/device_component_templates.py:779
|
||||
#: netbox/dcim/models/device_components.py:1342
|
||||
#: netbox/dcim/models/device_components.py:1344
|
||||
msgid "Manufacturer-assigned part identifier"
|
||||
msgstr ""
|
||||
|
||||
@@ -6631,83 +6631,83 @@ msgstr ""
|
||||
msgid "A module bay cannot belong to a module installed within it."
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/device_components.py:1243
|
||||
#: netbox/dcim/models/device_components.py:1245
|
||||
msgid "device bay"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/device_components.py:1244
|
||||
#: netbox/dcim/models/device_components.py:1246
|
||||
msgid "device bays"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/device_components.py:1251
|
||||
#: netbox/dcim/models/device_components.py:1253
|
||||
#, python-brace-format
|
||||
msgid "This type of device ({device_type}) does not support device bays."
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/device_components.py:1257
|
||||
#: netbox/dcim/models/device_components.py:1259
|
||||
msgid "Cannot install a device into itself."
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/device_components.py:1265
|
||||
#: netbox/dcim/models/device_components.py:1267
|
||||
#, python-brace-format
|
||||
msgid ""
|
||||
"Cannot install the specified device; device is already installed in {bay}."
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/device_components.py:1286
|
||||
#: netbox/dcim/models/device_components.py:1288
|
||||
msgid "inventory item role"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/device_components.py:1287
|
||||
#: netbox/dcim/models/device_components.py:1289
|
||||
msgid "inventory item roles"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/device_components.py:1346
|
||||
#: netbox/dcim/models/device_components.py:1348
|
||||
#: netbox/dcim/models/devices.py:533 netbox/dcim/models/modules.py:217
|
||||
#: netbox/dcim/models/racks.py:310
|
||||
#: netbox/virtualization/models/virtualmachines.py:125
|
||||
msgid "serial number"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/device_components.py:1354
|
||||
#: netbox/dcim/models/device_components.py:1356
|
||||
#: netbox/dcim/models/devices.py:541 netbox/dcim/models/modules.py:224
|
||||
#: netbox/dcim/models/racks.py:317
|
||||
msgid "asset tag"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/device_components.py:1355
|
||||
#: netbox/dcim/models/device_components.py:1357
|
||||
msgid "A unique tag used to identify this item"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/device_components.py:1358
|
||||
#: netbox/dcim/models/device_components.py:1360
|
||||
msgid "discovered"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/device_components.py:1360
|
||||
#: netbox/dcim/models/device_components.py:1362
|
||||
msgid "This item was automatically discovered"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/device_components.py:1378
|
||||
#: netbox/dcim/models/device_components.py:1380
|
||||
msgid "inventory item"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/device_components.py:1379
|
||||
#: netbox/dcim/models/device_components.py:1381
|
||||
msgid "inventory items"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/device_components.py:1387
|
||||
#: netbox/dcim/models/device_components.py:1389
|
||||
msgid "Cannot assign self as parent."
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/device_components.py:1395
|
||||
#: netbox/dcim/models/device_components.py:1397
|
||||
msgid "Parent inventory item does not belong to the same device."
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/device_components.py:1401
|
||||
#: netbox/dcim/models/device_components.py:1403
|
||||
msgid "Cannot move an inventory item with dependent children"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/device_components.py:1409
|
||||
#: netbox/dcim/models/device_components.py:1411
|
||||
msgid "Cannot assign inventory item to component on another device"
|
||||
msgstr ""
|
||||
|
||||
@@ -6867,12 +6867,12 @@ msgstr ""
|
||||
msgid "rack face"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/devices.py:598 netbox/dcim/models/devices.py:1218
|
||||
#: netbox/dcim/models/devices.py:598 netbox/dcim/models/devices.py:1223
|
||||
#: netbox/virtualization/models/virtualmachines.py:94
|
||||
msgid "primary IPv4"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/devices.py:606 netbox/dcim/models/devices.py:1226
|
||||
#: netbox/dcim/models/devices.py:606 netbox/dcim/models/devices.py:1231
|
||||
#: netbox/virtualization/models/virtualmachines.py:102
|
||||
msgid "primary IPv6"
|
||||
msgstr ""
|
||||
@@ -7020,68 +7020,68 @@ msgid ""
|
||||
"is currently designated as its master."
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/devices.py:1133
|
||||
#: netbox/dcim/models/devices.py:1138
|
||||
msgid "domain"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/devices.py:1146 netbox/dcim/models/devices.py:1147
|
||||
#: netbox/dcim/models/devices.py:1151 netbox/dcim/models/devices.py:1152
|
||||
msgid "virtual chassis"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/devices.py:1159
|
||||
#: netbox/dcim/models/devices.py:1164
|
||||
#, python-brace-format
|
||||
msgid "The selected master ({master}) is not assigned to this virtual chassis."
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/devices.py:1174
|
||||
#: netbox/dcim/models/devices.py:1179
|
||||
#, python-brace-format
|
||||
msgid ""
|
||||
"Unable to delete virtual chassis {self}. There are member interfaces which "
|
||||
"form a cross-chassis LAG interfaces."
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/devices.py:1207 netbox/vpn/models/l2vpn.py:42
|
||||
#: netbox/dcim/models/devices.py:1212 netbox/vpn/models/l2vpn.py:42
|
||||
msgid "identifier"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/devices.py:1208
|
||||
#: netbox/dcim/models/devices.py:1213
|
||||
msgid "Numeric identifier unique to the parent device"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/devices.py:1236 netbox/extras/models/customfields.py:231
|
||||
#: netbox/dcim/models/devices.py:1241 netbox/extras/models/customfields.py:231
|
||||
#: netbox/extras/models/models.py:111 netbox/extras/models/models.py:800
|
||||
#: netbox/netbox/models/__init__.py:120 netbox/netbox/models/__init__.py:155
|
||||
msgid "comments"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/devices.py:1252
|
||||
#: netbox/dcim/models/devices.py:1257
|
||||
msgid "virtual device context"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/devices.py:1253
|
||||
#: netbox/dcim/models/devices.py:1258
|
||||
msgid "virtual device contexts"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/devices.py:1282
|
||||
#: netbox/dcim/models/devices.py:1287
|
||||
#, python-brace-format
|
||||
msgid "{ip} is not an IPv{family} address."
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/devices.py:1288
|
||||
#: netbox/dcim/models/devices.py:1293
|
||||
msgid "Primary IP address must belong to an interface on the assigned device."
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/devices.py:1319
|
||||
#: netbox/dcim/models/devices.py:1324
|
||||
msgid "MAC addresses"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/devices.py:1351
|
||||
#: netbox/dcim/models/devices.py:1356
|
||||
msgid ""
|
||||
"Cannot unassign MAC Address while it is designated as the primary MAC for an "
|
||||
"object"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/devices.py:1355
|
||||
#: netbox/dcim/models/devices.py:1360
|
||||
msgid ""
|
||||
"Cannot reassign MAC Address while it is designated as the primary MAC for an "
|
||||
"object"
|
||||
|
||||
Reference in New Issue
Block a user