From 574a43fff77a0ef104e1ac4b3e1b2872050a3b69 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 18 Mar 2021 11:57:59 -0400 Subject: [PATCH] Enable attaching circuit terminations to clouds --- netbox/circuits/api/serializers.py | 14 ++- netbox/circuits/filters.py | 4 + netbox/circuits/forms.py | 14 ++- netbox/circuits/migrations/0027_cloud.py | 10 ++ netbox/circuits/models.py | 23 +++- .../circuits/circuittermination_edit.html | 21 +++- netbox/templates/circuits/cloud.html | 6 + .../circuits/inc/circuit_termination.html | 115 ++++++++++-------- 8 files changed, 140 insertions(+), 67 deletions(-) diff --git a/netbox/circuits/api/serializers.py b/netbox/circuits/api/serializers.py index 556721c94..5469049db 100644 --- a/netbox/circuits/api/serializers.py +++ b/netbox/circuits/api/serializers.py @@ -63,12 +63,13 @@ class CircuitTypeSerializer(OrganizationalModelSerializer): class CircuitCircuitTerminationSerializer(WritableNestedSerializer, ConnectedEndpointSerializer): url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail') site = NestedSiteSerializer() + cloud = NestedCloudSerializer() class Meta: model = CircuitTermination fields = [ - 'id', 'url', 'display', 'site', 'port_speed', 'upstream_speed', 'xconnect_id', 'connected_endpoint', - 'connected_endpoint_type', 'connected_endpoint_reachable', + 'id', 'url', 'display', 'site', 'cloud', 'port_speed', 'upstream_speed', 'xconnect_id', + 'connected_endpoint', 'connected_endpoint_type', 'connected_endpoint_reachable', ] @@ -93,13 +94,14 @@ class CircuitSerializer(PrimaryModelSerializer): class CircuitTerminationSerializer(BaseModelSerializer, CableTerminationSerializer, ConnectedEndpointSerializer): url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail') circuit = NestedCircuitSerializer() - site = NestedSiteSerializer() + site = NestedSiteSerializer(required=False) + cloud = NestedCloudSerializer(required=False) cable = NestedCableSerializer(read_only=True) class Meta: model = CircuitTermination fields = [ - 'id', 'url', 'display', 'circuit', 'term_side', 'site', 'port_speed', 'upstream_speed', 'xconnect_id', - 'pp_info', 'description', 'mark_connected', 'cable', 'cable_peer', 'cable_peer_type', 'connected_endpoint', - 'connected_endpoint_type', 'connected_endpoint_reachable', '_occupied', + 'id', 'url', 'display', 'circuit', 'term_side', 'site', 'cloud', 'port_speed', 'upstream_speed', + 'xconnect_id', 'pp_info', 'description', 'mark_connected', 'cable', 'cable_peer', 'cable_peer_type', + 'connected_endpoint', 'connected_endpoint_type', 'connected_endpoint_reachable', '_occupied', ] diff --git a/netbox/circuits/filters.py b/netbox/circuits/filters.py index 376cc2af7..6a6b2c012 100644 --- a/netbox/circuits/filters.py +++ b/netbox/circuits/filters.py @@ -221,6 +221,10 @@ class CircuitTerminationFilterSet(BaseFilterSet, CableTerminationFilterSet, Path to_field_name='slug', label='Site (slug)', ) + cloud_id = django_filters.ModelMultipleChoiceFilter( + queryset=Cloud.objects.all(), + label='Cloud (ID)', + ) class Meta: model = CircuitTermination diff --git a/netbox/circuits/forms.py b/netbox/circuits/forms.py index 295a3ea63..7285dad96 100644 --- a/netbox/circuits/forms.py +++ b/netbox/circuits/forms.py @@ -423,13 +423,18 @@ class CircuitTerminationForm(BootstrapMixin, forms.ModelForm): query_params={ 'region_id': '$region', 'group_id': '$site_group', - } + }, + required=False + ) + cloud = DynamicModelChoiceField( + queryset=Cloud.objects.all(), + required=False ) class Meta: model = CircuitTermination fields = [ - 'term_side', 'region', 'site_group', 'site', 'mark_connected', 'port_speed', 'upstream_speed', + 'term_side', 'region', 'site_group', 'site', 'cloud', 'mark_connected', 'port_speed', 'upstream_speed', 'xconnect_id', 'pp_info', 'description', ] help_texts = { @@ -442,3 +447,8 @@ class CircuitTerminationForm(BootstrapMixin, forms.ModelForm): 'port_speed': SelectSpeedWidget(), 'upstream_speed': SelectSpeedWidget(), } + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.fields['cloud'].widget.add_query_param('provider_id', self.instance.circuit.provider_id) diff --git a/netbox/circuits/migrations/0027_cloud.py b/netbox/circuits/migrations/0027_cloud.py index 36cceb7ca..889b5151e 100644 --- a/netbox/circuits/migrations/0027_cloud.py +++ b/netbox/circuits/migrations/0027_cloud.py @@ -37,4 +37,14 @@ class Migration(migrations.Migration): name='cloud', unique_together={('provider', 'name')}, ), + migrations.AddField( + model_name='circuittermination', + name='cloud', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='circuit_terminations', to='circuits.cloud'), + ), + migrations.AlterField( + model_name='circuittermination', + name='site', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='circuit_terminations', to='dcim.site'), + ), ] diff --git a/netbox/circuits/models.py b/netbox/circuits/models.py index d2f8a5b1d..b13dd9603 100644 --- a/netbox/circuits/models.py +++ b/netbox/circuits/models.py @@ -1,3 +1,4 @@ +from django.core.exceptions import ValidationError from django.db import models from django.urls import reverse @@ -300,7 +301,16 @@ class CircuitTermination(ChangeLoggedModel, PathEndpoint, CableTermination): site = models.ForeignKey( to='dcim.Site', on_delete=models.PROTECT, - related_name='circuit_terminations' + related_name='circuit_terminations', + blank=True, + null=True + ) + cloud = models.ForeignKey( + to=Cloud, + on_delete=models.PROTECT, + related_name='circuit_terminations', + blank=True, + null=True ) port_speed = models.PositiveIntegerField( verbose_name='Port speed (Kbps)', @@ -335,7 +345,16 @@ class CircuitTermination(ChangeLoggedModel, PathEndpoint, CableTermination): unique_together = ['circuit', 'term_side'] def __str__(self): - return 'Side {}'.format(self.get_term_side_display()) + return f"Side {self.get_term_side_display()}" + + def clean(self): + super().clean() + + # Must define either site *or* cloud + if self.site is None and self.cloud is None: + raise ValidationError("A circuit termination must attach to either a site or a cloud.") + if self.site and self.cloud: + raise ValidationError("A circuit termination cannot attach to both a site and a cloud.") def to_objectchange(self, action): # Annotate the parent Circuit diff --git a/netbox/templates/circuits/circuittermination_edit.html b/netbox/templates/circuits/circuittermination_edit.html index 4e737d16d..ebad75976 100644 --- a/netbox/templates/circuits/circuittermination_edit.html +++ b/netbox/templates/circuits/circuittermination_edit.html @@ -6,7 +6,7 @@ {% block form %}
-
Location
+
Termination
@@ -26,9 +26,22 @@

{{ form.term_side.value }}

- {% render_field form.region %} - {% render_field form.site_group %} - {% render_field form.site %} + {% with cloud_tab_active=form.initial.cloud %} + +
+
+ {% render_field form.region %} + {% render_field form.site_group %} + {% render_field form.site %} +
+
+ {% render_field form.cloud %} +
+
+ {% endwith %} {% render_field form.mark_connected %}
diff --git a/netbox/templates/circuits/cloud.html b/netbox/templates/circuits/cloud.html index 61dec5ead..268f64387 100644 --- a/netbox/templates/circuits/cloud.html +++ b/netbox/templates/circuits/cloud.html @@ -17,6 +17,12 @@ Cloud + + + + diff --git a/netbox/templates/circuits/inc/circuit_termination.html b/netbox/templates/circuits/inc/circuit_termination.html index 762dd1662..acfc4ee22 100644 --- a/netbox/templates/circuits/inc/circuit_termination.html +++ b/netbox/templates/circuits/inc/circuit_termination.html @@ -26,62 +26,71 @@ {% if termination %}
Provider + {{ object.provider }} +
Name {{ object.name }}
- - - - - - - + + + + + + - + + + {% else %} + + + + + {% endif %}
Site - {% if termination.site.region %} - {{ termination.site.region }} / - {% endif %} - {{ termination.site }} -
Termination - {% if termination.mark_connected %} - - Marked as connected - {% elif termination.cable %} - {% if perms.dcim.delete_cable %} - + {% if termination.site %} +
Site + {% if termination.site.region %} + {{ termination.site.region }} / {% endif %} - {{ termination.cable }} - - - - {% with peer=termination.get_cable_peer %} - to - {% if peer.device %} - {{ peer.device }} - {% elif peer.circuit %} - {{ peer.circuit }} + {{ termination.site }} +
Termination + {% if termination.mark_connected %} + + Marked as connected + {% elif termination.cable %} + {% if perms.dcim.delete_cable %} + {% endif %} - ({{ peer }}) - {% endwith %} - {% else %} - {% if perms.dcim.add_cable %} -
- - - - -
+ {{ termination.cable }} + + + + {% with peer=termination.get_cable_peer %} + to + {% if peer.device %} + {{ peer.device }} + {% elif peer.circuit %} + {{ peer.circuit }} + {% endif %} + ({{ peer }}) + {% endwith %} + {% else %} + {% if perms.dcim.add_cable %} +
+ + + + +
+ {% endif %} + Not defined {% endif %} - Not defined - {% endif %} -
Cloud + {{ termination.cloud }} +
Speed