diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 9368fa6ee..cc218c61c 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -962,14 +962,8 @@ class InventoryItemRoleSerializer(NetBoxModelSerializer): class CableSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:cable-detail') - termination_a_type = ContentTypeField( - queryset=ContentType.objects.filter(CABLE_TERMINATION_MODELS) - ) - termination_b_type = ContentTypeField( - queryset=ContentType.objects.filter(CABLE_TERMINATION_MODELS) - ) - termination_a = serializers.SerializerMethodField(read_only=True) - termination_b = serializers.SerializerMethodField(read_only=True) + # termination_a = serializers.SerializerMethodField(read_only=True) + # termination_b = serializers.SerializerMethodField(read_only=True) status = ChoiceField(choices=LinkStatusChoices, required=False) tenant = NestedTenantSerializer(required=False, allow_null=True) length_unit = ChoiceField(choices=CableLengthUnitChoices, allow_blank=True, required=False) @@ -977,9 +971,8 @@ class CableSerializer(NetBoxModelSerializer): class Meta: model = Cable fields = [ - 'id', 'url', 'display', 'termination_a_type', 'termination_a_ids', 'termination_a', 'termination_b_type', - 'termination_b_ids', 'termination_b', 'type', 'status', 'tenant', 'label', 'color', 'length', 'length_unit', - 'tags', 'custom_fields', 'created', 'last_updated', + 'id', 'url', 'display', 'type', 'status', 'tenant', 'label', 'color', + 'length', 'length_unit', 'tags', 'custom_fields', 'created', 'last_updated', ] def _get_termination(self, obj, side): diff --git a/netbox/dcim/forms/connections.py b/netbox/dcim/forms/connections.py index bcac15044..593a2b2fe 100644 --- a/netbox/dcim/forms/connections.py +++ b/netbox/dcim/forms/connections.py @@ -17,18 +17,47 @@ __all__ = ( ) -class ConnectCableToDeviceForm(TenancyForm, NetBoxModelForm): - """ - Base form for connecting a Cable to a Device component - """ - # Termination A - termination_a_ids = DynamicModelMultipleChoiceField( +class BaseCableConnectionForm(TenancyForm, NetBoxModelForm): + a_terminations = DynamicModelMultipleChoiceField( + queryset=Interface.objects.all(), + label='Name', + disabled_indicator='_occupied' + ) + b_terminations = DynamicModelMultipleChoiceField( queryset=Interface.objects.all(), label='Name', disabled_indicator='_occupied' ) - # Termination B + def save(self, commit=True): + instance = super().save(commit=commit) + + # Create CableTermination instances + terminations = [] + terminations.extend([ + CableTermination(cable=instance, cable_end='A', termination=termination) + for termination in self.cleaned_data['a_terminations'] + ]) + terminations.extend([ + CableTermination(cable=instance, cable_end='B', termination=termination) + for termination in self.cleaned_data['b_terminations'] + ]) + + if commit: + CableTermination.objects.bulk_create(terminations) + else: + instance.terminations = [ + *self.cleaned_data['a_terminations'], + *self.cleaned_data['b_terminations'], + ] + + return instance + + +class ConnectCableToDeviceForm(BaseCableConnectionForm): + """ + Base form for connecting a Cable to a Device component + """ termination_b_region = DynamicModelChoiceField( queryset=Region.objects.all(), label='Region', @@ -83,17 +112,12 @@ class ConnectCableToDeviceForm(TenancyForm, NetBoxModelForm): 'rack_id': '$termination_b_rack', } ) - termination_b_ids = DynamicModelMultipleChoiceField( - queryset=Interface.objects.all(), - label='Name', - disabled_indicator='_occupied' - ) class Meta: model = Cable fields = [ - 'termination_a_ids', 'termination_b_region', 'termination_b_sitegroup', 'termination_b_site', - 'termination_b_rack', 'termination_b_device', 'termination_b_ids', 'type', 'status', 'tenant_group', + 'a_terminations', 'termination_b_region', 'termination_b_sitegroup', 'termination_b_site', + 'termination_b_rack', 'termination_b_device', 'b_terminations', 'type', 'status', 'tenant_group', 'tenant', 'label', 'color', 'length', 'length_unit', 'tags', ] widgets = { @@ -102,17 +126,9 @@ class ConnectCableToDeviceForm(TenancyForm, NetBoxModelForm): 'length_unit': StaticSelect, } - def clean_termination_a_ids(self): - # Return the PK rather than the object - return [getattr(obj, 'pk') for obj in self.cleaned_data['termination_a_ids']] - - def clean_termination_b_ids(self): - # Return the PK rather than the object - return [getattr(obj, 'pk') for obj in self.cleaned_data['termination_b_ids']] - class ConnectCableToConsolePortForm(ConnectCableToDeviceForm): - termination_b_ids = DynamicModelMultipleChoiceField( + b_terminations = DynamicModelMultipleChoiceField( queryset=ConsolePort.objects.all(), label='Name', disabled_indicator='_occupied', @@ -123,7 +139,7 @@ class ConnectCableToConsolePortForm(ConnectCableToDeviceForm): class ConnectCableToConsoleServerPortForm(ConnectCableToDeviceForm): - termination_b_ids = DynamicModelMultipleChoiceField( + b_terminations = DynamicModelMultipleChoiceField( queryset=ConsoleServerPort.objects.all(), label='Name', disabled_indicator='_occupied', @@ -134,7 +150,7 @@ class ConnectCableToConsoleServerPortForm(ConnectCableToDeviceForm): class ConnectCableToPowerPortForm(ConnectCableToDeviceForm): - termination_b_ids = DynamicModelMultipleChoiceField( + b_terminations = DynamicModelMultipleChoiceField( queryset=PowerPort.objects.all(), label='Name', disabled_indicator='_occupied', @@ -145,7 +161,7 @@ class ConnectCableToPowerPortForm(ConnectCableToDeviceForm): class ConnectCableToPowerOutletForm(ConnectCableToDeviceForm): - termination_b_ids = DynamicModelMultipleChoiceField( + b_terminations = DynamicModelMultipleChoiceField( queryset=PowerOutlet.objects.all(), label='Name', disabled_indicator='_occupied', @@ -156,7 +172,7 @@ class ConnectCableToPowerOutletForm(ConnectCableToDeviceForm): class ConnectCableToInterfaceForm(ConnectCableToDeviceForm): - termination_b_ids = DynamicModelMultipleChoiceField( + b_terminations = DynamicModelMultipleChoiceField( queryset=Interface.objects.all(), label='Name', disabled_indicator='_occupied', @@ -168,7 +184,7 @@ class ConnectCableToInterfaceForm(ConnectCableToDeviceForm): class ConnectCableToFrontPortForm(ConnectCableToDeviceForm): - termination_b_ids = DynamicModelMultipleChoiceField( + b_terminations = DynamicModelMultipleChoiceField( queryset=FrontPort.objects.all(), label='Name', disabled_indicator='_occupied', @@ -179,7 +195,7 @@ class ConnectCableToFrontPortForm(ConnectCableToDeviceForm): class ConnectCableToRearPortForm(ConnectCableToDeviceForm): - termination_b_ids = DynamicModelMultipleChoiceField( + b_terminations = DynamicModelMultipleChoiceField( queryset=RearPort.objects.all(), label='Name', disabled_indicator='_occupied', @@ -189,15 +205,7 @@ class ConnectCableToRearPortForm(ConnectCableToDeviceForm): ) -class ConnectCableToCircuitTerminationForm(TenancyForm, NetBoxModelForm): - # Termination A - termination_a_ids = DynamicModelMultipleChoiceField( - queryset=Interface.objects.all(), - label='Side', - disabled_indicator='_occupied' - ) - - # Termination B +class ConnectCableToCircuitTerminationForm(BaseCableConnectionForm): termination_b_provider = DynamicModelChoiceField( queryset=Provider.objects.all(), label='Provider', @@ -236,7 +244,7 @@ class ConnectCableToCircuitTerminationForm(TenancyForm, NetBoxModelForm): 'site_id': '$termination_b_site', } ) - termination_b_ids = DynamicModelMultipleChoiceField( + b_terminations = DynamicModelMultipleChoiceField( queryset=CircuitTermination.objects.all(), label='Side', disabled_indicator='_occupied', @@ -247,29 +255,13 @@ class ConnectCableToCircuitTerminationForm(TenancyForm, NetBoxModelForm): class Meta(ConnectCableToDeviceForm.Meta): fields = [ - 'termination_a_ids', 'termination_b_provider', 'termination_b_region', 'termination_b_sitegroup', - 'termination_b_site', 'termination_b_circuit', 'termination_b_ids', 'type', 'status', 'tenant_group', + 'a_terminations', 'termination_b_provider', 'termination_b_region', 'termination_b_sitegroup', + 'termination_b_site', 'termination_b_circuit', 'b_terminations', 'type', 'status', 'tenant_group', 'tenant', 'label', 'color', 'length', 'length_unit', 'tags', ] - def clean_termination_a_id(self): - # Return the PK rather than the object - return getattr(self.cleaned_data['termination_a_id'], 'pk', None) - def clean_termination_b_id(self): - # Return the PK rather than the object - return getattr(self.cleaned_data['termination_b_id'], 'pk', None) - - -class ConnectCableToPowerFeedForm(TenancyForm, NetBoxModelForm): - # Termination A - termination_a_ids = DynamicModelMultipleChoiceField( - queryset=Interface.objects.all(), - label='Name', - disabled_indicator='_occupied' - ) - - # Termination B +class ConnectCableToPowerFeedForm(BaseCableConnectionForm): termination_b_region = DynamicModelChoiceField( queryset=Region.objects.all(), label='Region', @@ -312,7 +304,7 @@ class ConnectCableToPowerFeedForm(TenancyForm, NetBoxModelForm): 'location_id': '$termination_b_location', } ) - termination_b_ids = DynamicModelMultipleChoiceField( + b_terminations = DynamicModelMultipleChoiceField( queryset=PowerFeed.objects.all(), label='Name', disabled_indicator='_occupied', @@ -323,15 +315,7 @@ class ConnectCableToPowerFeedForm(TenancyForm, NetBoxModelForm): class Meta(ConnectCableToDeviceForm.Meta): fields = [ - 'termination_a_ids', 'termination_b_region', 'termination_b_sitegroup', 'termination_b_site', - 'termination_b_location', 'termination_b_powerpanel', 'termination_b_ids', 'type', 'status', 'tenant_group', + 'a_terminations', 'termination_b_region', 'termination_b_sitegroup', 'termination_b_site', + 'termination_b_location', 'termination_b_powerpanel', 'b_terminations', 'type', 'status', 'tenant_group', 'tenant', 'label', 'color', 'length', 'length_unit', 'tags', ] - - def clean_termination_a_id(self): - # Return the PK rather than the object - return getattr(self.cleaned_data['termination_a_id'], 'pk', None) - - def clean_termination_b_id(self): - # Return the PK rather than the object - return getattr(self.cleaned_data['termination_b_id'], 'pk', None) diff --git a/netbox/dcim/models/cables.py b/netbox/dcim/models/cables.py index 18de8cb58..5eb48fbf2 100644 --- a/netbox/dcim/models/cables.py +++ b/netbox/dcim/models/cables.py @@ -168,6 +168,16 @@ class Cable(NetBoxModel): def get_status_color(self): return LinkStatusChoices.colors.get(self.status) + def get_a_terminations(self): + return [ + term.termination for term in CableTermination.objects.filter(cable=self, cable_end='A') + ] + + def get_b_terminations(self): + return [ + term.termination for term in CableTermination.objects.filter(cable=self, cable_end='B') + ] + class CableTermination(models.Model): """ diff --git a/netbox/dcim/signals.py b/netbox/dcim/signals.py index fa75cf992..38349e680 100644 --- a/netbox/dcim/signals.py +++ b/netbox/dcim/signals.py @@ -81,34 +81,27 @@ def update_connected_endpoints(instance, created, raw=False, **kwargs): # TODO: Update link peer fields # Cache the Cable on its termination points - for term in instance.termination_a: - if term.cable != instance: + for term in instance.terminations.all(): + if term.termination.cable != instance: logger.debug(f"Updating termination A for cable {instance}: {term}") - term.cable = instance - # term._link_peer = instance.termination_b - term.save() - for term in instance.termination_b: - if term.cable != instance: - logger.debug(f"Updating termination B for cable {instance}") - term.cable = instance - # term._link_peer = instance.termination_a + term.termination.cable = instance term.save() - # Create/update cable paths - if created: - for termination in [*instance.termination_a, *instance.termination_b]: - if isinstance(termination, PathEndpoint): - create_cablepath(termination) - else: - rebuild_paths(termination) - elif instance.status != instance._orig_status: - # We currently don't support modifying either termination of an existing Cable. (This - # may change in the future.) However, we do need to capture status changes and update - # any CablePaths accordingly. - if instance.status != LinkStatusChoices.STATUS_CONNECTED: - CablePath.objects.filter(path__contains=instance).update(is_active=False) - else: - rebuild_paths(instance) + # # Create/update cable paths + # if created: + # for term in instance.terminations.all(): + # if isinstance(term.termination, PathEndpoint): + # create_cablepath(term.termination) + # else: + # rebuild_paths(term.termination) + # elif instance.status != instance._orig_status: + # # We currently don't support modifying either termination of an existing Cable. (This + # # may change in the future.) However, we do need to capture status changes and update + # # any CablePaths accordingly. + # if instance.status != LinkStatusChoices.STATUS_CONNECTED: + # CablePath.objects.filter(path__contains=instance).update(is_active=False) + # else: + # rebuild_paths(instance) @receiver(post_delete, sender=Cable) diff --git a/netbox/dcim/tables/template_code.py b/netbox/dcim/tables/template_code.py index 9911a938d..b72b4738f 100644 --- a/netbox/dcim/tables/template_code.py +++ b/netbox/dcim/tables/template_code.py @@ -139,9 +139,9 @@ CONSOLEPORT_BUTTONS = """
{% else %} @@ -171,9 +171,9 @@ CONSOLESERVERPORT_BUTTONS = """ {% else %} @@ -203,8 +203,8 @@ POWERPORT_BUTTONS = """ {% else %} @@ -230,7 +230,7 @@ POWEROUTLET_BUTTONS = """ {% if not record.mark_connected %} - + {% else %} @@ -280,10 +280,10 @@ INTERFACE_BUTTONS = """ {% else %} @@ -319,12 +319,12 @@ FRONTPORT_BUTTONS = """ {% else %} @@ -356,12 +356,12 @@ REARPORT_BUTTONS = """ {% else %} diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 43085702c..09e15676d 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -2829,24 +2829,14 @@ class CableCreateView(generic.ObjectEditView): # Always return a new instance return self.queryset.model() - def alter_object(self, obj, request, url_args, url_kwargs): - termination_a_type = url_kwargs.get('termination_a_type') - termination_a_ids = request.GET.get('termination_a_ids', []) - app_label, model = request.GET.get('termination_b_type').split('.') - self.termination_b_type = ContentType.objects.get(app_label=app_label, model=model) - - # Initialize Cable termination attributes - obj.termination_a_type = ContentType.objects.get_for_model(termination_a_type) - obj.termination_a_ids = termination_a_type.objects.filter(pk__in=termination_a_ids) - obj.termination_b_type = self.termination_b_type - - return obj - def get(self, request, *args, **kwargs): obj = self.get_object(**kwargs) obj = self.alter_object(obj, request, args, kwargs) initial_data = request.GET + app_label, model = request.GET.get('termination_b_type').split('.') + termination_b_type = ContentType.objects.get(app_label=app_label, model=model) + # TODO # # Set initial site and rack based on side A termination (if not already set) # termination_a_site = getattr(obj.termination_a.parent_object, 'site', None) @@ -2857,12 +2847,17 @@ class CableCreateView(generic.ObjectEditView): form = self.form(instance=obj, initial=initial_data) # Set the queryset of termination A - form.fields['termination_a_ids'].queryset = kwargs['termination_a_type'].objects.all() + form.fields['a_terminations'].queryset = kwargs['termination_a_type'].objects.all() + + # TODO Find a better way to infer the near-end parent object + termination_a = kwargs['termination_a_type'].objects.filter(pk__in=initial_data['a_terminations']).first() return render(request, self.template_name, { 'obj': obj, 'obj_type': Cable._meta.verbose_name, - 'termination_b_type': self.termination_b_type.name, + 'termination_a_type': kwargs['termination_a_type']._meta.model_name, + 'termination_a': termination_a, + 'termination_b_type': termination_b_type.name, 'form': form, 'return_url': self.get_return_url(request, obj), }) diff --git a/netbox/templates/circuits/inc/circuit_termination.html b/netbox/templates/circuits/inc/circuit_termination.html index 12fb85d57..71732938e 100644 --- a/netbox/templates/circuits/inc/circuit_termination.html +++ b/netbox/templates/circuits/inc/circuit_termination.html @@ -70,10 +70,10 @@ Connect {% endif %} diff --git a/netbox/templates/dcim/cable.html b/netbox/templates/dcim/cable.html index 712704f67..effb55c02 100644 --- a/netbox/templates/dcim/cable.html +++ b/netbox/templates/dcim/cable.html @@ -63,13 +63,13 @@Device | -{{ termination.0.device|linkify }} | +{{ terminations.0.device|linkify }} |
Site | -{{ termination.0.device.site|linkify }} | +{{ terminations.0.device.site|linkify }} |
Rack | -{{ termination.0.device.rack|linkify }} | +{{ terminations.0.device.rack|linkify }} |
Type | -{{ termination.0|meta:"verbose_name"|capfirst }} | +{{ terminations.0|meta:"verbose_name"|capfirst }} |
Component(s) | +Name(s) | - {% for term in termination %} + {% for term in terminations %} {{ term|linkify }}{% if not forloop.last %},{% endif %} {% endfor %} | @@ -32,12 +32,12 @@ {# Circuit termination #}
Provider | -{{ termination.0.circuit.provider|linkify }} | +{{ terminations.0.circuit.provider|linkify }} |
Circuit | - {% for term in termination %} + {% for term in terminations %} {{ term.circuit|linkify }} ({{ term }}){% if not forloop.last %},{% endif %} {% endfor %} | diff --git a/netbox/templates/dcim/interface.html b/netbox/templates/dcim/interface.html index 7181d2554..708ea7370 100644 --- a/netbox/templates/dcim/interface.html +++ b/netbox/templates/dcim/interface.html @@ -251,22 +251,22 @@