Merge branch 'develop' into develop-2.3

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

View File

@ -10,24 +10,23 @@ We have established a Google Groups Mailing List for issues and general
discussion. This is the best forum for obtaining assistance with NetBox discussion. This is the best forum for obtaining assistance with NetBox
installation. You can find us [here](https://groups.google.com/forum/#!forum/netbox-discuss). installation. You can find us [here](https://groups.google.com/forum/#!forum/netbox-discuss).
### Freenode IRC ### Slack
For real-time discussion, you can join the #netbox channel on [Freenode](https://freenode.net/). For real-time discussion, you can join the #netbox Slack channel on [NetworkToCode](https://slack.networktocode.com/).
You can connect to Freenode at irc.freenode.net using an IRC client, or you can
use their [webchat client](https://webchat.freenode.net/).
## Reporting Bugs ## Reporting Bugs
* First, ensure that you've installed the [latest stable version](https://github.com/digitalocean/netbox/releases) of * First, ensure that you've installed the [latest stable version](https://github.com/digitalocean/netbox/releases)
NetBox. If you're running an older version, it's possible that the bug has of NetBox. If you're running an older version, it's possible that the bug has
already been fixed. already been fixed.
* Next, check the GitHub [issues list](https://github.com/digitalocean/netbox/issues) to see if the bug you've found has already * Next, check the GitHub [issues list](https://github.com/digitalocean/netbox/issues)
been reported. If you think you may be experiencing a reported issue that to see if the bug you've found has already been reported. If you think you may
hasn't already been resolved, please click "add a reaction" in the top right be experiencing a reported issue that hasn't already been resolved, please
corner of the issue and add a thumbs up (+1). You mightalso want to add a click "add a reaction" in the top right corner of the issue and add a thumbs
comment describing how it's affecting your installation. This will allow us to up (+1). You mightalso want to add a comment describing how it's affecting your
prioritize bugs based on how many users are affected. installation. This will allow us to prioritize bugs based on how many users are
affected.
* If you haven't found an existing issue that describes your suspected bug, * If you haven't found an existing issue that describes your suspected bug,
please inquire about it on the mailing list. **Do not** file an issue until you please inquire about it on the mailing list. **Do not** file an issue until you
@ -44,7 +43,7 @@ include:
* Please avoid prepending any sort of tag (e.g. "[Bug]") to the issue title. * Please avoid prepending any sort of tag (e.g. "[Bug]") to the issue title.
The issue will be reviewed by a moderator after submission and the appropriate The issue will be reviewed by a moderator after submission and the appropriate
labels will be applied. labels will be applied for categorization.
* Keep in mind that we prioritize bugs based on their severity and how much * Keep in mind that we prioritize bugs based on their severity and how much
work is required to resolve them. It may take some time for someone to address work is required to resolve them. It may take some time for someone to address
@ -52,15 +51,15 @@ your issue.
## Feature Requests ## Feature Requests
* First, check the GitHub [issues list](https://github.com/digitalocean/netbox/issues) to see if the feature you're requesting * First, check the GitHub [issues list](https://github.com/digitalocean/netbox/issues)
is already listed. (Be sure to search closed issues as well, since some to see if the feature you're requesting is already listed. (Be sure to search
feature requests have been rejected.) If the feature you'd like to see has closed issues as well, since some feature requests have been rejected.) If the
already been requested and is open, click "add a reaction" in the top right feature you'd like to see has already been requested and is open, click "add a
corner of the issue and add a thumbs up (+1). This ensures that the issue has reaction" in the top right corner of the issue and add a thumbs up (+1). This
a better chance of receiving attention. Also feel free to add a comment with ensures that the issue has a better chance of receiving attention. Also feel
any additional justification for the feature. (However, note that comments with free to add a comment with any additional justification for the feature.
no substance other than a "+1" will be deleted. Please use GitHub's reactions (However, note that comments with no substance other than a "+1" will be
feature to indicate your support.) deleted. Please use GitHub's reactions feature to indicate your support.)
* Due to an excessive backlog of feature requests, we are not currently * Due to an excessive backlog of feature requests, we are not currently
accepting any proposals which substantially extend NetBox's functionality accepting any proposals which substantially extend NetBox's functionality
@ -88,7 +87,7 @@ following:
* Please avoid prepending any sort of tag (e.g. "[Feature]") to the issue * Please avoid prepending any sort of tag (e.g. "[Feature]") to the issue
title. The issue will be reviewed by a moderator after submission and the title. The issue will be reviewed by a moderator after submission and the
appropriate labels will be applied. appropriate labels will be applied for categorization.
## Submitting Pull Requests ## Submitting Pull Requests
@ -109,3 +108,10 @@ these checks):
* All tests pass when run with `./manage.py test` * All tests pass when run with `./manage.py test`
* PEP 8 compliance is enforced, with the exception that lines may be * PEP 8 compliance is enforced, with the exception that lines may be
greater than 80 characters in length greater than 80 characters in length
## Commenting
Only comment on an issue if you are sharing a relevant idea or constructive
feedback. **Do not** comment on an issue just to show your support (give the
top post a :+1: instead) or ask for an ETA. These comments will be deleted to
reduce noise in the discussion.

View File

@ -1,12 +1,18 @@
![NetBox](docs/netbox_logo.png "NetBox logo") ![NetBox](docs/netbox_logo.png "NetBox logo")
NetBox is an IP address management (IPAM) and data center infrastructure management (DCIM) tool. Initially conceived by the network engineering team at [DigitalOcean](https://www.digitalocean.com/), NetBox was developed specifically to address the needs of network and infrastructure engineers. NetBox is an IP address management (IPAM) and data center infrastructure
management (DCIM) tool. Initially conceived by the network engineering team at
[DigitalOcean](https://www.digitalocean.com/), NetBox was developed specifically
to address the needs of network and infrastructure engineers.
NetBox runs as a web application atop the [Django](https://www.djangoproject.com/) Python framework with a [PostgreSQL](http://www.postgresql.org/) database. For a complete list of requirements, see `requirements.txt`. The code is available [on GitHub](https://github.com/digitalocean/netbox). NetBox runs as a web application atop the [Django](https://www.djangoproject.com/)
Python framework with a [PostgreSQL](http://www.postgresql.org/) database. For a
complete list of requirements, see `requirements.txt`. The code is available [on GitHub](https://github.com/digitalocean/netbox).
The complete documentation for NetBox can be found at [Read the Docs](http://netbox.readthedocs.io/en/stable/). The complete documentation for NetBox can be found at [Read the Docs](http://netbox.readthedocs.io/en/stable/).
Questions? Comments? Please subscribe to [the netbox-discuss mailing list](https://groups.google.com/forum/#!forum/netbox-discuss), or join us on IRC in **#netbox** on **irc.freenode.net**! Questions? Comments? Please subscribe to [the netbox-discuss mailing list](https://groups.google.com/forum/#!forum/netbox-discuss),
or join us in the #netbox Slack channel on [NetworkToCode](https://slack.networktocode.com/)!
### Build Status ### Build Status
@ -27,7 +33,9 @@ NetBox is built against both Python 2.7 and 3.5. Python 3.5 is recommended.
# Installation # Installation
Please see [the documentation](http://netbox.readthedocs.io/en/stable/) for instructions on installing NetBox. To upgrade NetBox, please download the [latest release](https://github.com/digitalocean/netbox/releases) and run `upgrade.sh`. Please see [the documentation](http://netbox.readthedocs.io/en/stable/) for
instructions on installing NetBox. To upgrade NetBox, please download the [latest release](https://github.com/digitalocean/netbox/releases)
and run `upgrade.sh`.
## Alternative Installations ## Alternative Installations

View File

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

View File

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

View File

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

View File

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

View File

@ -66,6 +66,10 @@ RACK_ROLE = """
{% endif %} {% endif %}
""" """
RACK_DEVICE_COUNT = """
<a href="{% url 'dcim:device_list' %}?rack_id={{ record.pk }}">{{ value }}</a>
"""
RACKRESERVATION_ACTIONS = """ RACKRESERVATION_ACTIONS = """
{% if perms.dcim.change_rackreservation %} {% 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> <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 %} {% 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 = """ PLATFORM_ACTIONS = """
{% if perms.dcim.change_platform %} {% 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> <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): 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') get_utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False, verbose_name='Utilization')
class Meta(RackTable.Meta): class Meta(RackTable.Meta):
fields = ( 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): class DeviceRoleTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
name = tables.LinkColumn(verbose_name='Name') name = tables.LinkColumn(verbose_name='Name')
device_count = tables.Column(verbose_name='Devices') device_count = tables.TemplateColumn(
vm_count = tables.Column(verbose_name='VMs') 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') color = tables.TemplateColumn(COLOR_LABEL, verbose_name='Label')
slug = tables.Column(verbose_name='Slug') slug = tables.Column(verbose_name='Slug')
actions = tables.TemplateColumn(template_code=DEVICEROLE_ACTIONS, attrs={'td': {'class': 'text-right'}}, actions = tables.TemplateColumn(
verbose_name='') template_code=DEVICEROLE_ACTIONS,
attrs={'td': {'class': 'text-right'}},
verbose_name=''
)
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = DeviceRole model = DeviceRole
@ -375,10 +412,18 @@ class DeviceRoleTable(BaseTable):
class PlatformTable(BaseTable): class PlatformTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
name = tables.LinkColumn(verbose_name='Name') device_count = tables.TemplateColumn(
device_count = tables.Column(verbose_name='Devices') template_code=PLATFORM_DEVICE_COUNT,
vm_count = tables.Column(verbose_name='VMs') accessor=Accessor('devices.count'),
slug = tables.Column(verbose_name='Slug') 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( actions = tables.TemplateColumn(
template_code=PLATFORM_ACTIONS, template_code=PLATFORM_ACTIONS,
attrs={'td': {'class': 'text-right'}}, attrs={'td': {'class': 'text-right'}},

View File

@ -321,7 +321,7 @@ class RackListView(ObjectListView):
).prefetch_related( ).prefetch_related(
'devices__device_type' 'devices__device_type'
).annotate( ).annotate(
device_count=Count('devices', distinct=True) device_count=Count('devices')
) )
filter = filters.RackFilter filter = filters.RackFilter
filter_form = forms.RackFilterForm filter_form = forms.RackFilterForm
@ -763,10 +763,7 @@ class DeviceBayTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
# #
class DeviceRoleListView(ObjectListView): class DeviceRoleListView(ObjectListView):
queryset = DeviceRole.objects.annotate( queryset = DeviceRole.objects.all()
device_count=Count('devices', distinct=True),
vm_count=Count('virtual_machines', distinct=True)
)
table = tables.DeviceRoleTable table = tables.DeviceRoleTable
template_name = 'dcim/devicerole_list.html' template_name = 'dcim/devicerole_list.html'
@ -804,10 +801,7 @@ class DeviceRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
# #
class PlatformListView(ObjectListView): class PlatformListView(ObjectListView):
queryset = Platform.objects.annotate( queryset = Platform.objects.all()
device_count=Count('devices', distinct=True),
vm_count=Count('virtual_machines', distinct=True)
)
table = tables.PlatformTable table = tables.PlatformTable
template_name = 'dcim/platform_list.html' 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: for cf in custom_fields:
field_name = 'cf_{}'.format(str(cf.name)) field_name = 'cf_{}'.format(str(cf.name))
initial = cf.default if not bulk_edit else None
# Integer # Integer
if cf.type == CF_TYPE_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 # Boolean
elif cf.type == CF_TYPE_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'), (1, 'True'),
(0, 'False'), (0, 'False'),
) )
if cf.default.lower() in ['true', 'yes', '1']: if initial.lower() in ['true', 'yes', '1']:
initial = 1 initial = 1
elif cf.default.lower() in ['false', 'no', '0']: elif initial.lower() in ['false', 'no', '0']:
initial = 0 initial = 0
else: else:
initial = None initial = None
field = forms.NullBooleanField(required=cf.required, initial=initial, field = forms.NullBooleanField(
widget=forms.Select(choices=choices)) required=cf.required, initial=initial, widget=forms.Select(choices=choices)
)
# Date # Date
elif cf.type == CF_TYPE_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 # Select
elif cf.type == CF_TYPE_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 # URL
elif cf.type == CF_TYPE_URL: elif cf.type == CF_TYPE_URL:
field = LaxURLField(required=cf.required, initial=cf.default) field = LaxURLField(required=cf.required, initial=initial)
# Text # Text
else: 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.model = cf
field.label = cf.label if cf.label else cf.name.replace('_', ' ').capitalize() 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): def __str__(self):
return '{}: {}'.format(self.content_type, self.name) 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 Render the template to an HTTP response, delivered as a named file attachment
""" """
template = Template(self.template_code) template = Template(self.template_code)
mime_type = 'text/plain' if not self.mime_type else self.mime_type 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 # Replace CRLF-style line terminators
output = output.replace('\r\n', '\n') output = output.replace('\r\n', '\n')
# Build the response
response = HttpResponse(output, content_type=mime_type) response = HttpResponse(output, content_type=mime_type)
if self.file_extension: filename = 'netbox_{}{}'.format(
filename += '.{}'.format(self.file_extension) queryset.model._meta.verbose_name_plural,
'.{}'.format(self.file_extension) if self.file_extension else ''
)
response['Content-Disposition'] = 'attachment; filename="{}"'.format(filename) response['Content-Disposition'] = 'attachment; filename="{}"'.format(filename)
return response return response

View File

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

View File

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

View File

@ -37,6 +37,14 @@ UTILIZATION_GRAPH = """
{% if record.pk %}{% utilization_graph record.get_utilization %}{% else %}&mdash;{% endif %} {% 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 = """ ROLE_ACTIONS = """
{% if perms.ipam.change_role %} {% 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> <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): class RoleTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
name = tables.Column(verbose_name='Name') prefix_count = tables.TemplateColumn(
prefix_count = tables.Column(accessor=Accessor('count_prefixes'), orderable=False, verbose_name='Prefixes') accessor=Accessor('prefixes.count'),
vlan_count = tables.Column(accessor=Accessor('count_vlans'), orderable=False, verbose_name='VLANs') template_code=ROLE_PREFIX_COUNT,
slug = tables.Column(verbose_name='Slug') 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='') actions = tables.TemplateColumn(template_code=ROLE_ACTIONS, attrs={'td': {'class': 'text-right'}}, verbose_name='')
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):

View File

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

View File

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

View File

@ -239,6 +239,8 @@ class SecretRole(models.Model):
users = models.ManyToManyField(User, related_name='secretroles', blank=True) users = models.ManyToManyField(User, related_name='secretroles', blank=True)
groups = models.ManyToManyField(Group, related_name='secretroles', blank=True) groups = models.ManyToManyField(Group, related_name='secretroles', blank=True)
csv_headers = ['name', 'slug']
class Meta: class Meta:
ordering = ['name'] ordering = ['name']
@ -248,6 +250,12 @@ class SecretRole(models.Model):
def get_absolute_url(self): def get_absolute_url(self):
return "{}?role={}".format(reverse('secrets:secret_list'), self.slug) return "{}?role={}".format(reverse('secrets:secret_list'), self.slug)
def to_csv(self):
return (
self.name,
self.slug,
)
def has_member(self, user): def has_member(self, user):
""" """
Check whether the given user has belongs to this SecretRole. Note that superusers belong to all roles. 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' %} {% extends '_base.html' %}
{% load buttons %}
{% load helpers %} {% load helpers %}
{% block content %} {% block content %}
<div class="pull-right"> <div class="pull-right">
{% if perms.circuits.add_circuit %} {% if perms.circuits.add_circuit %}
<a href="{% url 'circuits:circuit_add' %}" class="btn btn-primary"> {% add_button 'circuits:circuit_add' %}
<span class="fa fa-plus" aria-hidden="true"></span> {% import_button 'circuits:circuit_import' %}
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>
{% endif %} {% endif %}
{% include 'inc/export_button.html' with obj_type='circuits' %} {% export_button content_type %}
</div> </div>
<h1>{% block title %}Circuits{% endblock %}</h1> <h1>{% block title %}Circuits{% endblock %}</h1>
<div class="row"> <div class="row">

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,18 +1,13 @@
{% extends '_base.html' %} {% extends '_base.html' %}
{% load buttons %}
{% block content %} {% block content %}
<div class="pull-right"> <div class="pull-right">
{% if perms.dcim.add_site %} {% if perms.dcim.add_site %}
<a href="{% url 'dcim:site_add' %}" class="btn btn-primary"> {% add_button 'dcim:site_add' %}
<span class="fa fa-plus" aria-hidden="true"></span> {% import_button 'dcim:site_import' %}
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>
{% endif %} {% endif %}
{% include 'inc/export_button.html' with obj_type='sites' %} {% export_button content_type %}
</div> </div>
<h1>{% block title %}Sites{% endblock %}</h1> <h1>{% block title %}Sites{% endblock %}</h1>
<div class="row"> <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' %} {% extends '_base.html' %}
{% load buttons %}
{% load humanize %} {% load humanize %}
{% load helpers %} {% load helpers %}
{% block content %} {% block content %}
<div class="pull-right"> <div class="pull-right">
{% if perms.ipam.add_aggregate %} {% if perms.ipam.add_aggregate %}
<a href="{% url 'ipam:aggregate_add' %}" class="btn btn-primary"> {% add_button 'ipam:aggregate_add' %}
<span class="fa fa-plus" aria-hidden="true"></span> {% import_button 'ipam:aggregate_import' %}
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>
{% endif %} {% endif %}
{% include 'inc/export_button.html' with obj_type='aggregates' %} {% export_button content_type %}
</div> </div>
<h1>{% block title %}Aggregates{% endblock %}</h1> <h1>{% block title %}Aggregates{% endblock %}</h1>
<div class="row"> <div class="row">

View File

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

View File

@ -1,4 +1,5 @@
{% extends '_base.html' %} {% extends '_base.html' %}
{% load buttons %}
{% load helpers %} {% load helpers %}
{% load form_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> <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> </div>
{% if perms.ipam.add_prefix %} {% if perms.ipam.add_prefix %}
<a href="{% url 'ipam:prefix_add' %}" class="btn btn-primary"> {% add_button 'ipam:prefix_add' %}
<span class="fa fa-plus" aria-hidden="true"></span> {% import_button 'ipam:prefix_import' %}
Add a prefix {% endif %}
</a> {% export_button content_type %}
<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' %}
</div> </div>
<h1>{% block title %}Prefixes{% endblock %}</h1> <h1>{% block title %}Prefixes{% endblock %}</h1>
<div class="row"> <div class="row">

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import csv import csv
import itertools from io import StringIO
import re import re
from django import forms from django import forms
@ -245,14 +245,10 @@ class CSVDataField(forms.CharField):
def to_python(self, value): 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 = [] 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) headers = next(reader)
for f in self.required_fields: for f in self.required_fields:
if f not in headers: 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 from __future__ import unicode_literals
import datetime
import six import six
from django.http import HttpResponse
def csv_format(data): def csv_format(data):
""" """
@ -15,12 +18,16 @@ def csv_format(data):
csv.append('') csv.append('')
continue 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 # Force conversion to string first so we can check for any commas
if not isinstance(value, six.string_types): if not isinstance(value, six.string_types):
value = '{}'.format(value) value = '{}'.format(value)
# Double-quote the value if it contains a comma # Double-quote the value if it contains a comma
if ',' in value: if ',' in value or '\n' in value:
csv.append('"{}"'.format(value)) csv.append('"{}"'.format(value))
else: else:
csv.append('{}'.format(value)) csv.append('{}'.format(value))
@ -28,6 +35,32 @@ def csv_format(data):
return ','.join(csv) 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): def foreground_color(bg_color):
""" """
Return the ideal foreground color (black or white) for a given background color in hexadecimal RGB format. 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 import transaction, IntegrityError
from django.db.models import ProtectedError from django.db.models import ProtectedError
from django.forms import CharField, Form, ModelMultipleChoiceField, MultipleHiddenInput, Textarea 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.shortcuts import get_object_or_404, redirect, render
from django.template.exceptions import TemplateSyntaxError from django.template.exceptions import TemplateSyntaxError
from django.urls import reverse from django.urls import reverse
@ -21,6 +20,7 @@ from django.views.generic import View
from django_tables2 import RequestConfig from django_tables2 import RequestConfig
from extras.models import CustomField, CustomFieldValue, ExportTemplate, UserAction from extras.models import CustomField, CustomFieldValue, ExportTemplate, UserAction
from utilities.utils import queryset_to_csv
from utilities.forms import BootstrapMixin, CSVDataField from utilities.forms import BootstrapMixin, CSVDataField
from .constants import M2M_FIELD_TYPES from .constants import M2M_FIELD_TYPES
from .error_handlers import handle_protectederror from .error_handlers import handle_protectederror
@ -80,7 +80,7 @@ class ObjectListView(View):
def get(self, request): def get(self, request):
model = self.queryset.model model = self.queryset.model
object_ct = ContentType.objects.get_for_model(model) content_type = ContentType.objects.get_for_model(model)
if self.filter: if self.filter:
self.queryset = self.filter(request.GET, self.queryset).qs self.queryset = self.filter(request.GET, self.queryset).qs
@ -93,27 +93,18 @@ class ObjectListView(View):
# Check for export template rendering # Check for export template rendering
if request.GET.get('export'): 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 queryset = CustomFieldQueryset(self.queryset, custom_fields) if custom_fields else self.queryset
try: try:
response = et.to_response(context_dict={'queryset': queryset}, return et.render_to_response(queryset)
filename='netbox_{}'.format(model._meta.verbose_name_plural))
return response
except TemplateSyntaxError: except TemplateSyntaxError:
messages.error(request, "There was an error rendering the selected export template ({})." messages.error(
.format(et.name)) request,
# Fall back to built-in CSV export "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'): elif 'export' in request.GET and hasattr(model, 'to_csv'):
headers = getattr(model, 'csv_headers', None) return queryset_to_csv(self.queryset)
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
# Provide a hook to tweak the queryset based on the request immediately prior to rendering the object list # Provide a hook to tweak the queryset based on the request immediately prior to rendering the object list
self.queryset = self.alter_queryset(request) self.queryset = self.alter_queryset(request)
@ -135,10 +126,10 @@ class ObjectListView(View):
RequestConfig(request, paginate).configure(table) RequestConfig(request, paginate).configure(table)
context = { context = {
'content_type': content_type,
'table': table, 'table': table,
'permissions': permissions, 'permissions': permissions,
'filter_form': self.filter_form(request.GET, label_suffix='') if self.filter_form else None, '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()) context.update(self.extra_context())

View File

@ -41,7 +41,7 @@ class ClusterTypeCSVForm(forms.ModelForm):
class Meta: class Meta:
model = ClusterType model = ClusterType
fields = ['name', 'slug'] fields = ClusterType.csv_headers
help_texts = { help_texts = {
'name': 'Name of cluster type', 'name': 'Name of cluster type',
} }
@ -64,7 +64,7 @@ class ClusterGroupCSVForm(forms.ModelForm):
class Meta: class Meta:
model = ClusterGroup model = ClusterGroup
fields = ['name', 'slug'] fields = ClusterGroup.csv_headers
help_texts = { help_texts = {
'name': 'Name of cluster group', 'name': 'Name of cluster group',
} }
@ -112,7 +112,7 @@ class ClusterCSVForm(forms.ModelForm):
class Meta: class Meta:
model = Cluster model = Cluster
fields = ['name', 'type', 'group', 'site', 'comments'] fields = Cluster.csv_headers
class ClusterBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm): class ClusterBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
@ -306,7 +306,7 @@ class VirtualMachineCSVForm(forms.ModelForm):
class Meta: class Meta:
model = VirtualMachine model = VirtualMachine
fields = ['name', 'status', 'cluster', 'role', 'tenant', 'platform', 'vcpus', 'memory', 'disk', 'comments'] fields = VirtualMachine.csv_headers
class VirtualMachineBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm): 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 dcim.models import Device
from extras.models import CustomFieldModel, CustomFieldValue from extras.models import CustomFieldModel, CustomFieldValue
from utilities.models import CreatedUpdatedModel from utilities.models import CreatedUpdatedModel
from utilities.utils import csv_format
from .constants import DEVICE_STATUS_ACTIVE, VM_STATUS_CHOICES, VM_STATUS_CLASSES from .constants import DEVICE_STATUS_ACTIVE, VM_STATUS_CHOICES, VM_STATUS_CLASSES
@ -31,6 +30,8 @@ class ClusterType(models.Model):
unique=True unique=True
) )
csv_headers = ['name', 'slug']
class Meta: class Meta:
ordering = ['name'] ordering = ['name']
@ -40,6 +41,12 @@ class ClusterType(models.Model):
def get_absolute_url(self): def get_absolute_url(self):
return "{}?type={}".format(reverse('virtualization:cluster_list'), self.slug) return "{}?type={}".format(reverse('virtualization:cluster_list'), self.slug)
def to_csv(self):
return (
self.name,
self.slug,
)
# #
# Cluster groups # Cluster groups
@ -58,6 +65,8 @@ class ClusterGroup(models.Model):
unique=True unique=True
) )
csv_headers = ['name', 'slug']
class Meta: class Meta:
ordering = ['name'] ordering = ['name']
@ -67,6 +76,12 @@ class ClusterGroup(models.Model):
def get_absolute_url(self): def get_absolute_url(self):
return "{}?group={}".format(reverse('virtualization:cluster_list'), self.slug) return "{}?group={}".format(reverse('virtualization:cluster_list'), self.slug)
def to_csv(self):
return (
self.name,
self.slug,
)
# #
# Clusters # Clusters
@ -109,9 +124,7 @@ class Cluster(CreatedUpdatedModel, CustomFieldModel):
object_id_field='obj_id' object_id_field='obj_id'
) )
csv_headers = [ csv_headers = ['name', 'type', 'group', 'site', 'comments']
'name', 'type', 'group', 'site', 'comments',
]
class Meta: class Meta:
ordering = ['name'] ordering = ['name']
@ -135,13 +148,13 @@ class Cluster(CreatedUpdatedModel, CustomFieldModel):
}) })
def to_csv(self): def to_csv(self):
return csv_format([ return (
self.name, self.name,
self.type.name, self.type.name,
self.group.name if self.group else None, self.group.name if self.group else None,
self.site.name if self.site else None, self.site.name if self.site else None,
self.comments, self.comments,
]) )
# #
@ -230,7 +243,7 @@ class VirtualMachine(CreatedUpdatedModel, CustomFieldModel):
) )
csv_headers = [ csv_headers = [
'name', 'status', 'cluster', 'tenant', 'platform', 'vcpus', 'memory', 'disk', 'comments', 'name', 'status', 'role', 'cluster', 'tenant', 'platform', 'vcpus', 'memory', 'disk', 'comments',
] ]
class Meta: class Meta:
@ -243,9 +256,10 @@ class VirtualMachine(CreatedUpdatedModel, CustomFieldModel):
return reverse('virtualization:virtualmachine', args=[self.pk]) return reverse('virtualization:virtualmachine', args=[self.pk])
def to_csv(self): def to_csv(self):
return csv_format([ return (
self.name, self.name,
self.get_status_display(), self.get_status_display(),
self.role.name if self.role else None,
self.cluster.name, self.cluster.name,
self.tenant.name if self.tenant else None, self.tenant.name if self.tenant else None,
self.platform.name if self.platform else None, self.platform.name if self.platform else None,
@ -253,7 +267,7 @@ class VirtualMachine(CreatedUpdatedModel, CustomFieldModel):
self.memory, self.memory,
self.disk, self.disk,
self.comments, self.comments,
]) )
def get_status_class(self): def get_status_class(self):
return VM_STATUS_CLASSES[self.status] return VM_STATUS_CLASSES[self.status]

View File

@ -80,8 +80,9 @@ class ClusterGroupTable(BaseTable):
class ClusterTable(BaseTable): class ClusterTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
name = tables.LinkColumn() name = tables.LinkColumn()
device_count = tables.Column(verbose_name='Devices') site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')])
vm_count = tables.Column(verbose_name='VMs') 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): class Meta(BaseTable.Meta):
model = Cluster model = Cluster

View File

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