mirror of
https://github.com/netbox-community/netbox.git
synced 2024-05-10 07:54:54 +00:00
Fixes #227: Introduces support for bulk import of child devices
This commit is contained in:
@ -426,7 +426,7 @@ class DeviceForm(forms.ModelForm, BootstrapMixin):
|
|||||||
self.fields['device_type'].choices = []
|
self.fields['device_type'].choices = []
|
||||||
|
|
||||||
|
|
||||||
class DeviceFromCSVForm(forms.ModelForm):
|
class BaseDeviceFromCSVForm(forms.ModelForm):
|
||||||
device_role = forms.ModelChoiceField(queryset=DeviceRole.objects.all(), to_field_name='name',
|
device_role = forms.ModelChoiceField(queryset=DeviceRole.objects.all(), to_field_name='name',
|
||||||
error_messages={'invalid_choice': 'Invalid device role.'})
|
error_messages={'invalid_choice': 'Invalid device role.'})
|
||||||
manufacturer = forms.ModelChoiceField(queryset=Manufacturer.objects.all(), to_field_name='name',
|
manufacturer = forms.ModelChoiceField(queryset=Manufacturer.objects.all(), to_field_name='name',
|
||||||
@ -434,23 +434,15 @@ class DeviceFromCSVForm(forms.ModelForm):
|
|||||||
model_name = forms.CharField()
|
model_name = forms.CharField()
|
||||||
platform = forms.ModelChoiceField(queryset=Platform.objects.all(), required=False, to_field_name='name',
|
platform = forms.ModelChoiceField(queryset=Platform.objects.all(), required=False, to_field_name='name',
|
||||||
error_messages={'invalid_choice': 'Invalid platform.'})
|
error_messages={'invalid_choice': 'Invalid platform.'})
|
||||||
site = forms.ModelChoiceField(queryset=Site.objects.all(), to_field_name='name', error_messages={
|
|
||||||
'invalid_choice': 'Invalid site name.',
|
|
||||||
})
|
|
||||||
rack_name = forms.CharField()
|
|
||||||
face = forms.CharField(required=False)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
fields = []
|
||||||
model = Device
|
model = Device
|
||||||
fields = ['name', 'device_role', 'manufacturer', 'model_name', 'platform', 'serial', 'site', 'rack_name',
|
|
||||||
'position', 'face']
|
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
|
|
||||||
manufacturer = self.cleaned_data.get('manufacturer')
|
manufacturer = self.cleaned_data.get('manufacturer')
|
||||||
model_name = self.cleaned_data.get('model_name')
|
model_name = self.cleaned_data.get('model_name')
|
||||||
site = self.cleaned_data.get('site')
|
|
||||||
rack_name = self.cleaned_data.get('rack_name')
|
|
||||||
|
|
||||||
# Validate device type
|
# Validate device type
|
||||||
if manufacturer and model_name:
|
if manufacturer and model_name:
|
||||||
@ -459,6 +451,25 @@ class DeviceFromCSVForm(forms.ModelForm):
|
|||||||
except DeviceType.DoesNotExist:
|
except DeviceType.DoesNotExist:
|
||||||
self.add_error('model_name', "Invalid device type ({} {})".format(manufacturer, model_name))
|
self.add_error('model_name', "Invalid device type ({} {})".format(manufacturer, model_name))
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceFromCSVForm(BaseDeviceFromCSVForm):
|
||||||
|
site = forms.ModelChoiceField(queryset=Site.objects.all(), to_field_name='name', error_messages={
|
||||||
|
'invalid_choice': 'Invalid site name.',
|
||||||
|
})
|
||||||
|
rack_name = forms.CharField()
|
||||||
|
face = forms.CharField(required=False)
|
||||||
|
|
||||||
|
class Meta(BaseDeviceFromCSVForm.Meta):
|
||||||
|
fields = ['name', 'device_role', 'manufacturer', 'model_name', 'platform', 'serial', 'site', 'rack_name',
|
||||||
|
'position', 'face']
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
|
||||||
|
super(DeviceFromCSVForm, self).clean()
|
||||||
|
|
||||||
|
site = self.cleaned_data.get('site')
|
||||||
|
rack_name = self.cleaned_data.get('rack_name')
|
||||||
|
|
||||||
# Validate rack
|
# Validate rack
|
||||||
if site and rack_name:
|
if site and rack_name:
|
||||||
try:
|
try:
|
||||||
@ -468,21 +479,54 @@ class DeviceFromCSVForm(forms.ModelForm):
|
|||||||
|
|
||||||
def clean_face(self):
|
def clean_face(self):
|
||||||
face = self.cleaned_data['face']
|
face = self.cleaned_data['face']
|
||||||
if face:
|
if not face:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return {
|
||||||
|
'front': 0,
|
||||||
|
'rear': 1,
|
||||||
|
}[face.lower()]
|
||||||
|
except KeyError:
|
||||||
|
raise forms.ValidationError('Invalid rack face ({}); must be "front" or "rear".'.format(face))
|
||||||
|
|
||||||
|
|
||||||
|
class ChildDeviceFromCSVForm(BaseDeviceFromCSVForm):
|
||||||
|
parent = FlexibleModelChoiceField(queryset=Device.objects.all(), to_field_name='name', required=False,
|
||||||
|
error_messages={'invalid_choice': 'Parent device not found.'})
|
||||||
|
device_bay_name = forms.CharField(required=False)
|
||||||
|
|
||||||
|
class Meta(BaseDeviceFromCSVForm.Meta):
|
||||||
|
fields = ['name', 'device_role', 'manufacturer', 'model_name', 'platform', 'serial', 'parent',
|
||||||
|
'device_bay_name']
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
|
||||||
|
super(ChildDeviceFromCSVForm, self).clean()
|
||||||
|
|
||||||
|
parent = self.cleaned_data.get('parent')
|
||||||
|
device_bay_name = self.cleaned_data.get('device_bay_name')
|
||||||
|
|
||||||
|
# Validate device bay
|
||||||
|
if parent and device_bay_name:
|
||||||
try:
|
try:
|
||||||
return {
|
device_bay = DeviceBay.objects.get(device=parent, name=device_bay_name)
|
||||||
'front': 0,
|
if device_bay.installed_device:
|
||||||
'rear': 1,
|
self.add_error('device_bay_name',
|
||||||
}[face.lower()]
|
"Device bay ({} {}) is already occupied".format(parent, device_bay_name))
|
||||||
except KeyError:
|
else:
|
||||||
raise forms.ValidationError('Invalid rack face ({}); must be "front" or "rear".'.format(face))
|
self.instance.parent_bay = device_bay
|
||||||
return face
|
except DeviceBay.DoesNotExist:
|
||||||
|
self.add_error('device_bay_name', "Parent device/bay ({} {}) not found".format(parent, device_bay_name))
|
||||||
|
|
||||||
|
|
||||||
class DeviceImportForm(BulkImportForm, BootstrapMixin):
|
class DeviceImportForm(BulkImportForm, BootstrapMixin):
|
||||||
csv = CSVDataField(csv_form=DeviceFromCSVForm)
|
csv = CSVDataField(csv_form=DeviceFromCSVForm)
|
||||||
|
|
||||||
|
|
||||||
|
class ChildDeviceImportForm(BulkImportForm, BootstrapMixin):
|
||||||
|
csv = CSVDataField(csv_form=ChildDeviceFromCSVForm)
|
||||||
|
|
||||||
|
|
||||||
class DeviceBulkEditForm(forms.Form, BootstrapMixin):
|
class DeviceBulkEditForm(forms.Form, BootstrapMixin):
|
||||||
pk = forms.ModelMultipleChoiceField(queryset=Device.objects.all(), widget=forms.MultipleHiddenInput)
|
pk = forms.ModelMultipleChoiceField(queryset=Device.objects.all(), widget=forms.MultipleHiddenInput)
|
||||||
device_type = forms.ModelChoiceField(queryset=DeviceType.objects.all(), required=False, label='Type')
|
device_type = forms.ModelChoiceField(queryset=DeviceType.objects.all(), required=False, label='Type')
|
||||||
|
@ -92,6 +92,7 @@ urlpatterns = [
|
|||||||
url(r'^devices/$', views.DeviceListView.as_view(), name='device_list'),
|
url(r'^devices/$', views.DeviceListView.as_view(), name='device_list'),
|
||||||
url(r'^devices/add/$', views.DeviceEditView.as_view(), name='device_add'),
|
url(r'^devices/add/$', views.DeviceEditView.as_view(), name='device_add'),
|
||||||
url(r'^devices/import/$', views.DeviceBulkImportView.as_view(), name='device_import'),
|
url(r'^devices/import/$', views.DeviceBulkImportView.as_view(), name='device_import'),
|
||||||
|
url(r'^devices/import/child-devices/$', views.ChildDeviceBulkImportView.as_view(), name='device_import_child'),
|
||||||
url(r'^devices/edit/$', views.DeviceBulkEditView.as_view(), name='device_bulk_edit'),
|
url(r'^devices/edit/$', views.DeviceBulkEditView.as_view(), name='device_bulk_edit'),
|
||||||
url(r'^devices/delete/$', views.DeviceBulkDeleteView.as_view(), name='device_bulk_delete'),
|
url(r'^devices/delete/$', views.DeviceBulkDeleteView.as_view(), name='device_bulk_delete'),
|
||||||
url(r'^devices/(?P<pk>\d+)/$', views.device, name='device'),
|
url(r'^devices/(?P<pk>\d+)/$', views.device, name='device'),
|
||||||
|
@ -609,6 +609,23 @@ class DeviceBulkImportView(PermissionRequiredMixin, BulkImportView):
|
|||||||
obj_list_url = 'dcim:device_list'
|
obj_list_url = 'dcim:device_list'
|
||||||
|
|
||||||
|
|
||||||
|
class ChildDeviceBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||||
|
permission_required = 'dcim.add_device'
|
||||||
|
form = forms.ChildDeviceImportForm
|
||||||
|
table = tables.DeviceImportTable
|
||||||
|
template_name = 'dcim/device_import_child.html'
|
||||||
|
obj_list_url = 'dcim:device_list'
|
||||||
|
|
||||||
|
def save_obj(self, obj):
|
||||||
|
# Inherent rack from parent device
|
||||||
|
obj.rack = obj.parent_bay.device.rack
|
||||||
|
obj.save()
|
||||||
|
# Save the reverse relation
|
||||||
|
device_bay = obj.parent_bay
|
||||||
|
device_bay.installed_device = obj
|
||||||
|
device_bay.save()
|
||||||
|
|
||||||
|
|
||||||
class DeviceBulkEditView(PermissionRequiredMixin, BulkEditView):
|
class DeviceBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||||
permission_required = 'dcim.change_device'
|
permission_required = 'dcim.change_device'
|
||||||
cls = Device
|
cls = Device
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
{% block title %}Device Import{% endblock %}
|
{% block title %}Device Import{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1>Device Import</h1>
|
{% include 'dcim/inc/_device_import_header.html' %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<form action="." method="post" class="form">
|
<form action="." method="post" class="form">
|
||||||
|
75
netbox/templates/dcim/device_import_child.html
Normal file
75
netbox/templates/dcim/device_import_child.html
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
{% extends '_base.html' %}
|
||||||
|
{% load render_table from django_tables2 %}
|
||||||
|
{% load form_helpers %}
|
||||||
|
|
||||||
|
{% block title %}Device Import{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% include 'dcim/inc/_device_import_header.html' with active_tab='child_import' %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<form action="." method="post" class="form">
|
||||||
|
{% csrf_token %}
|
||||||
|
{% render_form form %}
|
||||||
|
<div class="form-group">
|
||||||
|
<button type="submit" class="btn btn-primary">Submit</button>
|
||||||
|
<a href="{% url obj_list_url %}" class="btn btn-default">Cancel</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<h4>CSV Format</h4>
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Field</th>
|
||||||
|
<th>Description</th>
|
||||||
|
<th>Example</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>Name</td>
|
||||||
|
<td>Device name (optional)</td>
|
||||||
|
<td>Blade12</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Device role</td>
|
||||||
|
<td>Functional role of device</td>
|
||||||
|
<td>Blade Server</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Device manufacturer</td>
|
||||||
|
<td>Hardware manufacturer</td>
|
||||||
|
<td>Dell</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Device model</td>
|
||||||
|
<td>Hardware model</td>
|
||||||
|
<td>BS2000T</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Platform</td>
|
||||||
|
<td>Software running on device (optional)</td>
|
||||||
|
<td>Linux</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Serial</td>
|
||||||
|
<td>Serial number (optional)</td>
|
||||||
|
<td>CAB00577291</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Parent device</td>
|
||||||
|
<td>Parent device</td>
|
||||||
|
<td>Server101</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Device bay</td>
|
||||||
|
<td>Device bay name</td>
|
||||||
|
<td>Slot 4</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<h4>Example</h4>
|
||||||
|
<pre>Blade12,Blade Server,Dell,BS2000T,Linux,CAB00577291,Server101,Slot4</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
5
netbox/templates/dcim/inc/_device_import_header.html
Normal file
5
netbox/templates/dcim/inc/_device_import_header.html
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<h1>Device Import</h1>
|
||||||
|
<ul class="nav nav-tabs" style="margin-bottom: 20px">
|
||||||
|
<li role="presentation"{% if not active_tab %} class="active"{% endif %}><a href="{% url 'dcim:device_import' %}">Racked Devices</a></li>
|
||||||
|
<li role="presentation"{% if active_tab == 'child_import' %} class="active"{% endif %}><a href="{% url 'dcim:device_import_child' %}">Child Devices</a></li>
|
||||||
|
</ul>
|
Reference in New Issue
Block a user