Compare commits

...

10 Commits

Author SHA1 Message Date
Jonathan Senecal
1598ca9108 Merge c111c08315 into 21f4036782 2025-12-11 23:52:11 -06:00
github-actions
21f4036782 Update source translation strings
Some checks are pending
CodeQL / Analyze (${{ matrix.language }}) (none, actions) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, python) (push) Waiting to run
2025-12-12 05:03:16 +00:00
bctiemann
ce3738572c Merge pull request #20967 from netbox-community/20966-remove-stick-scroll
Some checks are pending
CI / build (20.x, 3.10) (push) Waiting to run
CI / build (20.x, 3.11) (push) Waiting to run
CI / build (20.x, 3.12) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, actions) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, python) (push) Waiting to run
Fixes #20966: Fix broken optgroup stickiness in ObjectType multiselect
2025-12-11 19:44:16 -05:00
bctiemann
cbb979934e Merge pull request #20958 from netbox-community/17976-manufacturer-devicetype_count
Fixes #17976: Remove devicetype_count from nested manufacturer to correct OpenAPI schema
2025-12-11 19:42:26 -05:00
bctiemann
642d83a4c6 Merge pull request #20937 from netbox-community/20560-bulk-import-prefix
Fixes #20560: Fix VLAN disambiguation in prefix bulk import
2025-12-11 19:40:59 -05:00
Jason Novinger
a06c12c6b8 Fixes #20966: Fix broken optgroup stickiness in ObjectType multiselect 2025-12-11 08:59:16 -06:00
Jeremy Stretch
59afa0b41d Fix test 2025-12-10 09:01:11 -05:00
Jeremy Stretch
14b246cb8a Fixes #17976: Remove devicetype_count from nested manufacturer to correct OpenAPI schema 2025-12-10 08:23:48 -05:00
Jason Novinger
9ae53fc232 Fixes #20560: Fix VLAN disambiguation in prefix bulk import 2025-12-05 16:39:28 -06:00
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
10 changed files with 522 additions and 112 deletions

View File

@@ -20,4 +20,4 @@ class ManufacturerSerializer(NetBoxModelSerializer):
'id', 'url', 'display_url', 'display', 'name', 'slug', 'description', 'tags', 'custom_fields', 'id', 'url', 'display_url', 'display', 'name', 'slug', 'description', 'tags', 'custom_fields',
'created', 'last_updated', 'devicetype_count', 'inventoryitem_count', 'platform_count', 'created', 'last_updated', 'devicetype_count', 'inventoryitem_count', 'platform_count',
] ]
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'devicetype_count') brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description')

View File

@@ -5,6 +5,7 @@ from django.core.exceptions import ObjectDoesNotExist
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from circuits.models import Circuit
from dcim.choices import * from dcim.choices import *
from dcim.constants import * from dcim.constants import *
from dcim.models import * from dcim.models import *
@@ -1414,19 +1415,52 @@ class MACAddressImportForm(NetBoxModelImportForm):
# #
class CableImportForm(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 # Termination A
side_a_site = CSVModelChoiceField( side_a_site = CSVModelChoiceField(
label=_('Side A site'), label=_('Side A site'),
queryset=Site.objects.all(), queryset=Site.objects.all(),
required=False, required=False,
to_field_name='name', 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( side_a_parent = forms.CharField(
label=_('Side A device'), label=_('Side A parent'),
queryset=Device.objects.all(), help_text=_('Device name, Circuit CID, or Power Panel name')
to_field_name='name',
help_text=_('Device name')
) )
side_a_type = CSVContentTypeField( side_a_type = CSVContentTypeField(
label=_('Side A type'), label=_('Side A type'),
@@ -1445,13 +1479,11 @@ class CableImportForm(NetBoxModelImportForm):
queryset=Site.objects.all(), queryset=Site.objects.all(),
required=False, required=False,
to_field_name='name', 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( side_b_parent = forms.CharField(
label=_('Side B device'), label=_('Side B parent'),
queryset=Device.objects.all(), help_text=_('Device name, Circuit CID, or Power Panel name')
to_field_name='name',
help_text=_('Device name')
) )
side_b_type = CSVContentTypeField( side_b_type = CSVContentTypeField(
label=_('Side B type'), label=_('Side B type'),
@@ -1500,7 +1532,7 @@ class CableImportForm(NetBoxModelImportForm):
class Meta: class Meta:
model = Cable model = Cable
fields = [ 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', 'side_b_name', 'type', 'status', 'tenant', 'label', 'color', 'length', 'length_unit', 'description',
'comments', 'tags', 'comments', 'tags',
] ]
@@ -1508,21 +1540,6 @@ class CableImportForm(NetBoxModelImportForm):
def __init__(self, data=None, *args, **kwargs): def __init__(self, data=None, *args, **kwargs):
super().__init__(data, *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): def _clean_side(self, side):
""" """
Derive a Cable's A/B termination objects. Derive a Cable's A/B termination objects.
@@ -1531,31 +1548,118 @@ class CableImportForm(NetBoxModelImportForm):
""" """
assert side in 'ab', f"Invalid side designation: {side}" 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') 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') 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 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: try:
if device.virtual_chassis and device.virtual_chassis.master == device and \ parent_ct = ContentType.objects.get(app_label=parent_app_label.lower(), model=parent_model_name.lower())
model.objects.filter(device=device, name=name).count() == 0: parent_model: Device | PowerPanel | Circuit = parent_ct.model_class()
termination_object = model.objects.get(device__in=device.virtual_chassis.members.all(), name=name) except ContentType.DoesNotExist: # pragma: no cover
else: # This should never happen
termination_object = model.objects.get(device=device, name=name) raise AssertionError(f'Unknown cable termination parent content type: {parent_content_type}')
if termination_object.cable is not None and termination_object.cable != self.instance:
raise forms.ValidationError( # Build query for parent lookup
_("Side {side_upper}: {device} {termination_object} is already connected").format( parent_query = {parent_accessor: parent_value}
side_upper=side.upper(), device=device, termination_object=termination_object # Add site to query if provided
) if site:
) parent_query['site'] = site
except ObjectDoesNotExist:
# Look up the parent object
try:
parent_object = parent_model.objects.get(**parent_query)
except parent_model.DoesNotExist:
raise forms.ValidationError( raise forms.ValidationError(
_("{side_upper} side termination not found: {device} {name}").format( _('Side {side_upper}: {model_name} not found: {value}').format(
side_upper=side.upper(), device=device, name=name 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]) setattr(self.instance, f'{side}_terminations', [termination_object])
return termination_object return termination_object

View File

@@ -531,7 +531,7 @@ class RackReservationTest(APIViewTestCases.APIViewTestCase):
class ManufacturerTest(APIViewTestCases.APIViewTestCase): class ManufacturerTest(APIViewTestCases.APIViewTestCase):
model = Manufacturer model = Manufacturer
brief_fields = ['description', 'devicetype_count', 'display', 'id', 'name', 'slug', 'url'] brief_fields = ['description', 'display', 'id', 'name', 'slug', 'url']
create_data = [ create_data = [
{ {
'name': 'Manufacturer 4', 'name': 'Manufacturer 4',

View File

@@ -1,8 +1,9 @@
from django.test import TestCase from django.test import TestCase
from circuits.models import Circuit, CircuitTermination, CircuitType, Provider, ProviderNetwork
from dcim.choices import ( from dcim.choices import (
DeviceFaceChoices, DeviceStatusChoices, InterfaceModeChoices, InterfaceTypeChoices, PortTypeChoices, CableTypeChoices, DeviceFaceChoices, DeviceStatusChoices, InterfaceModeChoices, InterfaceTypeChoices,
PowerOutletStatusChoices, PortTypeChoices, PowerOutletStatusChoices,
) )
from dcim.forms import * from dcim.forms import *
from dcim.models import * from dcim.models import *
@@ -411,3 +412,204 @@ class InterfaceTestCase(TestCase):
self.assertNotIn('untagged_vlan', form.cleaned_data.keys()) self.assertNotIn('untagged_vlan', form.cleaned_data.keys())
self.assertNotIn('tagged_vlans', form.cleaned_data.keys()) self.assertNotIn('tagged_vlans', form.cleaned_data.keys())
self.assertNotIn('qinq_svlan', 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))

View File

@@ -11,6 +11,7 @@ from core.models import ObjectType
from dcim.choices import * from dcim.choices import *
from dcim.constants import * from dcim.constants import *
from dcim.models import * from dcim.models import *
from circuits.models import Circuit, CircuitTermination, CircuitType, Provider
from ipam.models import ASN, RIR, VLAN, VRF from ipam.models import ASN, RIR, VLAN, VRF
from netbox.choices import CSVDelimiterChoices, ImportFormatChoices, WeightUnitChoices from netbox.choices import CSVDelimiterChoices, ImportFormatChoices, WeightUnitChoices
from tenancy.models import Tenant 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 2', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
Interface(device=devices[4], name='Interface 3', 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 1', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
Interface(device=devices[5], name='Interface 2', 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), Interface(device=devices[5], name='Interface 3', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
@@ -3507,6 +3508,22 @@ class CableTestCase(
) )
Interface.objects.bulk_create(interfaces) 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 = Cable(a_terminations=[interfaces[0]], b_terminations=[interfaces[3]], type=CableTypeChoices.TYPE_CAT6)
cable1.save() cable1.save()
cable2 = Cable(a_terminations=[interfaces[1]], b_terminations=[interfaces[4]], type=CableTypeChoices.TYPE_CAT6) cable2 = Cable(a_terminations=[interfaces[1]], b_terminations=[interfaces[4]], type=CableTypeChoices.TYPE_CAT6)
@@ -3532,7 +3549,7 @@ class CableTestCase(
cls.csv_data = { cls.csv_data = {
'default': ( '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 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 2,Device 4,dcim.interface,Interface 2",
"Device 3,dcim.interface,Interface 3,Device 4,dcim.interface,Interface 3", "Device 3,dcim.interface,Interface 3,Device 4,dcim.interface,Interface 3",
@@ -3545,12 +3562,28 @@ class CableTestCase(
'site-filtering': ( 'site-filtering': (
# Ensure that CSV bulk import supports assigning terminations from parent devices # Ensure that CSV bulk import supports assigning terminations from parent devices
# that share the same device name, provided those devices belong to different sites. # 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 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 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 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 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", "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",
) )
} }

View File

@@ -230,10 +230,6 @@ class PrefixImportForm(ScopedImportForm, NetBoxModelImportForm):
query |= Q(**{ query |= Q(**{
f"site__{self.fields['vlan_site'].to_field_name}": vlan_site f"site__{self.fields['vlan_site'].to_field_name}": vlan_site
}) })
# Don't Forget to include VLANs without a site in the filter
query |= Q(**{
f"site__{self.fields['vlan_site'].to_field_name}__isnull": True
})
if vlan_group: if vlan_group:
query &= Q(**{ query &= Q(**{

View File

@@ -564,6 +564,82 @@ vlan: 102
self.assertEqual(prefix.vlan.vid, 102) self.assertEqual(prefix.vlan.vid, 102)
self.assertEqual(prefix.scope, site) self.assertEqual(prefix.scope, site)
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
def test_prefix_import_with_vlan_site_multiple_vlans_same_vid(self):
"""
Test import when multiple VLANs exist with the same vid but different sites.
Ref: #20560
"""
site1 = Site.objects.get(name='Site 1')
site2 = Site.objects.get(name='Site 2')
# Create VLANs with the same vid but different sites
vlan1 = VLAN.objects.create(vid=1, name='VLAN1-Site1', site=site1)
VLAN.objects.create(vid=1, name='VLAN1-Site2', site=site2) # Create ambiguity
# Import prefix with vlan_site specified
IMPORT_DATA = f"""
prefix: 10.11.0.0/22
status: active
scope_type: dcim.site
scope_id: {site1.pk}
vlan_site: {site1.name}
vlan: 1
description: LOC02-MGMT
"""
# Add all required permissions to the test user
self.add_permissions('ipam.view_prefix', 'ipam.add_prefix')
form_data = {
'data': IMPORT_DATA,
'format': 'yaml'
}
response = self.client.post(reverse('ipam:prefix_bulk_import'), data=form_data, follow=True)
self.assertHttpStatus(response, 200)
# Verify the prefix was created with the correct VLAN
prefix = Prefix.objects.get(prefix='10.11.0.0/22')
self.assertEqual(prefix.vlan, vlan1)
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
def test_prefix_import_with_vlan_site_and_global_vlan(self):
"""
Test import when a global VLAN (no site) and site-specific VLAN exist with same vid.
When vlan_site is specified, should prefer the site-specific VLAN.
Ref: #20560
"""
site1 = Site.objects.get(name='Site 1')
# Create a global VLAN (no site) and a site-specific VLAN with the same vid
VLAN.objects.create(vid=10, name='VLAN10-Global', site=None) # Create ambiguity
vlan_site = VLAN.objects.create(vid=10, name='VLAN10-Site1', site=site1)
# Import prefix with vlan_site specified
IMPORT_DATA = f"""
prefix: 10.12.0.0/22
status: active
scope_type: dcim.site
scope_id: {site1.pk}
vlan_site: {site1.name}
vlan: 10
description: Test Site-Specific VLAN
"""
# Add all required permissions to the test user
self.add_permissions('ipam.view_prefix', 'ipam.add_prefix')
form_data = {
'data': IMPORT_DATA,
'format': 'yaml'
}
response = self.client.post(reverse('ipam:prefix_bulk_import'), data=form_data, follow=True)
self.assertHttpStatus(response, 200)
# Verify the prefix was created with the site-specific VLAN (not the global one)
prefix = Prefix.objects.get(prefix='10.12.0.0/22')
self.assertEqual(prefix.vlan, vlan_site)
class IPRangeTestCase(ViewTestCases.PrimaryObjectViewTestCase): class IPRangeTestCase(ViewTestCases.PrimaryObjectViewTestCase):
model = IPRange model = IPRange

File diff suppressed because one or more lines are too long

View File

@@ -36,7 +36,6 @@ form.object-edit {
// Make optgroup labels sticky when scrolling through select elements // Make optgroup labels sticky when scrolling through select elements
select[multiple] { select[multiple] {
optgroup { optgroup {
position: sticky;
top: 0; top: 0;
background-color: var(--bs-body-bg); background-color: var(--bs-body-bg);
font-style: normal; font-style: normal;

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-12-10 05:02+0000\n" "POT-Creation-Date: 2025-12-12 05:02+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -231,7 +231,7 @@ msgstr ""
#: netbox/dcim/tables/power.py:93 netbox/dcim/tables/racks.py:125 #: netbox/dcim/tables/power.py:93 netbox/dcim/tables/racks.py:125
#: netbox/dcim/tables/racks.py:215 netbox/dcim/tables/sites.py:151 #: netbox/dcim/tables/racks.py:215 netbox/dcim/tables/sites.py:151
#: netbox/extras/filtersets.py:662 netbox/ipam/forms/bulk_edit.py:479 #: netbox/extras/filtersets.py:662 netbox/ipam/forms/bulk_edit.py:479
#: netbox/ipam/forms/bulk_import.py:489 netbox/ipam/forms/filtersets.py:161 #: netbox/ipam/forms/bulk_import.py:485 netbox/ipam/forms/filtersets.py:161
#: netbox/ipam/forms/filtersets.py:236 netbox/ipam/forms/filtersets.py:457 #: netbox/ipam/forms/filtersets.py:236 netbox/ipam/forms/filtersets.py:457
#: netbox/ipam/forms/filtersets.py:552 netbox/ipam/forms/model_forms.py:673 #: netbox/ipam/forms/filtersets.py:552 netbox/ipam/forms/model_forms.py:673
#: netbox/ipam/tables/vlans.py:90 netbox/ipam/tables/vlans.py:200 #: netbox/ipam/tables/vlans.py:90 netbox/ipam/tables/vlans.py:200
@@ -784,8 +784,8 @@ msgstr ""
#: netbox/dcim/tables/sites.py:96 netbox/dcim/tables/sites.py:155 #: netbox/dcim/tables/sites.py:96 netbox/dcim/tables/sites.py:155
#: netbox/ipam/forms/bulk_edit.py:240 netbox/ipam/forms/bulk_edit.py:290 #: netbox/ipam/forms/bulk_edit.py:240 netbox/ipam/forms/bulk_edit.py:290
#: netbox/ipam/forms/bulk_edit.py:343 netbox/ipam/forms/bulk_edit.py:501 #: netbox/ipam/forms/bulk_edit.py:343 netbox/ipam/forms/bulk_edit.py:501
#: netbox/ipam/forms/bulk_import.py:195 netbox/ipam/forms/bulk_import.py:263 #: netbox/ipam/forms/bulk_import.py:195 netbox/ipam/forms/bulk_import.py:259
#: netbox/ipam/forms/bulk_import.py:299 netbox/ipam/forms/bulk_import.py:510 #: netbox/ipam/forms/bulk_import.py:295 netbox/ipam/forms/bulk_import.py:506
#: netbox/ipam/forms/filtersets.py:219 netbox/ipam/forms/filtersets.py:297 #: netbox/ipam/forms/filtersets.py:219 netbox/ipam/forms/filtersets.py:297
#: netbox/ipam/forms/filtersets.py:379 netbox/ipam/forms/filtersets.py:564 #: netbox/ipam/forms/filtersets.py:379 netbox/ipam/forms/filtersets.py:564
#: netbox/ipam/forms/model_forms.py:512 netbox/ipam/tables/ip.py:184 #: netbox/ipam/forms/model_forms.py:512 netbox/ipam/tables/ip.py:184
@@ -866,8 +866,8 @@ msgstr ""
#: netbox/ipam/forms/bulk_import.py:41 netbox/ipam/forms/bulk_import.py:70 #: netbox/ipam/forms/bulk_import.py:41 netbox/ipam/forms/bulk_import.py:70
#: netbox/ipam/forms/bulk_import.py:98 netbox/ipam/forms/bulk_import.py:118 #: netbox/ipam/forms/bulk_import.py:98 netbox/ipam/forms/bulk_import.py:118
#: netbox/ipam/forms/bulk_import.py:138 netbox/ipam/forms/bulk_import.py:167 #: netbox/ipam/forms/bulk_import.py:138 netbox/ipam/forms/bulk_import.py:167
#: netbox/ipam/forms/bulk_import.py:256 netbox/ipam/forms/bulk_import.py:292 #: netbox/ipam/forms/bulk_import.py:252 netbox/ipam/forms/bulk_import.py:288
#: netbox/ipam/forms/bulk_import.py:472 netbox/ipam/forms/bulk_import.py:503 #: netbox/ipam/forms/bulk_import.py:468 netbox/ipam/forms/bulk_import.py:499
#: netbox/ipam/forms/filtersets.py:50 netbox/ipam/forms/filtersets.py:70 #: netbox/ipam/forms/filtersets.py:50 netbox/ipam/forms/filtersets.py:70
#: netbox/ipam/forms/filtersets.py:102 netbox/ipam/forms/filtersets.py:123 #: netbox/ipam/forms/filtersets.py:102 netbox/ipam/forms/filtersets.py:123
#: netbox/ipam/forms/filtersets.py:146 netbox/ipam/forms/filtersets.py:182 #: netbox/ipam/forms/filtersets.py:146 netbox/ipam/forms/filtersets.py:182
@@ -1106,8 +1106,8 @@ msgstr ""
#: netbox/extras/filtersets.py:689 netbox/ipam/forms/bulk_edit.py:245 #: netbox/extras/filtersets.py:689 netbox/ipam/forms/bulk_edit.py:245
#: netbox/ipam/forms/bulk_edit.py:295 netbox/ipam/forms/bulk_edit.py:348 #: netbox/ipam/forms/bulk_edit.py:295 netbox/ipam/forms/bulk_edit.py:348
#: netbox/ipam/forms/bulk_edit.py:506 netbox/ipam/forms/bulk_import.py:200 #: netbox/ipam/forms/bulk_edit.py:506 netbox/ipam/forms/bulk_import.py:200
#: netbox/ipam/forms/bulk_import.py:268 netbox/ipam/forms/bulk_import.py:304 #: netbox/ipam/forms/bulk_import.py:264 netbox/ipam/forms/bulk_import.py:300
#: netbox/ipam/forms/bulk_import.py:515 netbox/ipam/forms/filtersets.py:247 #: netbox/ipam/forms/bulk_import.py:511 netbox/ipam/forms/filtersets.py:247
#: netbox/ipam/forms/filtersets.py:305 netbox/ipam/forms/filtersets.py:384 #: netbox/ipam/forms/filtersets.py:305 netbox/ipam/forms/filtersets.py:384
#: netbox/ipam/forms/filtersets.py:572 netbox/ipam/forms/model_forms.py:195 #: netbox/ipam/forms/filtersets.py:572 netbox/ipam/forms/model_forms.py:195
#: netbox/ipam/forms/model_forms.py:221 netbox/ipam/forms/model_forms.py:260 #: netbox/ipam/forms/model_forms.py:221 netbox/ipam/forms/model_forms.py:260
@@ -1160,8 +1160,8 @@ msgstr ""
#: netbox/dcim/forms/bulk_import.py:365 netbox/dcim/forms/bulk_import.py:597 #: netbox/dcim/forms/bulk_import.py:365 netbox/dcim/forms/bulk_import.py:597
#: netbox/dcim/forms/bulk_import.py:757 netbox/dcim/forms/bulk_import.py:1250 #: netbox/dcim/forms/bulk_import.py:757 netbox/dcim/forms/bulk_import.py:1250
#: netbox/dcim/forms/bulk_import.py:1681 netbox/ipam/forms/bulk_import.py:197 #: netbox/dcim/forms/bulk_import.py:1681 netbox/ipam/forms/bulk_import.py:197
#: netbox/ipam/forms/bulk_import.py:265 netbox/ipam/forms/bulk_import.py:301 #: netbox/ipam/forms/bulk_import.py:261 netbox/ipam/forms/bulk_import.py:297
#: netbox/ipam/forms/bulk_import.py:512 netbox/ipam/forms/bulk_import.py:525 #: netbox/ipam/forms/bulk_import.py:508 netbox/ipam/forms/bulk_import.py:521
#: netbox/virtualization/forms/bulk_import.py:62 #: netbox/virtualization/forms/bulk_import.py:62
#: netbox/virtualization/forms/bulk_import.py:93 #: netbox/virtualization/forms/bulk_import.py:93
#: netbox/vpn/forms/bulk_import.py:39 netbox/vpn/forms/bulk_import.py:266 #: netbox/vpn/forms/bulk_import.py:39 netbox/vpn/forms/bulk_import.py:266
@@ -1178,9 +1178,9 @@ msgstr ""
#: netbox/dcim/forms/bulk_import.py:1740 netbox/ipam/forms/bulk_import.py:45 #: netbox/dcim/forms/bulk_import.py:1740 netbox/ipam/forms/bulk_import.py:45
#: netbox/ipam/forms/bulk_import.py:74 netbox/ipam/forms/bulk_import.py:102 #: netbox/ipam/forms/bulk_import.py:74 netbox/ipam/forms/bulk_import.py:102
#: netbox/ipam/forms/bulk_import.py:122 netbox/ipam/forms/bulk_import.py:142 #: netbox/ipam/forms/bulk_import.py:122 netbox/ipam/forms/bulk_import.py:142
#: netbox/ipam/forms/bulk_import.py:171 netbox/ipam/forms/bulk_import.py:260 #: netbox/ipam/forms/bulk_import.py:171 netbox/ipam/forms/bulk_import.py:256
#: netbox/ipam/forms/bulk_import.py:296 netbox/ipam/forms/bulk_import.py:476 #: netbox/ipam/forms/bulk_import.py:292 netbox/ipam/forms/bulk_import.py:472
#: netbox/ipam/forms/bulk_import.py:507 #: netbox/ipam/forms/bulk_import.py:503
#: netbox/virtualization/forms/bulk_import.py:76 #: netbox/virtualization/forms/bulk_import.py:76
#: netbox/virtualization/forms/bulk_import.py:130 #: netbox/virtualization/forms/bulk_import.py:130
#: netbox/vpn/forms/bulk_import.py:63 netbox/wireless/forms/bulk_import.py:61 #: netbox/vpn/forms/bulk_import.py:63 netbox/wireless/forms/bulk_import.py:61
@@ -1224,7 +1224,7 @@ msgstr ""
#: netbox/dcim/forms/model_forms.py:1571 netbox/dcim/forms/model_forms.py:1738 #: netbox/dcim/forms/model_forms.py:1571 netbox/dcim/forms/model_forms.py:1738
#: netbox/dcim/forms/model_forms.py:1773 netbox/dcim/forms/model_forms.py:1903 #: netbox/dcim/forms/model_forms.py:1773 netbox/dcim/forms/model_forms.py:1903
#: netbox/dcim/tables/connections.py:65 netbox/dcim/tables/devices.py:1169 #: netbox/dcim/tables/connections.py:65 netbox/dcim/tables/devices.py:1169
#: netbox/ipam/forms/bulk_import.py:324 netbox/ipam/forms/model_forms.py:291 #: netbox/ipam/forms/bulk_import.py:320 netbox/ipam/forms/model_forms.py:291
#: netbox/ipam/forms/model_forms.py:300 netbox/ipam/tables/fhrp.py:64 #: netbox/ipam/forms/model_forms.py:300 netbox/ipam/tables/fhrp.py:64
#: netbox/ipam/tables/ip.py:330 netbox/ipam/tables/vlans.py:148 #: netbox/ipam/tables/ip.py:330 netbox/ipam/tables/vlans.py:148
#: netbox/templates/circuits/inc/circuit_termination_fields.html:52 #: netbox/templates/circuits/inc/circuit_termination_fields.html:52
@@ -1389,7 +1389,7 @@ msgstr ""
#: netbox/dcim/forms/bulk_import.py:104 netbox/dcim/forms/model_forms.py:126 #: netbox/dcim/forms/bulk_import.py:104 netbox/dcim/forms/model_forms.py:126
#: netbox/dcim/tables/sites.py:103 netbox/extras/forms/filtersets.py:582 #: netbox/dcim/tables/sites.py:103 netbox/extras/forms/filtersets.py:582
#: netbox/ipam/filtersets.py:982 netbox/ipam/forms/bulk_edit.py:488 #: netbox/ipam/filtersets.py:982 netbox/ipam/forms/bulk_edit.py:488
#: netbox/ipam/forms/bulk_import.py:496 netbox/ipam/forms/model_forms.py:571 #: netbox/ipam/forms/bulk_import.py:492 netbox/ipam/forms/model_forms.py:571
#: netbox/ipam/tables/fhrp.py:67 netbox/ipam/tables/vlans.py:94 #: netbox/ipam/tables/fhrp.py:67 netbox/ipam/tables/vlans.py:94
#: netbox/ipam/tables/vlans.py:205 #: netbox/ipam/tables/vlans.py:205
#: netbox/templates/circuits/circuitgroupassignment.html:22 #: netbox/templates/circuits/circuitgroupassignment.html:22
@@ -1984,7 +1984,7 @@ msgstr ""
#: netbox/dcim/tables/devices.py:862 netbox/dcim/tables/devices.py:921 #: netbox/dcim/tables/devices.py:862 netbox/dcim/tables/devices.py:921
#: netbox/dcim/tables/devices.py:989 netbox/dcim/tables/devices.py:1118 #: netbox/dcim/tables/devices.py:989 netbox/dcim/tables/devices.py:1118
#: netbox/dcim/tables/modules.py:87 netbox/extras/forms/filtersets.py:389 #: netbox/dcim/tables/modules.py:87 netbox/extras/forms/filtersets.py:389
#: netbox/ipam/forms/bulk_import.py:310 netbox/ipam/forms/filtersets.py:626 #: netbox/ipam/forms/bulk_import.py:306 netbox/ipam/forms/filtersets.py:626
#: netbox/ipam/forms/model_forms.py:334 netbox/ipam/tables/vlans.py:159 #: netbox/ipam/forms/model_forms.py:334 netbox/ipam/tables/vlans.py:159
#: netbox/templates/circuits/virtualcircuittermination.html:56 #: netbox/templates/circuits/virtualcircuittermination.html:56
#: netbox/templates/dcim/consoleport.html:20 #: netbox/templates/dcim/consoleport.html:20
@@ -3185,7 +3185,7 @@ msgstr ""
#: netbox/dcim/tables/devices.py:719 netbox/dcim/tables/devices.py:929 #: netbox/dcim/tables/devices.py:719 netbox/dcim/tables/devices.py:929
#: netbox/dcim/tables/devices.py:1016 netbox/dcim/tables/devices.py:1175 #: netbox/dcim/tables/devices.py:1016 netbox/dcim/tables/devices.py:1175
#: netbox/dcim/tables/sites.py:28 netbox/dcim/tables/sites.py:62 #: netbox/dcim/tables/sites.py:28 netbox/dcim/tables/sites.py:62
#: netbox/dcim/tables/sites.py:147 netbox/ipam/forms/bulk_import.py:582 #: netbox/dcim/tables/sites.py:147 netbox/ipam/forms/bulk_import.py:578
#: netbox/ipam/forms/model_forms.py:770 netbox/ipam/tables/fhrp.py:59 #: netbox/ipam/forms/model_forms.py:770 netbox/ipam/tables/fhrp.py:59
#: netbox/ipam/tables/ip.py:336 netbox/ipam/tables/services.py:45 #: netbox/ipam/tables/ip.py:336 netbox/ipam/tables/services.py:45
#: netbox/templates/dcim/devicerole.html:34 #: netbox/templates/dcim/devicerole.html:34
@@ -3963,7 +3963,7 @@ msgid "Is assigned"
msgstr "" msgstr ""
#: netbox/dcim/filtersets.py:1826 netbox/dcim/forms/bulk_import.py:1355 #: netbox/dcim/filtersets.py:1826 netbox/dcim/forms/bulk_import.py:1355
#: netbox/ipam/forms/bulk_import.py:338 #: netbox/ipam/forms/bulk_import.py:334
msgid "Is primary" msgid "Is primary"
msgstr "" msgstr ""
@@ -3991,7 +3991,7 @@ msgstr ""
#: netbox/ipam/filtersets.py:579 netbox/ipam/filtersets.py:590 #: netbox/ipam/filtersets.py:579 netbox/ipam/filtersets.py:590
#: netbox/ipam/forms/bulk_edit.py:226 netbox/ipam/forms/bulk_edit.py:282 #: netbox/ipam/forms/bulk_edit.py:226 netbox/ipam/forms/bulk_edit.py:282
#: netbox/ipam/forms/bulk_edit.py:329 netbox/ipam/forms/bulk_import.py:160 #: netbox/ipam/forms/bulk_edit.py:329 netbox/ipam/forms/bulk_import.py:160
#: netbox/ipam/forms/bulk_import.py:249 netbox/ipam/forms/bulk_import.py:285 #: netbox/ipam/forms/bulk_import.py:245 netbox/ipam/forms/bulk_import.py:281
#: netbox/ipam/forms/filtersets.py:69 netbox/ipam/forms/filtersets.py:180 #: netbox/ipam/forms/filtersets.py:69 netbox/ipam/forms/filtersets.py:180
#: netbox/ipam/forms/filtersets.py:332 netbox/ipam/forms/model_forms.py:66 #: netbox/ipam/forms/filtersets.py:332 netbox/ipam/forms/model_forms.py:66
#: netbox/ipam/forms/model_forms.py:209 netbox/ipam/forms/model_forms.py:257 #: netbox/ipam/forms/model_forms.py:209 netbox/ipam/forms/model_forms.py:257
@@ -4856,7 +4856,7 @@ msgid "available options"
msgstr "" msgstr ""
#: netbox/dcim/forms/bulk_import.py:138 netbox/dcim/forms/bulk_import.py:633 #: netbox/dcim/forms/bulk_import.py:138 netbox/dcim/forms/bulk_import.py:633
#: netbox/dcim/forms/bulk_import.py:1650 netbox/ipam/forms/bulk_import.py:493 #: netbox/dcim/forms/bulk_import.py:1650 netbox/ipam/forms/bulk_import.py:489
#: netbox/virtualization/forms/bulk_import.py:69 #: netbox/virtualization/forms/bulk_import.py:69
#: netbox/virtualization/forms/bulk_import.py:100 #: netbox/virtualization/forms/bulk_import.py:100
msgid "Assigned site" msgid "Assigned site"
@@ -5168,7 +5168,7 @@ msgid "Assigned Q-in-Q Service VLAN ID (filtered by VLAN group)"
msgstr "" msgstr ""
#: netbox/dcim/forms/bulk_import.py:1028 netbox/ipam/forms/bulk_import.py:164 #: netbox/dcim/forms/bulk_import.py:1028 netbox/ipam/forms/bulk_import.py:164
#: netbox/ipam/forms/bulk_import.py:253 netbox/ipam/forms/bulk_import.py:289 #: netbox/ipam/forms/bulk_import.py:249 netbox/ipam/forms/bulk_import.py:285
#: netbox/ipam/forms/filtersets.py:210 netbox/ipam/forms/filtersets.py:293 #: netbox/ipam/forms/filtersets.py:210 netbox/ipam/forms/filtersets.py:293
#: netbox/ipam/forms/filtersets.py:360 #: netbox/ipam/forms/filtersets.py:360
#: netbox/virtualization/forms/bulk_import.py:220 #: netbox/virtualization/forms/bulk_import.py:220
@@ -5247,11 +5247,11 @@ msgstr ""
msgid "Component type must be specified when component name is specified" msgid "Component type must be specified when component name is specified"
msgstr "" msgstr ""
#: netbox/dcim/forms/bulk_import.py:1338 netbox/ipam/forms/bulk_import.py:314 #: netbox/dcim/forms/bulk_import.py:1338 netbox/ipam/forms/bulk_import.py:310
msgid "Parent device of assigned interface (if any)" msgid "Parent device of assigned interface (if any)"
msgstr "" msgstr ""
#: netbox/dcim/forms/bulk_import.py:1341 netbox/ipam/forms/bulk_import.py:317 #: netbox/dcim/forms/bulk_import.py:1341 netbox/ipam/forms/bulk_import.py:313
#: netbox/virtualization/filtersets.py:259 #: netbox/virtualization/filtersets.py:259
#: netbox/virtualization/filtersets.py:310 #: netbox/virtualization/filtersets.py:310
#: netbox/virtualization/forms/bulk_edit.py:182 #: netbox/virtualization/forms/bulk_edit.py:182
@@ -5265,12 +5265,12 @@ msgstr ""
msgid "Virtual machine" msgid "Virtual machine"
msgstr "" msgstr ""
#: netbox/dcim/forms/bulk_import.py:1345 netbox/ipam/forms/bulk_import.py:321 #: netbox/dcim/forms/bulk_import.py:1345 netbox/ipam/forms/bulk_import.py:317
msgid "Parent VM of assigned interface (if any)" msgid "Parent VM of assigned interface (if any)"
msgstr "" msgstr ""
#: netbox/dcim/forms/bulk_import.py:1352 netbox/ipam/filtersets.py:1035 #: netbox/dcim/forms/bulk_import.py:1352 netbox/ipam/filtersets.py:1035
#: netbox/ipam/forms/bulk_import.py:328 #: netbox/ipam/forms/bulk_import.py:324
msgid "Assigned interface" msgid "Assigned interface"
msgstr "" msgstr ""
@@ -5654,7 +5654,7 @@ msgstr ""
msgid "Please select a {scope_type}." msgid "Please select a {scope_type}."
msgstr "" msgstr ""
#: netbox/dcim/forms/mixins.py:117 netbox/ipam/forms/bulk_import.py:466 #: netbox/dcim/forms/mixins.py:117 netbox/ipam/forms/bulk_import.py:462
msgid "Scope type (app & model)" msgid "Scope type (app & model)"
msgstr "" msgstr ""
@@ -6404,7 +6404,7 @@ msgstr ""
#: netbox/dcim/models/device_components.py:605 #: netbox/dcim/models/device_components.py:605
#: netbox/dcim/tables/devices.py:631 netbox/ipam/forms/bulk_edit.py:521 #: netbox/dcim/tables/devices.py:631 netbox/ipam/forms/bulk_edit.py:521
#: netbox/ipam/forms/bulk_import.py:528 netbox/ipam/forms/filtersets.py:587 #: netbox/ipam/forms/bulk_import.py:524 netbox/ipam/forms/filtersets.py:587
#: netbox/ipam/forms/model_forms.py:694 netbox/ipam/tables/vlans.py:109 #: netbox/ipam/forms/model_forms.py:694 netbox/ipam/tables/vlans.py:109
#: netbox/templates/dcim/interface.html:86 netbox/templates/ipam/vlan.html:77 #: netbox/templates/dcim/interface.html:86 netbox/templates/ipam/vlan.html:77
#: netbox/templates/virtualization/vminterface.html:60 #: netbox/templates/virtualization/vminterface.html:60
@@ -7324,8 +7324,8 @@ msgid "Locally-assigned identifier"
msgstr "" msgstr ""
#: netbox/dcim/models/racks.py:305 netbox/ipam/forms/bulk_import.py:204 #: netbox/dcim/models/racks.py:305 netbox/ipam/forms/bulk_import.py:204
#: netbox/ipam/forms/bulk_import.py:272 netbox/ipam/forms/bulk_import.py:307 #: netbox/ipam/forms/bulk_import.py:268 netbox/ipam/forms/bulk_import.py:303
#: netbox/ipam/forms/bulk_import.py:519 #: netbox/ipam/forms/bulk_import.py:515
#: netbox/virtualization/forms/bulk_import.py:123 #: netbox/virtualization/forms/bulk_import.py:123
msgid "Functional role" msgid "Functional role"
msgstr "" msgstr ""
@@ -7576,7 +7576,7 @@ msgid "U Height"
msgstr "" msgstr ""
#: netbox/dcim/tables/devices.py:210 netbox/dcim/tables/devices.py:1128 #: netbox/dcim/tables/devices.py:210 netbox/dcim/tables/devices.py:1128
#: netbox/ipam/forms/bulk_import.py:601 netbox/ipam/forms/model_forms.py:317 #: netbox/ipam/forms/bulk_import.py:597 netbox/ipam/forms/model_forms.py:317
#: netbox/ipam/forms/model_forms.py:330 netbox/ipam/tables/ip.py:314 #: netbox/ipam/forms/model_forms.py:330 netbox/ipam/tables/ip.py:314
#: netbox/ipam/tables/ip.py:381 netbox/ipam/tables/ip.py:391 #: netbox/ipam/tables/ip.py:381 netbox/ipam/tables/ip.py:391
#: netbox/ipam/tables/ip.py:414 netbox/templates/ipam/ipaddress.html:11 #: netbox/ipam/tables/ip.py:414 netbox/templates/ipam/ipaddress.html:11
@@ -10419,8 +10419,8 @@ msgid "DNS name"
msgstr "" msgstr ""
#: netbox/ipam/forms/bulk_edit.py:376 netbox/ipam/forms/bulk_edit.py:573 #: netbox/ipam/forms/bulk_edit.py:376 netbox/ipam/forms/bulk_edit.py:573
#: netbox/ipam/forms/bulk_import.py:447 netbox/ipam/forms/bulk_import.py:565 #: netbox/ipam/forms/bulk_import.py:443 netbox/ipam/forms/bulk_import.py:561
#: netbox/ipam/forms/bulk_import.py:593 netbox/ipam/forms/filtersets.py:414 #: netbox/ipam/forms/bulk_import.py:589 netbox/ipam/forms/filtersets.py:414
#: netbox/ipam/forms/filtersets.py:604 netbox/templates/ipam/fhrpgroup.html:22 #: netbox/ipam/forms/filtersets.py:604 netbox/templates/ipam/fhrpgroup.html:22
#: netbox/templates/ipam/inc/panels/fhrp_groups.html:24 #: netbox/templates/ipam/inc/panels/fhrp_groups.html:24
#: netbox/templates/ipam/service.html:34 #: netbox/templates/ipam/service.html:34
@@ -10464,7 +10464,7 @@ msgstr ""
msgid "VLAN ID ranges" msgid "VLAN ID ranges"
msgstr "" msgstr ""
#: netbox/ipam/forms/bulk_edit.py:516 netbox/ipam/forms/bulk_import.py:522 #: netbox/ipam/forms/bulk_edit.py:516 netbox/ipam/forms/bulk_import.py:518
#: netbox/ipam/forms/filtersets.py:579 netbox/ipam/models/vlans.py:250 #: netbox/ipam/forms/filtersets.py:579 netbox/ipam/models/vlans.py:250
#: netbox/ipam/tables/vlans.py:106 #: netbox/ipam/tables/vlans.py:106
msgid "Q-in-Q role" msgid "Q-in-Q role"
@@ -10478,7 +10478,7 @@ msgstr ""
msgid "Site & Group" msgid "Site & Group"
msgstr "" msgstr ""
#: netbox/ipam/forms/bulk_edit.py:557 netbox/ipam/forms/bulk_import.py:552 #: netbox/ipam/forms/bulk_edit.py:557 netbox/ipam/forms/bulk_import.py:548
#: netbox/ipam/forms/model_forms.py:726 netbox/ipam/tables/vlans.py:259 #: netbox/ipam/forms/model_forms.py:726 netbox/ipam/tables/vlans.py:259
#: netbox/templates/ipam/vlantranslationrule.html:14 #: netbox/templates/ipam/vlantranslationrule.html:14
#: netbox/vpn/forms/model_forms.py:322 netbox/vpn/forms/model_forms.py:359 #: netbox/vpn/forms/model_forms.py:322 netbox/vpn/forms/model_forms.py:359
@@ -10523,86 +10523,86 @@ msgstr ""
msgid "Scope ID" msgid "Scope ID"
msgstr "" msgstr ""
#: netbox/ipam/forms/bulk_import.py:331 netbox/ipam/forms/filtersets.py:636 #: netbox/ipam/forms/bulk_import.py:327 netbox/ipam/forms/filtersets.py:636
#: netbox/ipam/forms/model_forms.py:306 netbox/ipam/forms/model_forms.py:336 #: netbox/ipam/forms/model_forms.py:306 netbox/ipam/forms/model_forms.py:336
#: netbox/ipam/forms/model_forms.py:517 netbox/templates/ipam/fhrpgroup.html:19 #: netbox/ipam/forms/model_forms.py:517 netbox/templates/ipam/fhrpgroup.html:19
msgid "FHRP Group" msgid "FHRP Group"
msgstr "" msgstr ""
#: netbox/ipam/forms/bulk_import.py:335 #: netbox/ipam/forms/bulk_import.py:331
msgid "Assigned FHRP Group name" msgid "Assigned FHRP Group name"
msgstr "" msgstr ""
#: netbox/ipam/forms/bulk_import.py:339 #: netbox/ipam/forms/bulk_import.py:335
msgid "Make this the primary IP for the assigned device" msgid "Make this the primary IP for the assigned device"
msgstr "" msgstr ""
#: netbox/ipam/forms/bulk_import.py:343 #: netbox/ipam/forms/bulk_import.py:339
msgid "Is out-of-band" msgid "Is out-of-band"
msgstr "" msgstr ""
#: netbox/ipam/forms/bulk_import.py:344 #: netbox/ipam/forms/bulk_import.py:340
msgid "Designate this as the out-of-band IP address for the assigned device" msgid "Designate this as the out-of-band IP address for the assigned device"
msgstr "" msgstr ""
#: netbox/ipam/forms/bulk_import.py:398 #: netbox/ipam/forms/bulk_import.py:394
msgid "No device or virtual machine specified; cannot set as primary IP" msgid "No device or virtual machine specified; cannot set as primary IP"
msgstr "" msgstr ""
#: netbox/ipam/forms/bulk_import.py:402 #: netbox/ipam/forms/bulk_import.py:398
msgid "No device specified; cannot set as out-of-band IP" msgid "No device specified; cannot set as out-of-band IP"
msgstr "" msgstr ""
#: netbox/ipam/forms/bulk_import.py:406 #: netbox/ipam/forms/bulk_import.py:402
msgid "Cannot set out-of-band IP for virtual machines" msgid "Cannot set out-of-band IP for virtual machines"
msgstr "" msgstr ""
#: netbox/ipam/forms/bulk_import.py:410 #: netbox/ipam/forms/bulk_import.py:406
msgid "No interface specified; cannot set as primary IP" msgid "No interface specified; cannot set as primary IP"
msgstr "" msgstr ""
#: netbox/ipam/forms/bulk_import.py:414 #: netbox/ipam/forms/bulk_import.py:410
msgid "No interface specified; cannot set as out-of-band IP" msgid "No interface specified; cannot set as out-of-band IP"
msgstr "" msgstr ""
#: netbox/ipam/forms/bulk_import.py:451 #: netbox/ipam/forms/bulk_import.py:447
msgid "Auth type" msgid "Auth type"
msgstr "" msgstr ""
#: netbox/ipam/forms/bulk_import.py:500 #: netbox/ipam/forms/bulk_import.py:496
msgid "Assigned VLAN group" msgid "Assigned VLAN group"
msgstr "" msgstr ""
#: netbox/ipam/forms/bulk_import.py:532 #: netbox/ipam/forms/bulk_import.py:528
msgid "Service VLAN (for Q-in-Q/802.1ad customer VLANs)" msgid "Service VLAN (for Q-in-Q/802.1ad customer VLANs)"
msgstr "" msgstr ""
#: netbox/ipam/forms/bulk_import.py:555 netbox/ipam/models/vlans.py:369 #: netbox/ipam/forms/bulk_import.py:551 netbox/ipam/models/vlans.py:369
msgid "VLAN translation policy" msgid "VLAN translation policy"
msgstr "" msgstr ""
#: netbox/ipam/forms/bulk_import.py:567 netbox/ipam/forms/bulk_import.py:595 #: netbox/ipam/forms/bulk_import.py:563 netbox/ipam/forms/bulk_import.py:591
msgid "IP protocol" msgid "IP protocol"
msgstr "" msgstr ""
#: netbox/ipam/forms/bulk_import.py:579 #: netbox/ipam/forms/bulk_import.py:575
msgid "Parent type (app & model)" msgid "Parent type (app & model)"
msgstr "" msgstr ""
#: netbox/ipam/forms/bulk_import.py:586 #: netbox/ipam/forms/bulk_import.py:582
msgid "Parent object name" msgid "Parent object name"
msgstr "" msgstr ""
#: netbox/ipam/forms/bulk_import.py:590 #: netbox/ipam/forms/bulk_import.py:586
msgid "Parent object ID" msgid "Parent object ID"
msgstr "" msgstr ""
#: netbox/ipam/forms/bulk_import.py:642 #: netbox/ipam/forms/bulk_import.py:638
msgid "" msgid ""
"One of parent or parent_object_id must be included with parent_object_type" "One of parent or parent_object_id must be included with parent_object_type"
msgstr "" msgstr ""
#: netbox/ipam/forms/bulk_import.py:655 #: netbox/ipam/forms/bulk_import.py:651
#, python-brace-format #, python-brace-format
msgid "{ip} is not assigned to this parent." msgid "{ip} is not assigned to this parent."
msgstr "" msgstr ""