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 %}
{{ form.term_side.value }}
Provider | ++ {{ object.provider }} + | +||||||||||||
Name | {{ object.name }} | 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 %}
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 %}
-
-
- Disconnect
-
-
+ {% 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 %}
+
+
+ Disconnect
+
+
{% 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 |