feat: add cable reconnection

This commit is contained in:
hellerve 2021-08-05 10:30:11 +02:00
parent 4f7626828a
commit f8d7c024e7
5 changed files with 811 additions and 0 deletions

View File

@ -4269,6 +4269,574 @@ class CableCSVForm(CustomFieldModelCSVForm):
return length_unit if length_unit is not None else ''
class ReconnectCableToDeviceForm(BootstrapMixin, forms.ModelForm):
"""
Base form for connecting a Cable to a Device component
"""
termination_a_site = DynamicModelChoiceField(
queryset=Site.objects.all(),
label='Site',
required=False,
)
termination_a_rack = DynamicModelChoiceField(
queryset=Rack.objects.all(),
label='Rack',
required=False,
display_field='display_name',
null_option='None',
query_params={
'site_id': '$termination_a_site'
}
)
termination_a_device = DynamicModelChoiceField(
queryset=Device.objects.all(),
label='Device',
required=False,
display_field='display_name',
query_params={
'site_id': '$termination_a_site',
'rack_id': '$termination_a_rack',
}
)
termination_b_site = DynamicModelChoiceField(
queryset=Site.objects.all(),
label='Site',
required=False
)
termination_b_rack = DynamicModelChoiceField(
queryset=Rack.objects.all(),
label='Rack',
required=False,
display_field='display_name',
null_option='None',
query_params={
'site_id': '$termination_b_site'
}
)
termination_b_device = DynamicModelChoiceField(
queryset=Device.objects.all(),
label='Device',
required=False,
display_field='display_name',
query_params={
'site_id': '$termination_b_site',
'rack_id': '$termination_b_rack',
}
)
class Meta:
model = Cable
fields = [
'termination_a_site', 'termination_a_rack', 'termination_a_device', 'termination_a_id',
'termination_b_site', 'termination_b_rack', 'termination_b_device', 'termination_b_id'
]
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 ReconnectConsolePortToConsoleServerPortForm(ReconnectCableToDeviceForm):
termination_a_id = DynamicModelChoiceField(
queryset=ConsolePort.objects.all(),
label='Name',
disabled_indicator='cable',
query_params={
'device_id': '$termination_a_device'
}
)
termination_b_id = DynamicModelChoiceField(
queryset=ConsoleServerPort.objects.all(),
label='Name',
disabled_indicator='cable',
query_params={
'device_id': '$termination_b_device'
}
)
class ReconnectConsolePortToFrontPortForm(ReconnectCableToDeviceForm):
termination_a_id = DynamicModelChoiceField(
queryset=ConsolePort.objects.all(),
label='Name',
disabled_indicator='cable',
query_params={
'device_id': '$termination_a_device'
}
)
termination_b_id = DynamicModelChoiceField(
queryset=FrontPort.objects.all(),
label='Name',
disabled_indicator='cable',
query_params={
'device_id': '$termination_b_device'
}
)
class ReconnectConsolePortToRearPortForm(ReconnectCableToDeviceForm):
termination_a_id = DynamicModelChoiceField(
queryset=ConsolePort.objects.all(),
label='Name',
disabled_indicator='cable',
query_params={
'device_id': '$termination_a_device'
}
)
termination_b_id = DynamicModelChoiceField(
queryset=RearPort.objects.all(),
label='Name',
disabled_indicator='cable',
query_params={
'device_id': '$termination_b_device'
}
)
class ReconnectConsoleServerPortToFrontPortForm(ReconnectCableToDeviceForm):
termination_a_id = DynamicModelChoiceField(
queryset=ConsoleServerPort.objects.all(),
label='Name',
disabled_indicator='cable',
query_params={
'device_id': '$termination_a_device'
}
)
termination_b_id = DynamicModelChoiceField(
queryset=FrontPort.objects.all(),
label='Name',
disabled_indicator='cable',
query_params={
'device_id': '$termination_b_device'
}
)
class ReconnectConsoleServerPortToRearPortForm(ReconnectCableToDeviceForm):
termination_a_id = DynamicModelChoiceField(
queryset=ConsoleServerPort.objects.all(),
label='Name',
disabled_indicator='cable',
query_params={
'device_id': '$termination_a_device'
}
)
termination_b_id = DynamicModelChoiceField(
queryset=RearPort.objects.all(),
label='Name',
disabled_indicator='cable',
query_params={
'device_id': '$termination_b_device'
}
)
class ReconnectPowerfeedToPowerPortForm(ReconnectCableToDeviceForm):
termination_a_site = DynamicModelChoiceField(
queryset=Site.objects.all(),
label='Site',
required=False,
display_field='cid'
)
termination_a_rackgroup = DynamicModelChoiceField(
queryset=Location.objects.all(),
label='Location',
required=False,
display_field='cid',
query_params={
'site_id': '$termination_a_site'
}
)
termination_a_powerpanel = DynamicModelChoiceField(
queryset=PowerPanel.objects.all(),
label='Power Panel',
required=False,
query_params={
'site_id': '$termination_a_site',
'rack_group_id': '$termination_a_rackgroup',
}
)
termination_a_id = DynamicModelChoiceField(
queryset=PowerFeed.objects.all(),
label='Name',
disabled_indicator='cable',
query_params={
'power_panel_id': '$termination_a_powerpanel'
}
)
termination_b_id = DynamicModelChoiceField(
queryset=PowerPort.objects.all(),
label='Name',
disabled_indicator='cable',
query_params={
'device_id': '$termination_b_device'
}
)
class Meta:
model = Cable
fields = [
'termination_a_site', 'termination_a_rackgroup', 'termination_a_powerpanel', 'termination_a_id',
'termination_b_site', 'termination_b_rack', 'termination_b_device', 'termination_b_id'
]
class ReconnectPowerOutletToPowerPortForm(ReconnectCableToDeviceForm):
termination_a_id = DynamicModelChoiceField(
queryset=PowerOutlet.objects.all(),
label='Name',
disabled_indicator='cable',
query_params={
'device_id': '$termination_a_device'
}
)
termination_b_id = DynamicModelChoiceField(
queryset=PowerPort.objects.all(),
label='Name',
disabled_indicator='cable',
query_params={
'device_id': '$termination_b_device'
}
)
class ReconnectInterfaceForm(ReconnectCableToDeviceForm):
termination_a_id = DynamicModelChoiceField(
queryset=Interface.objects.all(),
label='Name',
disabled_indicator='cable',
query_params={
'device_id': '$termination_a_device',
'kind': 'physical',
}
)
termination_b_id = DynamicModelChoiceField(
queryset=Interface.objects.all(),
label='Name',
disabled_indicator='cable',
query_params={
'device_id': '$termination_b_device',
'kind': 'physical',
}
)
class ReconnectFrontPortToInterfaceForm(ReconnectCableToDeviceForm):
termination_a_id = DynamicModelChoiceField(
queryset=FrontPort.objects.all(),
label='Name',
disabled_indicator='cable',
query_params={
'device_id': '$termination_a_device'
}
)
termination_b_id = DynamicModelChoiceField(
queryset=Interface.objects.all(),
label='Name',
disabled_indicator='cable',
query_params={
'device_id': '$termination_b_device',
'kind': 'physical',
}
)
class ReconnectInterfaceToRearPortForm(ReconnectCableToDeviceForm):
termination_a_id = DynamicModelChoiceField(
queryset=Interface.objects.all(),
label='Name',
disabled_indicator='cable',
query_params={
'device_id': '$termination_a_device',
'kind': 'physical',
}
)
termination_b_id = DynamicModelChoiceField(
queryset=RearPort.objects.all(),
label='Name',
disabled_indicator='cable',
query_params={
'device_id': '$termination_b_device'
}
)
class ReconnectCircuitTerminationToInterfaceForm(ReconnectCableToDeviceForm):
termination_a_provider = DynamicModelChoiceField(
queryset=Provider.objects.all(),
label='Provider',
required=False
)
termination_a_site = DynamicModelChoiceField(
queryset=Site.objects.all(),
label='Site',
required=False
)
termination_a_circuit = DynamicModelChoiceField(
queryset=Circuit.objects.all(),
label='Circuit',
display_field='cid',
query_params={
'provider_id': '$termination_a_provider',
'site_id': '$termination_a_site',
}
)
termination_a_id = DynamicModelChoiceField(
queryset=CircuitTermination.objects.all(),
label='Side',
display_field='term_side',
disabled_indicator='cable',
query_params={
'circuit_id': '$termination_a_circuit'
}
)
termination_b_id = DynamicModelChoiceField(
queryset=Interface.objects.all(),
label='Name',
disabled_indicator='cable',
query_params={
'device_id': '$termination_b_device',
'kind': 'physical',
}
)
class Meta:
model = Cable
fields = [
'termination_a_provider', 'termination_a_site', 'termination_a_circuit', 'termination_a_id',
'termination_b_site', 'termination_b_rack', 'termination_b_device', 'termination_b_id'
]
class ReconnectCircuitTerminationForm(BootstrapMixin, forms.ModelForm):
termination_a_provider = DynamicModelChoiceField(
queryset=Provider.objects.all(),
label='Provider',
required=False
)
termination_a_site = DynamicModelChoiceField(
queryset=Site.objects.all(),
label='Site',
required=False
)
termination_a_circuit = DynamicModelChoiceField(
queryset=Circuit.objects.all(),
label='Circuit',
display_field='cid',
query_params={
'provider_id': '$termination_a_provider',
'site_id': '$termination_a_site',
}
)
termination_a_id = DynamicModelChoiceField(
queryset=CircuitTermination.objects.all(),
label='Side',
display_field='term_side',
disabled_indicator='cable',
query_params={
'circuit_id': '$termination_a_circuit'
}
)
termination_b_provider = DynamicModelChoiceField(
queryset=Provider.objects.all(),
label='Provider',
required=False
)
termination_b_site = DynamicModelChoiceField(
queryset=Site.objects.all(),
label='Site',
required=False
)
termination_b_circuit = DynamicModelChoiceField(
queryset=Circuit.objects.all(),
label='Circuit',
display_field='cid',
query_params={
'provider_id': '$termination_b_provider',
'site_id': '$termination_b_site',
}
)
termination_b_id = DynamicModelChoiceField(
queryset=CircuitTermination.objects.all(),
label='Side',
display_field='term_side',
disabled_indicator='cable',
query_params={
'circuit_id': '$termination_b_circuit'
}
)
class Meta:
model = Cable
fields = [
'termination_a_provider', 'termination_a_site', 'termination_a_circuit', 'termination_a_id',
'termination_b_provider', 'termination_b_site', 'termination_b_circuit', 'termination_b_id', 'type',
'status', 'label', 'color', 'length', 'length_unit',
]
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 ReconnectFrontPortForm(ReconnectCableToDeviceForm):
termination_a_id = DynamicModelChoiceField(
queryset=FrontPort.objects.all(),
label='Name',
disabled_indicator='cable',
query_params={
'device_id': '$termination_a_device'
}
)
termination_b_id = DynamicModelChoiceField(
queryset=FrontPort.objects.all(),
label='Name',
disabled_indicator='cable',
query_params={
'device_id': '$termination_b_device'
}
)
class ReconnectFrontPortToRearPortForm(ReconnectCableToDeviceForm):
termination_a_id = DynamicModelChoiceField(
queryset=FrontPort.objects.all(),
label='Name',
disabled_indicator='cable',
query_params={
'device_id': '$termination_a_device'
}
)
termination_b_id = DynamicModelChoiceField(
queryset=RearPort.objects.all(),
label='Name',
disabled_indicator='cable',
query_params={
'device_id': '$termination_b_device'
}
)
class ReconnectRearPortForm(ReconnectCableToDeviceForm):
termination_a_id = DynamicModelChoiceField(
queryset=RearPort.objects.all(),
label='Name',
disabled_indicator='cable',
query_params={
'device_id': '$termination_a_device'
}
)
termination_b_id = DynamicModelChoiceField(
queryset=RearPort.objects.all(),
label='Name',
disabled_indicator='cable',
query_params={
'device_id': '$termination_b_device'
}
)
class ReconnectCircuitTerminationToForm(BootstrapMixin, forms.ModelForm):
termination_a_provider = DynamicModelChoiceField(
queryset=Provider.objects.all(),
label='Provider',
required=False
)
termination_a_site = DynamicModelChoiceField(
queryset=Site.objects.all(),
label='Site',
required=False
)
termination_a_circuit = DynamicModelChoiceField(
queryset=Circuit.objects.all(),
label='Circuit',
display_field='cid',
query_params={
'provider_id': '$termination_a_provider',
'site_id': '$termination_a_site',
}
)
termination_a_id = DynamicModelChoiceField(
queryset=CircuitTermination.objects.all(),
label='Side',
display_field='term_side',
disabled_indicator='cable',
query_params={
'circuit_id': '$termination_a_circuit'
}
)
termination_b_site = DynamicModelChoiceField(
queryset=Site.objects.all(),
label='Site',
required=False
)
termination_b_rack = DynamicModelChoiceField(
queryset=Rack.objects.all(),
label='Rack',
required=False,
display_field='display_name',
null_option='None',
query_params={
'site_id': '$termination_b_site'
}
)
termination_b_device = DynamicModelChoiceField(
queryset=Device.objects.all(),
label='Device',
required=False,
display_field='display_name',
query_params={
'site_id': '$termination_b_site',
'rack_id': '$termination_b_rack',
}
)
class Meta:
model = Cable
fields = [
'termination_a_provider', 'termination_a_site', 'termination_a_circuit', 'termination_a_id',
'termination_b_site', 'termination_b_rack', 'termination_b_device', 'termination_b_id'
]
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 ReconnectCircuitTerminationToFrontPortForm(ReconnectCircuitTerminationToForm):
termination_b_id = DynamicModelChoiceField(
queryset=FrontPort.objects.all(),
label='Name',
disabled_indicator='cable',
query_params={
'device_id': '$termination_b_device'
}
)
class ReconnectCircuitTerminationToRearPortForm(ReconnectCircuitTerminationToForm):
termination_b_id = DynamicModelChoiceField(
queryset=RearPort.objects.all(),
label='Name',
disabled_indicator='cable',
query_params={
'device_id': '$termination_b_device'
}
)
class CableBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
pk = forms.ModelMultipleChoiceField(
queryset=Cable.objects.all(),

View File

@ -378,6 +378,7 @@ urlpatterns = [
path('cables/<int:pk>/delete/', views.CableDeleteView.as_view(), name='cable_delete'),
path('cables/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='cable_changelog', kwargs={'model': Cable}),
path('cables/<int:pk>/journal/', ObjectJournalView.as_view(), name='cable_journal', kwargs={'model': Cable}),
path(r'cables/<int:pk>/reconnect/', views.CableReconnectView.as_view(), name='cable_reconnect'),
# Console/power/interface connections (read-only)
path('console-connections/', views.ConsoleConnectionsListView.as_view(), name='console_connections_list'),

View File

@ -2478,6 +2478,104 @@ class CableBulkDeleteView(generic.BulkDeleteView):
table = tables.CableTable
class CableReconnectView(ObjectPermissionRequiredMixin, GetReturnURLMixin, View):
queryset = Cable.objects.prefetch_related('termination_a', 'termination_b')
template_name = 'dcim/cable_reconnect.html'
def get_required_permission(self):
return 'dcim.change_cable'
def dispatch(self, request, *args, pk=None, **kwargs):
self.obj = Cable.objects.get(pk=pk)
idx = (self.obj.termination_a_type.model, self.obj.termination_b_type.model)
idx_sorted = tuple(sorted(idx))
if idx != idx_sorted:
termination_a = self.obj.termination_a
self.obj.termination_a = self.obj.termination_b
self.obj.termination_b = termination_a
self.form_class = {
('circuittermination', 'circuittermination'): forms.ReconnectCircuitTerminationForm,
('circuittermination', 'rearport'): forms.ReconnectCircuitTerminationToRearPortForm,
('consoleport', 'consoleserverport'): forms.ReconnectConsolePortToConsoleServerPortForm,
('consoleport', 'frontport'): forms.ReconnectConsolePortToFrontPortForm,
('consoleport', 'rearport'): forms.ReconnectConsolePortToRearPortForm,
('consoleserverport', 'frontport'): forms.ReconnectConsoleServerPortToFrontPortForm,
('consoleserverport', 'rearport'): forms.ReconnectConsoleServerPortToRearPortForm,
('powerfeed', 'powerport'): forms.ReconnectPowerfeedToPowerPortForm,
('poweroutlet', 'powerport'): forms.ReconnectPowerOutletToPowerPortForm,
('circuittermination', 'interface'): forms.ReconnectCircuitTerminationToInterfaceForm,
('frontport', 'interface'): forms.ReconnectFrontPortToInterfaceForm,
('interface', 'rearport'): forms.ReconnectInterfaceToRearPortForm,
('interface', 'interface'): forms.ReconnectInterfaceForm,
('frontport', 'frontport'): forms.ReconnectFrontPortForm,
('frontport', 'rearport'): forms.ReconnectFrontPortToRearPortForm,
('circuittermination', 'frontport'): forms.ReconnectCircuitTerminationToFrontPortForm,
('rearport', 'rearport'): forms.ReconnectRearPortForm,
}[idx_sorted]
return super().dispatch(request, *args, **kwargs)
def prefill_form(self, initial_data, termination):
o = getattr(self.obj, termination)
if o and hasattr(o, 'device'):
device = o.device
initial_data['{}_device'.format(termination)] = device
if device.site:
initial_data['{}_site'.format(termination)] = device.site
if device.rack:
initial_data['{}_rack'.format(termination)] = device.rack
def get(self, request, *args, **kwargs):
# Parse initial data manually to avoid setting field values as lists
initial_data = {k: request.GET[k] for k in request.GET}
self.prefill_form(initial_data, 'termination_a')
self.prefill_form(initial_data, 'termination_b')
form = self.form_class(instance=self.obj, initial=initial_data)
return render(request, self.template_name, {
'obj': self.obj,
'obj_type': Cable._meta.verbose_name,
'form': form,
'return_url': self.get_return_url(request, self.obj),
})
def post(self, request, *args, **kwargs):
with transaction.atomic():
try:
if hasattr(self.obj.termination_a, '_path') and self.obj.termination_a._path_id:
self.obj.termination_a._path = None
except CablePath.DoesNotExist:
pass
try:
if hasattr(self.obj.termination_b, '_path') and self.obj.termination_b._path_id:
self.obj.termination_b._path = None
except CablePath.DoesNotExist:
pass
self.obj.delete()
self.obj.termination_a.cable = None
self.obj.termination_b.cable = None
form = self.form_class(request.POST, request.FILES, instance=self.obj)
if form.is_valid():
obj = form.save()
return redirect(self.get_return_url(request, obj))
return render(request, self.template_name, {
'obj': self.obj,
'obj_type': Cable._meta.verbose_name,
'form': form,
'return_url': self.get_return_url(request, self.obj),
})
#
# Connections
#

View File

@ -10,6 +10,10 @@
{% endblock %}
{% block buttons %}
{% if request.user|can_change:object %}
<a href="{% url 'dcim:cable_reconnect' pk=object.pk %}" class="btn btn-primary">
<span class="mdi mdi-ethernet-cable" aria-hidden="true"></span> Reconnect </a>
{% endif %}
{% if request.user|can_change:object %}
{% edit_button object %}
{% endif %}

View File

@ -0,0 +1,140 @@
{% extends 'base.html' %}
{% load static %}
{% load helpers %}
{% load form_helpers %}
{% block content %}
<form method="post" class="form form-horizontal">
{% csrf_token %}
{% for field in form.hidden_fields %}
{{ field }}
{% endfor %}
{% if form.non_field_errors %}
<div class="row">
<div class="col-md-6 col-md-offset-3">
<div class="panel panel-danger">
<div class="panel-heading"><strong>Errors</strong></div>
<div class="panel-body">
{{ form.non_field_errors }}
</div>
</div>
</div>
</div>
{% endif %}
{% with termination_a=form.instance.termination_a %}
<h3>{% block title %}Reconnect {{ form.instance.termination_a_type.name|bettertitle }}<->{{ form.instance.termination_b_type.name|bettertitle }} cable{% endblock %}</h3>
<div class="row">
<div class="col-md-5">
<div class="panel panel-default">
<div class="panel-heading text-center">
<strong>A Side</strong>
</div>
<div class="panel-body">
{% if tabs %}
<ul class="nav nav-tabs">
{% for url, link in tabs %}
<li role="presentation"><a href="{{ url }}">{{ link }}</a></li>
{% endfor %}
</ul>
{% endif %}
{% if 'termination_a_provider' in form.fields %}
{% render_field form.termination_a_provider %}
{% endif %}
{% if 'termination_a_site' in form.fields %}
{% render_field form.termination_a_site %}
{% endif %}
{% if 'termination_a_rackgroup' in form.fields %}
{% render_field form.termination_a_rackgroup %}
{% endif %}
{% if 'termination_a_rack' in form.fields %}
{% render_field form.termination_a_rack %}
{% endif %}
{% if 'termination_a_device' in form.fields %}
{% render_field form.termination_a_device %}
{% endif %}
{% if 'termination_a_type' in form.fields %}
{% render_field form.termination_a_type %}
{% endif %}
{% if 'termination_a_powerpanel' in form.fields %}
{% render_field form.termination_a_powerpanel %}
{% endif %}
{% if 'termination_a_circuit' in form.fields %}
{% render_field form.termination_a_circuit %}
{% endif %}
<div class="form-group">
<label class="col-md-3 control-label required">Type</label>
<div class="col-md-9">
<p class="form-control-static">{{ form.instance.termination_a_type.name|capfirst }}</p>
</div>
</div>
{% render_field form.termination_a_id %}
</div>
</div>
</div>
<div class="col-md-2 text-center" style="padding-top: 90px;">
<i class="fa fa-exchange fa-4x"></i>
</div>
<div class="col-md-5">
<div class="panel panel-default">
<div class="panel-heading text-center">
<strong>B Side</strong>
</div>
<div class="panel-body">
{% if tabs %}
<ul class="nav nav-tabs">
{% for url, link in tabs %}
<li role="presentation"><a href="{{ url }}">{{ link }}</a></li>
{% endfor %}
</ul>
{% endif %}
{% if 'termination_b_provider' in form.fields %}
{% render_field form.termination_b_provider %}
{% endif %}
{% if 'termination_b_site' in form.fields %}
{% render_field form.termination_b_site %}
{% endif %}
{% if 'termination_b_rackgroup' in form.fields %}
{% render_field form.termination_b_rackgroup %}
{% endif %}
{% if 'termination_b_rack' in form.fields %}
{% render_field form.termination_b_rack %}
{% endif %}
{% if 'termination_b_device' in form.fields %}
{% render_field form.termination_b_device %}
{% endif %}
{% if 'termination_b_type' in form.fields %}
{% render_field form.termination_b_type %}
{% endif %}
{% if 'termination_b_powerpanel' in form.fields %}
{% render_field form.termination_b_powerpanel %}
{% endif %}
{% if 'termination_b_circuit' in form.fields %}
{% render_field form.termination_b_circuit %}
{% endif %}
<div class="form-group">
<label class="col-md-3 control-label required">Type</label>
<div class="col-md-9">
<p class="form-control-static">{{ form.instance.termination_b_type.name|capfirst }}</p>
</div>
</div>
{% render_field form.termination_b_id %}
</div>
</div>
</div>
</div>
<div style="display: none">
{% include 'dcim/inc/cable_form.html' %}
</div>
<div class="form-group">
<div class="col-md-12 text-center">
<button type="submit" name="_update" class="btn btn-primary">Reconnect</button>
<a href="{{ return_url }}" class="btn btn-default">Cancel</a>
</div>
</div>
{% endwith %}
</form>
{% endblock %}
{% block javascript %}
<script src="{% static 'js/livesearch.js' %}?v{{ settings.VERSION }}"></script>
{% endblock %}