Skip to content

Commit

Permalink
Fixes #17954 - Handle CircuitTerminations in Cable Bulk Import
Browse files Browse the repository at this point in the history
  • Loading branch information
jsenecal committed Nov 12, 2024
1 parent 494d410 commit 2721455
Show file tree
Hide file tree
Showing 3 changed files with 57 additions and 7 deletions.
57 changes: 50 additions & 7 deletions netbox/dcim/forms/bulk_import.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from dcim.choices import *
from dcim.constants import *
from dcim.models import *
from circuits.models import Circuit
from extras.models import ConfigTemplate
from ipam.models import VRF, IPAddress
from netbox.forms import NetBoxModelImportForm
Expand Down Expand Up @@ -1171,8 +1172,18 @@ class CableImportForm(NetBoxModelImportForm):
label=_('Side A device'),
queryset=Device.objects.all(),
to_field_name='name',
required=False,
conditional=True,
help_text=_('Device name')
)
side_a_circuit = CSVModelChoiceField(
label=_('Side A circuit'),
queryset=Circuit.objects.all(),
to_field_name='cid',
required=False,
conditional=True,
help_text=_('Circuit ID'),
)
side_a_type = CSVContentTypeField(
label=_('Side A type'),
queryset=ContentType.objects.all(),
Expand All @@ -1189,8 +1200,18 @@ class CableImportForm(NetBoxModelImportForm):
label=_('Side B device'),
queryset=Device.objects.all(),
to_field_name='name',
required=False,
conditional=True,
help_text=_('Device name')
)
side_b_circuit = CSVModelChoiceField(
label=_('Side A device'),
queryset=Circuit.objects.all(),
to_field_name='cid',
required=False,
conditional=True,
help_text=_('Circuit ID'),
)
side_b_type = CSVContentTypeField(
label=_('Side B type'),
queryset=ContentType.objects.all(),
Expand Down Expand Up @@ -1232,7 +1253,7 @@ class CableImportForm(NetBoxModelImportForm):
class Meta:
model = Cable
fields = [
'side_a_device', 'side_a_type', 'side_a_name', 'side_b_device', 'side_b_type', 'side_b_name', 'type',
'side_a_device', 'side_a_circuit', 'side_a_type', 'side_a_name', 'side_b_device', 'side_b_circuit', 'side_b_type', 'side_b_name', 'type',
'status', 'tenant', 'label', 'color', 'length', 'length_unit', 'description', 'comments', 'tags',
]

Expand All @@ -1245,18 +1266,40 @@ def _clean_side(self, side):
assert side in 'ab', f"Invalid side designation: {side}"

device = self.cleaned_data.get(f'side_{side}_device')
circuit = self.cleaned_data.get(f'side_{side}_circuit')
content_type = self.cleaned_data.get(f'side_{side}_type')
name = self.cleaned_data.get(f'side_{side}_name')
if not device or not content_type or not name:

if not (device or circuit) or not content_type or not name:
return None

if device and circuit:
raise forms.ValidationError(
_("Side {side_upper}: Both `device` and `circuit` cannot be specified at the same time").format(side_upper=side.upper())
)

model = content_type.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)

# Should never happen as we return None above if we don't have a device or circuit
assert device or circuit

if device:
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)
self.fields[f'side_{side}_circuit'].required = False
elif circuit:
termination_object = model.objects.get(circuit=circuit, term_side=name.upper())
if termination_object.provider_network is not None:
raise forms.ValidationError(
_("Side {side_upper}: {circuit} {termination_object} is already connected to a Provider Network").format(
side_upper=side.upper(), circuit=circuit, termination_object=termination_object
)
)

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(
Expand Down
2 changes: 2 additions & 0 deletions netbox/templates/generic/bulk_import.html
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,8 @@ <h2 class="card-header">{% trans "Field Options" %}</h2>
<td>
{% if field.required %}
{% checkmark True true="Required" %}
{% elif field.conditional %}
{% tag "Conditional" %}
{% else %}
{{ ''|placeholder }}
{% endif %}
Expand Down
5 changes: 5 additions & 0 deletions netbox/utilities/forms/fields/csv.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ class CSVModelChoiceField(forms.ModelChoiceField):
'invalid_choice': _('Object not found: %(value)s'),
}

def __init__(self, conditional=False, *args, **kwargs):
# Used to trigger conditional validation in the forms
self.conditional = conditional
super().__init__(*args, **kwargs)

def to_python(self, value):
try:
return super().to_python(value)
Expand Down

0 comments on commit 2721455

Please sign in to comment.