diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index e3db78ec4..4954099dd 100644 --- a/netbox/dcim/forms.py +++ b/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') diff --git a/netbox/dcim/urls.py b/netbox/dcim/urls.py index 1c76e63d2..52bfcdfdb 100644 --- a/netbox/dcim/urls.py +++ b/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\d+)/$', views.device, name='device'), diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index c97e2a3e1..2fdbbd0a7 100644 --- a/netbox/dcim/views.py +++ b/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 diff --git a/netbox/templates/dcim/device_import.html b/netbox/templates/dcim/device_import.html index bcc2d0710..f0d1cca6d 100644 --- a/netbox/templates/dcim/device_import.html +++ b/netbox/templates/dcim/device_import.html @@ -5,7 +5,7 @@ {% block title %}Device Import{% endblock %} {% block content %} -

Device Import

+{% include 'dcim/inc/_device_import_header.html' %}
diff --git a/netbox/templates/dcim/device_import_child.html b/netbox/templates/dcim/device_import_child.html new file mode 100644 index 000000000..5b9a14541 --- /dev/null +++ b/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' %} +
+
+ + {% csrf_token %} + {% render_form form %} +
+ + Cancel +
+ +

CSV Format

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescriptionExample
NameDevice name (optional)Blade12
Device roleFunctional role of deviceBlade Server
Device manufacturerHardware manufacturerDell
Device modelHardware modelBS2000T
PlatformSoftware running on device (optional)Linux
SerialSerial number (optional)CAB00577291
Parent deviceParent deviceServer101
Device bayDevice bay nameSlot 4
+

Example

+
Blade12,Blade Server,Dell,BS2000T,Linux,CAB00577291,Server101,Slot4
+
+
+{% endblock %} diff --git a/netbox/templates/dcim/inc/_device_import_header.html b/netbox/templates/dcim/inc/_device_import_header.html new file mode 100644 index 000000000..57dd1b46e --- /dev/null +++ b/netbox/templates/dcim/inc/_device_import_header.html @@ -0,0 +1,5 @@ +

Device Import

+