Browse Source

Fixes #227: Introduces support for bulk import of child devices

Jeremy Stretch 8 years ago
parent
commit
dd62caf2f0

+ 62 - 18
netbox/dcim/forms.py

@@ -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')

+ 1 - 0
netbox/dcim/urls.py

@@ -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'),

+ 17 - 0
netbox/dcim/views.py

@@ -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

+ 1 - 1
netbox/templates/dcim/device_import.html

@@ -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 - 0
netbox/templates/dcim/device_import_child.html

@@ -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 - 0
netbox/templates/dcim/inc/_device_import_header.html

@@ -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>