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 = []
|
||||
|
||||
|
||||
class DeviceFromCSVForm(forms.ModelForm):
|
||||
class BaseDeviceFromCSVForm(forms.ModelForm):
|
||||
device_role = forms.ModelChoiceField(queryset=DeviceRole.objects.all(), to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Invalid device role.'})
|
||||
manufacturer = forms.ModelChoiceField(queryset=Manufacturer.objects.all(), to_field_name='name',
|
||||
@ -434,23 +434,15 @@ class DeviceFromCSVForm(forms.ModelForm):
|
||||
model_name = forms.CharField()
|
||||
platform = forms.ModelChoiceField(queryset=Platform.objects.all(), required=False, to_field_name='name',
|
||||
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:
|
||||
fields = []
|
||||
model = Device
|
||||
fields = ['name', 'device_role', 'manufacturer', 'model_name', 'platform', 'serial', 'site', 'rack_name',
|
||||
'position', 'face']
|
||||
|
||||
def clean(self):
|
||||
|
||||
manufacturer = self.cleaned_data.get('manufacturer')
|
||||
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
|
||||
if manufacturer and model_name:
|
||||
@ -459,6 +451,25 @@ class DeviceFromCSVForm(forms.ModelForm):
|
||||
except DeviceType.DoesNotExist:
|
||||
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
|
||||
if site and rack_name:
|
||||
try:
|
||||
@ -468,21 +479,54 @@ class DeviceFromCSVForm(forms.ModelForm):
|
||||
|
||||
def clean_face(self):
|
||||
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:
|
||||
return {
|
||||
'front': 0,
|
||||
'rear': 1,
|
||||
}[face.lower()]
|
||||
except KeyError:
|
||||
raise forms.ValidationError('Invalid rack face ({}); must be "front" or "rear".'.format(face))
|
||||
return face
|
||||
device_bay = DeviceBay.objects.get(device=parent, name=device_bay_name)
|
||||
if device_bay.installed_device:
|
||||
self.add_error('device_bay_name',
|
||||
"Device bay ({} {}) is already occupied".format(parent, device_bay_name))
|
||||
else:
|
||||
self.instance.parent_bay = device_bay
|
||||
except DeviceBay.DoesNotExist:
|
||||
self.add_error('device_bay_name', "Parent device/bay ({} {}) not found".format(parent, device_bay_name))
|
||||
|
||||
|
||||
class DeviceImportForm(BulkImportForm, BootstrapMixin):
|
||||
csv = CSVDataField(csv_form=DeviceFromCSVForm)
|
||||
|
||||
|
||||
class ChildDeviceImportForm(BulkImportForm, BootstrapMixin):
|
||||
csv = CSVDataField(csv_form=ChildDeviceFromCSVForm)
|
||||
|
||||
|
||||
class DeviceBulkEditForm(forms.Form, BootstrapMixin):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=Device.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
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/add/$', views.DeviceEditView.as_view(), name='device_add'),
|
||||
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/delete/$', views.DeviceBulkDeleteView.as_view(), name='device_bulk_delete'),
|
||||
url(r'^devices/(?P<pk>\d+)/$', views.device, name='device'),
|
||||
|
@ -609,6 +609,23 @@ class DeviceBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
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):
|
||||
permission_required = 'dcim.change_device'
|
||||
cls = Device
|
||||
|
@ -5,7 +5,7 @@
|
||||
{% block title %}Device Import{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Device Import</h1>
|
||||
{% include 'dcim/inc/_device_import_header.html' %}
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<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