Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable YAML/JSON-based DeviceType import #3621

Merged
merged 20 commits into from
Oct 17, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
f8fdca4
Initial work on JSON/YAML-based DeviceType import
jeremystretch Sep 5, 2019
5266fc6
Extend DeviceType import to include related objects
jeremystretch Sep 20, 2019
60b70b6
Add RearPortTemplate power_port field
jeremystretch Sep 20, 2019
5049c6c
Add test for DeviceType import
jeremystretch Sep 20, 2019
15b2a7e
Fix form rendering; enable toggling of redirect to imported object
jeremystretch Sep 24, 2019
2621f17
Remove legacy CSV-based DeviceType import
jeremystretch Sep 24, 2019
30ee232
Move JSON/YAML data valdiation to ImportForm
jeremystretch Sep 24, 2019
0615d36
Force validation of individual objects within a MultiObjectField
jeremystretch Sep 24, 2019
93154ab
Merge branch 'develop' into 451-devicetype-import
jeremystretch Sep 25, 2019
47f1feb
Capture import form field default values
jeremystretch Sep 25, 2019
5f3528c
Capture MultiObjectField default form field values
jeremystretch Sep 25, 2019
36d4f0d
Fix typo
jeremystretch Sep 25, 2019
edc1b52
Adopted a different approach to importing related objects
jeremystretch Sep 27, 2019
ee4e68b
Rewrote test for DeviceType import
jeremystretch Oct 1, 2019
88d61db
Fix YAMLLoadWarning
jeremystretch Oct 1, 2019
6892b79
Enforce object creation permissions
jeremystretch Oct 1, 2019
807d849
PEP8 fix
jeremystretch Oct 1, 2019
553fe0f
Merge branch 'develop' into 451-devicetype-import
jeremystretch Oct 10, 2019
d787c35
Added slug choices for interface and port types
jeremystretch Oct 11, 2019
2ffbced
Rework InterfaceTypes and PortTypes classes
jeremystretch Oct 17, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -655,11 +656,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