Enable attaching circuit terminations to clouds

This commit is contained in:
Jeremy Stretch 2021-03-18 11:57:59 -04:00
parent 6ff8a267e9
commit 574a43fff7
8 changed files with 140 additions and 67 deletions

View File

@ -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', 'id', 'url', 'display', 'site', 'cloud', 'port_speed', 'upstream_speed', 'xconnect_id',
'connected_endpoint_type', 'connected_endpoint_reachable', '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', 'id', 'url', 'display', 'circuit', 'term_side', 'site', 'cloud', 'port_speed', 'upstream_speed',
'pp_info', 'description', 'mark_connected', 'cable', 'cable_peer', 'cable_peer_type', 'connected_endpoint', 'xconnect_id', 'pp_info', 'description', 'mark_connected', 'cable', 'cable_peer', 'cable_peer_type',
'connected_endpoint_type', 'connected_endpoint_reachable', '_occupied', 'connected_endpoint', 'connected_endpoint_type', 'connected_endpoint_reachable', '_occupied',
] ]

View File

@ -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

View File

@ -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)

View File

@ -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'),
),
] ]

View File

@ -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

View File

@ -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 %} {% with cloud_tab_active=form.initial.cloud %}
{% render_field form.site_group %} <ul class="nav nav-tabs" role="tablist">
{% render_field form.site %} <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>

View File

@ -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>

View File

@ -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> {% if termination.site %}
<td>Site</td> <tr>
<td> <td>Site</td>
{% if termination.site.region %} <td>
<a href="{{ termination.site.region.get_absolute_url }}">{{ termination.site.region }}</a> / {% if termination.site.region %}
{% endif %} <a href="{{ termination.site.region.get_absolute_url }}">{{ termination.site.region }}</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 %}
<a href="{{ termination.cable.get_absolute_url }}">{{ termination.cable }}</a> <a href="{{ termination.site.get_absolute_url }}">{{ termination.site }}</a>
<a href="{% url 'circuits:circuittermination_trace' pk=termination.pk %}" class="btn btn-primary btn-xs" title="Trace"> </td>
<i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i> </tr>
</a> <tr>
{% with peer=termination.get_cable_peer %} <td>Termination</td>
to <td>
{% if peer.device %} {% if termination.mark_connected %}
<a href="{{ peer.device.get_absolute_url }}">{{ peer.device }}</a> <span class="text-success"><i class="mdi mdi-check-bold"></i></span>
{% elif peer.circuit %} <span class="text-muted">Marked as connected</span>
<a href="{{ peer.circuit.get_absolute_url }}">{{ peer.circuit }}</a> {% 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 }}) <a href="{{ termination.cable.get_absolute_url }}">{{ termination.cable }}</a>
{% endwith %} <a href="{% url 'circuits:circuittermination_trace' pk=termination.pk %}" class="btn btn-primary btn-xs" title="Trace">
{% else %} <i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i>
{% if perms.dcim.add_cable %} </a>
<div class="pull-right"> {% with peer=termination.get_cable_peer %}
<span class="dropdown"> to
<button type="button" class="btn btn-success btn-xs dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> {% if peer.device %}
<span class="mdi mdi-ethernet-cable" aria-hidden="true"></span> Connect <a href="{{ peer.device.get_absolute_url }}">{{ peer.device }}</a>
</button> {% elif peer.circuit %}
<ul class="dropdown-menu dropdown-menu-right"> <a href="{{ peer.circuit.get_absolute_url }}">{{ peer.circuit }}</a>
<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> {% endif %}
<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> ({{ peer }})
<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> {% endwith %}
<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> {% else %}
</ul> {% if perms.dcim.add_cable %}
</span> <div class="pull-right">
</div> <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> </td>
{% endif %} </tr>
</td> {% else %}
</tr> <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>