Merge pull request #3911 from netbox-community/3801-devicetype-yaml

Closes #3801: Change DeviceType export from CSV to YAML
This commit is contained in:
Jeremy Stretch 2020-01-13 15:43:20 -05:00 committed by GitHub
commit 8f636d9636
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 127 additions and 25 deletions

View File

@ -235,6 +235,7 @@ PATCH) to maintain backward compatibility. This behavior will be discontinued be
* [#3664](https://github.com/digitalocean/netbox/issues/3664) - Enable applying configuration contexts by tags * [#3664](https://github.com/digitalocean/netbox/issues/3664) - Enable applying configuration contexts by tags
* [#3706](https://github.com/digitalocean/netbox/issues/3706) - Increase `available_power` maximum value on PowerFeed * [#3706](https://github.com/digitalocean/netbox/issues/3706) - Increase `available_power` maximum value on PowerFeed
* [#3731](https://github.com/digitalocean/netbox/issues/3731) - Change Graph.type to a ContentType foreign key field * [#3731](https://github.com/digitalocean/netbox/issues/3731) - Change Graph.type to a ContentType foreign key field
* [#3801](https://github.com/digitalocean/netbox/issues/3801) - Use YAML for export of device types
## Bug Fixes (From Beta) ## Bug Fixes (From Beta)

View File

@ -2,6 +2,7 @@ from collections import OrderedDict
from itertools import count, groupby from itertools import count, groupby
import svgwrite import svgwrite
import yaml
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
@ -25,15 +26,14 @@ from utilities.fields import ColorField
from utilities.managers import NaturalOrderingManager from utilities.managers import NaturalOrderingManager
from utilities.models import ChangeLoggedModel from utilities.models import ChangeLoggedModel
from utilities.utils import foreground_color, to_meters from utilities.utils import foreground_color, to_meters
from .device_components import (
CableTermination, ConsolePort, ConsoleServerPort, DeviceBay, FrontPort, Interface, InventoryItem, PowerOutlet,
PowerPort, RearPort,
)
from .device_component_templates import ( from .device_component_templates import (
ConsolePortTemplate, ConsoleServerPortTemplate, DeviceBayTemplate, FrontPortTemplate, InterfaceTemplate, ConsolePortTemplate, ConsoleServerPortTemplate, DeviceBayTemplate, FrontPortTemplate, InterfaceTemplate,
PowerOutletTemplate, PowerPortTemplate, RearPortTemplate, PowerOutletTemplate, PowerPortTemplate, RearPortTemplate,
) )
from .device_components import (
CableTermination, ConsolePort, ConsoleServerPort, DeviceBay, FrontPort, Interface, InventoryItem, PowerOutlet,
PowerPort, RearPort,
)
__all__ = ( __all__ = (
'Cable', 'Cable',
@ -1003,17 +1003,92 @@ class DeviceType(ChangeLoggedModel, CustomFieldModel):
def get_absolute_url(self): def get_absolute_url(self):
return reverse('dcim:devicetype', args=[self.pk]) return reverse('dcim:devicetype', args=[self.pk])
def to_csv(self): def to_yaml(self):
return ( data = OrderedDict((
self.manufacturer.name, ('manufacturer', self.manufacturer.name),
self.model, ('model', self.model),
self.slug, ('slug', self.slug),
self.part_number, ('part_number', self.part_number),
self.u_height, ('u_height', self.u_height),
self.is_full_depth, ('is_full_depth', self.is_full_depth),
self.get_subdevice_role_display(), ('subdevice_role', self.subdevice_role),
self.comments, ('comments', self.comments),
) ))
# Component templates
if self.consoleport_templates.exists():
data['console-ports'] = [
{
'name': c.name,
'type': c.type,
}
for c in self.consoleport_templates.all()
]
if self.consoleserverport_templates.exists():
data['console-server-ports'] = [
{
'name': c.name,
'type': c.type,
}
for c in self.consoleserverport_templates.all()
]
if self.powerport_templates.exists():
data['power-ports'] = [
{
'name': c.name,
'type': c.type,
'maximum_draw': c.maximum_draw,
'allocated_draw': c.allocated_draw,
}
for c in self.powerport_templates.all()
]
if self.poweroutlet_templates.exists():
data['power-outlets'] = [
{
'name': c.name,
'type': c.type,
'power_port': c.power_port.name if c.power_port else None,
'feed_leg': c.feed_leg,
}
for c in self.poweroutlet_templates.all()
]
if self.interface_templates.exists():
data['interfaces'] = [
{
'name': c.name,
'type': c.type,
'mgmt_only': c.mgmt_only,
}
for c in self.interface_templates.all()
]
if self.frontport_templates.exists():
data['front-ports'] = [
{
'name': c.name,
'type': c.type,
'rear_port': c.rear_port.name,
'rear_port_position': c.rear_port_position,
}
for c in self.frontport_templates.all()
]
if self.rearport_templates.exists():
data['rear-ports'] = [
{
'name': c.name,
'type': c.type,
'positions': c.positions,
}
for c in self.rearport_templates.all()
]
if self.device_bay_templates.exists():
data['device-bays'] = [
{
'name': c.name,
}
for c in self.device_bay_templates.all()
]
return yaml.dump(dict(data), sort_keys=False)
def clean(self): def clean(self):

View File

@ -1,5 +1,6 @@
import urllib.parse import urllib.parse
import yaml
from django.test import Client, TestCase from django.test import Client, TestCase
from django.urls import reverse from django.urls import reverse
@ -327,6 +328,17 @@ class DeviceTypeTestCase(TestCase):
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params))) response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
def test_devicetype_export(self):
url = reverse('dcim:devicetype_list')
response = self.client.get('{}?export'.format(url))
self.assertEqual(response.status_code, 200)
data = list(yaml.load_all(response.content, Loader=yaml.SafeLoader))
self.assertEqual(len(data), 3)
self.assertEqual(data[0]['manufacturer'], 'Manufacturer 1')
self.assertEqual(data[0]['model'], 'Device Type 1')
def test_devicetype(self): def test_devicetype(self):
devicetype = DeviceType.objects.first() devicetype = DeviceType.objects.first()

View File

@ -2056,7 +2056,8 @@ class ConsoleConnectionsListView(PermissionRequiredMixin, ObjectListView):
obj.get_connection_status_display(), obj.get_connection_status_display(),
]) ])
csv_data.append(csv) csv_data.append(csv)
return csv_data
return '\n'.join(csv_data)
class PowerConnectionsListView(PermissionRequiredMixin, ObjectListView): class PowerConnectionsListView(PermissionRequiredMixin, ObjectListView):
@ -2087,7 +2088,8 @@ class PowerConnectionsListView(PermissionRequiredMixin, ObjectListView):
obj.get_connection_status_display(), obj.get_connection_status_display(),
]) ])
csv_data.append(csv) csv_data.append(csv)
return csv_data
return '\n'.join(csv_data)
class InterfaceConnectionsListView(PermissionRequiredMixin, ObjectListView): class InterfaceConnectionsListView(PermissionRequiredMixin, ObjectListView):
@ -2126,7 +2128,8 @@ class InterfaceConnectionsListView(PermissionRequiredMixin, ObjectListView):
obj.get_connection_status_display(), obj.get_connection_status_display(),
]) ])
csv_data.append(csv) csv_data.append(csv)
return csv_data
return '\n'.join(csv_data)
# #

View File

@ -75,6 +75,14 @@ class ObjectListView(View):
table = None table = None
template_name = None template_name = None
def queryset_to_yaml(self):
"""
Export the queryset of objects as concatenated YAML documents.
"""
yaml_data = [obj.to_yaml() for obj in self.queryset]
return '---\n'.join(yaml_data)
def queryset_to_csv(self): def queryset_to_csv(self):
""" """
Export the queryset of objects as comma-separated value (CSV), using the model's to_csv() method. Export the queryset of objects as comma-separated value (CSV), using the model's to_csv() method.
@ -90,7 +98,7 @@ class ObjectListView(View):
data = csv_format(obj.to_csv()) data = csv_format(obj.to_csv())
csv_data.append(data) csv_data.append(data)
return csv_data return '\n'.join(csv_data)
def get(self, request): def get(self, request):
@ -121,13 +129,16 @@ class ObjectListView(View):
) )
) )
# Check for YAML export support
elif 'export' in request.GET and hasattr(model, 'to_yaml'):
response = HttpResponse(self.queryset_to_yaml(), content_type='text/yaml')
filename = 'netbox_{}.yaml'.format(self.queryset.model._meta.verbose_name_plural)
response['Content-Disposition'] = 'attachment; filename="{}"'.format(filename)
return response
# Fall back to built-in CSV formatting if export requested but no template specified # Fall back to built-in CSV formatting if export requested but no template specified
elif 'export' in request.GET and hasattr(model, 'to_csv'): elif 'export' in request.GET and hasattr(model, 'to_csv'):
data = self.queryset_to_csv() response = HttpResponse(self.queryset_to_csv(), content_type='text/csv')
response = HttpResponse(
'\n'.join(data),
content_type='text/csv'
)
filename = 'netbox_{}.csv'.format(self.queryset.model._meta.verbose_name_plural) filename = 'netbox_{}.csv'.format(self.queryset.model._meta.verbose_name_plural)
response['Content-Disposition'] = 'attachment; filename="{}"'.format(filename) response['Content-Disposition'] = 'attachment; filename="{}"'.format(filename)
return response return response