WIP: Initial work on the cable connection form

This commit is contained in:
Jeremy Stretch 2018-10-22 16:58:24 -04:00
parent a36b120c8b
commit 471bddea09
13 changed files with 275 additions and 20 deletions

View File

@ -284,12 +284,9 @@ CONNECTION_STATUS_CHOICES = [
]
# Cable endpoint types
CABLE_ENDPOINT_TYPES = (
'consoleport', 'consoleserverport', 'interface', 'poweroutlet', 'powerport',
)
CABLE_CONNECTION_TYPES = CABLE_ENDPOINT_TYPES + (
'frontpanelport', 'rearpanelport',
)
CABLE_ENDPOINT_TYPES = [
'consoleport', 'consoleserverport', 'interface', 'poweroutlet', 'powerport', 'frontpanelport', 'rearpanelport',
]
# Cable types
# TODO: Add more types
@ -299,3 +296,24 @@ CABLE_TYPE_CHOICES = (
(CABLE_TYPE_COPPER, 'Copper'),
(CABLE_TYPE_FIBER, 'Fiber'),
)
CABLE_ENDPOINT_TYPE_CHOICES = {
# (API endpoint, human-friendly name)
'consoleport': ('console-ports', 'Console port'),
'consoleserverport': ('console-server-ports', 'Console server port'),
'powerport': ('power-ports', 'Power port'),
'poweroutlet': ('power-outlets', 'Power outlet'),
'interface': ('interfaces', 'Interface'),
'frontpanelport': ('front-panel-ports', 'Front panel port'),
'rearpanelport': ('rear-panel-ports', 'Rear panel port'),
}
COMPATIBLE_ENDPOINT_TYPES = {
'consoleport': ['consoleserverport', 'frontpanelport', 'rearpanelport'],
'consoleserverport': ['consoleport', 'frontpanelport', 'rearpanelport'],
'powerport': ['poweroutlet'],
'poweroutlet': ['powerport'],
'interface': ['interface', 'frontpanelport', 'rearpanelport'],
'frontpanelport': ['consoleport', 'consoleserverport', 'interface', 'frontpanelport', 'rearpanelport'],
'rearpanelport': ['consoleport', 'consoleserverport', 'interface', 'frontpanelport', 'rearpanelport'],
}

View File

@ -2,6 +2,7 @@ import re
from django import forms
from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
from django.contrib.postgres.forms.array import SimpleArrayField
from django.db.models import Count, Q
from mptt.forms import TreeNodeChoiceField
@ -19,11 +20,12 @@ from utilities.forms import (
BulkEditNullBooleanSelect, ChainedFieldsMixin, ChainedModelChoiceField, CommentField, ComponentForm,
ConfirmationForm, CSVChoiceField, ExpandableNameField, FilterChoiceField, FilterTreeNodeMultipleChoiceField,
FlexibleModelChoiceField, JSONField, Livesearch, SelectWithDisabled, SelectWithPK, SmallTextarea, SlugField,
ContentTypeSelect
)
from virtualization.models import Cluster
from .constants import *
from .models import (
DeviceBay, DeviceBayTemplate, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate,
Cable, DeviceBay, DeviceBayTemplate, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate,
Device, DeviceRole, DeviceType, FrontPanelPort, FrontPanelPortTemplate, Interface, InterfaceConnection,
InterfaceTemplate, Manufacturer, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort,
PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RearPanelPort, RearPanelPortTemplate, Region, Site,
@ -2298,6 +2300,95 @@ class RearPanelPortBulkRenameForm(BulkRenameForm):
)
#
# Cables
#
class CableForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm):
endpoint_b_site = forms.ModelChoiceField(
queryset=Site.objects.all(),
label='Site',
required=False,
widget=forms.Select(
attrs={'filter-for': 'endpoint_b_rack'}
)
)
endpoint_b_rack = ChainedModelChoiceField(
queryset=Rack.objects.all(),
chains=(
('site', 'endpoint_b_site'),
),
label='Rack',
required=False,
widget=APISelect(
api_url='/api/dcim/racks/?site_id={{endpoint_b_site}}',
attrs={'filter-for': 'endpoint_b_device', 'nullable': 'true'}
)
)
endpoint_b_device = ChainedModelChoiceField(
queryset=Device.objects.all(),
chains=(
('site', 'endpoint_b_site'),
('rack', 'endpoint_b_rack'),
),
label='Device',
required=False,
widget=APISelect(
api_url='/api/dcim/devices/?site_id={{endpoint_b_site}}&rack_id={{endpoint_b_rack}}',
display_field='display_name',
attrs={'filter-for': 'endpoint_b_id'}
)
)
livesearch = forms.CharField(
required=False,
label='Device',
widget=Livesearch(
query_key='q',
query_url='dcim-api:device-list',
field_to_update='endpoint_b_device'
)
)
endpoint_b_type = forms.ModelChoiceField(
queryset=ContentType.objects.all(),
label='Type',
widget=ContentTypeSelect(
attrs={'filter-for': 'endpoint_b_id'}
)
)
endpoint_b_id = forms.ChoiceField(
choices=[],
label='Name',
widget=APISelect(
api_url='/api/dcim/{{endpoint_b_type}}s/?device_id={{endpoint_b_device}}',
disabled_indicator='is_connected'
)
)
class Meta:
model = Cable
fields = [
'endpoint_b_site', 'endpoint_b_rack', 'endpoint_b_device', 'livesearch', 'endpoint_b_type',
'endpoint_b_id', 'status', 'label',
]
def __init__(self, *args, **kwargs):
super(CableForm, self).__init__(*args, **kwargs)
# Define available types for endpoint B based on the type of endpoint A
endpoint_a_type = self.instance.endpoint_a._meta.model_name
self.fields['endpoint_b_type'].queryset = ContentType.objects.filter(
model__in=COMPATIBLE_ENDPOINT_TYPES.get(endpoint_a_type)
)
def clean(self):
# Assign endpoint B
cleaned_data = super(CableForm, self).clean()
#
# Device bays
#

View File

@ -2455,7 +2455,7 @@ class Cable(ChangeLoggedModel):
"""
endpoint_a_type = models.ForeignKey(
to=ContentType,
limit_choices_to={'model__in': CABLE_CONNECTION_TYPES},
limit_choices_to={'model__in': CABLE_ENDPOINT_TYPES},
on_delete=models.PROTECT,
related_name='+'
)
@ -2466,7 +2466,7 @@ class Cable(ChangeLoggedModel):
)
endpoint_b_type = models.ForeignKey(
to=ContentType,
limit_choices_to={'model__in': CABLE_CONNECTION_TYPES},
limit_choices_to={'model__in': CABLE_ENDPOINT_TYPES},
on_delete=models.PROTECT,
related_name='+'
)

View File

@ -161,7 +161,8 @@ urlpatterns = [
url(r'^devices/console-ports/add/$', views.DeviceBulkAddConsolePortView.as_view(), name='device_bulk_add_consoleport'),
url(r'^devices/(?P<pk>\d+)/console-ports/add/$', views.ConsolePortCreateView.as_view(), name='consoleport_add'),
url(r'^devices/(?P<pk>\d+)/console-ports/delete/$', views.ConsolePortBulkDeleteView.as_view(), name='consoleport_bulk_delete'),
url(r'^console-ports/(?P<pk>\d+)/connect/$', views.ConsolePortConnectView.as_view(), name='consoleport_connect'),
# url(r'^console-ports/(?P<pk>\d+)/connect/$', views.ConsolePortConnectView.as_view(), name='consoleport_connect'),
url(r'^console-ports/(?P<endpoint_a_id>\d+)/connect/$', views.CableConnectView.as_view(), name='consoleport_connect', kwargs={'endpoint_a_type': 'consoleport'}),
url(r'^console-ports/(?P<pk>\d+)/disconnect/$', views.ConsolePortDisconnectView.as_view(), name='consoleport_disconnect'),
url(r'^console-ports/(?P<pk>\d+)/edit/$', views.ConsolePortEditView.as_view(), name='consoleport_edit'),
url(r'^console-ports/(?P<pk>\d+)/delete/$', views.ConsolePortDeleteView.as_view(), name='consoleport_delete'),
@ -171,7 +172,8 @@ urlpatterns = [
url(r'^devices/(?P<pk>\d+)/console-server-ports/add/$', views.ConsoleServerPortCreateView.as_view(), name='consoleserverport_add'),
url(r'^devices/(?P<pk>\d+)/console-server-ports/disconnect/$', views.ConsoleServerPortBulkDisconnectView.as_view(), name='consoleserverport_bulk_disconnect'),
url(r'^devices/(?P<pk>\d+)/console-server-ports/delete/$', views.ConsoleServerPortBulkDeleteView.as_view(), name='consoleserverport_bulk_delete'),
url(r'^console-server-ports/(?P<pk>\d+)/connect/$', views.ConsoleServerPortConnectView.as_view(), name='consoleserverport_connect'),
# url(r'^console-server-ports/(?P<pk>\d+)/connect/$', views.ConsoleServerPortConnectView.as_view(), name='consoleserverport_connect'),
url(r'^console-server-ports/(?P<endpoint_a_id>\d+)/connect/$', views.CableConnectView.as_view(), name='consoleserverport_connect', kwargs={'endpoint_a_type': 'consoleserverport'}),
url(r'^console-server-ports/(?P<pk>\d+)/disconnect/$', views.ConsoleServerPortDisconnectView.as_view(), name='consoleserverport_disconnect'),
url(r'^console-server-ports/(?P<pk>\d+)/edit/$', views.ConsoleServerPortEditView.as_view(), name='consoleserverport_edit'),
url(r'^console-server-ports/(?P<pk>\d+)/delete/$', views.ConsoleServerPortDeleteView.as_view(), name='consoleserverport_delete'),
@ -181,7 +183,8 @@ urlpatterns = [
url(r'^devices/power-ports/add/$', views.DeviceBulkAddPowerPortView.as_view(), name='device_bulk_add_powerport'),
url(r'^devices/(?P<pk>\d+)/power-ports/add/$', views.PowerPortCreateView.as_view(), name='powerport_add'),
url(r'^devices/(?P<pk>\d+)/power-ports/delete/$', views.PowerPortBulkDeleteView.as_view(), name='powerport_bulk_delete'),
url(r'^power-ports/(?P<pk>\d+)/connect/$', views.PowerPortConnectView.as_view(), name='powerport_connect'),
# url(r'^power-ports/(?P<pk>\d+)/connect/$', views.PowerPortConnectView.as_view(), name='powerport_connect'),
url(r'^power-ports/(?P<endpoint_a_id>\d+)/connect/$', views.CableConnectView.as_view(), name='powerport_connect', kwargs={'endpoint_a_type': 'powerport'}),
url(r'^power-ports/(?P<pk>\d+)/disconnect/$', views.PowerPortDisconnectView.as_view(), name='powerport_disconnect'),
url(r'^power-ports/(?P<pk>\d+)/edit/$', views.PowerPortEditView.as_view(), name='powerport_edit'),
url(r'^power-ports/(?P<pk>\d+)/delete/$', views.PowerPortDeleteView.as_view(), name='powerport_delete'),
@ -191,7 +194,8 @@ urlpatterns = [
url(r'^devices/(?P<pk>\d+)/power-outlets/add/$', views.PowerOutletCreateView.as_view(), name='poweroutlet_add'),
url(r'^devices/(?P<pk>\d+)/power-outlets/disconnect/$', views.PowerOutletBulkDisconnectView.as_view(), name='poweroutlet_bulk_disconnect'),
url(r'^devices/(?P<pk>\d+)/power-outlets/delete/$', views.PowerOutletBulkDeleteView.as_view(), name='poweroutlet_bulk_delete'),
url(r'^power-outlets/(?P<pk>\d+)/connect/$', views.PowerOutletConnectView.as_view(), name='poweroutlet_connect'),
# url(r'^power-outlets/(?P<pk>\d+)/connect/$', views.PowerOutletConnectView.as_view(), name='poweroutlet_connect'),
url(r'^power-outlets/(?P<endpoint_a_id>\d+)/connect/$', views.CableConnectView.as_view(), name='poweroutlet_connect', kwargs={'endpoint_a_type': 'poweroutlet'}),
url(r'^power-outlets/(?P<pk>\d+)/disconnect/$', views.PowerOutletDisconnectView.as_view(), name='poweroutlet_disconnect'),
url(r'^power-outlets/(?P<pk>\d+)/edit/$', views.PowerOutletEditView.as_view(), name='poweroutlet_edit'),
url(r'^power-outlets/(?P<pk>\d+)/delete/$', views.PowerOutletDeleteView.as_view(), name='poweroutlet_delete'),

View File

@ -1,12 +1,14 @@
from operator import attrgetter
from django.apps import apps
from django.contrib import messages
from django.contrib.auth.mixins import PermissionRequiredMixin
from django.core.exceptions import ObjectDoesNotExist
from django.core.paginator import EmptyPage, PageNotAnInteger
from django.db import transaction
from django.db.models import Count, Q
from django.forms import modelformset_factory
from django.http import HttpResponseRedirect
from django.http import Http404, HttpResponseRedirect
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from django.utils.html import escape
@ -30,7 +32,7 @@ from virtualization.models import VirtualMachine
from . import filters, forms, tables
from .constants import CONNECTION_STATUS_CONNECTED
from .models import (
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
DeviceBayTemplate, DeviceRole, DeviceType, FrontPanelPort, FrontPanelPortTemplate, Interface, InterfaceConnection,
InterfaceTemplate, Manufacturer, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort,
PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RearPanelPort, RearPanelPortTemplate, Region, Site,
@ -2152,6 +2154,27 @@ class InterfaceConnectionsListView(ObjectListView):
template_name = 'dcim/interface_connections_list.html'
class CableConnectView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.add_cable'
model = Cable
model_form = forms.CableForm
template_name = 'dcim/cable_connect.html'
def alter_obj(self, obj, request, url_args, url_kwargs):
# Retrieve endpoint A based on the given type and PK
endpoint_a_type = url_kwargs.get('endpoint_a_type')
endpoint_a_id = url_kwargs.get('endpoint_a_id')
try:
model = apps.get_model(
app_label='dcim',
model_name=endpoint_a_type
)
obj.endpoint_a = model.objects.get(pk=endpoint_a_id)
except ObjectDoesNotExist:
raise Http404
return obj
#
# Inventory items
#

View File

@ -93,7 +93,10 @@ $(document).ready(function() {
var rendered_url = api_url;
while (match = filter_regex.exec(api_url)) {
var filter_field = $('#id_' + match[1]);
if (filter_field.val()) {
var custom_attr = $('option:selected', filter_field).attr('api-value');
if (custom_attr) {
rendered_url = rendered_url.replace(match[0], custom_attr);
} else if (filter_field.val()) {
rendered_url = rendered_url.replace(match[0], filter_field.val());
} else if (filter_field.attr('nullable') == 'true') {
rendered_url = rendered_url.replace(match[0], '0');

View File

@ -0,0 +1,106 @@
{% extends '_base.html' %}
{% load static from staticfiles %}
{% load form_helpers %}
{% block content %}
<form action="." 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 endpoint_a=form.instance.endpoint_a %}
<h3>{% block title %}Connect {{ endpoint_a.device }} {{ endpoint_a }}{% 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">
<div class="form-group">
<label class="col-md-3 control-label required">Site</label>
<div class="col-md-9">
<p class="form-control-static">{{ endpoint_a.device.site }}</p>
</div>
</div>
<div class="form-group">
<label class="col-md-3 control-label required">Rack</label>
<div class="col-md-9">
<p class="form-control-static">{{ endpoint_a.device.rack|default:"None" }}</p>
</div>
</div>
<div class="form-group">
<label class="col-md-3 control-label required">Device</label>
<div class="col-md-9">
<p class="form-control-static">{{ endpoint_a.device }}</p>
</div>
</div>
<div class="form-group">
<label class="col-md-3 control-label required">Name</label>
<div class="col-md-9">
<p class="form-control-static">{{ endpoint_a }}</p>
</div>
</div>
</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">
<ul class="nav nav-tabs" role="tablist">
<li role="presentation" class="active"><a href="#search" aria-controls="search" role="tab" data-toggle="tab">Search</a></li>
<li role="presentation"><a href="#select" aria-controls="home" role="tab" data-toggle="tab">Select</a></li>
</ul>
<div class="tab-content">
<div class="tab-pane active" id="search">
{% render_field form.livesearch %}
</div>
<div class="tab-pane" id="select">
{% render_field form.endpoint_b_site %}
{% render_field form.endpoint_b_rack %}
{% render_field form.endpoint_b_device %}
</div>
</div>
{% render_field form.endpoint_b_type %}
{% render_field form.endpoint_b_id %}
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-4 col-md-offset-4">
{% render_field form.status %}
{% render_field form.label %}
</div>
</div>
<div class="form-group">
<div class="col-md-12 text-center">
<button type="submit" name="_update" class="btn btn-primary">Connect</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 %}

View File

@ -30,7 +30,7 @@
<i class="glyphicon glyphicon-resize-full" aria-hidden="true"></i>
</a>
{% else %}
<a href="{% url 'dcim:consoleport_connect' pk=cp.pk %}" title="Connect" class="btn btn-success btn-xs">
<a href="{% url 'dcim:consoleport_connect' endpoint_a_id=cp.pk %}" title="Connect" class="btn btn-success btn-xs">
<i class="glyphicon glyphicon-resize-small" aria-hidden="true"></i>
</a>
{% endif %}

View File

@ -35,7 +35,7 @@
<i class="glyphicon glyphicon-resize-full" aria-hidden="true"></i>
</a>
{% else %}
<a href="{% url 'dcim:consoleserverport_connect' pk=csp.pk %}" title="Connect" class="btn btn-success btn-xs">
<a href="{% url 'dcim:consoleserverport_connect' endpoint_a_id=csp.pk %}" title="Connect" class="btn btn-success btn-xs">
<i class="glyphicon glyphicon-resize-small" aria-hidden="true"></i>
</a>
{% endif %}

View File

@ -35,7 +35,7 @@
<i class="glyphicon glyphicon-resize-full" aria-hidden="true"></i>
</a>
{% else %}
<a href="{% url 'dcim:poweroutlet_connect' pk=po.pk %}" title="Connect" class="btn btn-success btn-xs">
<a href="{% url 'dcim:poweroutlet_connect' endpoint_a_id=po.pk %}" title="Connect" class="btn btn-success btn-xs">
<i class="glyphicon glyphicon-resize-small" aria-hidden="true"></i>
</a>
{% endif %}

View File

@ -30,7 +30,7 @@
<i class="glyphicon glyphicon-resize-full" aria-hidden="true"></i>
</a>
{% else %}
<a href="{% url 'dcim:powerport_connect' pk=pp.pk %}" title="Connect" class="btn btn-success btn-xs">
<a href="{% url 'dcim:powerport_connect' endpoint_a_id=pp.pk %}" title="Connect" class="btn btn-success btn-xs">
<i class="glyphicon glyphicon-resize-small" aria-hidden="true"></i>
</a>
{% endif %}

View File

@ -207,6 +207,15 @@ class SelectWithPK(forms.Select):
option_template_name = 'widgets/select_option_with_pk.html'
class ContentTypeSelect(forms.Select):
"""
Appends an `api-value` attribute equal to the slugified model name for each ContentType. For example:
<option value="37" api-value="console-server-port">console server port</option>
This attribute can be used to reference the relevant API endpoint for a particular ContentType.
"""
option_template_name = 'widgets/select_contenttype.html'
class ArrayFieldSelectMultiple(SelectWithDisabled, forms.SelectMultiple):
"""
MultiSelect widget for a SimpleArrayField. Choices must be populated on the widget.

View File

@ -0,0 +1 @@
<option value="{{ widget.value }}"{% include "django/forms/widgets/attrs.html" %}{% if widget.value %} api-value="{{ widget.label|slugify }}"{% endif %}>{{ widget.label.label|default:widget.label }}</option>