Skip to content

Commit

Permalink
Merge pull request #3621 from netbox-community/451-devicetype-import
Browse files Browse the repository at this point in the history
Enable YAML/JSON-based DeviceType import
  • Loading branch information
jeremystretch authored Oct 17, 2019
2 parents bfce177 + 2ffbced commit c689373
Show file tree
Hide file tree
Showing 12 changed files with 908 additions and 85 deletions.
421 changes: 400 additions & 21 deletions netbox/dcim/constants.py

Large diffs are not rendered by default.

157 changes: 139 additions & 18 deletions netbox/dcim/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
APISelect, APISelectMultiple, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm,
BulkEditNullBooleanSelect, ChainedFieldsMixin, ChainedModelChoiceField, ColorSelect, CommentField, ComponentForm,
ConfirmationForm, CSVChoiceField, ExpandableNameField, FilterChoiceField, FlexibleModelChoiceField, JSONField,
SelectWithPK, SmallTextarea, SlugField, StaticSelect2, StaticSelect2Multiple, BOOLEAN_WITH_BLANK_CHOICES
SelectWithPK, SmallTextarea, SlugField, StaticSelect2, StaticSelect2Multiple, BOOLEAN_WITH_BLANK_CHOICES,
)
from virtualization.models import Cluster, ClusterGroup
from .constants import *
Expand Down Expand Up @@ -828,29 +828,17 @@ class Meta:
}


class DeviceTypeCSVForm(forms.ModelForm):
class DeviceTypeImportForm(BootstrapMixin, forms.ModelForm):
manufacturer = forms.ModelChoiceField(
queryset=Manufacturer.objects.all(),
required=True,
to_field_name='name',
help_text='Manufacturer name',
error_messages={
'invalid_choice': 'Manufacturer not found.',
}
)
subdevice_role = CSVChoiceField(
choices=SUBDEVICE_ROLE_CHOICES,
required=False,
help_text='Parent/child status'
to_field_name='name'
)

class Meta:
model = DeviceType
fields = DeviceType.csv_headers
help_texts = {
'model': 'Model name',
'slug': 'URL-friendly slug',
}
fields = [
'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role',
]


class DeviceTypeBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
Expand Down Expand Up @@ -1232,6 +1220,139 @@ class DeviceBayTemplateCreateForm(ComponentForm):
)


#
# Component template import forms
#

class ComponentTemplateImportForm(BootstrapMixin, forms.ModelForm):

def __init__(self, device_type, data=None, *args, **kwargs):

# Must pass the parent DeviceType on form initialization
data.update({
'device_type': device_type.pk,
})

super().__init__(data, *args, **kwargs)

def clean_device_type(self):

data = self.cleaned_data['device_type']

# Limit fields referencing other components to the parent DeviceType
for field_name, field in self.fields.items():
if isinstance(field, forms.ModelChoiceField) and field_name != 'device_type':
field.queryset = field.queryset.filter(device_type=data)

return data


class ConsolePortTemplateImportForm(ComponentTemplateImportForm):

class Meta:
model = ConsolePortTemplate
fields = [
'device_type', 'name',
]


class ConsoleServerPortTemplateImportForm(ComponentTemplateImportForm):

class Meta:
model = ConsoleServerPortTemplate
fields = [
'device_type', 'name',
]


class PowerPortTemplateImportForm(ComponentTemplateImportForm):

class Meta:
model = PowerPortTemplate
fields = [
'device_type', 'name', 'maximum_draw', 'allocated_draw',
]


class PowerOutletTemplateImportForm(ComponentTemplateImportForm):
power_port = forms.ModelChoiceField(
queryset=PowerPortTemplate.objects.all(),
to_field_name='name',
required=False
)

class Meta:
model = PowerOutletTemplate
fields = [
'device_type', 'name', 'power_port', 'feed_leg',
]


class InterfaceTemplateImportForm(ComponentTemplateImportForm):
type = forms.ChoiceField(
choices=InterfaceTypes.TYPE_CHOICES
)

class Meta:
model = InterfaceTemplate
fields = [
'device_type', 'name', 'type', 'mgmt_only',
]

def clean_type(self):
# Convert slug value to field integer value
slug = self.cleaned_data['type']
return InterfaceTypes.slug_to_integer(slug)


class FrontPortTemplateImportForm(ComponentTemplateImportForm):
type = forms.ChoiceField(
choices=PortTypes.TYPE_CHOICES
)
rear_port = forms.ModelChoiceField(
queryset=RearPortTemplate.objects.all(),
to_field_name='name',
required=False
)

class Meta:
model = FrontPortTemplate
fields = [
'device_type', 'name', 'type', 'rear_port', 'rear_port_position',
]

def clean_type(self):
# Convert slug value to field integer value
slug = self.cleaned_data['type']
return PortTypes.slug_to_integer(slug)


class RearPortTemplateImportForm(ComponentTemplateImportForm):
type = forms.ChoiceField(
choices=PortTypes.TYPE_CHOICES
)

class Meta:
model = RearPortTemplate
fields = [
'device_type', 'name', 'type', 'positions',
]

def clean_type(self):
# Convert slug value to field integer value
slug = self.cleaned_data['type']
return PortTypes.slug_to_integer(slug)


class DeviceBayTemplateImportForm(ComponentTemplateImportForm):

class Meta:
model = DeviceBayTemplate
fields = [
'device_type', 'name',
]


#
# Device roles
#
Expand Down
133 changes: 130 additions & 3 deletions netbox/dcim/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@
from django.test import Client, TestCase
from django.urls import reverse

from dcim.constants import CABLE_TYPE_CAT6, IFACE_TYPE_1GE_FIXED
from dcim.constants import *
from dcim.models import (
Cable, Device, DeviceRole, DeviceType, Interface, InventoryItem, Manufacturer, Platform, Rack, RackGroup,
RackReservation, RackRole, Site, Region, VirtualChassis,
Cable, ConsolePortTemplate, ConsoleServerPortTemplate, Device, DeviceBayTemplate, DeviceRole, DeviceType,
FrontPortTemplate, Interface, InterfaceTemplate, InventoryItem, Manufacturer, Platform, PowerPortTemplate,
PowerOutletTemplate, Rack, RackGroup, RackReservation, RackRole, RearPortTemplate, Site, Region, VirtualChassis,
)
from utilities.testing import create_test_user

Expand Down Expand Up @@ -221,6 +222,132 @@ def test_devicetype(self):
response = self.client.get(devicetype.get_absolute_url())
self.assertEqual(response.status_code, 200)

def test_devicetype_import(self):

IMPORT_DATA = """
manufacturer: Generic
model: TEST-1000
slug: test-1000
u_height: 2
console-ports:
- name: Console Port 1
- name: Console Port 2
- name: Console Port 3
console-server-ports:
- name: Console Server Port 1
- name: Console Server Port 2
- name: Console Server Port 3
power-ports:
- name: Power Port 1
- name: Power Port 2
- name: Power Port 3
power-outlets:
- name: Power Outlet 1
power_port: Power Port 1
feed_leg: 1
- name: Power Outlet 2
power_port: Power Port 1
feed_leg: 1
- name: Power Outlet 3
power_port: Power Port 1
feed_leg: 1
interfaces:
- name: Interface 1
type: 1000base-t
mgmt_only: true
- name: Interface 2
type: 1000base-t
- name: Interface 3
type: 1000base-t
rear-ports:
- name: Rear Port 1
type: 8p8c
- name: Rear Port 2
type: 8p8c
- name: Rear Port 3
type: 8p8c
front-ports:
- name: Front Port 1
type: 8p8c
rear_port: Rear Port 1
- name: Front Port 2
type: 8p8c
rear_port: Rear Port 2
- name: Front Port 3
type: 8p8c
rear_port: Rear Port 3
device-bays:
- name: Device Bay 1
- name: Device Bay 2
- name: Device Bay 3
"""

# Create the manufacturer
Manufacturer(name='Generic', slug='generic').save()

# Authenticate as user with necessary permissions
user = create_test_user(username='testuser2', permissions=[
'dcim.view_devicetype',
'dcim.add_devicetype',
'dcim.add_consoleporttemplate',
'dcim.add_consoleserverporttemplate',
'dcim.add_powerporttemplate',
'dcim.add_poweroutlettemplate',
'dcim.add_interfacetemplate',
'dcim.add_frontporttemplate',
'dcim.add_rearporttemplate',
'dcim.add_devicebaytemplate',
])
self.client.force_login(user)

form_data = {
'data': IMPORT_DATA,
'format': 'yaml'
}
response = self.client.post(reverse('dcim:devicetype_import'), data=form_data, follow=True)
self.assertEqual(response.status_code, 200)

dt = DeviceType.objects.get(model='TEST-1000')

# Verify all of the components were created
self.assertEqual(dt.consoleport_templates.count(), 3)
cp1 = ConsolePortTemplate.objects.first()
self.assertEqual(cp1.name, 'Console Port 1')

self.assertEqual(dt.consoleserverport_templates.count(), 3)
csp1 = ConsoleServerPortTemplate.objects.first()
self.assertEqual(csp1.name, 'Console Server Port 1')

self.assertEqual(dt.powerport_templates.count(), 3)
pp1 = PowerPortTemplate.objects.first()
self.assertEqual(pp1.name, 'Power Port 1')

self.assertEqual(dt.poweroutlet_templates.count(), 3)
po1 = PowerOutletTemplate.objects.first()
self.assertEqual(po1.name, 'Power Outlet 1')
self.assertEqual(po1.power_port, pp1)
self.assertEqual(po1.feed_leg, POWERFEED_LEG_A)

self.assertEqual(dt.interface_templates.count(), 3)
iface1 = InterfaceTemplate.objects.first()
self.assertEqual(iface1.name, 'Interface 1')
self.assertEqual(iface1.type, IFACE_TYPE_1GE_FIXED)
self.assertTrue(iface1.mgmt_only)

self.assertEqual(dt.rearport_templates.count(), 3)
rp1 = RearPortTemplate.objects.first()
self.assertEqual(rp1.name, 'Rear Port 1')

self.assertEqual(dt.frontport_templates.count(), 3)
fp1 = FrontPortTemplate.objects.first()
self.assertEqual(fp1.name, 'Front Port 1')
self.assertEqual(fp1.rear_port, rp1)
self.assertEqual(fp1.rear_port_position, 1)

self.assertEqual(dt.device_bay_templates.count(), 3)
db1 = DeviceBayTemplate.objects.first()
self.assertEqual(db1.name, 'Device Bay 1')


class DeviceRoleTestCase(TestCase):

Expand Down
2 changes: 1 addition & 1 deletion netbox/dcim/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@
# Device types
path(r'device-types/', views.DeviceTypeListView.as_view(), name='devicetype_list'),
path(r'device-types/add/', views.DeviceTypeCreateView.as_view(), name='devicetype_add'),
path(r'device-types/import/', views.DeviceTypeBulkImportView.as_view(), name='devicetype_import'),
path(r'device-types/import/', views.DeviceTypeImportView.as_view(), name='devicetype_import'),
path(r'device-types/edit/', views.DeviceTypeBulkEditView.as_view(), name='devicetype_bulk_edit'),
path(r'device-types/delete/', views.DeviceTypeBulkDeleteView.as_view(), name='devicetype_bulk_delete'),
path(r'device-types/<int:pk>/', views.DeviceTypeView.as_view(), name='devicetype'),
Expand Down
33 changes: 27 additions & 6 deletions netbox/dcim/views.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from collections import OrderedDict
import re

from django.conf import settings
Expand Down Expand Up @@ -26,7 +27,7 @@
from utilities.utils import csv_format
from utilities.views import (
BulkComponentCreateView, BulkDeleteView, BulkEditView, BulkImportView, ComponentCreateView, GetReturnURLMixin,
ObjectDeleteView, ObjectEditView, ObjectListView,
ObjectImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
)
from virtualization.models import VirtualMachine
from . import filters, forms, tables
Expand Down Expand Up @@ -653,11 +654,31 @@ class DeviceTypeDeleteView(PermissionRequiredMixin, ObjectDeleteView):
default_return_url = 'dcim:devicetype_list'


class DeviceTypeBulkImportView(PermissionRequiredMixin, BulkImportView):
permission_required = 'dcim.add_devicetype'
model_form = forms.DeviceTypeCSVForm
table = tables.DeviceTypeTable
default_return_url = 'dcim:devicetype_list'
class DeviceTypeImportView(PermissionRequiredMixin, ObjectImportView):
permission_required = [
'dcim.add_devicetype',
'dcim.add_consoleporttemplate',
'dcim.add_consoleserverporttemplate',
'dcim.add_powerporttemplate',
'dcim.add_poweroutlettemplate',
'dcim.add_interfacetemplate',
'dcim.add_frontporttemplate',
'dcim.add_rearporttemplate',
'dcim.add_devicebaytemplate',
]
model = DeviceType
model_form = forms.DeviceTypeImportForm
related_object_forms = OrderedDict((
('console-ports', forms.ConsolePortTemplateImportForm),
('console-server-ports', forms.ConsoleServerPortTemplateImportForm),
('power-ports', forms.PowerPortTemplateImportForm),
('power-outlets', forms.PowerOutletTemplateImportForm),
('interfaces', forms.InterfaceTemplateImportForm),
('rear-ports', forms.RearPortTemplateImportForm),
('front-ports', forms.FrontPortTemplateImportForm),
('device-bays', forms.DeviceBayTemplateImportForm),
))
default_return_url = 'dcim:devicetype_import'


class DeviceTypeBulkEditView(PermissionRequiredMixin, BulkEditView):
Expand Down
2 changes: 1 addition & 1 deletion netbox/templates/dcim/device_import.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{% extends 'utilities/obj_import.html' %}
{% extends 'utilities/obj_bulk_import.html' %}

{% block tabs %}
{% include 'dcim/inc/device_import_header.html' %}
Expand Down
Loading

0 comments on commit c689373

Please sign in to comment.