Introduce CableBulkImportView

This commit is contained in:
Jeremy Stretch 2018-10-31 17:05:25 -04:00
parent cd243a90d0
commit 55c632ace7
4 changed files with 130 additions and 473 deletions

View File

@ -4,6 +4,7 @@ from django import forms
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.contrib.postgres.forms.array import SimpleArrayField from django.contrib.postgres.forms.array import SimpleArrayField
from django.core.exceptions import ObjectDoesNotExist
from django.db.models import Count, Q from django.db.models import Count, Q
from mptt.forms import TreeNodeChoiceField from mptt.forms import TreeNodeChoiceField
from natsort import natsorted from natsort import natsorted
@ -1271,157 +1272,6 @@ class ConsolePortCreateForm(ComponentForm):
tags = TagField(required=False) tags = TagField(required=False)
class ConsoleConnectionCSVForm(forms.ModelForm):
console_server = FlexibleModelChoiceField(
queryset=Device.objects.all(),
to_field_name='name',
help_text='Console server name or ID',
error_messages={
'invalid_choice': 'Console server not found',
}
)
connected_endpoint = forms.CharField(
help_text='Console server port'
)
device = FlexibleModelChoiceField(
queryset=Device.objects.all(),
to_field_name='name',
help_text='Device name or ID',
error_messages={
'invalid_choice': 'Device not found',
}
)
console_port = forms.CharField(
help_text='Console port name'
)
connection_status = CSVChoiceField(
choices=CONNECTION_STATUS_CHOICES,
help_text='Connection status'
)
class Meta:
model = ConsolePort
fields = ['console_server', 'connected_endpoint', 'device', 'console_port', 'connection_status']
def clean_console_port(self):
console_port_name = self.cleaned_data.get('console_port')
if not self.cleaned_data.get('device') or not console_port_name:
return None
try:
# Retrieve console port by name
consoleport = ConsolePort.objects.get(
device=self.cleaned_data['device'], name=console_port_name
)
# Check if the console port is already connected
if consoleport.connected_endpoint is not None:
raise forms.ValidationError("{} {} is already connected".format(
self.cleaned_data['device'], console_port_name
))
except ConsolePort.DoesNotExist:
raise forms.ValidationError("Invalid console port ({} {})".format(
self.cleaned_data['device'], console_port_name
))
self.instance = consoleport
return consoleport
def clean_connected_endpoint(self):
consoleserverport_name = self.cleaned_data.get('connected_endpoint')
if not self.cleaned_data.get('console_server') or not consoleserverport_name:
return None
try:
# Retrieve console server port by name
consoleserverport = ConsoleServerPort.objects.get(
device=self.cleaned_data['console_server'], name=consoleserverport_name
)
# Check if the console server port is already connected
if ConsolePort.objects.filter(connected_endpoint=consoleserverport).count():
raise forms.ValidationError("{} {} is already connected".format(
self.cleaned_data['console_server'], consoleserverport_name
))
except ConsoleServerPort.DoesNotExist:
raise forms.ValidationError("Invalid console server port ({} {})".format(
self.cleaned_data['console_server'], consoleserverport_name
))
return consoleserverport
class ConsolePortConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm):
site = forms.ModelChoiceField(
queryset=Site.objects.all(),
required=False,
widget=forms.Select(
attrs={'filter-for': 'rack'}
)
)
rack = ChainedModelChoiceField(
queryset=Rack.objects.all(),
chains=(
('site', 'site'),
),
label='Rack',
required=False,
widget=APISelect(
api_url='/api/dcim/racks/?site_id={{site}}',
attrs={'filter-for': 'console_server', 'nullable': 'true'}
)
)
console_server = ChainedModelChoiceField(
queryset=Device.objects.all(),
chains=(
('site', 'site'),
('rack', 'rack'),
),
label='Console Server',
required=False,
widget=APISelect(
api_url='/api/dcim/devices/?site_id={{site}}&rack_id={{rack}}',
display_field='display_name',
attrs={'filter-for': 'connected_endpoint'}
)
)
livesearch = forms.CharField(
required=False,
label='Console Server',
widget=Livesearch(
query_key='q',
query_url='dcim-api:device-list',
field_to_update='console_server',
)
)
connected_endpoint = ChainedModelChoiceField(
queryset=ConsoleServerPort.objects.all(),
chains=(
('device', 'console_server'),
),
label='Port',
widget=APISelect(
api_url='/api/dcim/console-server-ports/?device_id={{console_server}}',
disabled_indicator='cable',
)
)
class Meta:
model = ConsolePort
fields = ['site', 'rack', 'console_server', 'livesearch', 'connected_endpoint', 'connection_status']
labels = {
'connected_endpoint': 'Port',
'connection_status': 'Status',
}
def __init__(self, *args, **kwargs):
super(ConsolePortConnectionForm, self).__init__(*args, **kwargs)
if not self.instance.pk:
raise RuntimeError("ConsolePortConnectionForm must be initialized with an existing ConsolePort instance.")
# #
# Console server ports # Console server ports
# #
@ -1442,76 +1292,6 @@ class ConsoleServerPortCreateForm(ComponentForm):
tags = TagField(required=False) tags = TagField(required=False)
class ConsoleServerPortConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.Form):
site = forms.ModelChoiceField(
queryset=Site.objects.all(),
required=False,
widget=forms.Select(
attrs={'filter-for': 'rack'}
)
)
rack = ChainedModelChoiceField(
queryset=Rack.objects.all(),
chains=(
('site', 'site'),
),
label='Rack',
required=False,
widget=APISelect(
api_url='/api/dcim/racks/?site_id={{site}}',
attrs={'filter-for': 'device', 'nullable': 'true'}
)
)
device = ChainedModelChoiceField(
queryset=Device.objects.all(),
chains=(
('site', 'site'),
('rack', 'rack'),
),
label='Device',
required=False,
widget=APISelect(
api_url='/api/dcim/devices/?site_id={{site}}&rack_id={{rack}}',
display_field='display_name',
attrs={'filter-for': 'port'}
)
)
livesearch = forms.CharField(
required=False,
label='Device',
widget=Livesearch(
query_key='q',
query_url='dcim-api:device-list',
field_to_update='device'
)
)
port = ChainedModelChoiceField(
queryset=ConsolePort.objects.all(),
chains=(
('device', 'device'),
),
label='Port',
widget=APISelect(
api_url='/api/dcim/console-ports/?device_id={{device}}',
disabled_indicator='cable'
)
)
connection_status = forms.BooleanField(
required=False,
initial=CONNECTION_STATUS_CONNECTED,
label='Status',
widget=forms.Select(
choices=CONNECTION_STATUS_CHOICES
)
)
class Meta:
fields = ['site', 'rack', 'device', 'livesearch', 'port', 'connection_status']
labels = {
'connection_status': 'Status',
}
class ConsoleServerPortBulkRenameForm(BulkRenameForm): class ConsoleServerPortBulkRenameForm(BulkRenameForm):
pk = forms.ModelMultipleChoiceField(queryset=ConsoleServerPort.objects.all(), widget=forms.MultipleHiddenInput) pk = forms.ModelMultipleChoiceField(queryset=ConsoleServerPort.objects.all(), widget=forms.MultipleHiddenInput)
@ -1540,157 +1320,6 @@ class PowerPortCreateForm(ComponentForm):
tags = TagField(required=False) tags = TagField(required=False)
class PowerConnectionCSVForm(forms.ModelForm):
pdu = FlexibleModelChoiceField(
queryset=Device.objects.all(),
to_field_name='name',
help_text='PDU name or ID',
error_messages={
'invalid_choice': 'PDU not found.',
}
)
connected_endpoint = forms.CharField(
help_text='Power outlet name'
)
device = FlexibleModelChoiceField(
queryset=Device.objects.all(),
to_field_name='name',
help_text='Device name or ID',
error_messages={
'invalid_choice': 'Device not found',
}
)
power_port = forms.CharField(
help_text='Power port name'
)
connection_status = CSVChoiceField(
choices=CONNECTION_STATUS_CHOICES,
help_text='Connection status'
)
class Meta:
model = PowerPort
fields = ['pdu', 'connected_endpoint', 'device', 'power_port', 'connection_status']
def clean_power_port(self):
power_port_name = self.cleaned_data.get('power_port')
if not self.cleaned_data.get('device') or not power_port_name:
return None
try:
# Retrieve power port by name
powerport = PowerPort.objects.get(
device=self.cleaned_data['device'], name=power_port_name
)
# Check if the power port is already connected
if powerport.connected_endpoint is not None:
raise forms.ValidationError("{} {} is already connected".format(
self.cleaned_data['device'], power_port_name
))
except PowerPort.DoesNotExist:
raise forms.ValidationError("Invalid power port ({} {})".format(
self.cleaned_data['device'], power_port_name
))
self.instance = powerport
return powerport
def clean_connected_endpoint(self):
poweroutlet_name = self.cleaned_data.get('connected_endpoint')
if not self.cleaned_data.get('pdu') or not poweroutlet_name:
return None
try:
# Retrieve power outlet by name
poweroutlet = PowerOutlet.objects.get(
device=self.cleaned_data['pdu'], name=poweroutlet_name
)
# Check if the power outlet is already connected
if PowerPort.objects.filter(connected_endpoint=poweroutlet).count():
raise forms.ValidationError("{} {} is already connected".format(
self.cleaned_data['pdu'], poweroutlet_name
))
except PowerOutlet.DoesNotExist:
raise forms.ValidationError("Invalid power outlet ({} {})".format(
self.cleaned_data['pdu'], poweroutlet_name
))
return poweroutlet
class PowerPortConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm):
site = forms.ModelChoiceField(
queryset=Site.objects.all(),
required=False,
widget=forms.Select(
attrs={'filter-for': 'rack'}
)
)
rack = ChainedModelChoiceField(
queryset=Rack.objects.all(),
chains=(
('site', 'site'),
),
label='Rack',
required=False,
widget=APISelect(
api_url='/api/dcim/racks/?site_id={{site}}',
attrs={'filter-for': 'pdu', 'nullable': 'true'}
)
)
pdu = ChainedModelChoiceField(
queryset=Device.objects.all(),
chains=(
('site', 'site'),
('rack', 'rack'),
),
label='PDU',
required=False,
widget=APISelect(
api_url='/api/dcim/devices/?site_id={{site}}&rack_id={{rack}}',
display_field='display_name',
attrs={'filter-for': 'connected_endpoint'}
)
)
livesearch = forms.CharField(
required=False,
label='PDU',
widget=Livesearch(
query_key='q',
query_url='dcim-api:device-list',
field_to_update='pdu'
)
)
connected_endpoint = ChainedModelChoiceField(
queryset=PowerOutlet.objects.all(),
chains=(
('device', 'pdu'),
),
label='Outlet',
widget=APISelect(
api_url='/api/dcim/power-outlets/?device_id={{pdu}}',
disabled_indicator='cable'
)
)
class Meta:
model = PowerPort
fields = ['site', 'rack', 'pdu', 'livesearch', 'connected_endpoint', 'connection_status']
labels = {
'connected_endpoint': 'Outlet',
'connection_status': 'Status',
}
def __init__(self, *args, **kwargs):
super(PowerPortConnectionForm, self).__init__(*args, **kwargs)
if not self.instance.pk:
raise RuntimeError("PowerPortConnectionForm must be initialized with an existing PowerPort instance.")
# #
# Power outlets # Power outlets
# #
@ -1711,76 +1340,6 @@ class PowerOutletCreateForm(ComponentForm):
tags = TagField(required=False) tags = TagField(required=False)
class PowerOutletConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.Form):
site = forms.ModelChoiceField(
queryset=Site.objects.all(),
required=False,
widget=forms.Select(
attrs={'filter-for': 'rack'}
)
)
rack = ChainedModelChoiceField(
queryset=Rack.objects.all(),
chains=(
('site', 'site'),
),
label='Rack',
required=False,
widget=APISelect(
api_url='/api/dcim/racks/?site_id={{site}}',
attrs={'filter-for': 'device', 'nullable': 'true'}
)
)
device = ChainedModelChoiceField(
queryset=Device.objects.all(),
chains=(
('site', 'site'),
('rack', 'rack'),
),
label='Device',
required=False,
widget=APISelect(
api_url='/api/dcim/devices/?site_id={{site}}&rack_id={{rack}}',
display_field='display_name',
attrs={'filter-for': 'port'}
)
)
livesearch = forms.CharField(
required=False,
label='Device',
widget=Livesearch(
query_key='q',
query_url='dcim-api:device-list',
field_to_update='device'
)
)
port = ChainedModelChoiceField(
queryset=PowerPort.objects.all(),
chains=(
('device', 'device'),
),
label='Port',
widget=APISelect(
api_url='/api/dcim/power-ports/?device_id={{device}}',
disabled_indicator='cable'
)
)
connection_status = forms.BooleanField(
required=False,
initial=CONNECTION_STATUS_CONNECTED,
label='Status',
widget=forms.Select(
choices=CONNECTION_STATUS_CHOICES
)
)
class Meta:
fields = ['site', 'rack', 'device', 'livesearch', 'port', 'connection_status']
labels = {
'connection_status': 'Status',
}
class PowerOutletBulkRenameForm(BulkRenameForm): class PowerOutletBulkRenameForm(BulkRenameForm):
pk = forms.ModelMultipleChoiceField(queryset=PowerOutlet.objects.all(), widget=forms.MultipleHiddenInput) pk = forms.ModelMultipleChoiceField(queryset=PowerOutlet.objects.all(), widget=forms.MultipleHiddenInput)
@ -2199,6 +1758,108 @@ class CableForm(BootstrapMixin, forms.ModelForm):
fields = ('type', 'status', 'label', 'color', 'length', 'length_unit') fields = ('type', 'status', 'label', 'color', 'length', 'length_unit')
class CableCSVForm(forms.ModelForm):
# Termination A
side_a_device = FlexibleModelChoiceField(
queryset=Device.objects.all(),
to_field_name='name',
help_text='Console server name or ID',
error_messages={
'invalid_choice': 'Side A device not found',
}
)
side_a_type = forms.ModelChoiceField(
queryset=ContentType.objects.all(),
limit_choices_to={'model__in': CABLE_TERMINATION_TYPES},
to_field_name='model'
)
side_a_name = forms.CharField()
# Termination B
side_b_device = FlexibleModelChoiceField(
queryset=Device.objects.all(),
to_field_name='name',
help_text='Console server name or ID',
error_messages={
'invalid_choice': 'Side B device not found',
}
)
side_b_type = forms.ModelChoiceField(
queryset=ContentType.objects.all(),
limit_choices_to={'model__in': CABLE_TERMINATION_TYPES},
to_field_name='model'
)
side_b_name = forms.CharField()
# Cable attributes
status = CSVChoiceField(
choices=CONNECTION_STATUS_CHOICES,
required=False,
help_text='Connection status'
)
class Meta:
model = Cable
fields = [
'side_a_device', 'side_a_type', 'side_a_name', 'side_b_device', 'side_b_type', 'side_b_name', 'status',
'label',
]
# TODO: Merge the clean() methods for either end
def clean_side_a_name(self):
device = self.cleaned_data.get('side_a_device')
content_type = self.cleaned_data.get('side_a_type')
name = self.cleaned_data.get('side_a_name')
if not device or not content_type or not name:
return None
model = content_type.model_class()
try:
termination_object = model.objects.get(
device=device,
name=name
)
if termination_object.cable is not None:
raise forms.ValidationError(
"Side A: {} {} is already connected".format(device, termination_object)
)
except ObjectDoesNotExist:
raise forms.ValidationError(
"A side termination not found: {} {}".format(device, name)
)
self.instance.termination_a = termination_object
return termination_object
def clean_side_b_name(self):
device = self.cleaned_data.get('side_b_device')
content_type = self.cleaned_data.get('side_b_type')
name = self.cleaned_data.get('side_b_name')
if not device or not content_type or not name:
return None
model = content_type.model_class()
try:
termination_object = model.objects.get(
device=device,
name=name
)
if termination_object.cable is not None:
raise forms.ValidationError(
"Side B: {} {} is already connected".format(device, termination_object)
)
except ObjectDoesNotExist:
raise forms.ValidationError(
"B side termination not found: {} {}".format(device, name)
)
self.instance.termination_b = termination_object
return termination_object
class CableFilterForm(BootstrapMixin, forms.Form): class CableFilterForm(BootstrapMixin, forms.Form):
model = Cable model = Cable
q = forms.CharField(required=False, label='Search') q = forms.CharField(required=False, label='Search')

View File

@ -251,17 +251,15 @@ urlpatterns = [
# Cables # Cables
url(r'^cables/$', views.CableListView.as_view(), name='cable_list'), url(r'^cables/$', views.CableListView.as_view(), name='cable_list'),
url(r'^cables/import/$', views.CableBulkImportView.as_view(), name='cable_import'),
url(r'^cables/(?P<pk>\d+)/$', views.CableView.as_view(), name='cable'), url(r'^cables/(?P<pk>\d+)/$', views.CableView.as_view(), name='cable'),
url(r'^cables/(?P<pk>\d+)/edit/$', views.CableEditView.as_view(), name='cable_edit'), url(r'^cables/(?P<pk>\d+)/edit/$', views.CableEditView.as_view(), name='cable_edit'),
url(r'^cables/(?P<pk>\d+)/delete/$', views.CableDeleteView.as_view(), name='cable_delete'), url(r'^cables/(?P<pk>\d+)/delete/$', views.CableDeleteView.as_view(), name='cable_delete'),
# Console/power/interface connections # Console/power/interface connections (read-only)
url(r'^console-connections/$', views.ConsoleConnectionsListView.as_view(), name='console_connections_list'), url(r'^console-connections/$', views.ConsoleConnectionsListView.as_view(), name='console_connections_list'),
# url(r'^console-connections/import/$', views.ConsoleConnectionsBulkImportView.as_view(), name='console_connections_import'),
url(r'^power-connections/$', views.PowerConnectionsListView.as_view(), name='power_connections_list'), url(r'^power-connections/$', views.PowerConnectionsListView.as_view(), name='power_connections_list'),
# url(r'^power-connections/import/$', views.PowerConnectionsBulkImportView.as_view(), name='power_connections_import'),
url(r'^interface-connections/$', views.InterfaceConnectionsListView.as_view(), name='interface_connections_list'), url(r'^interface-connections/$', views.InterfaceConnectionsListView.as_view(), name='interface_connections_list'),
# url(r'^interface-connections/import/$', views.InterfaceConnectionsBulkImportView.as_view(), name='interface_connections_import'),
# Virtual chassis # Virtual chassis
url(r'^virtual-chassis/$', views.VirtualChassisListView.as_view(), name='virtualchassis_list'), url(r'^virtual-chassis/$', views.VirtualChassisListView.as_view(), name='virtualchassis_list'),

View File

@ -1121,13 +1121,6 @@ class ConsolePortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
table = tables.ConsolePortTable table = tables.ConsolePortTable
class ConsoleConnectionsBulkImportView(PermissionRequiredMixin, BulkImportView):
permission_required = 'dcim.change_consoleport'
model_form = forms.ConsoleConnectionCSVForm
table = tables.ConsoleConnectionTable
default_return_url = 'dcim:console_connections_list'
# #
# Console server ports # Console server ports
# #
@ -1212,13 +1205,6 @@ class PowerPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
table = tables.PowerPortTable table = tables.PowerPortTable
class PowerConnectionsBulkImportView(PermissionRequiredMixin, BulkImportView):
permission_required = 'dcim.change_powerport'
model_form = forms.PowerConnectionCSVForm
table = tables.PowerConnectionTable
default_return_url = 'dcim:power_connections_list'
# #
# Power outlets # Power outlets
# #
@ -1645,6 +1631,21 @@ class CableView(View):
}) })
class CableTraceView(View):
"""
Trace a cable path beginning from the given termination.
"""
def get(self, request, model, pk):
obj = get_object_or_404(model, pk=pk)
return render(request, 'dcim/cable_trace.html', {
'obj': obj,
'trace': obj.trace(),
})
class CableCreateView(PermissionRequiredMixin, ObjectEditView): class CableCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.add_cable' permission_required = 'dcim.add_cable'
model = Cable model = Cable
@ -1674,19 +1675,11 @@ class CableDeleteView(PermissionRequiredMixin, ObjectDeleteView):
default_return_url = 'dcim:cable_list' default_return_url = 'dcim:cable_list'
class CableTraceView(View): class CableBulkImportView(PermissionRequiredMixin, BulkImportView):
""" permission_required = 'dcim.add_cable'
Trace a cable path beginning from the given termination. model_form = forms.CableCSVForm
""" table = tables.CableTable
default_return_url = 'dcim:cable_list'
def get(self, request, model, pk):
obj = get_object_or_404(model, pk=pk)
return render(request, 'dcim/cable_trace.html', {
'obj': obj,
'trace': obj.trace(),
})
# #

View File

@ -180,6 +180,11 @@
<li class="divider"></li> <li class="divider"></li>
<li class="dropdown-header">Connections</li> <li class="dropdown-header">Connections</li>
<li> <li>
{% if perms.dcim.add_cable %}
<div class="buttons pull-right">
<a href="{% url 'dcim:cable_import' %}" class="btn btn-xs btn-info" title="Import"><i class="fa fa-download"></i></a>
</div>
{% endif %}
<a href="{% url 'dcim:cable_list' %}">Cables</a> <a href="{% url 'dcim:cable_list' %}">Cables</a>
</li> </li>
<li> <li>