Просмотр исходного кода

Enable attaching circuit terminations to clouds

Jeremy Stretch 5 лет назад
Родитель
Сommit
574a43fff7

+ 8 - 6
netbox/circuits/api/serializers.py

@@ -63,12 +63,13 @@ class CircuitTypeSerializer(OrganizationalModelSerializer):
 class CircuitCircuitTerminationSerializer(WritableNestedSerializer, ConnectedEndpointSerializer):
 class CircuitCircuitTerminationSerializer(WritableNestedSerializer, ConnectedEndpointSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail')
     url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail')
     site = NestedSiteSerializer()
     site = NestedSiteSerializer()
+    cloud = NestedCloudSerializer()
 
 
     class Meta:
     class Meta:
         model = CircuitTermination
         model = CircuitTermination
         fields = [
         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):
 class CircuitTerminationSerializer(BaseModelSerializer, CableTerminationSerializer, ConnectedEndpointSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail')
     url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail')
     circuit = NestedCircuitSerializer()
     circuit = NestedCircuitSerializer()
-    site = NestedSiteSerializer()
+    site = NestedSiteSerializer(required=False)
+    cloud = NestedCloudSerializer(required=False)
     cable = NestedCableSerializer(read_only=True)
     cable = NestedCableSerializer(read_only=True)
 
 
     class Meta:
     class Meta:
         model = CircuitTermination
         model = CircuitTermination
         fields = [
         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',
         ]
         ]

+ 4 - 0
netbox/circuits/filters.py

@@ -221,6 +221,10 @@ class CircuitTerminationFilterSet(BaseFilterSet, CableTerminationFilterSet, Path
         to_field_name='slug',
         to_field_name='slug',
         label='Site (slug)',
         label='Site (slug)',
     )
     )
+    cloud_id = django_filters.ModelMultipleChoiceFilter(
+        queryset=Cloud.objects.all(),
+        label='Cloud (ID)',
+    )
 
 
     class Meta:
     class Meta:
         model = CircuitTermination
         model = CircuitTermination

+ 12 - 2
netbox/circuits/forms.py

@@ -423,13 +423,18 @@ class CircuitTerminationForm(BootstrapMixin, forms.ModelForm):
         query_params={
         query_params={
             'region_id': '$region',
             'region_id': '$region',
             'group_id': '$site_group',
             'group_id': '$site_group',
-        }
+        },
+        required=False
+    )
+    cloud = DynamicModelChoiceField(
+        queryset=Cloud.objects.all(),
+        required=False
     )
     )
 
 
     class Meta:
     class Meta:
         model = CircuitTermination
         model = CircuitTermination
         fields = [
         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',
             'xconnect_id', 'pp_info', 'description',
         ]
         ]
         help_texts = {
         help_texts = {
@@ -442,3 +447,8 @@ class CircuitTerminationForm(BootstrapMixin, forms.ModelForm):
             'port_speed': SelectSpeedWidget(),
             'port_speed': SelectSpeedWidget(),
             'upstream_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)

+ 10 - 0
netbox/circuits/migrations/0027_cloud.py

@@ -37,4 +37,14 @@ class Migration(migrations.Migration):
             name='cloud',
             name='cloud',
             unique_together={('provider', 'name')},
             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'),
+        ),
     ]
     ]

+ 21 - 2
netbox/circuits/models.py

@@ -1,3 +1,4 @@
+from django.core.exceptions import ValidationError
 from django.db import models
 from django.db import models
 from django.urls import reverse
 from django.urls import reverse
 
 
@@ -300,7 +301,16 @@ class CircuitTermination(ChangeLoggedModel, PathEndpoint, CableTermination):
     site = models.ForeignKey(
     site = models.ForeignKey(
         to='dcim.Site',
         to='dcim.Site',
         on_delete=models.PROTECT,
         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(
     port_speed = models.PositiveIntegerField(
         verbose_name='Port speed (Kbps)',
         verbose_name='Port speed (Kbps)',
@@ -335,7 +345,16 @@ class CircuitTermination(ChangeLoggedModel, PathEndpoint, CableTermination):
         unique_together = ['circuit', 'term_side']
         unique_together = ['circuit', 'term_side']
 
 
     def __str__(self):
     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):
     def to_objectchange(self, action):
         # Annotate the parent Circuit
         # Annotate the parent Circuit

+ 17 - 4
netbox/templates/circuits/circuittermination_edit.html

@@ -6,7 +6,7 @@
 
 
 {% block form %}
 {% block form %}
   <div class="panel panel-default">
   <div class="panel panel-default">
-      <div class="panel-heading"><strong>Location</strong></div>
+      <div class="panel-heading"><strong>Termination</strong></div>
       <div class="panel-body">
       <div class="panel-body">
           <div class="form-group">
           <div class="form-group">
               <label class="col-md-3 control-label">Provider</label>
               <label class="col-md-3 control-label">Provider</label>
@@ -26,9 +26,22 @@
                   <p class="form-control-static">{{ form.term_side.value }}</p>
                   <p class="form-control-static">{{ form.term_side.value }}</p>
               </div>
               </div>
           </div>
           </div>
-          {% render_field form.region %}
-          {% render_field form.site_group %}
-          {% render_field form.site %}
+          {% with cloud_tab_active=form.initial.cloud %}
+              <ul class="nav nav-tabs" role="tablist">
+                  <li role="presentation"{% if not cloud_tab_active %} class="active"{% endif %}><a href="#site" role="tab" data-toggle="tab">Site</a></li>
+                  <li role="presentation"{% if cloud_tab_active %} class="active"{% endif %}><a href="#cloud" role="tab" data-toggle="tab">Cloud</a></li>
+              </ul>
+              <div class="tab-content">
+                  <div class="tab-pane{% if not cloud_tab_active %} active{% endif %}" id="site">
+                      {% render_field form.region %}
+                      {% render_field form.site_group %}
+                      {% render_field form.site %}
+                  </div>
+                  <div class="tab-pane{% if cloud_tab_active %} active{% endif %}" id="cloud">
+                      {% render_field form.cloud %}
+                  </div>
+              </div>
+          {% endwith %}
           {% render_field form.mark_connected %}
           {% render_field form.mark_connected %}
       </div>
       </div>
   </div>
   </div>

+ 6 - 0
netbox/templates/circuits/cloud.html

@@ -17,6 +17,12 @@
                 <strong>Cloud</strong>
                 <strong>Cloud</strong>
             </div>
             </div>
             <table class="table table-hover panel-body attr-table">
             <table class="table table-hover panel-body attr-table">
+                <tr>
+                    <td>Provider</td>
+                    <td>
+                        <a href="{{ object.provider.get_absolute_url }}">{{ object.provider }}</a>
+                    </td>
+                </tr>
                 <tr>
                 <tr>
                     <td>Name</td>
                     <td>Name</td>
                     <td>{{ object.name }}</td>
                     <td>{{ object.name }}</td>

+ 62 - 53
netbox/templates/circuits/inc/circuit_termination.html

@@ -26,62 +26,71 @@
     </div>
     </div>
     {% if termination %}
     {% if termination %}
         <table class="table table-hover panel-body attr-table">
         <table class="table table-hover panel-body attr-table">
-            <tr>
-                <td>Site</td>
-                <td>
-                    {% if termination.site.region %}
-                        <a href="{{ termination.site.region.get_absolute_url }}">{{ termination.site.region }}</a> /
-                    {% endif %}
-                    <a href="{{ termination.site.get_absolute_url }}">{{ termination.site }}</a>
-                </td>
-            </tr>
-            <tr>
-                <td>Termination</td>
-                <td>
-                    {% if termination.mark_connected %}
-                        <span class="text-success"><i class="mdi mdi-check-bold"></i></span>
-                        <span class="text-muted">Marked as connected</span>
-                    {% elif termination.cable %}
-                        {% if perms.dcim.delete_cable %}
-                            <div class="pull-right">
-                                <a href="{% url 'dcim:cable_delete' pk=termination.cable.pk %}?return_url={{ termination.circuit.get_absolute_url }}" title="Remove cable" class="btn btn-danger btn-xs">
-                                    <i class="mdi mdi-ethernet-cable-off" aria-hidden="true"></i> Disconnect
-                                </a>
-                            </div>
+            {% if termination.site %}
+                <tr>
+                    <td>Site</td>
+                    <td>
+                        {% if termination.site.region %}
+                            <a href="{{ termination.site.region.get_absolute_url }}">{{ termination.site.region }}</a> /
                         {% endif %}
                         {% endif %}
-                        <a href="{{ termination.cable.get_absolute_url }}">{{ termination.cable }}</a>
-                        <a href="{% url 'circuits:circuittermination_trace' pk=termination.pk %}" class="btn btn-primary btn-xs" title="Trace">
-                            <i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i>
-                        </a>
-                        {% with peer=termination.get_cable_peer %}
-                            to
-                            {% if peer.device %}
-                                <a href="{{ peer.device.get_absolute_url }}">{{ peer.device }}</a>
-                            {% elif peer.circuit %}
-                                <a href="{{ peer.circuit.get_absolute_url }}">{{ peer.circuit }}</a>
+                        <a href="{{ termination.site.get_absolute_url }}">{{ termination.site }}</a>
+                    </td>
+                </tr>
+                <tr>
+                    <td>Termination</td>
+                    <td>
+                        {% if termination.mark_connected %}
+                            <span class="text-success"><i class="mdi mdi-check-bold"></i></span>
+                            <span class="text-muted">Marked as connected</span>
+                        {% elif termination.cable %}
+                            {% if perms.dcim.delete_cable %}
+                                <div class="pull-right">
+                                    <a href="{% url 'dcim:cable_delete' pk=termination.cable.pk %}?return_url={{ termination.circuit.get_absolute_url }}" title="Remove cable" class="btn btn-danger btn-xs">
+                                        <i class="mdi mdi-ethernet-cable-off" aria-hidden="true"></i> Disconnect
+                                    </a>
+                                </div>
                             {% endif %}
                             {% endif %}
-                            ({{ peer }})
-                        {% endwith %}
-                    {% else %}
-                        {% if perms.dcim.add_cable %}
-                            <div class="pull-right">
-                                <span class="dropdown">
-                                    <button type="button" class="btn btn-success btn-xs dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
-                                        <span class="mdi mdi-ethernet-cable" aria-hidden="true"></span> Connect
-                                    </button>
-                                    <ul class="dropdown-menu dropdown-menu-right">
-                                        <li><a href="{% url 'circuits:circuittermination_connect' termination_a_id=termination.pk termination_b_type='interface' %}?termination_b_site={{ termination.site.pk }}&return_url={{ object.get_absolute_url }}">Interface</a></li>
-                                        <li><a href="{% url 'circuits:circuittermination_connect' termination_a_id=termination.pk termination_b_type='front-port' %}?termination_b_site={{ termination.site.pk }}&return_url={{ object.get_absolute_url }}">Front Port</a></li>
-                                        <li><a href="{% url 'circuits:circuittermination_connect' termination_a_id=termination.pk termination_b_type='rear-port' %}?termination_b_site={{ termination.site.pk }}&return_url={{ object.get_absolute_url }}">Rear Port</a></li>
-                                        <li><a href="{% url 'circuits:circuittermination_connect' termination_a_id=termination.pk termination_b_type='circuit-termination' %}?termination_b_site={{ termination.site.pk }}&return_url={{ object.get_absolute_url }}">Circuit Termination</a></li>
-                                    </ul>
-                                </span>
-                            </div>
+                            <a href="{{ termination.cable.get_absolute_url }}">{{ termination.cable }}</a>
+                            <a href="{% url 'circuits:circuittermination_trace' pk=termination.pk %}" class="btn btn-primary btn-xs" title="Trace">
+                                <i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i>
+                            </a>
+                            {% with peer=termination.get_cable_peer %}
+                                to
+                                {% if peer.device %}
+                                    <a href="{{ peer.device.get_absolute_url }}">{{ peer.device }}</a>
+                                {% elif peer.circuit %}
+                                    <a href="{{ peer.circuit.get_absolute_url }}">{{ peer.circuit }}</a>
+                                {% endif %}
+                                ({{ peer }})
+                            {% endwith %}
+                        {% else %}
+                            {% if perms.dcim.add_cable %}
+                                <div class="pull-right">
+                                    <span class="dropdown">
+                                        <button type="button" class="btn btn-success btn-xs dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+                                            <span class="mdi mdi-ethernet-cable" aria-hidden="true"></span> Connect
+                                        </button>
+                                        <ul class="dropdown-menu dropdown-menu-right">
+                                            <li><a href="{% url 'circuits:circuittermination_connect' termination_a_id=termination.pk termination_b_type='interface' %}?termination_b_site={{ termination.site.pk }}&return_url={{ object.get_absolute_url }}">Interface</a></li>
+                                            <li><a href="{% url 'circuits:circuittermination_connect' termination_a_id=termination.pk termination_b_type='front-port' %}?termination_b_site={{ termination.site.pk }}&return_url={{ object.get_absolute_url }}">Front Port</a></li>
+                                            <li><a href="{% url 'circuits:circuittermination_connect' termination_a_id=termination.pk termination_b_type='rear-port' %}?termination_b_site={{ termination.site.pk }}&return_url={{ object.get_absolute_url }}">Rear Port</a></li>
+                                            <li><a href="{% url 'circuits:circuittermination_connect' termination_a_id=termination.pk termination_b_type='circuit-termination' %}?termination_b_site={{ termination.site.pk }}&return_url={{ object.get_absolute_url }}">Circuit Termination</a></li>
+                                        </ul>
+                                    </span>
+                                </div>
+                            {% endif %}
+                            <span class="text-muted">Not defined</span>
                         {% endif %}
                         {% endif %}
-                        <span class="text-muted">Not defined</span>
-                    {% endif %}
-                </td>
-            </tr>
+                    </td>
+                </tr>
+            {% else %}
+                <tr>
+                    <td>Cloud</td>
+                    <td>
+                        <a href="{{ termination.cloud.get_absolute_url }}">{{ termination.cloud }}</a>
+                    </td>
+                </tr>
+            {% endif %}
             <tr>
             <tr>
                 <td>Speed</td>
                 <td>Speed</td>
                 <td>
                 <td>