Merge branch 'develop' into develop-2.3

This commit is contained in:
Jeremy Stretch
2018-02-06 14:58:11 -05:00
62 changed files with 571 additions and 515 deletions

View File

@@ -44,7 +44,7 @@ class ProviderCSVForm(forms.ModelForm):
class Meta:
model = Provider
fields = ['name', 'slug', 'asn', 'account', 'portal_url', 'comments']
fields = Provider.csv_headers
help_texts = {
'name': 'Provider name',
'asn': '32-bit autonomous system number',
@@ -90,7 +90,7 @@ class CircuitTypeCSVForm(forms.ModelForm):
class Meta:
model = CircuitType
fields = ['name', 'slug']
fields = CircuitType.csv_headers
help_texts = {
'name': 'Name of circuit type',
}

View File

@@ -10,7 +10,6 @@ from dcim.fields import ASNField
from extras.models import CustomFieldModel, CustomFieldValue
from tenancy.models import Tenant
from utilities.models import CreatedUpdatedModel
from utilities.utils import csv_format
from .constants import CIRCUIT_STATUS_ACTIVE, CIRCUIT_STATUS_CHOICES, TERM_SIDE_CHOICES
@@ -30,7 +29,7 @@ class Provider(CreatedUpdatedModel, CustomFieldModel):
comments = models.TextField(blank=True)
custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
csv_headers = ['name', 'slug', 'asn', 'account', 'portal_url']
csv_headers = ['name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments']
class Meta:
ordering = ['name']
@@ -42,13 +41,16 @@ class Provider(CreatedUpdatedModel, CustomFieldModel):
return reverse('circuits:provider', args=[self.slug])
def to_csv(self):
return csv_format([
return (
self.name,
self.slug,
self.asn,
self.account,
self.portal_url,
])
self.noc_contact,
self.admin_contact,
self.comments,
)
@python_2_unicode_compatible
@@ -60,6 +62,8 @@ class CircuitType(models.Model):
name = models.CharField(max_length=50, unique=True)
slug = models.SlugField(unique=True)
csv_headers = ['name', 'slug']
class Meta:
ordering = ['name']
@@ -69,6 +73,12 @@ class CircuitType(models.Model):
def get_absolute_url(self):
return "{}?type={}".format(reverse('circuits:circuit_list'), self.slug)
def to_csv(self):
return (
self.name,
self.slug,
)
@python_2_unicode_compatible
class Circuit(CreatedUpdatedModel, CustomFieldModel):
@@ -88,7 +98,9 @@ class Circuit(CreatedUpdatedModel, CustomFieldModel):
comments = models.TextField(blank=True)
custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
csv_headers = ['cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', 'description']
csv_headers = [
'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', 'description', 'comments',
]
class Meta:
ordering = ['provider', 'cid']
@@ -101,16 +113,17 @@ class Circuit(CreatedUpdatedModel, CustomFieldModel):
return reverse('circuits:circuit', args=[self.pk])
def to_csv(self):
return csv_format([
return (
self.cid,
self.provider.name,
self.type.name,
self.get_status_display(),
self.tenant.name if self.tenant else None,
self.install_date.isoformat() if self.install_date else None,
self.install_date,
self.commit_rate,
self.description,
])
self.comments,
)
def get_status_class(self):
return STATUS_CLASSES[self.status]

View File

@@ -83,9 +83,7 @@ class RegionCSVForm(forms.ModelForm):
class Meta:
model = Region
fields = [
'name', 'slug', 'parent',
]
fields = Region.csv_headers
help_texts = {
'name': 'Region name',
'slug': 'URL-friendly slug',
@@ -153,10 +151,7 @@ class SiteCSVForm(forms.ModelForm):
class Meta:
model = Site
fields = [
'name', 'slug', 'status', 'region', 'tenant', 'facility', 'asn', 'description', 'physical_address',
'shipping_address', 'contact_name', 'contact_phone', 'contact_email', 'time_zone', 'comments',
]
fields = Site.csv_headers
help_texts = {
'name': 'Site name',
'slug': 'URL-friendly slug',
@@ -224,9 +219,7 @@ class RackGroupCSVForm(forms.ModelForm):
class Meta:
model = RackGroup
fields = [
'site', 'name', 'slug',
]
fields = RackGroup.csv_headers
help_texts = {
'name': 'Name of rack group',
'slug': 'URL-friendly slug',
@@ -254,7 +247,7 @@ class RackRoleCSVForm(forms.ModelForm):
class Meta:
model = RackRole
fields = ['name', 'slug', 'color']
fields = RackRole.csv_headers
help_texts = {
'name': 'Name of rack role',
'color': 'RGB color in hexadecimal (e.g. 00ff00)'
@@ -341,10 +334,7 @@ class RackCSVForm(forms.ModelForm):
class Meta:
model = Rack
fields = [
'site', 'group_name', 'name', 'facility_id', 'tenant', 'role', 'serial', 'type', 'width', 'u_height',
'desc_units',
]
fields = Rack.csv_headers
help_texts = {
'name': 'Rack name',
'u_height': 'Height in rack units',
@@ -478,9 +468,7 @@ class ManufacturerForm(BootstrapMixin, forms.ModelForm):
class ManufacturerCSVForm(forms.ModelForm):
class Meta:
model = Manufacturer
fields = [
'name', 'slug'
]
fields = Manufacturer.csv_headers
help_texts = {
'name': 'Manufacturer name',
'slug': 'URL-friendly slug',
@@ -526,8 +514,7 @@ class DeviceTypeCSVForm(forms.ModelForm):
class Meta:
model = DeviceType
fields = ['manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'is_console_server',
'is_pdu', 'is_network_device', 'subdevice_role', 'interface_ordering', 'comments']
fields = DeviceType.csv_headers
help_texts = {
'model': 'Model name',
'slug': 'URL-friendly slug',
@@ -692,7 +679,7 @@ class DeviceRoleCSVForm(forms.ModelForm):
class Meta:
model = DeviceRole
fields = ['name', 'slug', 'color', 'vm_role']
fields = DeviceRole.csv_headers
help_texts = {
'name': 'Name of device role',
'color': 'RGB color in hexadecimal (e.g. 00ff00)'
@@ -716,7 +703,7 @@ class PlatformCSVForm(forms.ModelForm):
class Meta:
model = Platform
fields = ['name', 'slug', 'manufacturer', 'napalm_driver']
fields = Platform.csv_headers
help_texts = {
'name': 'Platform name',
'manufacturer': 'Manufacturer name',
@@ -970,7 +957,7 @@ class DeviceCSVForm(BaseDeviceCSVForm):
class Meta(BaseDeviceCSVForm.Meta):
fields = [
'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag', 'status',
'site', 'rack_group', 'rack_name', 'position', 'face', 'cluster',
'site', 'rack_group', 'rack_name', 'position', 'face', 'cluster', 'comments',
]
def clean(self):
@@ -1019,7 +1006,7 @@ class ChildDeviceCSVForm(BaseDeviceCSVForm):
class Meta(BaseDeviceCSVForm.Meta):
fields = [
'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag', 'status',
'parent', 'device_bay_name', 'cluster',
'parent', 'device_bay_name', 'cluster', 'comments',
]
def clean(self):
@@ -2096,7 +2083,7 @@ class InterfaceConnectionCSVForm(forms.ModelForm):
class Meta:
model = InterfaceConnection
fields = ['device_a', 'interface_a', 'device_b', 'interface_b', 'connection_status']
fields = InterfaceConnection.csv_headers
def clean_interface_a(self):
@@ -2238,7 +2225,7 @@ class InventoryItemCSVForm(forms.ModelForm):
class Meta:
model = InventoryItem
fields = ['device', 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description']
fields = InventoryItem.csv_headers
class InventoryItemBulkEditForm(BootstrapMixin, BulkEditForm):

View File

@@ -23,7 +23,6 @@ from tenancy.models import Tenant
from utilities.fields import ColorField, NullableCharField
from utilities.managers import NaturalOrderByManager
from utilities.models import CreatedUpdatedModel
from utilities.utils import csv_format
from .constants import *
from .fields import ASNField, MACAddressField
from .querysets import InterfaceQuerySet
@@ -44,9 +43,7 @@ class Region(MPTTModel):
name = models.CharField(max_length=50, unique=True)
slug = models.SlugField(unique=True)
csv_headers = [
'name', 'slug', 'parent',
]
csv_headers = ['name', 'slug', 'parent']
class MPTTMeta:
order_insertion_by = ['name']
@@ -58,11 +55,11 @@ class Region(MPTTModel):
return "{}?region={}".format(reverse('dcim:site_list'), self.slug)
def to_csv(self):
return csv_format([
return (
self.name,
self.slug,
self.parent.name if self.parent else None,
])
)
#
@@ -102,8 +99,8 @@ class Site(CreatedUpdatedModel, CustomFieldModel):
objects = SiteManager()
csv_headers = [
'name', 'slug', 'status', 'region', 'tenant', 'facility', 'asn', 'time_zone', 'description', 'contact_name',
'contact_phone', 'contact_email',
'name', 'slug', 'status', 'region', 'tenant', 'facility', 'asn', 'time_zone', 'description', 'physical_address',
'shipping_address', 'contact_name', 'contact_phone', 'contact_email', 'comments',
]
class Meta:
@@ -116,7 +113,7 @@ class Site(CreatedUpdatedModel, CustomFieldModel):
return reverse('dcim:site', args=[self.slug])
def to_csv(self):
return csv_format([
return (
self.name,
self.slug,
self.get_status_display(),
@@ -126,10 +123,13 @@ class Site(CreatedUpdatedModel, CustomFieldModel):
self.asn,
self.time_zone,
self.description,
self.physical_address,
self.shipping_address,
self.contact_name,
self.contact_phone,
self.contact_email,
])
self.comments,
)
def get_status_class(self):
return STATUS_CLASSES[self.status]
@@ -175,9 +175,7 @@ class RackGroup(models.Model):
slug = models.SlugField()
site = models.ForeignKey('Site', related_name='rack_groups', on_delete=models.CASCADE)
csv_headers = [
'site', 'name', 'slug',
]
csv_headers = ['site', 'name', 'slug']
class Meta:
ordering = ['site', 'name']
@@ -193,11 +191,11 @@ class RackGroup(models.Model):
return "{}?group_id={}".format(reverse('dcim:rack_list'), self.pk)
def to_csv(self):
return csv_format([
return (
self.site,
self.name,
self.slug,
])
)
@python_2_unicode_compatible
@@ -209,6 +207,8 @@ class RackRole(models.Model):
slug = models.SlugField(unique=True)
color = ColorField()
csv_headers = ['name', 'slug', 'color']
class Meta:
ordering = ['name']
@@ -218,6 +218,13 @@ class RackRole(models.Model):
def get_absolute_url(self):
return "{}?role={}".format(reverse('dcim:rack_list'), self.slug)
def to_csv(self):
return (
self.name,
self.slug,
self.color,
)
class RackManager(NaturalOrderByManager):
@@ -253,7 +260,7 @@ class Rack(CreatedUpdatedModel, CustomFieldModel):
csv_headers = [
'site', 'group_name', 'name', 'facility_id', 'tenant', 'role', 'type', 'serial', 'width', 'u_height',
'desc_units',
'desc_units', 'comments',
]
class Meta:
@@ -303,7 +310,7 @@ class Rack(CreatedUpdatedModel, CustomFieldModel):
Device.objects.filter(rack=self).update(site_id=self.site.pk)
def to_csv(self):
return csv_format([
return (
self.site.name,
self.group.name if self.group else None,
self.name,
@@ -315,7 +322,8 @@ class Rack(CreatedUpdatedModel, CustomFieldModel):
self.width,
self.u_height,
self.desc_units,
])
self.comments,
)
@property
def units(self):
@@ -491,9 +499,7 @@ class Manufacturer(models.Model):
name = models.CharField(max_length=50, unique=True)
slug = models.SlugField(unique=True)
csv_headers = [
'name', 'slug',
]
csv_headers = ['name', 'slug']
class Meta:
ordering = ['name']
@@ -505,10 +511,10 @@ class Manufacturer(models.Model):
return "{}?manufacturer={}".format(reverse('dcim:devicetype_list'), self.slug)
def to_csv(self):
return csv_format([
return (
self.name,
self.slug,
])
)
@python_2_unicode_compatible
@@ -551,7 +557,7 @@ class DeviceType(models.Model, CustomFieldModel):
csv_headers = [
'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'is_console_server',
'is_pdu', 'is_network_device', 'subdevice_role', 'interface_ordering',
'is_pdu', 'is_network_device', 'subdevice_role', 'interface_ordering', 'comments',
]
class Meta:
@@ -574,7 +580,7 @@ class DeviceType(models.Model, CustomFieldModel):
return reverse('dcim:devicetype', args=[self.pk])
def to_csv(self):
return csv_format([
return (
self.manufacturer.name,
self.model,
self.slug,
@@ -586,7 +592,8 @@ class DeviceType(models.Model, CustomFieldModel):
self.is_network_device,
self.get_subdevice_role_display() if self.subdevice_role else None,
self.get_interface_ordering_display(),
])
self.comments,
)
def clean(self):
@@ -766,6 +773,8 @@ class DeviceRole(models.Model):
help_text="Virtual machines may be assigned to this role"
)
csv_headers = ['name', 'slug', 'color', 'vm_role']
class Meta:
ordering = ['name']
@@ -775,6 +784,14 @@ class DeviceRole(models.Model):
def get_absolute_url(self):
return "{}?role={}".format(reverse('dcim:device_list'), self.slug)
def to_csv(self):
return (
self.name,
self.slug,
self.color,
self.vm_role,
)
@python_2_unicode_compatible
class Platform(models.Model):
@@ -805,6 +822,8 @@ class Platform(models.Model):
verbose_name="Legacy RPC client"
)
csv_headers = ['name', 'slug', 'manufacturer', 'napalm_driver']
class Meta:
ordering = ['name']
@@ -814,6 +833,14 @@ class Platform(models.Model):
def get_absolute_url(self):
return "{}?platform={}".format(reverse('dcim:device_list'), self.slug)
def to_csv(self):
return (
self.name,
self.slug,
self.manufacturer.name if self.manufacturer else None,
self.napalm_driver,
)
class DeviceManager(NaturalOrderByManager):
@@ -892,7 +919,7 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
csv_headers = [
'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag', 'status',
'site', 'rack_group', 'rack_name', 'position', 'face',
'site', 'rack_group', 'rack_name', 'position', 'face', 'comments',
]
class Meta:
@@ -1049,7 +1076,7 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
Device.objects.filter(parent_bay__device=self).update(site=self.site, rack=self.rack)
def to_csv(self):
return csv_format([
return (
self.name or '',
self.device_role.name,
self.tenant.name if self.tenant else None,
@@ -1064,7 +1091,8 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
self.rack.name if self.rack else None,
self.position,
self.get_face_display(),
])
self.comments,
)
@property
def display_name(self):
@@ -1158,15 +1186,14 @@ class ConsolePort(models.Model):
def get_absolute_url(self):
return self.device.get_absolute_url()
# Used for connections export
def to_csv(self):
return csv_format([
return (
self.cs_port.device.identifier if self.cs_port else None,
self.cs_port.name if self.cs_port else None,
self.device.identifier,
self.name,
self.get_connection_status_display(),
])
)
#
@@ -1241,15 +1268,14 @@ class PowerPort(models.Model):
def get_absolute_url(self):
return self.device.get_absolute_url()
# Used for connections export
def to_csv(self):
return csv_format([
return (
self.power_outlet.device.identifier if self.power_outlet else None,
self.power_outlet.name if self.power_outlet else None,
self.device.identifier,
self.name,
self.get_connection_status_display(),
])
)
#
@@ -1501,15 +1527,14 @@ class InterfaceConnection(models.Model):
except ObjectDoesNotExist:
pass
# Used for connections export
def to_csv(self):
return csv_format([
return (
self.interface_a.device.identifier,
self.interface_a.name,
self.interface_b.device.identifier,
self.interface_b.name,
self.get_connection_status_display(),
])
)
#
@@ -1575,7 +1600,7 @@ class InventoryItem(models.Model):
description = models.CharField(max_length=100, blank=True)
csv_headers = [
'device', 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description',
'device', 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered', 'description',
]
class Meta:
@@ -1589,15 +1614,16 @@ class InventoryItem(models.Model):
return self.device.get_absolute_url()
def to_csv(self):
return csv_format([
return (
self.device.name or '{' + self.device.pk + '}',
self.name,
self.manufacturer.name if self.manufacturer else None,
self.part_id,
self.serial,
self.asset_tag,
self.description
])
self.discovered,
self.description,
)
#
@@ -1632,4 +1658,4 @@ class VirtualChassis(models.Model):
if self.pk and self.master not in self.members.all():
raise ValidationError({
'master': "The selected master is not assigned to this virtual chassis."
})
})

View File

@@ -66,6 +66,10 @@ RACK_ROLE = """
{% endif %}
"""
RACK_DEVICE_COUNT = """
<a href="{% url 'dcim:device_list' %}?rack_id={{ record.pk }}">{{ value }}</a>
"""
RACKRESERVATION_ACTIONS = """
{% if perms.dcim.change_rackreservation %}
<a href="{% url 'dcim:rackreservation_edit' pk=record.pk %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
@@ -84,6 +88,22 @@ MANUFACTURER_ACTIONS = """
{% endif %}
"""
DEVICEROLE_DEVICE_COUNT = """
<a href="{% url 'dcim:device_list' %}?role={{ record.slug }}">{{ value }}</a>
"""
DEVICEROLE_VM_COUNT = """
<a href="{% url 'virtualization:virtualmachine_list' %}?role={{ record.slug }}">{{ value }}</a>
"""
PLATFORM_DEVICE_COUNT = """
<a href="{% url 'dcim:device_list' %}?platform={{ record.slug }}">{{ value }}</a>
"""
PLATFORM_VM_COUNT = """
<a href="{% url 'virtualization:virtualmachine_list' %}?platform={{ record.slug }}">{{ value }}</a>
"""
PLATFORM_ACTIONS = """
{% if perms.dcim.change_platform %}
<a href="{% url 'dcim:platform_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
@@ -211,12 +231,16 @@ class RackTable(BaseTable):
class RackDetailTable(RackTable):
devices = tables.Column(accessor=Accessor('device_count'))
device_count = tables.TemplateColumn(
template_code=RACK_DEVICE_COUNT,
verbose_name='Devices'
)
get_utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False, verbose_name='Utilization')
class Meta(RackTable.Meta):
fields = (
'pk', 'name', 'site', 'group', 'facility_id', 'tenant', 'role', 'u_height', 'devices', 'get_utilization'
'pk', 'name', 'site', 'group', 'facility_id', 'tenant', 'role', 'u_height', 'device_count',
'get_utilization',
)
@@ -357,12 +381,25 @@ class DeviceBayTemplateTable(BaseTable):
class DeviceRoleTable(BaseTable):
pk = ToggleColumn()
name = tables.LinkColumn(verbose_name='Name')
device_count = tables.Column(verbose_name='Devices')
vm_count = tables.Column(verbose_name='VMs')
device_count = tables.TemplateColumn(
template_code=DEVICEROLE_DEVICE_COUNT,
accessor=Accessor('devices.count'),
orderable=False,
verbose_name='Devices'
)
vm_count = tables.TemplateColumn(
template_code=DEVICEROLE_VM_COUNT,
accessor=Accessor('virtual_machines.count'),
orderable=False,
verbose_name='VMs'
)
color = tables.TemplateColumn(COLOR_LABEL, verbose_name='Label')
slug = tables.Column(verbose_name='Slug')
actions = tables.TemplateColumn(template_code=DEVICEROLE_ACTIONS, attrs={'td': {'class': 'text-right'}},
verbose_name='')
actions = tables.TemplateColumn(
template_code=DEVICEROLE_ACTIONS,
attrs={'td': {'class': 'text-right'}},
verbose_name=''
)
class Meta(BaseTable.Meta):
model = DeviceRole
@@ -375,10 +412,18 @@ class DeviceRoleTable(BaseTable):
class PlatformTable(BaseTable):
pk = ToggleColumn()
name = tables.LinkColumn(verbose_name='Name')
device_count = tables.Column(verbose_name='Devices')
vm_count = tables.Column(verbose_name='VMs')
slug = tables.Column(verbose_name='Slug')
device_count = tables.TemplateColumn(
template_code=PLATFORM_DEVICE_COUNT,
accessor=Accessor('devices.count'),
orderable=False,
verbose_name='Devices'
)
vm_count = tables.TemplateColumn(
template_code=PLATFORM_VM_COUNT,
accessor=Accessor('virtual_machines.count'),
orderable=False,
verbose_name='VMs'
)
actions = tables.TemplateColumn(
template_code=PLATFORM_ACTIONS,
attrs={'td': {'class': 'text-right'}},

View File

@@ -321,7 +321,7 @@ class RackListView(ObjectListView):
).prefetch_related(
'devices__device_type'
).annotate(
device_count=Count('devices', distinct=True)
device_count=Count('devices')
)
filter = filters.RackFilter
filter_form = forms.RackFilterForm
@@ -763,10 +763,7 @@ class DeviceBayTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
#
class DeviceRoleListView(ObjectListView):
queryset = DeviceRole.objects.annotate(
device_count=Count('devices', distinct=True),
vm_count=Count('virtual_machines', distinct=True)
)
queryset = DeviceRole.objects.all()
table = tables.DeviceRoleTable
template_name = 'dcim/devicerole_list.html'
@@ -804,10 +801,7 @@ class DeviceRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
#
class PlatformListView(ObjectListView):
queryset = Platform.objects.annotate(
device_count=Count('devices', distinct=True),
vm_count=Count('virtual_machines', distinct=True)
)
queryset = Platform.objects.all()
table = tables.PlatformTable
template_name = 'dcim/platform_list.html'

View File

@@ -22,10 +22,11 @@ def get_custom_fields_for_model(content_type, filterable_only=False, bulk_edit=F
for cf in custom_fields:
field_name = 'cf_{}'.format(str(cf.name))
initial = cf.default if not bulk_edit else None
# Integer
if cf.type == CF_TYPE_INTEGER:
field = forms.IntegerField(required=cf.required, initial=cf.default)
field = forms.IntegerField(required=cf.required, initial=initial)
# Boolean
elif cf.type == CF_TYPE_BOOLEAN:
@@ -34,18 +35,19 @@ def get_custom_fields_for_model(content_type, filterable_only=False, bulk_edit=F
(1, 'True'),
(0, 'False'),
)
if cf.default.lower() in ['true', 'yes', '1']:
if initial.lower() in ['true', 'yes', '1']:
initial = 1
elif cf.default.lower() in ['false', 'no', '0']:
elif initial.lower() in ['false', 'no', '0']:
initial = 0
else:
initial = None
field = forms.NullBooleanField(required=cf.required, initial=initial,
widget=forms.Select(choices=choices))
field = forms.NullBooleanField(
required=cf.required, initial=initial, widget=forms.Select(choices=choices)
)
# Date
elif cf.type == CF_TYPE_DATE:
field = forms.DateField(required=cf.required, initial=cf.default, help_text="Date format: YYYY-MM-DD")
field = forms.DateField(required=cf.required, initial=initial, help_text="Date format: YYYY-MM-DD")
# Select
elif cf.type == CF_TYPE_SELECT:
@@ -56,11 +58,11 @@ def get_custom_fields_for_model(content_type, filterable_only=False, bulk_edit=F
# URL
elif cf.type == CF_TYPE_URL:
field = LaxURLField(required=cf.required, initial=cf.default)
field = LaxURLField(required=cf.required, initial=initial)
# Text
else:
field = forms.CharField(max_length=255, required=cf.required, initial=cf.default)
field = forms.CharField(max_length=255, required=cf.required, initial=initial)
field.model = cf
field.label = cf.label if cf.label else cf.name.replace('_', ' ').capitalize()

View File

@@ -223,19 +223,25 @@ class ExportTemplate(models.Model):
def __str__(self):
return '{}: {}'.format(self.content_type, self.name)
def to_response(self, context_dict, filename):
def render_to_response(self, queryset):
"""
Render the template to an HTTP response, delivered as a named file attachment
"""
template = Template(self.template_code)
mime_type = 'text/plain' if not self.mime_type else self.mime_type
output = template.render(Context(context_dict))
output = template.render(Context({'queryset': queryset}))
# Replace CRLF-style line terminators
output = output.replace('\r\n', '\n')
# Build the response
response = HttpResponse(output, content_type=mime_type)
if self.file_extension:
filename += '.{}'.format(self.file_extension)
filename = 'netbox_{}{}'.format(
queryset.model._meta.verbose_name_plural,
'.{}'.format(self.file_extension) if self.file_extension else ''
)
response['Content-Disposition'] = 'attachment; filename="{}"'.format(filename)
return response

View File

@@ -57,7 +57,7 @@ class VRFCSVForm(forms.ModelForm):
class Meta:
model = VRF
fields = ['name', 'rd', 'tenant', 'enforce_unique', 'description']
fields = VRF.csv_headers
help_texts = {
'name': 'VRF name',
}
@@ -102,7 +102,7 @@ class RIRCSVForm(forms.ModelForm):
class Meta:
model = RIR
fields = ['name', 'slug', 'is_private']
fields = RIR.csv_headers
help_texts = {
'name': 'RIR name',
}
@@ -144,7 +144,7 @@ class AggregateCSVForm(forms.ModelForm):
class Meta:
model = Aggregate
fields = ['prefix', 'rir', 'date_added', 'description']
fields = Aggregate.csv_headers
class AggregateBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
@@ -185,7 +185,7 @@ class RoleCSVForm(forms.ModelForm):
class Meta:
model = Role
fields = ['name', 'slug']
fields = Role.csv_headers
help_texts = {
'name': 'Role name',
}
@@ -299,9 +299,7 @@ class PrefixCSVForm(forms.ModelForm):
class Meta:
model = Prefix
fields = [
'prefix', 'vrf', 'tenant', 'site', 'vlan_group', 'vlan_vid', 'status', 'role', 'is_pool', 'description',
]
fields = Prefix.csv_headers
def clean(self):
@@ -609,10 +607,7 @@ class IPAddressCSVForm(forms.ModelForm):
class Meta:
model = IPAddress
fields = [
'address', 'vrf', 'tenant', 'status', 'role', 'device', 'virtual_machine', 'interface_name', 'is_primary',
'description',
]
fields = IPAddress.csv_headers
def clean(self):
@@ -759,7 +754,7 @@ class VLANGroupCSVForm(forms.ModelForm):
class Meta:
model = VLANGroup
fields = ['site', 'name', 'slug']
fields = VLANGroup.csv_headers
help_texts = {
'name': 'Name of VLAN group',
}
@@ -849,7 +844,7 @@ class VLANCSVForm(forms.ModelForm):
class Meta:
model = VLAN
fields = ['site', 'group_name', 'vid', 'name', 'tenant', 'status', 'role', 'description']
fields = VLAN.csv_headers
help_texts = {
'vid': 'Numeric VLAN ID (1-4095)',
'name': 'VLAN name',

View File

@@ -14,7 +14,6 @@ from dcim.models import Interface
from extras.models import CustomFieldModel, CustomFieldValue
from tenancy.models import Tenant
from utilities.models import CreatedUpdatedModel
from utilities.utils import csv_format
from .constants import *
from .fields import IPNetworkField, IPAddressField
from .querysets import PrefixQuerySet
@@ -49,13 +48,13 @@ class VRF(CreatedUpdatedModel, CustomFieldModel):
return reverse('ipam:vrf', args=[self.pk])
def to_csv(self):
return csv_format([
return (
self.name,
self.rd,
self.tenant.name if self.tenant else None,
self.enforce_unique,
self.description,
])
)
@property
def display_name(self):
@@ -75,6 +74,8 @@ class RIR(models.Model):
is_private = models.BooleanField(default=False, verbose_name='Private',
help_text='IP space managed by this RIR is considered private')
csv_headers = ['name', 'slug', 'is_private']
class Meta:
ordering = ['name']
verbose_name = 'RIR'
@@ -86,6 +87,13 @@ class RIR(models.Model):
def get_absolute_url(self):
return "{}?rir={}".format(reverse('ipam:aggregate_list'), self.slug)
def to_csv(self):
return (
self.name,
self.slug,
self.is_private,
)
@python_2_unicode_compatible
class Aggregate(CreatedUpdatedModel, CustomFieldModel):
@@ -147,12 +155,12 @@ class Aggregate(CreatedUpdatedModel, CustomFieldModel):
super(Aggregate, self).save(*args, **kwargs)
def to_csv(self):
return csv_format([
return (
self.prefix,
self.rir.name,
self.date_added.isoformat() if self.date_added else None,
self.date_added,
self.description,
])
)
def get_utilization(self):
"""
@@ -173,19 +181,20 @@ class Role(models.Model):
slug = models.SlugField(unique=True)
weight = models.PositiveSmallIntegerField(default=1000)
csv_headers = ['name', 'slug', 'weight']
class Meta:
ordering = ['weight', 'name']
def __str__(self):
return self.name
@property
def count_prefixes(self):
return self.prefixes.count()
@property
def count_vlans(self):
return self.vlans.count()
def to_csv(self):
return (
self.name,
self.slug,
self.weight,
)
@python_2_unicode_compatible
@@ -262,7 +271,7 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel):
super(Prefix, self).save(*args, **kwargs)
def to_csv(self):
return csv_format([
return (
self.prefix,
self.vrf.rd if self.vrf else None,
self.tenant.name if self.tenant else None,
@@ -273,7 +282,7 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel):
self.role.name if self.role else None,
self.is_pool,
self.description,
])
)
def get_status_class(self):
return STATUS_CHOICE_CLASSES[self.status]
@@ -461,7 +470,7 @@ class IPAddress(CreatedUpdatedModel, CustomFieldModel):
else:
is_primary = False
return csv_format([
return (
self.address,
self.vrf.rd if self.vrf else None,
self.tenant.name if self.tenant else None,
@@ -472,7 +481,7 @@ class IPAddress(CreatedUpdatedModel, CustomFieldModel):
self.interface.name if self.interface else None,
is_primary,
self.description,
])
)
@property
def device(self):
@@ -502,6 +511,8 @@ class VLANGroup(models.Model):
slug = models.SlugField()
site = models.ForeignKey('dcim.Site', related_name='vlan_groups', on_delete=models.PROTECT, blank=True, null=True)
csv_headers = ['name', 'slug', 'site']
class Meta:
ordering = ['site', 'name']
unique_together = [
@@ -517,6 +528,13 @@ class VLANGroup(models.Model):
def get_absolute_url(self):
return "{}?group_id={}".format(reverse('ipam:vlan_list'), self.pk)
def to_csv(self):
return (
self.name,
self.slug,
self.site.name if self.site else None,
)
def get_next_available_vid(self):
"""
Return the first available VLAN ID (1-4094) in the group.
@@ -577,7 +595,7 @@ class VLAN(CreatedUpdatedModel, CustomFieldModel):
})
def to_csv(self):
return csv_format([
return (
self.site.name if self.site else None,
self.group.name if self.group else None,
self.vid,
@@ -586,7 +604,7 @@ class VLAN(CreatedUpdatedModel, CustomFieldModel):
self.get_status_display(),
self.role.name if self.role else None,
self.description,
])
)
@property
def display_name(self):

View File

@@ -37,6 +37,14 @@ UTILIZATION_GRAPH = """
{% if record.pk %}{% utilization_graph record.get_utilization %}{% else %}&mdash;{% endif %}
"""
ROLE_PREFIX_COUNT = """
<a href="{% url 'ipam:prefix_list' %}?role={{ record.slug }}">{{ value }}</a>
"""
ROLE_VLAN_COUNT = """
<a href="{% url 'ipam:vlan_list' %}?role={{ record.slug }}">{{ value }}</a>
"""
ROLE_ACTIONS = """
{% if perms.ipam.change_role %}
<a href="{% url 'ipam:role_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
@@ -220,10 +228,18 @@ class AggregateDetailTable(AggregateTable):
class RoleTable(BaseTable):
pk = ToggleColumn()
name = tables.Column(verbose_name='Name')
prefix_count = tables.Column(accessor=Accessor('count_prefixes'), orderable=False, verbose_name='Prefixes')
vlan_count = tables.Column(accessor=Accessor('count_vlans'), orderable=False, verbose_name='VLANs')
slug = tables.Column(verbose_name='Slug')
prefix_count = tables.TemplateColumn(
accessor=Accessor('prefixes.count'),
template_code=ROLE_PREFIX_COUNT,
orderable=False,
verbose_name='Prefixes'
)
vlan_count = tables.TemplateColumn(
accessor=Accessor('vlans.count'),
template_code=ROLE_VLAN_COUNT,
orderable=False,
verbose_name='VLANs'
)
actions = tables.TemplateColumn(template_code=ROLE_ACTIONS, attrs={'td': {'class': 'text-right'}}, verbose_name='')
class Meta(BaseTable.Meta):

View File

@@ -119,7 +119,7 @@ SEARCH_TYPES = OrderedDict((
}),
# Virtualization
('cluster', {
'queryset': Cluster.objects.all(),
'queryset': Cluster.objects.select_related('type', 'group'),
'filter': ClusterFilter,
'table': ClusterTable,
'url': 'virtualization:cluster_list',

View File

@@ -47,7 +47,7 @@ class SecretRoleCSVForm(forms.ModelForm):
class Meta:
model = SecretRole
fields = ['name', 'slug']
fields = SecretRole.csv_headers
help_texts = {
'name': 'Name of secret role',
}
@@ -98,7 +98,7 @@ class SecretCSVForm(forms.ModelForm):
class Meta:
model = Secret
fields = ['device', 'role', 'name', 'plaintext']
fields = Secret.csv_headers
help_texts = {
'name': 'Name or username',
}

View File

@@ -239,6 +239,8 @@ class SecretRole(models.Model):
users = models.ManyToManyField(User, related_name='secretroles', blank=True)
groups = models.ManyToManyField(Group, related_name='secretroles', blank=True)
csv_headers = ['name', 'slug']
class Meta:
ordering = ['name']
@@ -248,6 +250,12 @@ class SecretRole(models.Model):
def get_absolute_url(self):
return "{}?role={}".format(reverse('secrets:secret_list'), self.slug)
def to_csv(self):
return (
self.name,
self.slug,
)
def has_member(self, user):
"""
Check whether the given user has belongs to this SecretRole. Note that superusers belong to all roles.

View File

@@ -1,19 +1,14 @@
{% extends '_base.html' %}
{% load buttons %}
{% load helpers %}
{% block content %}
<div class="pull-right">
{% if perms.circuits.add_circuit %}
<a href="{% url 'circuits:circuit_add' %}" class="btn btn-primary">
<span class="fa fa-plus" aria-hidden="true"></span>
Add a circuit
</a>
<a href="{% url 'circuits:circuit_import' %}" class="btn btn-info">
<span class="fa fa-download" aria-hidden="true"></span>
Import circuits
</a>
{% add_button 'circuits:circuit_add' %}
{% import_button 'circuits:circuit_import' %}
{% endif %}
{% include 'inc/export_button.html' with obj_type='circuits' %}
{% export_button content_type %}
</div>
<h1>{% block title %}Circuits{% endblock %}</h1>
<div class="row">

View File

@@ -1,18 +1,14 @@
{% extends '_base.html' %}
{% load buttons %}
{% load helpers %}
{% block content %}
<div class="pull-right">
{% if perms.circuits.add_circuittype %}
<a href="{% url 'circuits:circuittype_add' %}" class="btn btn-primary">
<span class="fa fa-plus" aria-hidden="true"></span>
Add a circuit type
</a>
<a href="{% url 'circuits:circuittype_import' %}" class="btn btn-info">
<span class="fa fa-download" aria-hidden="true"></span>
Import circuit types
</a>
{% add_button 'circuits:circuittype_add' %}
{% import_button 'circuits:circuittype_import' %}
{% endif %}
{% export_button content_type %}
</div>
<h1>{% block title %}Circuit Types{% endblock %}</h1>
<div class="row">

View File

@@ -1,18 +1,13 @@
{% extends '_base.html' %}
{% load buttons %}
{% block content %}
<div class="pull-right">
{% if perms.circuits.add_provider %}
<a href="{% url 'circuits:provider_add' %}" class="btn btn-primary">
<span class="fa fa-plus" aria-hidden="true"></span>
Add a provider
</a>
<a href="{% url 'circuits:provider_import' %}" class="btn btn-info">
<span class="fa fa-download" aria-hidden="true"></span>
Import providers
</a>
{% add_button 'circuits:provider_add' %}
{% import_button 'circuits:provider_import' %}
{% endif %}
{% include 'inc/export_button.html' with obj_type='providers' %}
{% export_button content_type %}
</div>
<h1>{% block title %}Providers{% endblock %}</h1>
<div class="row">

View File

@@ -1,14 +1,12 @@
{% extends '_base.html' %}
{% load buttons %}
{% block content %}
<div class="pull-right">
{% if perms.dcim.change_consoleport %}
<a href="{% url 'dcim:console_connections_import' %}" class="btn btn-info">
<span class="fa fa-download" aria-hidden="true"></span>
Import connections
</a>
{% import_button 'dcim:console_connections_import' %}
{% endif %}
{% include 'inc/export_button.html' with obj_type='connections' %}
{% export_button content_type %}
</div>
<h1>{% block title %}Console Connections{% endblock %}</h1>
<div class="row">

View File

@@ -1,19 +1,14 @@
{% extends '_base.html' %}
{% load buttons %}
{% load helpers %}
{% block content %}
<div class="pull-right">
{% if perms.dcim.add_device %}
<a href="{% url 'dcim:device_add' %}" class="btn btn-primary">
<span class="fa fa-plus" aria-hidden="true"></span>
Add a device
</a>
<a href="{% url 'dcim:device_import' %}" class="btn btn-info">
<span class="fa fa-download" aria-hidden="true"></span>
Import devices
</a>
{% add_button 'dcim:device_add' %}
{% import_button 'dcim:device_import' %}
{% endif %}
{% include 'inc/export_button.html' with obj_type='devices' %}
{% export_button content_type %}
</div>
<h1>{% block title %}Devices{% endblock %}</h1>
<div class="row">

View File

@@ -1,18 +1,14 @@
{% extends '_base.html' %}
{% load buttons %}
{% load helpers %}
{% block content %}
<div class="pull-right">
{% if perms.dcim.add_devicerole %}
<a href="{% url 'dcim:devicerole_add' %}" class="btn btn-primary">
<span class="fa fa-plus" aria-hidden="true"></span>
Add a device role
</a>
<a href="{% url 'dcim:devicerole_import' %}" class="btn btn-info">
<span class="fa fa-download" aria-hidden="true"></span>
Import device roles
</a>
{% add_button 'dcim:devicerole_add' %}
{% import_button 'dcim:devicerole_import' %}
{% endif %}
{% export_button content_type %}
</div>
<h1>{% block title %}Device Roles{% endblock %}</h1>
<div class="row">

View File

@@ -1,19 +1,14 @@
{% extends '_base.html' %}
{% load buttons %}
{% load helpers %}
{% block content %}
<div class="pull-right">
{% if perms.dcim.add_devicetype %}
<a href="{% url 'dcim:devicetype_add' %}" class="btn btn-primary">
<span class="fa fa-plus" aria-hidden="true"></span>
Add a device type
</a>
<a href="{% url 'dcim:devicetype_import' %}" class="btn btn-info">
<span class="fa fa-download" aria-hidden="true"></span>
Import device types
</a>
{% add_button 'dcim:devicetype_add' %}
{% import_button 'dcim:devicetype_import' %}
{% endif %}
{% include 'inc/export_button.html' with obj_type='device types' %}
{% export_button content_type %}
</div>
<h1>{% block title %}Device Types{% endblock %}</h1>
<div class="row">

View File

@@ -1,14 +1,12 @@
{% extends '_base.html' %}
{% load buttons %}
{% block content %}
<div class="pull-right">
{% if perms.dcim.add_interfaceconnection %}
<a href="{% url 'dcim:interface_connections_import' %}" class="btn btn-info">
<span class="fa fa-download" aria-hidden="true"></span>
Import connections
</a>
{% import_button 'dcim:interface_connections_import' %}
{% endif %}
{% include 'inc/export_button.html' with obj_type='connections' %}
{% export_button content_type %}
</div>
<h1>{% block title %}Interface Connections{% endblock %}</h1>
<div class="row">

View File

@@ -1,15 +1,13 @@
{% extends '_base.html' %}
{% load buttons %}
{% load helpers %}
{% block content %}
<div class="pull-right">
{% if perms.dcim.add_devicetype %}
<a href="{% url 'dcim:inventoryitem_import' %}" class="btn btn-info">
<span class="fa fa-download" aria-hidden="true"></span>
Import inventory items
</a>
{% import_button 'dcim:inventoryitem_import' %}
{% endif %}
{% include 'inc/export_button.html' with obj_type='inventory items' %}
{% export_button content_type %}
</div>
<h1>{% block title %}Inventory Items{% endblock %}</h1>
<div class="row">

View File

@@ -1,19 +1,14 @@
{% extends '_base.html' %}
{% load buttons %}
{% load helpers %}
{% block content %}
<div class="pull-right">
{% if perms.dcim.add_manufacturer %}
<a href="{% url 'dcim:manufacturer_add' %}" class="btn btn-primary">
<span class="fa fa-plus" aria-hidden="true"></span>
Add a manufacturer
</a>
<a href="{% url 'dcim:manufacturer_import' %}" class="btn btn-info">
<span class="fa fa-download" aria-hidden="true"></span>
Import manufacturers
</a>
{% add_button 'dcim:manufacturer_add' %}
{% import_button 'dcim:manufacturer_import' %}
{% endif %}
{% include 'inc/export_button.html' with obj_type='manufacturers' %}
{% export_button content_type %}
</div>
<h1>{% block title %}Manufacturers{% endblock %}</h1>
<div class="row">

View File

@@ -1,18 +1,14 @@
{% extends '_base.html' %}
{% load buttons %}
{% load helpers %}
{% block content %}
<div class="pull-right">
{% if perms.dcim.add_platform %}
<a href="{% url 'dcim:platform_add' %}" class="btn btn-primary">
<span class="fa fa-plus" aria-hidden="true"></span>
Add a platform
</a>
<a href="{% url 'dcim:platform_import' %}" class="btn btn-info">
<span class="fa fa-download" aria-hidden="true"></span>
Import platforms
</a>
{% add_button 'dcim:platform_add' %}
{% import_button 'dcim:platform_import' %}
{% endif %}
{% export_button content_type %}
</div>
<h1>{% block title %}Platforms{% endblock %}</h1>
<div class="row">

View File

@@ -1,14 +1,12 @@
{% extends '_base.html' %}
{% load buttons %}
{% block content %}
<div class="pull-right">
{% if perms.dcim.change_powerport %}
<a href="{% url 'dcim:power_connections_import' %}" class="btn btn-info">
<span class="fa fa-download" aria-hidden="true"></span>
Import connections
</a>
{% import_button 'dcim:power_connections_import' %}
{% endif %}
{% include 'inc/export_button.html' with obj_type='connections' %}
{% export_button content_type %}
</div>
<h1>{% block title %}Power Connections{% endblock %}</h1>
<div class="row">

View File

@@ -1,19 +1,14 @@
{% extends '_base.html' %}
{% load buttons %}
{% load helpers %}
{% block content %}
<div class="pull-right">
{% if perms.dcim.add_rack %}
<a href="{% url 'dcim:rack_add' %}" class="btn btn-primary">
<span class="fa fa-plus" aria-hidden="true"></span>
Add a rack
</a>
<a href="{% url 'dcim:rack_import' %}" class="btn btn-info">
<span class="fa fa-download" aria-hidden="true"></span>
Import racks
</a>
{% add_button 'dcim:rack_add' %}
{% import_button 'dcim:rack_import' %}
{% endif %}
{% include 'inc/export_button.html' with obj_type='racks' %}
{% export_button content_type %}
</div>
<h1>{% block title %}Racks{% endblock %}</h1>
<div class="row">

View File

@@ -1,19 +1,14 @@
{% extends '_base.html' %}
{% load buttons %}
{% load helpers %}
{% block content %}
<div class="pull-right">
{% if perms.dcim.add_rackgroup %}
<a href="{% url 'dcim:rackgroup_add' %}" class="btn btn-primary">
<span class="fa fa-plus" aria-hidden="true"></span>
Add a rack group
</a>
<a href="{% url 'dcim:rackgroup_import' %}" class="btn btn-info">
<span class="fa fa-download" aria-hidden="true"></span>
Import rack groups
</a>
{% add_button 'dcim:rackgroup_add' %}
{% import_button 'dcim:rackgroup_import' %}
{% endif %}
{% include 'inc/export_button.html' with obj_type='rack groups' %}
{% export_button content_type %}
</div>
<h1>{% block title %}Rack Groups{% endblock %}</h1>
<div class="row">

View File

@@ -1,19 +1,14 @@
{% extends '_base.html' %}
{% load buttons %}
{% load helpers %}
{% block content %}
<div class="pull-right">
{% if perms.dcim.add_region %}
<a href="{% url 'dcim:region_add' %}" class="btn btn-primary">
<span class="fa fa-plus" aria-hidden="true"></span>
Add a region
</a>
<a href="{% url 'dcim:region_import' %}" class="btn btn-info">
<span class="fa fa-download" aria-hidden="true"></span>
Import regions
</a>
{% add_button 'dcim:region_add' %}
{% import_button 'dcim:region_import' %}
{% endif %}
{% include 'inc/export_button.html' with obj_type='regions' %}
{% export_button content_type %}
</div>
<h1>{% block title %}Regions{% endblock %}</h1>
<div class="row">

View File

@@ -1,18 +1,13 @@
{% extends '_base.html' %}
{% load buttons %}
{% block content %}
<div class="pull-right">
{% if perms.dcim.add_site %}
<a href="{% url 'dcim:site_add' %}" class="btn btn-primary">
<span class="fa fa-plus" aria-hidden="true"></span>
Add a site
</a>
<a href="{% url 'dcim:site_import' %}" class="btn btn-info">
<span class="fa fa-download" aria-hidden="true"></span>
Import sites
</a>
{% add_button 'dcim:site_add' %}
{% import_button 'dcim:site_import' %}
{% endif %}
{% include 'inc/export_button.html' with obj_type='sites' %}
{% export_button content_type %}
</div>
<h1>{% block title %}Sites{% endblock %}</h1>
<div class="row">

View File

@@ -1,20 +0,0 @@
{% if export_templates %}
<div class="btn-group">
<button type="button" class="btn btn-success dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="fa fa-upload" aria-hidden="true"></span>
Export {{ obj_type }} <span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li><a href="?{% if request.GET %}{{ request.GET.urlencode }}&{% endif %}export">CSV (default)</a></li>
<li class="divider"></li>
{% for et in export_templates %}
<li><a href="?{% if request.GET %}{{ request.GET.urlencode }}&{% endif %}export={{ et.name }}"{% if et.description %} title="{{ et.description }}"{% endif %}>{{ et.name }}</a></li>
{% endfor %}
</ul>
</div>
{% else %}
<a href="?{% if request.GET %}{{ request.GET.urlencode }}&{% endif %}export" class="btn btn-success">
<span class="fa fa-upload" aria-hidden="true"></span>
Export {{ obj_type }}
</a>
{% endif %}

View File

@@ -1,20 +1,15 @@
{% extends '_base.html' %}
{% load buttons %}
{% load humanize %}
{% load helpers %}
{% block content %}
<div class="pull-right">
{% if perms.ipam.add_aggregate %}
<a href="{% url 'ipam:aggregate_add' %}" class="btn btn-primary">
<span class="fa fa-plus" aria-hidden="true"></span>
Add an aggregate
</a>
<a href="{% url 'ipam:aggregate_import' %}" class="btn btn-info">
<span class="fa fa-download" aria-hidden="true"></span>
Import aggregates
</a>
{% add_button 'ipam:aggregate_add' %}
{% import_button 'ipam:aggregate_import' %}
{% endif %}
{% include 'inc/export_button.html' with obj_type='aggregates' %}
{% export_button content_type %}
</div>
<h1>{% block title %}Aggregates{% endblock %}</h1>
<div class="row">

View File

@@ -1,19 +1,14 @@
{% extends '_base.html' %}
{% load buttons %}
{% load helpers %}
{% block content %}
<div class="pull-right">
{% if perms.ipam.add_ipaddress %}
<a href="{% url 'ipam:ipaddress_add' %}" class="btn btn-primary">
<span class="fa fa-plus" aria-hidden="true"></span>
Add an IP
</a>
<a href="{% url 'ipam:ipaddress_import' %}" class="btn btn-info">
<span class="fa fa-download" aria-hidden="true"></span>
Import IPs
</a>
{% endif %}
{% include 'inc/export_button.html' with obj_type='IPs' %}
{% add_button 'ipam:ipaddress_add' %}
{% import_button 'ipam:ipaddress_import' %}
{% endif %}
{% export_button content_type %}
</div>
<h1>{% block title %}IP Addresses{% endblock %}</h1>
<div class="row">

View File

@@ -1,4 +1,5 @@
{% extends '_base.html' %}
{% load buttons %}
{% load helpers %}
{% load form_helpers %}
@@ -9,16 +10,10 @@
<a href="{% url 'ipam:prefix_list' %}{% querystring request expand='on' page=1 %}" class="btn btn-default{% if request.GET.expand %} active{% endif %}">Expand</a>
</div>
{% if perms.ipam.add_prefix %}
<a href="{% url 'ipam:prefix_add' %}" class="btn btn-primary">
<span class="fa fa-plus" aria-hidden="true"></span>
Add a prefix
</a>
<a href="{% url 'ipam:prefix_import' %}" class="btn btn-info">
<span class="fa fa-download" aria-hidden="true"></span>
Import prefixes
</a>
{% endif %}
{% include 'inc/export_button.html' with obj_type='prefixes' %}
{% add_button 'ipam:prefix_add' %}
{% import_button 'ipam:prefix_import' %}
{% endif %}
{% export_button content_type %}
</div>
<h1>{% block title %}Prefixes{% endblock %}</h1>
<div class="row">

View File

@@ -1,4 +1,5 @@
{% extends '_base.html' %}
{% load buttons %}
{% load humanize %}
{% load helpers %}
@@ -16,15 +17,10 @@
</a>
{% endif %}
{% if perms.ipam.add_rir %}
<a href="{% url 'ipam:rir_add' %}" class="btn btn-primary">
<span class="fa fa-plus" aria-hidden="true"></span>
Add a RIR
</a>
<a href="{% url 'ipam:rir_import' %}" class="btn btn-info">
<span class="fa fa-download" aria-hidden="true"></span>
Import RIRs
</a>
{% add_button 'ipam:rir_add' %}
{% import_button 'ipam:rir_import' %}
{% endif %}
{% export_button content_type %}
</div>
<h1>{% block title %}RIRs{% endblock %}</h1>
<div class="row">

View File

@@ -1,18 +1,14 @@
{% extends '_base.html' %}
{% load buttons %}
{% load helpers %}
{% block content %}
<div class="pull-right">
{% if perms.ipam.add_role %}
<a href="{% url 'ipam:role_add' %}" class="btn btn-primary">
<span class="fa fa-plus" aria-hidden="true"></span>
Add a role
</a>
<a href="{% url 'ipam:role_import' %}" class="btn btn-info">
<span class="fa fa-download" aria-hidden="true"></span>
Import roles
</a>
{% add_button 'ipam:role_add' %}
{% import_button 'ipam:role_import' %}
{% endif %}
{% export_button content_type %}
</div>
<h1>{% block title %}Prefix/VLAN Roles{% endblock %}</h1>
<div class="row">

View File

@@ -1,20 +1,15 @@
{% extends '_base.html' %}
{% load buttons %}
{% load helpers %}
{% load form_helpers %}
{% block content %}
<div class="pull-right">
{% if perms.ipam.add_vlan %}
<a href="{% url 'ipam:vlan_add' %}" class="btn btn-primary">
<span class="fa fa-plus" aria-hidden="true"></span>
Add a VLAN
</a>
<a href="{% url 'ipam:vlan_import' %}" class="btn btn-info">
<span class="fa fa-download" aria-hidden="true"></span>
Import VLANs
</a>
{% endif %}
{% include 'inc/export_button.html' with obj_type='VLANs' %}
{% add_button 'ipam:vlan_add' %}
{% import_button 'ipam:vlan_import' %}
{% endif %}
{% export_button content_type %}
</div>
<h1>{% block title %}VLANs{% endblock %}</h1>
<div class="row">

View File

@@ -1,18 +1,14 @@
{% extends '_base.html' %}
{% load buttons %}
{% load helpers %}
{% block content %}
<div class="pull-right">
{% if perms.ipam.add_vlangroup %}
<a href="{% url 'ipam:vlangroup_add' %}" class="btn btn-primary">
<span class="fa fa-plus" aria-hidden="true"></span>
Add a VLAN group
</a>
<a href="{% url 'ipam:vlangroup_import' %}" class="btn btn-info">
<span class="fa fa-download" aria-hidden="true"></span>
Import VLAN groups
</a>
{% add_button 'ipam:vlangroup_add' %}
{% import_button 'ipam:vlangroup_import' %}
{% endif %}
{% export_button content_type %}
</div>
<h1>{% block title %}VLAN Groups{% endblock %}</h1>
<div class="row">

View File

@@ -5,16 +5,10 @@
{% block content %}
<div class="pull-right">
{% if perms.ipam.add_vrf %}
<a href="{% url 'ipam:vrf_add' %}" class="btn btn-primary">
<span class="fa fa-plus" aria-hidden="true"></span>
Add a VRF
</a>
<a href="{% url 'ipam:vrf_import' %}" class="btn btn-info">
<span class="fa fa-download" aria-hidden="true"></span>
Import VRFs
</a>
{% endif %}
{% include 'inc/export_button.html' with obj_type='VRFs' %}
{% add_button 'ipam:vrf_add' %}
{% import_button 'ipam:vrf_import' %}
{% endif %}
{% export_button content_type %}
</div>
<h1>{% block title %}VRFs{% endblock %}</h1>
<div class="row">

View File

@@ -1,13 +1,11 @@
{% extends '_base.html' %}
{% load buttons %}
{% load helpers %}
{% block content %}
<div class="pull-right">
{% if perms.secrets.add_secret %}
<a href="{% url 'secrets:secret_import' %}" class="btn btn-info">
<span class="fa fa-download" aria-hidden="true"></span>
Import secrets
</a>
{% import_button 'secrets:secret_import' %}
{% endif %}
</div>
<h1>{% block title %}Secrets{% endblock %}</h1>

View File

@@ -1,18 +1,14 @@
{% extends '_base.html' %}
{% load buttons %}
{% load helpers %}
{% block content %}
<div class="pull-right">
{% if perms.dcim.add_devicerole %}
<a href="{% url 'secrets:secretrole_add' %}" class="btn btn-primary">
<span class="fa fa-plus" aria-hidden="true"></span>
Add a secret role
</a>
<a href="{% url 'secrets:secretrole_import' %}" class="btn btn-info">
<span class="fa fa-download" aria-hidden="true"></span>
Import secret roles
</a>
{% if perms.secrets.add_secretrole %}
{% add_button 'secrets:secretrole_add' %}
{% import_button 'secrets:secretrole_import' %}
{% endif %}
{% export_button content_type %}
</div>
<h1>{% block title %}Secret Roles{% endblock %}</h1>
<div class="row">

View File

@@ -1,19 +1,14 @@
{% extends '_base.html' %}
{% load buttons %}
{% load helpers %}
{% block content %}
<div class="pull-right">
{% if perms.tenancy.add_tenant %}
<a href="{% url 'tenancy:tenant_add' %}" class="btn btn-primary">
<span class="fa fa-plus" aria-hidden="true"></span>
Add a tenant
</a>
<a href="{% url 'tenancy:tenant_import' %}" class="btn btn-info">
<span class="fa fa-download" aria-hidden="true"></span>
Import tenants
</a>
{% add_button 'tenancy:tenant_add' %}
{% import_button 'tenancy:tenant_import' %}
{% endif %}
{% include 'inc/export_button.html' with obj_type='tenants' %}
{% export_button content_type %}
</div>
<h1>{% block title %}Tenants{% endblock %}</h1>
<div class="row">

View File

@@ -1,18 +1,14 @@
{% extends '_base.html' %}
{% load buttons %}
{% load helpers %}
{% block content %}
<div class="pull-right">
{% if perms.tenancy.add_tenantgroup %}
<a href="{% url 'tenancy:tenantgroup_add' %}" class="btn btn-primary">
<span class="fa fa-plus" aria-hidden="true"></span>
Add a tenant group
</a>
<a href="{% url 'tenancy:tenantgroup_import' %}" class="btn btn-info">
<span class="fa fa-download" aria-hidden="true"></span>
Import tenant groups
</a>
{% add_button 'tenancy:tenantgroup_add' %}
{% import_button 'tenancy:tenantgroup_import' %}
{% endif %}
{% export_button content_type %}
</div>
<h1>{% block title %}Tenant Groups{% endblock %}</h1>
<div class="row">

View File

@@ -1,18 +1,13 @@
{% extends '_base.html' %}
{% load buttons %}
{% block content %}
<div class="pull-right">
{% if perms.virtualization.add_cluster %}
<a href="{% url 'virtualization:cluster_add' %}" class="btn btn-primary">
<span class="fa fa-plus" aria-hidden="true"></span>
Add a cluster
</a>
<a href="{% url 'virtualization:cluster_import' %}" class="btn btn-info">
<span class="fa fa-download" aria-hidden="true"></span>
Import clusters
</a>
{% add_button 'virtualization:cluster_add' %}
{% import_button 'virtualization:cluster_import' %}
{% endif %}
{% include 'inc/export_button.html' with obj_type='clusters' %}
{% export_button content_type %}
</div>
<h1>{% block title %}Clusters{% endblock %}</h1>
<div class="row">

View File

@@ -1,18 +1,14 @@
{% extends '_base.html' %}
{% load buttons %}
{% load helpers %}
{% block content %}
<div class="pull-right">
{% if perms.virtualization.add_clustergroup %}
<a href="{% url 'virtualization:clustergroup_add' %}" class="btn btn-primary">
<span class="fa fa-plus" aria-hidden="true"></span>
Add a cluster group
</a>
<a href="{% url 'virtualization:clustergroup_import' %}" class="btn btn-info">
<span class="fa fa-download" aria-hidden="true"></span>
Import cluster groups
</a>
{% add_button 'virtualization:clustergroup_add' %}
{% import_button 'virtualization:clustergroup_import' %}
{% endif %}
{% export_button content_type %}
</div>
<h1>{% block title %}Cluster Groups{% endblock %}</h1>
<div class="row">

View File

@@ -1,18 +1,14 @@
{% extends '_base.html' %}
{% load buttons %}
{% load helpers %}
{% block content %}
<div class="pull-right">
{% if perms.virtualization.add_clustertype %}
<a href="{% url 'virtualization:clustertype_add' %}" class="btn btn-primary">
<span class="fa fa-plus" aria-hidden="true"></span>
Add a cluster type
</a>
<a href="{% url 'virtualization:clustertype_import' %}" class="btn btn-info">
<span class="fa fa-download" aria-hidden="true"></span>
Import cluster types
</a>
{% add_button 'virtualization:clustertype_add' %}
{% import_button 'virtualization:clustertype_import' %}
{% endif %}
{% export_button content_type %}
</div>
<h1>{% block title %}Cluster Types{% endblock %}</h1>
<div class="row">

View File

@@ -1,18 +1,13 @@
{% extends '_base.html' %}
{% load buttons %}
{% block content %}
<div class="pull-right">
{% if perms.virtualization.add_virtualmachine %}
<a href="{% url 'virtualization:virtualmachine_add' %}" class="btn btn-primary">
<span class="fa fa-plus" aria-hidden="true"></span>
Add a virtual machine
</a>
<a href="{% url 'virtualization:virtualmachine_import' %}" class="btn btn-info">
<span class="fa fa-download" aria-hidden="true"></span>
Import virtual machines
</a>
{% add_button 'virtualization:virtualmachine_add' %}
{% import_button 'virtualization:virtualmachine_import' %}
{% endif %}
{% include 'inc/export_button.html' with obj_type='virtual machines' %}
{% export_button content_type %}
</div>
<h1>{% block title %}Virtual Machines{% endblock %}</h1>
<div class="row">

View File

@@ -27,7 +27,7 @@ class TenantGroupCSVForm(forms.ModelForm):
class Meta:
model = TenantGroup
fields = ['name', 'slug']
fields = TenantGroup.csv_headers
help_texts = {
'name': 'Group name',
}
@@ -60,7 +60,7 @@ class TenantCSVForm(forms.ModelForm):
class Meta:
model = Tenant
fields = ['name', 'slug', 'group', 'description', 'comments']
fields = Tenant.csv_headers
help_texts = {
'name': 'Tenant name',
'comments': 'Free-form comments'

View File

@@ -7,7 +7,6 @@ from django.utils.encoding import python_2_unicode_compatible
from extras.models import CustomFieldModel, CustomFieldValue
from utilities.models import CreatedUpdatedModel
from utilities.utils import csv_format
@python_2_unicode_compatible
@@ -18,6 +17,8 @@ class TenantGroup(models.Model):
name = models.CharField(max_length=50, unique=True)
slug = models.SlugField(unique=True)
csv_headers = ['name', 'slug']
class Meta:
ordering = ['name']
@@ -27,6 +28,12 @@ class TenantGroup(models.Model):
def get_absolute_url(self):
return "{}?group={}".format(reverse('tenancy:tenant_list'), self.slug)
def to_csv(self):
return (
self.name,
self.slug,
)
@python_2_unicode_compatible
class Tenant(CreatedUpdatedModel, CustomFieldModel):
@@ -41,7 +48,7 @@ class Tenant(CreatedUpdatedModel, CustomFieldModel):
comments = models.TextField(blank=True)
custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
csv_headers = ['name', 'slug', 'group', 'description']
csv_headers = ['name', 'slug', 'group', 'description', 'comments']
class Meta:
ordering = ['group', 'name']
@@ -53,9 +60,10 @@ class Tenant(CreatedUpdatedModel, CustomFieldModel):
return reverse('tenancy:tenant', args=[self.slug])
def to_csv(self):
return csv_format([
return (
self.name,
self.slug,
self.group.name if self.group else None,
self.description,
])
self.comments,
)

View File

@@ -1,7 +1,7 @@
from __future__ import unicode_literals
import csv
import itertools
from io import StringIO
import re
from django import forms
@@ -245,14 +245,10 @@ class CSVDataField(forms.CharField):
def to_python(self, value):
# Python 2's csv module has problems with Unicode
if not isinstance(value, str):
value = value.encode('utf-8')
records = []
reader = csv.reader(value.splitlines())
reader = csv.reader(StringIO(value))
# Consume and valdiate the first line of CSV data as column headers
# Consume and validate the first line of CSV data as column headers
headers = next(reader)
for f in self.required_fields:
if f not in headers:

View File

@@ -0,0 +1,3 @@
<a href="{% url add_url %}" class="btn btn-primary">
<span class="fa fa-plus" aria-hidden="true"></span> Add
</a>

View File

@@ -0,0 +1,19 @@
{% if export_templates %}
<div class="btn-group">
<button type="button" class="btn btn-success dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="fa fa-upload" aria-hidden="true"></span>
Export <span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li><a href="?{% if url_params %}{{ url_params.urlencode }}&{% endif %}export">CSV (default)</a></li>
<li class="divider"></li>
{% for et in export_templates %}
<li><a href="?{% if url_params %}{{ url_params.urlencode }}&{% endif %}export={{ et.name }}"{% if et.description %} title="{{ et.description }}"{% endif %}>{{ et.name }}</a></li>
{% endfor %}
</ul>
</div>
{% else %}
<a href="?{% if url_params %}{{ url_params.urlencode }}&{% endif %}export" class="btn btn-success">
<span class="fa fa-upload" aria-hidden="true"></span> Export
</a>
{% endif %}

View File

@@ -0,0 +1,3 @@
<a href="{% url import_url %}" class="btn btn-info">
<span class="fa fa-download" aria-hidden="true"></span> Import
</a>

View File

@@ -0,0 +1,26 @@
from __future__ import unicode_literals
from django import template
from extras.models import ExportTemplate
register = template.Library()
@register.inclusion_tag('buttons/add.html')
def add_button(url):
return {'add_url': url}
@register.inclusion_tag('buttons/import.html')
def import_button(url):
return {'import_url': url}
@register.inclusion_tag('buttons/export.html', takes_context=True)
def export_button(context, content_type=None):
export_templates = ExportTemplate.objects.filter(content_type=content_type)
return {
'url_params': context['request'].GET,
'export_templates': export_templates,
}

View File

@@ -1,7 +1,10 @@
from __future__ import unicode_literals
import datetime
import six
from django.http import HttpResponse
def csv_format(data):
"""
@@ -15,12 +18,16 @@ def csv_format(data):
csv.append('')
continue
# Convert dates to ISO format
if isinstance(value, (datetime.date, datetime.datetime)):
value = value.isoformat()
# Force conversion to string first so we can check for any commas
if not isinstance(value, six.string_types):
value = '{}'.format(value)
# Double-quote the value if it contains a comma
if ',' in value:
if ',' in value or '\n' in value:
csv.append('"{}"'.format(value))
else:
csv.append('{}'.format(value))
@@ -28,6 +35,32 @@ def csv_format(data):
return ','.join(csv)
def queryset_to_csv(queryset):
"""
Export a queryset of objects as CSV, using the model's to_csv() method.
"""
output = []
# Start with the column headers
headers = ','.join(queryset.model.csv_headers)
output.append(headers)
# Iterate through the queryset
for obj in queryset:
data = csv_format(obj.to_csv())
output.append(data)
# Build the HTTP response
response = HttpResponse(
'\n'.join(output),
content_type='text/csv'
)
filename = 'netbox_{}.csv'.format(queryset.model._meta.verbose_name_plural)
response['Content-Disposition'] = 'attachment; filename="{}"'.format(filename)
return response
def foreground_color(bg_color):
"""
Return the ideal foreground color (black or white) for a given background color in hexadecimal RGB format.

View File

@@ -10,7 +10,6 @@ from django.core.exceptions import ValidationError
from django.db import transaction, IntegrityError
from django.db.models import ProtectedError
from django.forms import CharField, Form, ModelMultipleChoiceField, MultipleHiddenInput, Textarea
from django.http import HttpResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.template.exceptions import TemplateSyntaxError
from django.urls import reverse
@@ -21,6 +20,7 @@ from django.views.generic import View
from django_tables2 import RequestConfig
from extras.models import CustomField, CustomFieldValue, ExportTemplate, UserAction
from utilities.utils import queryset_to_csv
from utilities.forms import BootstrapMixin, CSVDataField
from .constants import M2M_FIELD_TYPES
from .error_handlers import handle_protectederror
@@ -80,7 +80,7 @@ class ObjectListView(View):
def get(self, request):
model = self.queryset.model
object_ct = ContentType.objects.get_for_model(model)
content_type = ContentType.objects.get_for_model(model)
if self.filter:
self.queryset = self.filter(request.GET, self.queryset).qs
@@ -93,27 +93,18 @@ class ObjectListView(View):
# Check for export template rendering
if request.GET.get('export'):
et = get_object_or_404(ExportTemplate, content_type=object_ct, name=request.GET.get('export'))
et = get_object_or_404(ExportTemplate, content_type=content_type, name=request.GET.get('export'))
queryset = CustomFieldQueryset(self.queryset, custom_fields) if custom_fields else self.queryset
try:
response = et.to_response(context_dict={'queryset': queryset},
filename='netbox_{}'.format(model._meta.verbose_name_plural))
return response
return et.render_to_response(queryset)
except TemplateSyntaxError:
messages.error(request, "There was an error rendering the selected export template ({})."
.format(et.name))
# Fall back to built-in CSV export
messages.error(
request,
"There was an error rendering the selected export template ({}).".format(et.name)
)
# Fall back to built-in CSV export if no template was specified
elif 'export' in request.GET and hasattr(model, 'to_csv'):
headers = getattr(model, 'csv_headers', None)
output = ','.join(headers) + '\n' if headers else ''
output += '\n'.join([obj.to_csv() for obj in self.queryset])
response = HttpResponse(
output,
content_type='text/csv'
)
response['Content-Disposition'] = 'attachment; filename="netbox_{}.csv"'\
.format(self.queryset.model._meta.verbose_name_plural)
return response
return queryset_to_csv(self.queryset)
# Provide a hook to tweak the queryset based on the request immediately prior to rendering the object list
self.queryset = self.alter_queryset(request)
@@ -135,10 +126,10 @@ class ObjectListView(View):
RequestConfig(request, paginate).configure(table)
context = {
'content_type': content_type,
'table': table,
'permissions': permissions,
'filter_form': self.filter_form(request.GET, label_suffix='') if self.filter_form else None,
'export_templates': ExportTemplate.objects.filter(content_type=object_ct),
}
context.update(self.extra_context())

View File

@@ -41,7 +41,7 @@ class ClusterTypeCSVForm(forms.ModelForm):
class Meta:
model = ClusterType
fields = ['name', 'slug']
fields = ClusterType.csv_headers
help_texts = {
'name': 'Name of cluster type',
}
@@ -64,7 +64,7 @@ class ClusterGroupCSVForm(forms.ModelForm):
class Meta:
model = ClusterGroup
fields = ['name', 'slug']
fields = ClusterGroup.csv_headers
help_texts = {
'name': 'Name of cluster group',
}
@@ -112,7 +112,7 @@ class ClusterCSVForm(forms.ModelForm):
class Meta:
model = Cluster
fields = ['name', 'type', 'group', 'site', 'comments']
fields = Cluster.csv_headers
class ClusterBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
@@ -306,7 +306,7 @@ class VirtualMachineCSVForm(forms.ModelForm):
class Meta:
model = VirtualMachine
fields = ['name', 'status', 'cluster', 'role', 'tenant', 'platform', 'vcpus', 'memory', 'disk', 'comments']
fields = VirtualMachine.csv_headers
class VirtualMachineBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):

View File

@@ -10,7 +10,6 @@ from django.utils.encoding import python_2_unicode_compatible
from dcim.models import Device
from extras.models import CustomFieldModel, CustomFieldValue
from utilities.models import CreatedUpdatedModel
from utilities.utils import csv_format
from .constants import DEVICE_STATUS_ACTIVE, VM_STATUS_CHOICES, VM_STATUS_CLASSES
@@ -31,6 +30,8 @@ class ClusterType(models.Model):
unique=True
)
csv_headers = ['name', 'slug']
class Meta:
ordering = ['name']
@@ -40,6 +41,12 @@ class ClusterType(models.Model):
def get_absolute_url(self):
return "{}?type={}".format(reverse('virtualization:cluster_list'), self.slug)
def to_csv(self):
return (
self.name,
self.slug,
)
#
# Cluster groups
@@ -58,6 +65,8 @@ class ClusterGroup(models.Model):
unique=True
)
csv_headers = ['name', 'slug']
class Meta:
ordering = ['name']
@@ -67,6 +76,12 @@ class ClusterGroup(models.Model):
def get_absolute_url(self):
return "{}?group={}".format(reverse('virtualization:cluster_list'), self.slug)
def to_csv(self):
return (
self.name,
self.slug,
)
#
# Clusters
@@ -109,9 +124,7 @@ class Cluster(CreatedUpdatedModel, CustomFieldModel):
object_id_field='obj_id'
)
csv_headers = [
'name', 'type', 'group', 'site', 'comments',
]
csv_headers = ['name', 'type', 'group', 'site', 'comments']
class Meta:
ordering = ['name']
@@ -135,13 +148,13 @@ class Cluster(CreatedUpdatedModel, CustomFieldModel):
})
def to_csv(self):
return csv_format([
return (
self.name,
self.type.name,
self.group.name if self.group else None,
self.site.name if self.site else None,
self.comments,
])
)
#
@@ -230,7 +243,7 @@ class VirtualMachine(CreatedUpdatedModel, CustomFieldModel):
)
csv_headers = [
'name', 'status', 'cluster', 'tenant', 'platform', 'vcpus', 'memory', 'disk', 'comments',
'name', 'status', 'role', 'cluster', 'tenant', 'platform', 'vcpus', 'memory', 'disk', 'comments',
]
class Meta:
@@ -243,9 +256,10 @@ class VirtualMachine(CreatedUpdatedModel, CustomFieldModel):
return reverse('virtualization:virtualmachine', args=[self.pk])
def to_csv(self):
return csv_format([
return (
self.name,
self.get_status_display(),
self.role.name if self.role else None,
self.cluster.name,
self.tenant.name if self.tenant else None,
self.platform.name if self.platform else None,
@@ -253,7 +267,7 @@ class VirtualMachine(CreatedUpdatedModel, CustomFieldModel):
self.memory,
self.disk,
self.comments,
])
)
def get_status_class(self):
return VM_STATUS_CLASSES[self.status]

View File

@@ -80,8 +80,9 @@ class ClusterGroupTable(BaseTable):
class ClusterTable(BaseTable):
pk = ToggleColumn()
name = tables.LinkColumn()
device_count = tables.Column(verbose_name='Devices')
vm_count = tables.Column(verbose_name='VMs')
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')])
device_count = tables.Column(accessor=Accessor('devices.count'), orderable=False, verbose_name='Devices')
vm_count = tables.Column(accessor=Accessor('virtual_machines.count'), orderable=False, verbose_name='VMs')
class Meta(BaseTable.Meta):
model = Cluster

View File

@@ -99,10 +99,7 @@ class ClusterGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
#
class ClusterListView(ObjectListView):
queryset = Cluster.objects.annotate(
device_count=Count('devices', distinct=True),
vm_count=Count('virtual_machines', distinct=True)
)
queryset = Cluster.objects.select_related('type', 'group')
table = tables.ClusterTable
filter = filters.ClusterFilter
filter_form = forms.ClusterFilterForm
@@ -162,10 +159,7 @@ class ClusterBulkEditView(PermissionRequiredMixin, BulkEditView):
class ClusterBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'virtualization.delete_cluster'
cls = Cluster
queryset = Cluster.objects.annotate(
device_count=Count('devices', distinct=True),
vm_count=Count('virtual_machines', distinct=True)
)
queryset = Cluster.objects.all()
table = tables.ClusterTable
default_return_url = 'virtualization:cluster_list'