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