mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-23 04:22:01 -06:00
Merge branch 'develop' into develop-2.3
This commit is contained in:
commit
73c64272d8
@ -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
|
||||
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/).
|
||||
You can connect to Freenode at irc.freenode.net using an IRC client, or you can
|
||||
use their [webchat client](https://webchat.freenode.net/).
|
||||
For real-time discussion, you can join the #netbox Slack channel on [NetworkToCode](https://slack.networktocode.com/).
|
||||
|
||||
## Reporting Bugs
|
||||
|
||||
* First, ensure that you've installed the [latest stable version](https://github.com/digitalocean/netbox/releases) of
|
||||
NetBox. If you're running an older version, it's possible that the bug has
|
||||
* First, ensure that you've installed the [latest stable version](https://github.com/digitalocean/netbox/releases)
|
||||
of NetBox. If you're running an older version, it's possible that the bug has
|
||||
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
|
||||
been reported. If you think you may be experiencing a reported issue that
|
||||
hasn't already been resolved, please click "add a reaction" in the top right
|
||||
corner of the issue and add a thumbs up (+1). You mightalso want to add a
|
||||
comment describing how it's affecting your installation. This will allow us to
|
||||
prioritize bugs based on how many users are affected.
|
||||
* Next, check the GitHub [issues list](https://github.com/digitalocean/netbox/issues)
|
||||
to see if the bug you've found has already been reported. If you think you may
|
||||
be experiencing a reported issue that hasn't already been resolved, please
|
||||
click "add a reaction" in the top right corner of the issue and add a thumbs
|
||||
up (+1). You mightalso want to add a comment describing how it's affecting your
|
||||
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,
|
||||
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.
|
||||
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
|
||||
work is required to resolve them. It may take some time for someone to address
|
||||
@ -52,15 +51,15 @@ your issue.
|
||||
|
||||
## Feature Requests
|
||||
|
||||
* First, check the GitHub [issues list](https://github.com/digitalocean/netbox/issues) to see if the feature you're requesting
|
||||
is already listed. (Be sure to search closed issues as well, since some
|
||||
feature requests have been rejected.) If the feature you'd like to see has
|
||||
already been requested and is open, click "add a reaction" in the top right
|
||||
corner of the issue and add a thumbs up (+1). This ensures that the issue has
|
||||
a better chance of receiving attention. Also feel free to add a comment with
|
||||
any additional justification for the feature. (However, note that comments with
|
||||
no substance other than a "+1" will be deleted. Please use GitHub's reactions
|
||||
feature to indicate your support.)
|
||||
* First, check the GitHub [issues list](https://github.com/digitalocean/netbox/issues)
|
||||
to see if the feature you're requesting is already listed. (Be sure to search
|
||||
closed issues as well, since some feature requests have been rejected.) If the
|
||||
feature you'd like to see has already been requested and is open, click "add a
|
||||
reaction" in the top right corner of the issue and add a thumbs up (+1). This
|
||||
ensures that the issue has a better chance of receiving attention. Also feel
|
||||
free to add a comment with any additional justification for the feature.
|
||||
(However, note that comments with no substance other than a "+1" will be
|
||||
deleted. Please use GitHub's reactions feature to indicate your support.)
|
||||
|
||||
* Due to an excessive backlog of feature requests, we are not currently
|
||||
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
|
||||
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
|
||||
|
||||
@ -109,3 +108,10 @@ these checks):
|
||||
* All tests pass when run with `./manage.py test`
|
||||
* PEP 8 compliance is enforced, with the exception that lines may be
|
||||
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.
|
||||
|
16
README.md
16
README.md
@ -1,12 +1,18 @@
|
||||

|
||||
|
||||
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/).
|
||||
|
||||
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
|
||||
|
||||
@ -27,7 +33,9 @@ NetBox is built against both Python 2.7 and 3.5. Python 3.5 is recommended.
|
||||
|
||||
# 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
|
||||
|
||||
|
@ -44,7 +44,7 @@ class ProviderCSVForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = Provider
|
||||
fields = ['name', 'slug', 'asn', 'account', 'portal_url', 'comments']
|
||||
fields = Provider.csv_headers
|
||||
help_texts = {
|
||||
'name': 'Provider name',
|
||||
'asn': '32-bit autonomous system number',
|
||||
@ -90,7 +90,7 @@ class CircuitTypeCSVForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = CircuitType
|
||||
fields = ['name', 'slug']
|
||||
fields = CircuitType.csv_headers
|
||||
help_texts = {
|
||||
'name': 'Name of circuit type',
|
||||
}
|
||||
|
@ -10,7 +10,6 @@ from dcim.fields import ASNField
|
||||
from extras.models import CustomFieldModel, CustomFieldValue
|
||||
from tenancy.models import Tenant
|
||||
from utilities.models import CreatedUpdatedModel
|
||||
from utilities.utils import csv_format
|
||||
from .constants import CIRCUIT_STATUS_ACTIVE, CIRCUIT_STATUS_CHOICES, TERM_SIDE_CHOICES
|
||||
|
||||
|
||||
@ -30,7 +29,7 @@ class Provider(CreatedUpdatedModel, CustomFieldModel):
|
||||
comments = models.TextField(blank=True)
|
||||
custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
|
||||
|
||||
csv_headers = ['name', 'slug', 'asn', 'account', 'portal_url']
|
||||
csv_headers = ['name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments']
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
@ -42,13 +41,16 @@ class Provider(CreatedUpdatedModel, CustomFieldModel):
|
||||
return reverse('circuits:provider', args=[self.slug])
|
||||
|
||||
def to_csv(self):
|
||||
return csv_format([
|
||||
return (
|
||||
self.name,
|
||||
self.slug,
|
||||
self.asn,
|
||||
self.account,
|
||||
self.portal_url,
|
||||
])
|
||||
self.noc_contact,
|
||||
self.admin_contact,
|
||||
self.comments,
|
||||
)
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
@ -60,6 +62,8 @@ class CircuitType(models.Model):
|
||||
name = models.CharField(max_length=50, unique=True)
|
||||
slug = models.SlugField(unique=True)
|
||||
|
||||
csv_headers = ['name', 'slug']
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
|
||||
@ -69,6 +73,12 @@ class CircuitType(models.Model):
|
||||
def get_absolute_url(self):
|
||||
return "{}?type={}".format(reverse('circuits:circuit_list'), self.slug)
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
self.name,
|
||||
self.slug,
|
||||
)
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class Circuit(CreatedUpdatedModel, CustomFieldModel):
|
||||
@ -88,7 +98,9 @@ class Circuit(CreatedUpdatedModel, CustomFieldModel):
|
||||
comments = models.TextField(blank=True)
|
||||
custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
|
||||
|
||||
csv_headers = ['cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', 'description']
|
||||
csv_headers = [
|
||||
'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', 'description', 'comments',
|
||||
]
|
||||
|
||||
class Meta:
|
||||
ordering = ['provider', 'cid']
|
||||
@ -101,16 +113,17 @@ class Circuit(CreatedUpdatedModel, CustomFieldModel):
|
||||
return reverse('circuits:circuit', args=[self.pk])
|
||||
|
||||
def to_csv(self):
|
||||
return csv_format([
|
||||
return (
|
||||
self.cid,
|
||||
self.provider.name,
|
||||
self.type.name,
|
||||
self.get_status_display(),
|
||||
self.tenant.name if self.tenant else None,
|
||||
self.install_date.isoformat() if self.install_date else None,
|
||||
self.install_date,
|
||||
self.commit_rate,
|
||||
self.description,
|
||||
])
|
||||
self.comments,
|
||||
)
|
||||
|
||||
def get_status_class(self):
|
||||
return STATUS_CLASSES[self.status]
|
||||
|
@ -83,9 +83,7 @@ class RegionCSVForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = Region
|
||||
fields = [
|
||||
'name', 'slug', 'parent',
|
||||
]
|
||||
fields = Region.csv_headers
|
||||
help_texts = {
|
||||
'name': 'Region name',
|
||||
'slug': 'URL-friendly slug',
|
||||
@ -153,10 +151,7 @@ class SiteCSVForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = Site
|
||||
fields = [
|
||||
'name', 'slug', 'status', 'region', 'tenant', 'facility', 'asn', 'description', 'physical_address',
|
||||
'shipping_address', 'contact_name', 'contact_phone', 'contact_email', 'time_zone', 'comments',
|
||||
]
|
||||
fields = Site.csv_headers
|
||||
help_texts = {
|
||||
'name': 'Site name',
|
||||
'slug': 'URL-friendly slug',
|
||||
@ -224,9 +219,7 @@ class RackGroupCSVForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = RackGroup
|
||||
fields = [
|
||||
'site', 'name', 'slug',
|
||||
]
|
||||
fields = RackGroup.csv_headers
|
||||
help_texts = {
|
||||
'name': 'Name of rack group',
|
||||
'slug': 'URL-friendly slug',
|
||||
@ -254,7 +247,7 @@ class RackRoleCSVForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = RackRole
|
||||
fields = ['name', 'slug', 'color']
|
||||
fields = RackRole.csv_headers
|
||||
help_texts = {
|
||||
'name': 'Name of rack role',
|
||||
'color': 'RGB color in hexadecimal (e.g. 00ff00)'
|
||||
@ -341,10 +334,7 @@ class RackCSVForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = Rack
|
||||
fields = [
|
||||
'site', 'group_name', 'name', 'facility_id', 'tenant', 'role', 'serial', 'type', 'width', 'u_height',
|
||||
'desc_units',
|
||||
]
|
||||
fields = Rack.csv_headers
|
||||
help_texts = {
|
||||
'name': 'Rack name',
|
||||
'u_height': 'Height in rack units',
|
||||
@ -478,9 +468,7 @@ class ManufacturerForm(BootstrapMixin, forms.ModelForm):
|
||||
class ManufacturerCSVForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Manufacturer
|
||||
fields = [
|
||||
'name', 'slug'
|
||||
]
|
||||
fields = Manufacturer.csv_headers
|
||||
help_texts = {
|
||||
'name': 'Manufacturer name',
|
||||
'slug': 'URL-friendly slug',
|
||||
@ -526,8 +514,7 @@ class DeviceTypeCSVForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = DeviceType
|
||||
fields = ['manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'is_console_server',
|
||||
'is_pdu', 'is_network_device', 'subdevice_role', 'interface_ordering', 'comments']
|
||||
fields = DeviceType.csv_headers
|
||||
help_texts = {
|
||||
'model': 'Model name',
|
||||
'slug': 'URL-friendly slug',
|
||||
@ -692,7 +679,7 @@ class DeviceRoleCSVForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = DeviceRole
|
||||
fields = ['name', 'slug', 'color', 'vm_role']
|
||||
fields = DeviceRole.csv_headers
|
||||
help_texts = {
|
||||
'name': 'Name of device role',
|
||||
'color': 'RGB color in hexadecimal (e.g. 00ff00)'
|
||||
@ -716,7 +703,7 @@ class PlatformCSVForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = Platform
|
||||
fields = ['name', 'slug', 'manufacturer', 'napalm_driver']
|
||||
fields = Platform.csv_headers
|
||||
help_texts = {
|
||||
'name': 'Platform name',
|
||||
'manufacturer': 'Manufacturer name',
|
||||
@ -970,7 +957,7 @@ class DeviceCSVForm(BaseDeviceCSVForm):
|
||||
class Meta(BaseDeviceCSVForm.Meta):
|
||||
fields = [
|
||||
'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag', 'status',
|
||||
'site', 'rack_group', 'rack_name', 'position', 'face', 'cluster',
|
||||
'site', 'rack_group', 'rack_name', 'position', 'face', 'cluster', 'comments',
|
||||
]
|
||||
|
||||
def clean(self):
|
||||
@ -1019,7 +1006,7 @@ class ChildDeviceCSVForm(BaseDeviceCSVForm):
|
||||
class Meta(BaseDeviceCSVForm.Meta):
|
||||
fields = [
|
||||
'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag', 'status',
|
||||
'parent', 'device_bay_name', 'cluster',
|
||||
'parent', 'device_bay_name', 'cluster', 'comments',
|
||||
]
|
||||
|
||||
def clean(self):
|
||||
@ -2096,7 +2083,7 @@ class InterfaceConnectionCSVForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = InterfaceConnection
|
||||
fields = ['device_a', 'interface_a', 'device_b', 'interface_b', 'connection_status']
|
||||
fields = InterfaceConnection.csv_headers
|
||||
|
||||
def clean_interface_a(self):
|
||||
|
||||
@ -2238,7 +2225,7 @@ class InventoryItemCSVForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = InventoryItem
|
||||
fields = ['device', 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description']
|
||||
fields = InventoryItem.csv_headers
|
||||
|
||||
|
||||
class InventoryItemBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
|
@ -23,7 +23,6 @@ from tenancy.models import Tenant
|
||||
from utilities.fields import ColorField, NullableCharField
|
||||
from utilities.managers import NaturalOrderByManager
|
||||
from utilities.models import CreatedUpdatedModel
|
||||
from utilities.utils import csv_format
|
||||
from .constants import *
|
||||
from .fields import ASNField, MACAddressField
|
||||
from .querysets import InterfaceQuerySet
|
||||
@ -44,9 +43,7 @@ class Region(MPTTModel):
|
||||
name = models.CharField(max_length=50, unique=True)
|
||||
slug = models.SlugField(unique=True)
|
||||
|
||||
csv_headers = [
|
||||
'name', 'slug', 'parent',
|
||||
]
|
||||
csv_headers = ['name', 'slug', 'parent']
|
||||
|
||||
class MPTTMeta:
|
||||
order_insertion_by = ['name']
|
||||
@ -58,11 +55,11 @@ class Region(MPTTModel):
|
||||
return "{}?region={}".format(reverse('dcim:site_list'), self.slug)
|
||||
|
||||
def to_csv(self):
|
||||
return csv_format([
|
||||
return (
|
||||
self.name,
|
||||
self.slug,
|
||||
self.parent.name if self.parent else None,
|
||||
])
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
@ -102,8 +99,8 @@ class Site(CreatedUpdatedModel, CustomFieldModel):
|
||||
objects = SiteManager()
|
||||
|
||||
csv_headers = [
|
||||
'name', 'slug', 'status', 'region', 'tenant', 'facility', 'asn', 'time_zone', 'description', 'contact_name',
|
||||
'contact_phone', 'contact_email',
|
||||
'name', 'slug', 'status', 'region', 'tenant', 'facility', 'asn', 'time_zone', 'description', 'physical_address',
|
||||
'shipping_address', 'contact_name', 'contact_phone', 'contact_email', 'comments',
|
||||
]
|
||||
|
||||
class Meta:
|
||||
@ -116,7 +113,7 @@ class Site(CreatedUpdatedModel, CustomFieldModel):
|
||||
return reverse('dcim:site', args=[self.slug])
|
||||
|
||||
def to_csv(self):
|
||||
return csv_format([
|
||||
return (
|
||||
self.name,
|
||||
self.slug,
|
||||
self.get_status_display(),
|
||||
@ -126,10 +123,13 @@ class Site(CreatedUpdatedModel, CustomFieldModel):
|
||||
self.asn,
|
||||
self.time_zone,
|
||||
self.description,
|
||||
self.physical_address,
|
||||
self.shipping_address,
|
||||
self.contact_name,
|
||||
self.contact_phone,
|
||||
self.contact_email,
|
||||
])
|
||||
self.comments,
|
||||
)
|
||||
|
||||
def get_status_class(self):
|
||||
return STATUS_CLASSES[self.status]
|
||||
@ -175,9 +175,7 @@ class RackGroup(models.Model):
|
||||
slug = models.SlugField()
|
||||
site = models.ForeignKey('Site', related_name='rack_groups', on_delete=models.CASCADE)
|
||||
|
||||
csv_headers = [
|
||||
'site', 'name', 'slug',
|
||||
]
|
||||
csv_headers = ['site', 'name', 'slug']
|
||||
|
||||
class Meta:
|
||||
ordering = ['site', 'name']
|
||||
@ -193,11 +191,11 @@ class RackGroup(models.Model):
|
||||
return "{}?group_id={}".format(reverse('dcim:rack_list'), self.pk)
|
||||
|
||||
def to_csv(self):
|
||||
return csv_format([
|
||||
return (
|
||||
self.site,
|
||||
self.name,
|
||||
self.slug,
|
||||
])
|
||||
)
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
@ -209,6 +207,8 @@ class RackRole(models.Model):
|
||||
slug = models.SlugField(unique=True)
|
||||
color = ColorField()
|
||||
|
||||
csv_headers = ['name', 'slug', 'color']
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
|
||||
@ -218,6 +218,13 @@ class RackRole(models.Model):
|
||||
def get_absolute_url(self):
|
||||
return "{}?role={}".format(reverse('dcim:rack_list'), self.slug)
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
self.name,
|
||||
self.slug,
|
||||
self.color,
|
||||
)
|
||||
|
||||
|
||||
class RackManager(NaturalOrderByManager):
|
||||
|
||||
@ -253,7 +260,7 @@ class Rack(CreatedUpdatedModel, CustomFieldModel):
|
||||
|
||||
csv_headers = [
|
||||
'site', 'group_name', 'name', 'facility_id', 'tenant', 'role', 'type', 'serial', 'width', 'u_height',
|
||||
'desc_units',
|
||||
'desc_units', 'comments',
|
||||
]
|
||||
|
||||
class Meta:
|
||||
@ -303,7 +310,7 @@ class Rack(CreatedUpdatedModel, CustomFieldModel):
|
||||
Device.objects.filter(rack=self).update(site_id=self.site.pk)
|
||||
|
||||
def to_csv(self):
|
||||
return csv_format([
|
||||
return (
|
||||
self.site.name,
|
||||
self.group.name if self.group else None,
|
||||
self.name,
|
||||
@ -315,7 +322,8 @@ class Rack(CreatedUpdatedModel, CustomFieldModel):
|
||||
self.width,
|
||||
self.u_height,
|
||||
self.desc_units,
|
||||
])
|
||||
self.comments,
|
||||
)
|
||||
|
||||
@property
|
||||
def units(self):
|
||||
@ -491,9 +499,7 @@ class Manufacturer(models.Model):
|
||||
name = models.CharField(max_length=50, unique=True)
|
||||
slug = models.SlugField(unique=True)
|
||||
|
||||
csv_headers = [
|
||||
'name', 'slug',
|
||||
]
|
||||
csv_headers = ['name', 'slug']
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
@ -505,10 +511,10 @@ class Manufacturer(models.Model):
|
||||
return "{}?manufacturer={}".format(reverse('dcim:devicetype_list'), self.slug)
|
||||
|
||||
def to_csv(self):
|
||||
return csv_format([
|
||||
return (
|
||||
self.name,
|
||||
self.slug,
|
||||
])
|
||||
)
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
@ -551,7 +557,7 @@ class DeviceType(models.Model, CustomFieldModel):
|
||||
|
||||
csv_headers = [
|
||||
'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'is_console_server',
|
||||
'is_pdu', 'is_network_device', 'subdevice_role', 'interface_ordering',
|
||||
'is_pdu', 'is_network_device', 'subdevice_role', 'interface_ordering', 'comments',
|
||||
]
|
||||
|
||||
class Meta:
|
||||
@ -574,7 +580,7 @@ class DeviceType(models.Model, CustomFieldModel):
|
||||
return reverse('dcim:devicetype', args=[self.pk])
|
||||
|
||||
def to_csv(self):
|
||||
return csv_format([
|
||||
return (
|
||||
self.manufacturer.name,
|
||||
self.model,
|
||||
self.slug,
|
||||
@ -586,7 +592,8 @@ class DeviceType(models.Model, CustomFieldModel):
|
||||
self.is_network_device,
|
||||
self.get_subdevice_role_display() if self.subdevice_role else None,
|
||||
self.get_interface_ordering_display(),
|
||||
])
|
||||
self.comments,
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
|
||||
@ -766,6 +773,8 @@ class DeviceRole(models.Model):
|
||||
help_text="Virtual machines may be assigned to this role"
|
||||
)
|
||||
|
||||
csv_headers = ['name', 'slug', 'color', 'vm_role']
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
|
||||
@ -775,6 +784,14 @@ class DeviceRole(models.Model):
|
||||
def get_absolute_url(self):
|
||||
return "{}?role={}".format(reverse('dcim:device_list'), self.slug)
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
self.name,
|
||||
self.slug,
|
||||
self.color,
|
||||
self.vm_role,
|
||||
)
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class Platform(models.Model):
|
||||
@ -805,6 +822,8 @@ class Platform(models.Model):
|
||||
verbose_name="Legacy RPC client"
|
||||
)
|
||||
|
||||
csv_headers = ['name', 'slug', 'manufacturer', 'napalm_driver']
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
|
||||
@ -814,6 +833,14 @@ class Platform(models.Model):
|
||||
def get_absolute_url(self):
|
||||
return "{}?platform={}".format(reverse('dcim:device_list'), self.slug)
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
self.name,
|
||||
self.slug,
|
||||
self.manufacturer.name if self.manufacturer else None,
|
||||
self.napalm_driver,
|
||||
)
|
||||
|
||||
|
||||
class DeviceManager(NaturalOrderByManager):
|
||||
|
||||
@ -892,7 +919,7 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
|
||||
|
||||
csv_headers = [
|
||||
'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag', 'status',
|
||||
'site', 'rack_group', 'rack_name', 'position', 'face',
|
||||
'site', 'rack_group', 'rack_name', 'position', 'face', 'comments',
|
||||
]
|
||||
|
||||
class Meta:
|
||||
@ -1049,7 +1076,7 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
|
||||
Device.objects.filter(parent_bay__device=self).update(site=self.site, rack=self.rack)
|
||||
|
||||
def to_csv(self):
|
||||
return csv_format([
|
||||
return (
|
||||
self.name or '',
|
||||
self.device_role.name,
|
||||
self.tenant.name if self.tenant else None,
|
||||
@ -1064,7 +1091,8 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
|
||||
self.rack.name if self.rack else None,
|
||||
self.position,
|
||||
self.get_face_display(),
|
||||
])
|
||||
self.comments,
|
||||
)
|
||||
|
||||
@property
|
||||
def display_name(self):
|
||||
@ -1158,15 +1186,14 @@ class ConsolePort(models.Model):
|
||||
def get_absolute_url(self):
|
||||
return self.device.get_absolute_url()
|
||||
|
||||
# Used for connections export
|
||||
def to_csv(self):
|
||||
return csv_format([
|
||||
return (
|
||||
self.cs_port.device.identifier if self.cs_port else None,
|
||||
self.cs_port.name if self.cs_port else None,
|
||||
self.device.identifier,
|
||||
self.name,
|
||||
self.get_connection_status_display(),
|
||||
])
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
@ -1241,15 +1268,14 @@ class PowerPort(models.Model):
|
||||
def get_absolute_url(self):
|
||||
return self.device.get_absolute_url()
|
||||
|
||||
# Used for connections export
|
||||
def to_csv(self):
|
||||
return csv_format([
|
||||
return (
|
||||
self.power_outlet.device.identifier if self.power_outlet else None,
|
||||
self.power_outlet.name if self.power_outlet else None,
|
||||
self.device.identifier,
|
||||
self.name,
|
||||
self.get_connection_status_display(),
|
||||
])
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
@ -1501,15 +1527,14 @@ class InterfaceConnection(models.Model):
|
||||
except ObjectDoesNotExist:
|
||||
pass
|
||||
|
||||
# Used for connections export
|
||||
def to_csv(self):
|
||||
return csv_format([
|
||||
return (
|
||||
self.interface_a.device.identifier,
|
||||
self.interface_a.name,
|
||||
self.interface_b.device.identifier,
|
||||
self.interface_b.name,
|
||||
self.get_connection_status_display(),
|
||||
])
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
@ -1575,7 +1600,7 @@ class InventoryItem(models.Model):
|
||||
description = models.CharField(max_length=100, blank=True)
|
||||
|
||||
csv_headers = [
|
||||
'device', 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description',
|
||||
'device', 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered', 'description',
|
||||
]
|
||||
|
||||
class Meta:
|
||||
@ -1589,15 +1614,16 @@ class InventoryItem(models.Model):
|
||||
return self.device.get_absolute_url()
|
||||
|
||||
def to_csv(self):
|
||||
return csv_format([
|
||||
return (
|
||||
self.device.name or '{' + self.device.pk + '}',
|
||||
self.name,
|
||||
self.manufacturer.name if self.manufacturer else None,
|
||||
self.part_id,
|
||||
self.serial,
|
||||
self.asset_tag,
|
||||
self.description
|
||||
])
|
||||
self.discovered,
|
||||
self.description,
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
|
@ -66,6 +66,10 @@ RACK_ROLE = """
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
RACK_DEVICE_COUNT = """
|
||||
<a href="{% url 'dcim:device_list' %}?rack_id={{ record.pk }}">{{ value }}</a>
|
||||
"""
|
||||
|
||||
RACKRESERVATION_ACTIONS = """
|
||||
{% if perms.dcim.change_rackreservation %}
|
||||
<a href="{% url 'dcim:rackreservation_edit' pk=record.pk %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
@ -84,6 +88,22 @@ MANUFACTURER_ACTIONS = """
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
DEVICEROLE_DEVICE_COUNT = """
|
||||
<a href="{% url 'dcim:device_list' %}?role={{ record.slug }}">{{ value }}</a>
|
||||
"""
|
||||
|
||||
DEVICEROLE_VM_COUNT = """
|
||||
<a href="{% url 'virtualization:virtualmachine_list' %}?role={{ record.slug }}">{{ value }}</a>
|
||||
"""
|
||||
|
||||
PLATFORM_DEVICE_COUNT = """
|
||||
<a href="{% url 'dcim:device_list' %}?platform={{ record.slug }}">{{ value }}</a>
|
||||
"""
|
||||
|
||||
PLATFORM_VM_COUNT = """
|
||||
<a href="{% url 'virtualization:virtualmachine_list' %}?platform={{ record.slug }}">{{ value }}</a>
|
||||
"""
|
||||
|
||||
PLATFORM_ACTIONS = """
|
||||
{% if perms.dcim.change_platform %}
|
||||
<a href="{% url 'dcim:platform_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
@ -211,12 +231,16 @@ class RackTable(BaseTable):
|
||||
|
||||
|
||||
class RackDetailTable(RackTable):
|
||||
devices = tables.Column(accessor=Accessor('device_count'))
|
||||
device_count = tables.TemplateColumn(
|
||||
template_code=RACK_DEVICE_COUNT,
|
||||
verbose_name='Devices'
|
||||
)
|
||||
get_utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False, verbose_name='Utilization')
|
||||
|
||||
class Meta(RackTable.Meta):
|
||||
fields = (
|
||||
'pk', 'name', 'site', 'group', 'facility_id', 'tenant', 'role', 'u_height', 'devices', 'get_utilization'
|
||||
'pk', 'name', 'site', 'group', 'facility_id', 'tenant', 'role', 'u_height', 'device_count',
|
||||
'get_utilization',
|
||||
)
|
||||
|
||||
|
||||
@ -357,12 +381,25 @@ class DeviceBayTemplateTable(BaseTable):
|
||||
class DeviceRoleTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.LinkColumn(verbose_name='Name')
|
||||
device_count = tables.Column(verbose_name='Devices')
|
||||
vm_count = tables.Column(verbose_name='VMs')
|
||||
device_count = tables.TemplateColumn(
|
||||
template_code=DEVICEROLE_DEVICE_COUNT,
|
||||
accessor=Accessor('devices.count'),
|
||||
orderable=False,
|
||||
verbose_name='Devices'
|
||||
)
|
||||
vm_count = tables.TemplateColumn(
|
||||
template_code=DEVICEROLE_VM_COUNT,
|
||||
accessor=Accessor('virtual_machines.count'),
|
||||
orderable=False,
|
||||
verbose_name='VMs'
|
||||
)
|
||||
color = tables.TemplateColumn(COLOR_LABEL, verbose_name='Label')
|
||||
slug = tables.Column(verbose_name='Slug')
|
||||
actions = tables.TemplateColumn(template_code=DEVICEROLE_ACTIONS, attrs={'td': {'class': 'text-right'}},
|
||||
verbose_name='')
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=DEVICEROLE_ACTIONS,
|
||||
attrs={'td': {'class': 'text-right'}},
|
||||
verbose_name=''
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = DeviceRole
|
||||
@ -375,10 +412,18 @@ class DeviceRoleTable(BaseTable):
|
||||
|
||||
class PlatformTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.LinkColumn(verbose_name='Name')
|
||||
device_count = tables.Column(verbose_name='Devices')
|
||||
vm_count = tables.Column(verbose_name='VMs')
|
||||
slug = tables.Column(verbose_name='Slug')
|
||||
device_count = tables.TemplateColumn(
|
||||
template_code=PLATFORM_DEVICE_COUNT,
|
||||
accessor=Accessor('devices.count'),
|
||||
orderable=False,
|
||||
verbose_name='Devices'
|
||||
)
|
||||
vm_count = tables.TemplateColumn(
|
||||
template_code=PLATFORM_VM_COUNT,
|
||||
accessor=Accessor('virtual_machines.count'),
|
||||
orderable=False,
|
||||
verbose_name='VMs'
|
||||
)
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=PLATFORM_ACTIONS,
|
||||
attrs={'td': {'class': 'text-right'}},
|
||||
|
@ -321,7 +321,7 @@ class RackListView(ObjectListView):
|
||||
).prefetch_related(
|
||||
'devices__device_type'
|
||||
).annotate(
|
||||
device_count=Count('devices', distinct=True)
|
||||
device_count=Count('devices')
|
||||
)
|
||||
filter = filters.RackFilter
|
||||
filter_form = forms.RackFilterForm
|
||||
@ -763,10 +763,7 @@ class DeviceBayTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
#
|
||||
|
||||
class DeviceRoleListView(ObjectListView):
|
||||
queryset = DeviceRole.objects.annotate(
|
||||
device_count=Count('devices', distinct=True),
|
||||
vm_count=Count('virtual_machines', distinct=True)
|
||||
)
|
||||
queryset = DeviceRole.objects.all()
|
||||
table = tables.DeviceRoleTable
|
||||
template_name = 'dcim/devicerole_list.html'
|
||||
|
||||
@ -804,10 +801,7 @@ class DeviceRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
#
|
||||
|
||||
class PlatformListView(ObjectListView):
|
||||
queryset = Platform.objects.annotate(
|
||||
device_count=Count('devices', distinct=True),
|
||||
vm_count=Count('virtual_machines', distinct=True)
|
||||
)
|
||||
queryset = Platform.objects.all()
|
||||
table = tables.PlatformTable
|
||||
template_name = 'dcim/platform_list.html'
|
||||
|
||||
|
@ -22,10 +22,11 @@ def get_custom_fields_for_model(content_type, filterable_only=False, bulk_edit=F
|
||||
|
||||
for cf in custom_fields:
|
||||
field_name = 'cf_{}'.format(str(cf.name))
|
||||
initial = cf.default if not bulk_edit else None
|
||||
|
||||
# Integer
|
||||
if cf.type == CF_TYPE_INTEGER:
|
||||
field = forms.IntegerField(required=cf.required, initial=cf.default)
|
||||
field = forms.IntegerField(required=cf.required, initial=initial)
|
||||
|
||||
# Boolean
|
||||
elif cf.type == CF_TYPE_BOOLEAN:
|
||||
@ -34,18 +35,19 @@ def get_custom_fields_for_model(content_type, filterable_only=False, bulk_edit=F
|
||||
(1, 'True'),
|
||||
(0, 'False'),
|
||||
)
|
||||
if cf.default.lower() in ['true', 'yes', '1']:
|
||||
if initial.lower() in ['true', 'yes', '1']:
|
||||
initial = 1
|
||||
elif cf.default.lower() in ['false', 'no', '0']:
|
||||
elif initial.lower() in ['false', 'no', '0']:
|
||||
initial = 0
|
||||
else:
|
||||
initial = None
|
||||
field = forms.NullBooleanField(required=cf.required, initial=initial,
|
||||
widget=forms.Select(choices=choices))
|
||||
field = forms.NullBooleanField(
|
||||
required=cf.required, initial=initial, widget=forms.Select(choices=choices)
|
||||
)
|
||||
|
||||
# Date
|
||||
elif cf.type == CF_TYPE_DATE:
|
||||
field = forms.DateField(required=cf.required, initial=cf.default, help_text="Date format: YYYY-MM-DD")
|
||||
field = forms.DateField(required=cf.required, initial=initial, help_text="Date format: YYYY-MM-DD")
|
||||
|
||||
# Select
|
||||
elif cf.type == CF_TYPE_SELECT:
|
||||
@ -56,11 +58,11 @@ def get_custom_fields_for_model(content_type, filterable_only=False, bulk_edit=F
|
||||
|
||||
# URL
|
||||
elif cf.type == CF_TYPE_URL:
|
||||
field = LaxURLField(required=cf.required, initial=cf.default)
|
||||
field = LaxURLField(required=cf.required, initial=initial)
|
||||
|
||||
# Text
|
||||
else:
|
||||
field = forms.CharField(max_length=255, required=cf.required, initial=cf.default)
|
||||
field = forms.CharField(max_length=255, required=cf.required, initial=initial)
|
||||
|
||||
field.model = cf
|
||||
field.label = cf.label if cf.label else cf.name.replace('_', ' ').capitalize()
|
||||
|
@ -223,19 +223,25 @@ class ExportTemplate(models.Model):
|
||||
def __str__(self):
|
||||
return '{}: {}'.format(self.content_type, self.name)
|
||||
|
||||
def to_response(self, context_dict, filename):
|
||||
def render_to_response(self, queryset):
|
||||
"""
|
||||
Render the template to an HTTP response, delivered as a named file attachment
|
||||
"""
|
||||
template = Template(self.template_code)
|
||||
mime_type = 'text/plain' if not self.mime_type else self.mime_type
|
||||
output = template.render(Context(context_dict))
|
||||
output = template.render(Context({'queryset': queryset}))
|
||||
|
||||
# Replace CRLF-style line terminators
|
||||
output = output.replace('\r\n', '\n')
|
||||
|
||||
# Build the response
|
||||
response = HttpResponse(output, content_type=mime_type)
|
||||
if self.file_extension:
|
||||
filename += '.{}'.format(self.file_extension)
|
||||
filename = 'netbox_{}{}'.format(
|
||||
queryset.model._meta.verbose_name_plural,
|
||||
'.{}'.format(self.file_extension) if self.file_extension else ''
|
||||
)
|
||||
response['Content-Disposition'] = 'attachment; filename="{}"'.format(filename)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
|
@ -57,7 +57,7 @@ class VRFCSVForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = VRF
|
||||
fields = ['name', 'rd', 'tenant', 'enforce_unique', 'description']
|
||||
fields = VRF.csv_headers
|
||||
help_texts = {
|
||||
'name': 'VRF name',
|
||||
}
|
||||
@ -102,7 +102,7 @@ class RIRCSVForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = RIR
|
||||
fields = ['name', 'slug', 'is_private']
|
||||
fields = RIR.csv_headers
|
||||
help_texts = {
|
||||
'name': 'RIR name',
|
||||
}
|
||||
@ -144,7 +144,7 @@ class AggregateCSVForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = Aggregate
|
||||
fields = ['prefix', 'rir', 'date_added', 'description']
|
||||
fields = Aggregate.csv_headers
|
||||
|
||||
|
||||
class AggregateBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
@ -185,7 +185,7 @@ class RoleCSVForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = Role
|
||||
fields = ['name', 'slug']
|
||||
fields = Role.csv_headers
|
||||
help_texts = {
|
||||
'name': 'Role name',
|
||||
}
|
||||
@ -299,9 +299,7 @@ class PrefixCSVForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = Prefix
|
||||
fields = [
|
||||
'prefix', 'vrf', 'tenant', 'site', 'vlan_group', 'vlan_vid', 'status', 'role', 'is_pool', 'description',
|
||||
]
|
||||
fields = Prefix.csv_headers
|
||||
|
||||
def clean(self):
|
||||
|
||||
@ -609,10 +607,7 @@ class IPAddressCSVForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = IPAddress
|
||||
fields = [
|
||||
'address', 'vrf', 'tenant', 'status', 'role', 'device', 'virtual_machine', 'interface_name', 'is_primary',
|
||||
'description',
|
||||
]
|
||||
fields = IPAddress.csv_headers
|
||||
|
||||
def clean(self):
|
||||
|
||||
@ -759,7 +754,7 @@ class VLANGroupCSVForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = VLANGroup
|
||||
fields = ['site', 'name', 'slug']
|
||||
fields = VLANGroup.csv_headers
|
||||
help_texts = {
|
||||
'name': 'Name of VLAN group',
|
||||
}
|
||||
@ -849,7 +844,7 @@ class VLANCSVForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = VLAN
|
||||
fields = ['site', 'group_name', 'vid', 'name', 'tenant', 'status', 'role', 'description']
|
||||
fields = VLAN.csv_headers
|
||||
help_texts = {
|
||||
'vid': 'Numeric VLAN ID (1-4095)',
|
||||
'name': 'VLAN name',
|
||||
|
@ -14,7 +14,6 @@ from dcim.models import Interface
|
||||
from extras.models import CustomFieldModel, CustomFieldValue
|
||||
from tenancy.models import Tenant
|
||||
from utilities.models import CreatedUpdatedModel
|
||||
from utilities.utils import csv_format
|
||||
from .constants import *
|
||||
from .fields import IPNetworkField, IPAddressField
|
||||
from .querysets import PrefixQuerySet
|
||||
@ -49,13 +48,13 @@ class VRF(CreatedUpdatedModel, CustomFieldModel):
|
||||
return reverse('ipam:vrf', args=[self.pk])
|
||||
|
||||
def to_csv(self):
|
||||
return csv_format([
|
||||
return (
|
||||
self.name,
|
||||
self.rd,
|
||||
self.tenant.name if self.tenant else None,
|
||||
self.enforce_unique,
|
||||
self.description,
|
||||
])
|
||||
)
|
||||
|
||||
@property
|
||||
def display_name(self):
|
||||
@ -75,6 +74,8 @@ class RIR(models.Model):
|
||||
is_private = models.BooleanField(default=False, verbose_name='Private',
|
||||
help_text='IP space managed by this RIR is considered private')
|
||||
|
||||
csv_headers = ['name', 'slug', 'is_private']
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
verbose_name = 'RIR'
|
||||
@ -86,6 +87,13 @@ class RIR(models.Model):
|
||||
def get_absolute_url(self):
|
||||
return "{}?rir={}".format(reverse('ipam:aggregate_list'), self.slug)
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
self.name,
|
||||
self.slug,
|
||||
self.is_private,
|
||||
)
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class Aggregate(CreatedUpdatedModel, CustomFieldModel):
|
||||
@ -147,12 +155,12 @@ class Aggregate(CreatedUpdatedModel, CustomFieldModel):
|
||||
super(Aggregate, self).save(*args, **kwargs)
|
||||
|
||||
def to_csv(self):
|
||||
return csv_format([
|
||||
return (
|
||||
self.prefix,
|
||||
self.rir.name,
|
||||
self.date_added.isoformat() if self.date_added else None,
|
||||
self.date_added,
|
||||
self.description,
|
||||
])
|
||||
)
|
||||
|
||||
def get_utilization(self):
|
||||
"""
|
||||
@ -173,19 +181,20 @@ class Role(models.Model):
|
||||
slug = models.SlugField(unique=True)
|
||||
weight = models.PositiveSmallIntegerField(default=1000)
|
||||
|
||||
csv_headers = ['name', 'slug', 'weight']
|
||||
|
||||
class Meta:
|
||||
ordering = ['weight', 'name']
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@property
|
||||
def count_prefixes(self):
|
||||
return self.prefixes.count()
|
||||
|
||||
@property
|
||||
def count_vlans(self):
|
||||
return self.vlans.count()
|
||||
def to_csv(self):
|
||||
return (
|
||||
self.name,
|
||||
self.slug,
|
||||
self.weight,
|
||||
)
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
@ -262,7 +271,7 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel):
|
||||
super(Prefix, self).save(*args, **kwargs)
|
||||
|
||||
def to_csv(self):
|
||||
return csv_format([
|
||||
return (
|
||||
self.prefix,
|
||||
self.vrf.rd if self.vrf else None,
|
||||
self.tenant.name if self.tenant else None,
|
||||
@ -273,7 +282,7 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel):
|
||||
self.role.name if self.role else None,
|
||||
self.is_pool,
|
||||
self.description,
|
||||
])
|
||||
)
|
||||
|
||||
def get_status_class(self):
|
||||
return STATUS_CHOICE_CLASSES[self.status]
|
||||
@ -461,7 +470,7 @@ class IPAddress(CreatedUpdatedModel, CustomFieldModel):
|
||||
else:
|
||||
is_primary = False
|
||||
|
||||
return csv_format([
|
||||
return (
|
||||
self.address,
|
||||
self.vrf.rd if self.vrf else None,
|
||||
self.tenant.name if self.tenant else None,
|
||||
@ -472,7 +481,7 @@ class IPAddress(CreatedUpdatedModel, CustomFieldModel):
|
||||
self.interface.name if self.interface else None,
|
||||
is_primary,
|
||||
self.description,
|
||||
])
|
||||
)
|
||||
|
||||
@property
|
||||
def device(self):
|
||||
@ -502,6 +511,8 @@ class VLANGroup(models.Model):
|
||||
slug = models.SlugField()
|
||||
site = models.ForeignKey('dcim.Site', related_name='vlan_groups', on_delete=models.PROTECT, blank=True, null=True)
|
||||
|
||||
csv_headers = ['name', 'slug', 'site']
|
||||
|
||||
class Meta:
|
||||
ordering = ['site', 'name']
|
||||
unique_together = [
|
||||
@ -517,6 +528,13 @@ class VLANGroup(models.Model):
|
||||
def get_absolute_url(self):
|
||||
return "{}?group_id={}".format(reverse('ipam:vlan_list'), self.pk)
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
self.name,
|
||||
self.slug,
|
||||
self.site.name if self.site else None,
|
||||
)
|
||||
|
||||
def get_next_available_vid(self):
|
||||
"""
|
||||
Return the first available VLAN ID (1-4094) in the group.
|
||||
@ -577,7 +595,7 @@ class VLAN(CreatedUpdatedModel, CustomFieldModel):
|
||||
})
|
||||
|
||||
def to_csv(self):
|
||||
return csv_format([
|
||||
return (
|
||||
self.site.name if self.site else None,
|
||||
self.group.name if self.group else None,
|
||||
self.vid,
|
||||
@ -586,7 +604,7 @@ class VLAN(CreatedUpdatedModel, CustomFieldModel):
|
||||
self.get_status_display(),
|
||||
self.role.name if self.role else None,
|
||||
self.description,
|
||||
])
|
||||
)
|
||||
|
||||
@property
|
||||
def display_name(self):
|
||||
|
@ -37,6 +37,14 @@ UTILIZATION_GRAPH = """
|
||||
{% if record.pk %}{% utilization_graph record.get_utilization %}{% else %}—{% endif %}
|
||||
"""
|
||||
|
||||
ROLE_PREFIX_COUNT = """
|
||||
<a href="{% url 'ipam:prefix_list' %}?role={{ record.slug }}">{{ value }}</a>
|
||||
"""
|
||||
|
||||
ROLE_VLAN_COUNT = """
|
||||
<a href="{% url 'ipam:vlan_list' %}?role={{ record.slug }}">{{ value }}</a>
|
||||
"""
|
||||
|
||||
ROLE_ACTIONS = """
|
||||
{% if perms.ipam.change_role %}
|
||||
<a href="{% url 'ipam:role_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
@ -220,10 +228,18 @@ class AggregateDetailTable(AggregateTable):
|
||||
|
||||
class RoleTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.Column(verbose_name='Name')
|
||||
prefix_count = tables.Column(accessor=Accessor('count_prefixes'), orderable=False, verbose_name='Prefixes')
|
||||
vlan_count = tables.Column(accessor=Accessor('count_vlans'), orderable=False, verbose_name='VLANs')
|
||||
slug = tables.Column(verbose_name='Slug')
|
||||
prefix_count = tables.TemplateColumn(
|
||||
accessor=Accessor('prefixes.count'),
|
||||
template_code=ROLE_PREFIX_COUNT,
|
||||
orderable=False,
|
||||
verbose_name='Prefixes'
|
||||
)
|
||||
vlan_count = tables.TemplateColumn(
|
||||
accessor=Accessor('vlans.count'),
|
||||
template_code=ROLE_VLAN_COUNT,
|
||||
orderable=False,
|
||||
verbose_name='VLANs'
|
||||
)
|
||||
actions = tables.TemplateColumn(template_code=ROLE_ACTIONS, attrs={'td': {'class': 'text-right'}}, verbose_name='')
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
|
@ -119,7 +119,7 @@ SEARCH_TYPES = OrderedDict((
|
||||
}),
|
||||
# Virtualization
|
||||
('cluster', {
|
||||
'queryset': Cluster.objects.all(),
|
||||
'queryset': Cluster.objects.select_related('type', 'group'),
|
||||
'filter': ClusterFilter,
|
||||
'table': ClusterTable,
|
||||
'url': 'virtualization:cluster_list',
|
||||
|
@ -47,7 +47,7 @@ class SecretRoleCSVForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = SecretRole
|
||||
fields = ['name', 'slug']
|
||||
fields = SecretRole.csv_headers
|
||||
help_texts = {
|
||||
'name': 'Name of secret role',
|
||||
}
|
||||
@ -98,7 +98,7 @@ class SecretCSVForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = Secret
|
||||
fields = ['device', 'role', 'name', 'plaintext']
|
||||
fields = Secret.csv_headers
|
||||
help_texts = {
|
||||
'name': 'Name or username',
|
||||
}
|
||||
|
@ -239,6 +239,8 @@ class SecretRole(models.Model):
|
||||
users = models.ManyToManyField(User, related_name='secretroles', blank=True)
|
||||
groups = models.ManyToManyField(Group, related_name='secretroles', blank=True)
|
||||
|
||||
csv_headers = ['name', 'slug']
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
|
||||
@ -248,6 +250,12 @@ class SecretRole(models.Model):
|
||||
def get_absolute_url(self):
|
||||
return "{}?role={}".format(reverse('secrets:secret_list'), self.slug)
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
self.name,
|
||||
self.slug,
|
||||
)
|
||||
|
||||
def has_member(self, user):
|
||||
"""
|
||||
Check whether the given user has belongs to this SecretRole. Note that superusers belong to all roles.
|
||||
|
@ -1,19 +1,14 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load buttons %}
|
||||
{% load helpers %}
|
||||
|
||||
{% block content %}
|
||||
<div class="pull-right">
|
||||
{% if perms.circuits.add_circuit %}
|
||||
<a href="{% url 'circuits:circuit_add' %}" class="btn btn-primary">
|
||||
<span class="fa fa-plus" aria-hidden="true"></span>
|
||||
Add a circuit
|
||||
</a>
|
||||
<a href="{% url 'circuits:circuit_import' %}" class="btn btn-info">
|
||||
<span class="fa fa-download" aria-hidden="true"></span>
|
||||
Import circuits
|
||||
</a>
|
||||
{% add_button 'circuits:circuit_add' %}
|
||||
{% import_button 'circuits:circuit_import' %}
|
||||
{% endif %}
|
||||
{% include 'inc/export_button.html' with obj_type='circuits' %}
|
||||
{% export_button content_type %}
|
||||
</div>
|
||||
<h1>{% block title %}Circuits{% endblock %}</h1>
|
||||
<div class="row">
|
||||
|
@ -1,18 +1,14 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load buttons %}
|
||||
{% load helpers %}
|
||||
|
||||
{% block content %}
|
||||
<div class="pull-right">
|
||||
{% if perms.circuits.add_circuittype %}
|
||||
<a href="{% url 'circuits:circuittype_add' %}" class="btn btn-primary">
|
||||
<span class="fa fa-plus" aria-hidden="true"></span>
|
||||
Add a circuit type
|
||||
</a>
|
||||
<a href="{% url 'circuits:circuittype_import' %}" class="btn btn-info">
|
||||
<span class="fa fa-download" aria-hidden="true"></span>
|
||||
Import circuit types
|
||||
</a>
|
||||
{% add_button 'circuits:circuittype_add' %}
|
||||
{% import_button 'circuits:circuittype_import' %}
|
||||
{% endif %}
|
||||
{% export_button content_type %}
|
||||
</div>
|
||||
<h1>{% block title %}Circuit Types{% endblock %}</h1>
|
||||
<div class="row">
|
||||
|
@ -1,18 +1,13 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load buttons %}
|
||||
|
||||
{% block content %}
|
||||
<div class="pull-right">
|
||||
{% if perms.circuits.add_provider %}
|
||||
<a href="{% url 'circuits:provider_add' %}" class="btn btn-primary">
|
||||
<span class="fa fa-plus" aria-hidden="true"></span>
|
||||
Add a provider
|
||||
</a>
|
||||
<a href="{% url 'circuits:provider_import' %}" class="btn btn-info">
|
||||
<span class="fa fa-download" aria-hidden="true"></span>
|
||||
Import providers
|
||||
</a>
|
||||
{% add_button 'circuits:provider_add' %}
|
||||
{% import_button 'circuits:provider_import' %}
|
||||
{% endif %}
|
||||
{% include 'inc/export_button.html' with obj_type='providers' %}
|
||||
{% export_button content_type %}
|
||||
</div>
|
||||
<h1>{% block title %}Providers{% endblock %}</h1>
|
||||
<div class="row">
|
||||
|
@ -1,14 +1,12 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load buttons %}
|
||||
|
||||
{% block content %}
|
||||
<div class="pull-right">
|
||||
{% if perms.dcim.change_consoleport %}
|
||||
<a href="{% url 'dcim:console_connections_import' %}" class="btn btn-info">
|
||||
<span class="fa fa-download" aria-hidden="true"></span>
|
||||
Import connections
|
||||
</a>
|
||||
{% import_button 'dcim:console_connections_import' %}
|
||||
{% endif %}
|
||||
{% include 'inc/export_button.html' with obj_type='connections' %}
|
||||
{% export_button content_type %}
|
||||
</div>
|
||||
<h1>{% block title %}Console Connections{% endblock %}</h1>
|
||||
<div class="row">
|
||||
|
@ -1,19 +1,14 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load buttons %}
|
||||
{% load helpers %}
|
||||
|
||||
{% block content %}
|
||||
<div class="pull-right">
|
||||
{% if perms.dcim.add_device %}
|
||||
<a href="{% url 'dcim:device_add' %}" class="btn btn-primary">
|
||||
<span class="fa fa-plus" aria-hidden="true"></span>
|
||||
Add a device
|
||||
</a>
|
||||
<a href="{% url 'dcim:device_import' %}" class="btn btn-info">
|
||||
<span class="fa fa-download" aria-hidden="true"></span>
|
||||
Import devices
|
||||
</a>
|
||||
{% add_button 'dcim:device_add' %}
|
||||
{% import_button 'dcim:device_import' %}
|
||||
{% endif %}
|
||||
{% include 'inc/export_button.html' with obj_type='devices' %}
|
||||
{% export_button content_type %}
|
||||
</div>
|
||||
<h1>{% block title %}Devices{% endblock %}</h1>
|
||||
<div class="row">
|
||||
|
@ -1,18 +1,14 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load buttons %}
|
||||
{% load helpers %}
|
||||
|
||||
{% block content %}
|
||||
<div class="pull-right">
|
||||
{% if perms.dcim.add_devicerole %}
|
||||
<a href="{% url 'dcim:devicerole_add' %}" class="btn btn-primary">
|
||||
<span class="fa fa-plus" aria-hidden="true"></span>
|
||||
Add a device role
|
||||
</a>
|
||||
<a href="{% url 'dcim:devicerole_import' %}" class="btn btn-info">
|
||||
<span class="fa fa-download" aria-hidden="true"></span>
|
||||
Import device roles
|
||||
</a>
|
||||
{% add_button 'dcim:devicerole_add' %}
|
||||
{% import_button 'dcim:devicerole_import' %}
|
||||
{% endif %}
|
||||
{% export_button content_type %}
|
||||
</div>
|
||||
<h1>{% block title %}Device Roles{% endblock %}</h1>
|
||||
<div class="row">
|
||||
|
@ -1,19 +1,14 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load buttons %}
|
||||
{% load helpers %}
|
||||
|
||||
{% block content %}
|
||||
<div class="pull-right">
|
||||
{% if perms.dcim.add_devicetype %}
|
||||
<a href="{% url 'dcim:devicetype_add' %}" class="btn btn-primary">
|
||||
<span class="fa fa-plus" aria-hidden="true"></span>
|
||||
Add a device type
|
||||
</a>
|
||||
<a href="{% url 'dcim:devicetype_import' %}" class="btn btn-info">
|
||||
<span class="fa fa-download" aria-hidden="true"></span>
|
||||
Import device types
|
||||
</a>
|
||||
{% add_button 'dcim:devicetype_add' %}
|
||||
{% import_button 'dcim:devicetype_import' %}
|
||||
{% endif %}
|
||||
{% include 'inc/export_button.html' with obj_type='device types' %}
|
||||
{% export_button content_type %}
|
||||
</div>
|
||||
<h1>{% block title %}Device Types{% endblock %}</h1>
|
||||
<div class="row">
|
||||
|
@ -1,14 +1,12 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load buttons %}
|
||||
|
||||
{% block content %}
|
||||
<div class="pull-right">
|
||||
{% if perms.dcim.add_interfaceconnection %}
|
||||
<a href="{% url 'dcim:interface_connections_import' %}" class="btn btn-info">
|
||||
<span class="fa fa-download" aria-hidden="true"></span>
|
||||
Import connections
|
||||
</a>
|
||||
{% import_button 'dcim:interface_connections_import' %}
|
||||
{% endif %}
|
||||
{% include 'inc/export_button.html' with obj_type='connections' %}
|
||||
{% export_button content_type %}
|
||||
</div>
|
||||
<h1>{% block title %}Interface Connections{% endblock %}</h1>
|
||||
<div class="row">
|
||||
|
@ -1,15 +1,13 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load buttons %}
|
||||
{% load helpers %}
|
||||
|
||||
{% block content %}
|
||||
<div class="pull-right">
|
||||
{% if perms.dcim.add_devicetype %}
|
||||
<a href="{% url 'dcim:inventoryitem_import' %}" class="btn btn-info">
|
||||
<span class="fa fa-download" aria-hidden="true"></span>
|
||||
Import inventory items
|
||||
</a>
|
||||
{% import_button 'dcim:inventoryitem_import' %}
|
||||
{% endif %}
|
||||
{% include 'inc/export_button.html' with obj_type='inventory items' %}
|
||||
{% export_button content_type %}
|
||||
</div>
|
||||
<h1>{% block title %}Inventory Items{% endblock %}</h1>
|
||||
<div class="row">
|
||||
|
@ -1,19 +1,14 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load buttons %}
|
||||
{% load helpers %}
|
||||
|
||||
{% block content %}
|
||||
<div class="pull-right">
|
||||
{% if perms.dcim.add_manufacturer %}
|
||||
<a href="{% url 'dcim:manufacturer_add' %}" class="btn btn-primary">
|
||||
<span class="fa fa-plus" aria-hidden="true"></span>
|
||||
Add a manufacturer
|
||||
</a>
|
||||
<a href="{% url 'dcim:manufacturer_import' %}" class="btn btn-info">
|
||||
<span class="fa fa-download" aria-hidden="true"></span>
|
||||
Import manufacturers
|
||||
</a>
|
||||
{% add_button 'dcim:manufacturer_add' %}
|
||||
{% import_button 'dcim:manufacturer_import' %}
|
||||
{% endif %}
|
||||
{% include 'inc/export_button.html' with obj_type='manufacturers' %}
|
||||
{% export_button content_type %}
|
||||
</div>
|
||||
<h1>{% block title %}Manufacturers{% endblock %}</h1>
|
||||
<div class="row">
|
||||
|
@ -1,18 +1,14 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load buttons %}
|
||||
{% load helpers %}
|
||||
|
||||
{% block content %}
|
||||
<div class="pull-right">
|
||||
{% if perms.dcim.add_platform %}
|
||||
<a href="{% url 'dcim:platform_add' %}" class="btn btn-primary">
|
||||
<span class="fa fa-plus" aria-hidden="true"></span>
|
||||
Add a platform
|
||||
</a>
|
||||
<a href="{% url 'dcim:platform_import' %}" class="btn btn-info">
|
||||
<span class="fa fa-download" aria-hidden="true"></span>
|
||||
Import platforms
|
||||
</a>
|
||||
{% add_button 'dcim:platform_add' %}
|
||||
{% import_button 'dcim:platform_import' %}
|
||||
{% endif %}
|
||||
{% export_button content_type %}
|
||||
</div>
|
||||
<h1>{% block title %}Platforms{% endblock %}</h1>
|
||||
<div class="row">
|
||||
|
@ -1,14 +1,12 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load buttons %}
|
||||
|
||||
{% block content %}
|
||||
<div class="pull-right">
|
||||
{% if perms.dcim.change_powerport %}
|
||||
<a href="{% url 'dcim:power_connections_import' %}" class="btn btn-info">
|
||||
<span class="fa fa-download" aria-hidden="true"></span>
|
||||
Import connections
|
||||
</a>
|
||||
{% import_button 'dcim:power_connections_import' %}
|
||||
{% endif %}
|
||||
{% include 'inc/export_button.html' with obj_type='connections' %}
|
||||
{% export_button content_type %}
|
||||
</div>
|
||||
<h1>{% block title %}Power Connections{% endblock %}</h1>
|
||||
<div class="row">
|
||||
|
@ -1,19 +1,14 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load buttons %}
|
||||
{% load helpers %}
|
||||
|
||||
{% block content %}
|
||||
<div class="pull-right">
|
||||
{% if perms.dcim.add_rack %}
|
||||
<a href="{% url 'dcim:rack_add' %}" class="btn btn-primary">
|
||||
<span class="fa fa-plus" aria-hidden="true"></span>
|
||||
Add a rack
|
||||
</a>
|
||||
<a href="{% url 'dcim:rack_import' %}" class="btn btn-info">
|
||||
<span class="fa fa-download" aria-hidden="true"></span>
|
||||
Import racks
|
||||
</a>
|
||||
{% add_button 'dcim:rack_add' %}
|
||||
{% import_button 'dcim:rack_import' %}
|
||||
{% endif %}
|
||||
{% include 'inc/export_button.html' with obj_type='racks' %}
|
||||
{% export_button content_type %}
|
||||
</div>
|
||||
<h1>{% block title %}Racks{% endblock %}</h1>
|
||||
<div class="row">
|
||||
|
@ -1,19 +1,14 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load buttons %}
|
||||
{% load helpers %}
|
||||
|
||||
{% block content %}
|
||||
<div class="pull-right">
|
||||
{% if perms.dcim.add_rackgroup %}
|
||||
<a href="{% url 'dcim:rackgroup_add' %}" class="btn btn-primary">
|
||||
<span class="fa fa-plus" aria-hidden="true"></span>
|
||||
Add a rack group
|
||||
</a>
|
||||
<a href="{% url 'dcim:rackgroup_import' %}" class="btn btn-info">
|
||||
<span class="fa fa-download" aria-hidden="true"></span>
|
||||
Import rack groups
|
||||
</a>
|
||||
{% add_button 'dcim:rackgroup_add' %}
|
||||
{% import_button 'dcim:rackgroup_import' %}
|
||||
{% endif %}
|
||||
{% include 'inc/export_button.html' with obj_type='rack groups' %}
|
||||
{% export_button content_type %}
|
||||
</div>
|
||||
<h1>{% block title %}Rack Groups{% endblock %}</h1>
|
||||
<div class="row">
|
||||
|
@ -1,19 +1,14 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load buttons %}
|
||||
{% load helpers %}
|
||||
|
||||
{% block content %}
|
||||
<div class="pull-right">
|
||||
{% if perms.dcim.add_region %}
|
||||
<a href="{% url 'dcim:region_add' %}" class="btn btn-primary">
|
||||
<span class="fa fa-plus" aria-hidden="true"></span>
|
||||
Add a region
|
||||
</a>
|
||||
<a href="{% url 'dcim:region_import' %}" class="btn btn-info">
|
||||
<span class="fa fa-download" aria-hidden="true"></span>
|
||||
Import regions
|
||||
</a>
|
||||
{% add_button 'dcim:region_add' %}
|
||||
{% import_button 'dcim:region_import' %}
|
||||
{% endif %}
|
||||
{% include 'inc/export_button.html' with obj_type='regions' %}
|
||||
{% export_button content_type %}
|
||||
</div>
|
||||
<h1>{% block title %}Regions{% endblock %}</h1>
|
||||
<div class="row">
|
||||
|
@ -1,18 +1,13 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load buttons %}
|
||||
|
||||
{% block content %}
|
||||
<div class="pull-right">
|
||||
{% if perms.dcim.add_site %}
|
||||
<a href="{% url 'dcim:site_add' %}" class="btn btn-primary">
|
||||
<span class="fa fa-plus" aria-hidden="true"></span>
|
||||
Add a site
|
||||
</a>
|
||||
<a href="{% url 'dcim:site_import' %}" class="btn btn-info">
|
||||
<span class="fa fa-download" aria-hidden="true"></span>
|
||||
Import sites
|
||||
</a>
|
||||
{% add_button 'dcim:site_add' %}
|
||||
{% import_button 'dcim:site_import' %}
|
||||
{% endif %}
|
||||
{% include 'inc/export_button.html' with obj_type='sites' %}
|
||||
{% export_button content_type %}
|
||||
</div>
|
||||
<h1>{% block title %}Sites{% endblock %}</h1>
|
||||
<div class="row">
|
||||
|
@ -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 %}
|
@ -1,20 +1,15 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load buttons %}
|
||||
{% load humanize %}
|
||||
{% load helpers %}
|
||||
|
||||
{% block content %}
|
||||
<div class="pull-right">
|
||||
{% if perms.ipam.add_aggregate %}
|
||||
<a href="{% url 'ipam:aggregate_add' %}" class="btn btn-primary">
|
||||
<span class="fa fa-plus" aria-hidden="true"></span>
|
||||
Add an aggregate
|
||||
</a>
|
||||
<a href="{% url 'ipam:aggregate_import' %}" class="btn btn-info">
|
||||
<span class="fa fa-download" aria-hidden="true"></span>
|
||||
Import aggregates
|
||||
</a>
|
||||
{% add_button 'ipam:aggregate_add' %}
|
||||
{% import_button 'ipam:aggregate_import' %}
|
||||
{% endif %}
|
||||
{% include 'inc/export_button.html' with obj_type='aggregates' %}
|
||||
{% export_button content_type %}
|
||||
</div>
|
||||
<h1>{% block title %}Aggregates{% endblock %}</h1>
|
||||
<div class="row">
|
||||
|
@ -1,19 +1,14 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load buttons %}
|
||||
{% load helpers %}
|
||||
|
||||
{% block content %}
|
||||
<div class="pull-right">
|
||||
{% if perms.ipam.add_ipaddress %}
|
||||
<a href="{% url 'ipam:ipaddress_add' %}" class="btn btn-primary">
|
||||
<span class="fa fa-plus" aria-hidden="true"></span>
|
||||
Add an IP
|
||||
</a>
|
||||
<a href="{% url 'ipam:ipaddress_import' %}" class="btn btn-info">
|
||||
<span class="fa fa-download" aria-hidden="true"></span>
|
||||
Import IPs
|
||||
</a>
|
||||
{% add_button 'ipam:ipaddress_add' %}
|
||||
{% import_button 'ipam:ipaddress_import' %}
|
||||
{% endif %}
|
||||
{% include 'inc/export_button.html' with obj_type='IPs' %}
|
||||
{% export_button content_type %}
|
||||
</div>
|
||||
<h1>{% block title %}IP Addresses{% endblock %}</h1>
|
||||
<div class="row">
|
||||
|
@ -1,4 +1,5 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load buttons %}
|
||||
{% load helpers %}
|
||||
{% load form_helpers %}
|
||||
|
||||
@ -9,16 +10,10 @@
|
||||
<a href="{% url 'ipam:prefix_list' %}{% querystring request expand='on' page=1 %}" class="btn btn-default{% if request.GET.expand %} active{% endif %}">Expand</a>
|
||||
</div>
|
||||
{% if perms.ipam.add_prefix %}
|
||||
<a href="{% url 'ipam:prefix_add' %}" class="btn btn-primary">
|
||||
<span class="fa fa-plus" aria-hidden="true"></span>
|
||||
Add a prefix
|
||||
</a>
|
||||
<a href="{% url 'ipam:prefix_import' %}" class="btn btn-info">
|
||||
<span class="fa fa-download" aria-hidden="true"></span>
|
||||
Import prefixes
|
||||
</a>
|
||||
{% add_button 'ipam:prefix_add' %}
|
||||
{% import_button 'ipam:prefix_import' %}
|
||||
{% endif %}
|
||||
{% include 'inc/export_button.html' with obj_type='prefixes' %}
|
||||
{% export_button content_type %}
|
||||
</div>
|
||||
<h1>{% block title %}Prefixes{% endblock %}</h1>
|
||||
<div class="row">
|
||||
|
@ -1,4 +1,5 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load buttons %}
|
||||
{% load humanize %}
|
||||
{% load helpers %}
|
||||
|
||||
@ -16,15 +17,10 @@
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if perms.ipam.add_rir %}
|
||||
<a href="{% url 'ipam:rir_add' %}" class="btn btn-primary">
|
||||
<span class="fa fa-plus" aria-hidden="true"></span>
|
||||
Add a RIR
|
||||
</a>
|
||||
<a href="{% url 'ipam:rir_import' %}" class="btn btn-info">
|
||||
<span class="fa fa-download" aria-hidden="true"></span>
|
||||
Import RIRs
|
||||
</a>
|
||||
{% add_button 'ipam:rir_add' %}
|
||||
{% import_button 'ipam:rir_import' %}
|
||||
{% endif %}
|
||||
{% export_button content_type %}
|
||||
</div>
|
||||
<h1>{% block title %}RIRs{% endblock %}</h1>
|
||||
<div class="row">
|
||||
|
@ -1,18 +1,14 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load buttons %}
|
||||
{% load helpers %}
|
||||
|
||||
{% block content %}
|
||||
<div class="pull-right">
|
||||
{% if perms.ipam.add_role %}
|
||||
<a href="{% url 'ipam:role_add' %}" class="btn btn-primary">
|
||||
<span class="fa fa-plus" aria-hidden="true"></span>
|
||||
Add a role
|
||||
</a>
|
||||
<a href="{% url 'ipam:role_import' %}" class="btn btn-info">
|
||||
<span class="fa fa-download" aria-hidden="true"></span>
|
||||
Import roles
|
||||
</a>
|
||||
{% add_button 'ipam:role_add' %}
|
||||
{% import_button 'ipam:role_import' %}
|
||||
{% endif %}
|
||||
{% export_button content_type %}
|
||||
</div>
|
||||
<h1>{% block title %}Prefix/VLAN Roles{% endblock %}</h1>
|
||||
<div class="row">
|
||||
|
@ -1,20 +1,15 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load buttons %}
|
||||
{% load helpers %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block content %}
|
||||
<div class="pull-right">
|
||||
{% if perms.ipam.add_vlan %}
|
||||
<a href="{% url 'ipam:vlan_add' %}" class="btn btn-primary">
|
||||
<span class="fa fa-plus" aria-hidden="true"></span>
|
||||
Add a VLAN
|
||||
</a>
|
||||
<a href="{% url 'ipam:vlan_import' %}" class="btn btn-info">
|
||||
<span class="fa fa-download" aria-hidden="true"></span>
|
||||
Import VLANs
|
||||
</a>
|
||||
{% add_button 'ipam:vlan_add' %}
|
||||
{% import_button 'ipam:vlan_import' %}
|
||||
{% endif %}
|
||||
{% include 'inc/export_button.html' with obj_type='VLANs' %}
|
||||
{% export_button content_type %}
|
||||
</div>
|
||||
<h1>{% block title %}VLANs{% endblock %}</h1>
|
||||
<div class="row">
|
||||
|
@ -1,18 +1,14 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load buttons %}
|
||||
{% load helpers %}
|
||||
|
||||
{% block content %}
|
||||
<div class="pull-right">
|
||||
{% if perms.ipam.add_vlangroup %}
|
||||
<a href="{% url 'ipam:vlangroup_add' %}" class="btn btn-primary">
|
||||
<span class="fa fa-plus" aria-hidden="true"></span>
|
||||
Add a VLAN group
|
||||
</a>
|
||||
<a href="{% url 'ipam:vlangroup_import' %}" class="btn btn-info">
|
||||
<span class="fa fa-download" aria-hidden="true"></span>
|
||||
Import VLAN groups
|
||||
</a>
|
||||
{% add_button 'ipam:vlangroup_add' %}
|
||||
{% import_button 'ipam:vlangroup_import' %}
|
||||
{% endif %}
|
||||
{% export_button content_type %}
|
||||
</div>
|
||||
<h1>{% block title %}VLAN Groups{% endblock %}</h1>
|
||||
<div class="row">
|
||||
|
@ -5,16 +5,10 @@
|
||||
{% block content %}
|
||||
<div class="pull-right">
|
||||
{% if perms.ipam.add_vrf %}
|
||||
<a href="{% url 'ipam:vrf_add' %}" class="btn btn-primary">
|
||||
<span class="fa fa-plus" aria-hidden="true"></span>
|
||||
Add a VRF
|
||||
</a>
|
||||
<a href="{% url 'ipam:vrf_import' %}" class="btn btn-info">
|
||||
<span class="fa fa-download" aria-hidden="true"></span>
|
||||
Import VRFs
|
||||
</a>
|
||||
{% add_button 'ipam:vrf_add' %}
|
||||
{% import_button 'ipam:vrf_import' %}
|
||||
{% endif %}
|
||||
{% include 'inc/export_button.html' with obj_type='VRFs' %}
|
||||
{% export_button content_type %}
|
||||
</div>
|
||||
<h1>{% block title %}VRFs{% endblock %}</h1>
|
||||
<div class="row">
|
||||
|
@ -1,13 +1,11 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load buttons %}
|
||||
{% load helpers %}
|
||||
|
||||
{% block content %}
|
||||
<div class="pull-right">
|
||||
{% if perms.secrets.add_secret %}
|
||||
<a href="{% url 'secrets:secret_import' %}" class="btn btn-info">
|
||||
<span class="fa fa-download" aria-hidden="true"></span>
|
||||
Import secrets
|
||||
</a>
|
||||
{% import_button 'secrets:secret_import' %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<h1>{% block title %}Secrets{% endblock %}</h1>
|
||||
|
@ -1,18 +1,14 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load buttons %}
|
||||
{% load helpers %}
|
||||
|
||||
{% block content %}
|
||||
<div class="pull-right">
|
||||
{% if perms.dcim.add_devicerole %}
|
||||
<a href="{% url 'secrets:secretrole_add' %}" class="btn btn-primary">
|
||||
<span class="fa fa-plus" aria-hidden="true"></span>
|
||||
Add a secret role
|
||||
</a>
|
||||
<a href="{% url 'secrets:secretrole_import' %}" class="btn btn-info">
|
||||
<span class="fa fa-download" aria-hidden="true"></span>
|
||||
Import secret roles
|
||||
</a>
|
||||
{% if perms.secrets.add_secretrole %}
|
||||
{% add_button 'secrets:secretrole_add' %}
|
||||
{% import_button 'secrets:secretrole_import' %}
|
||||
{% endif %}
|
||||
{% export_button content_type %}
|
||||
</div>
|
||||
<h1>{% block title %}Secret Roles{% endblock %}</h1>
|
||||
<div class="row">
|
||||
|
@ -1,19 +1,14 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load buttons %}
|
||||
{% load helpers %}
|
||||
|
||||
{% block content %}
|
||||
<div class="pull-right">
|
||||
{% if perms.tenancy.add_tenant %}
|
||||
<a href="{% url 'tenancy:tenant_add' %}" class="btn btn-primary">
|
||||
<span class="fa fa-plus" aria-hidden="true"></span>
|
||||
Add a tenant
|
||||
</a>
|
||||
<a href="{% url 'tenancy:tenant_import' %}" class="btn btn-info">
|
||||
<span class="fa fa-download" aria-hidden="true"></span>
|
||||
Import tenants
|
||||
</a>
|
||||
{% add_button 'tenancy:tenant_add' %}
|
||||
{% import_button 'tenancy:tenant_import' %}
|
||||
{% endif %}
|
||||
{% include 'inc/export_button.html' with obj_type='tenants' %}
|
||||
{% export_button content_type %}
|
||||
</div>
|
||||
<h1>{% block title %}Tenants{% endblock %}</h1>
|
||||
<div class="row">
|
||||
|
@ -1,18 +1,14 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load buttons %}
|
||||
{% load helpers %}
|
||||
|
||||
{% block content %}
|
||||
<div class="pull-right">
|
||||
{% if perms.tenancy.add_tenantgroup %}
|
||||
<a href="{% url 'tenancy:tenantgroup_add' %}" class="btn btn-primary">
|
||||
<span class="fa fa-plus" aria-hidden="true"></span>
|
||||
Add a tenant group
|
||||
</a>
|
||||
<a href="{% url 'tenancy:tenantgroup_import' %}" class="btn btn-info">
|
||||
<span class="fa fa-download" aria-hidden="true"></span>
|
||||
Import tenant groups
|
||||
</a>
|
||||
{% add_button 'tenancy:tenantgroup_add' %}
|
||||
{% import_button 'tenancy:tenantgroup_import' %}
|
||||
{% endif %}
|
||||
{% export_button content_type %}
|
||||
</div>
|
||||
<h1>{% block title %}Tenant Groups{% endblock %}</h1>
|
||||
<div class="row">
|
||||
|
@ -1,18 +1,13 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load buttons %}
|
||||
|
||||
{% block content %}
|
||||
<div class="pull-right">
|
||||
{% if perms.virtualization.add_cluster %}
|
||||
<a href="{% url 'virtualization:cluster_add' %}" class="btn btn-primary">
|
||||
<span class="fa fa-plus" aria-hidden="true"></span>
|
||||
Add a cluster
|
||||
</a>
|
||||
<a href="{% url 'virtualization:cluster_import' %}" class="btn btn-info">
|
||||
<span class="fa fa-download" aria-hidden="true"></span>
|
||||
Import clusters
|
||||
</a>
|
||||
{% add_button 'virtualization:cluster_add' %}
|
||||
{% import_button 'virtualization:cluster_import' %}
|
||||
{% endif %}
|
||||
{% include 'inc/export_button.html' with obj_type='clusters' %}
|
||||
{% export_button content_type %}
|
||||
</div>
|
||||
<h1>{% block title %}Clusters{% endblock %}</h1>
|
||||
<div class="row">
|
||||
|
@ -1,18 +1,14 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load buttons %}
|
||||
{% load helpers %}
|
||||
|
||||
{% block content %}
|
||||
<div class="pull-right">
|
||||
{% if perms.virtualization.add_clustergroup %}
|
||||
<a href="{% url 'virtualization:clustergroup_add' %}" class="btn btn-primary">
|
||||
<span class="fa fa-plus" aria-hidden="true"></span>
|
||||
Add a cluster group
|
||||
</a>
|
||||
<a href="{% url 'virtualization:clustergroup_import' %}" class="btn btn-info">
|
||||
<span class="fa fa-download" aria-hidden="true"></span>
|
||||
Import cluster groups
|
||||
</a>
|
||||
{% add_button 'virtualization:clustergroup_add' %}
|
||||
{% import_button 'virtualization:clustergroup_import' %}
|
||||
{% endif %}
|
||||
{% export_button content_type %}
|
||||
</div>
|
||||
<h1>{% block title %}Cluster Groups{% endblock %}</h1>
|
||||
<div class="row">
|
||||
|
@ -1,18 +1,14 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load buttons %}
|
||||
{% load helpers %}
|
||||
|
||||
{% block content %}
|
||||
<div class="pull-right">
|
||||
{% if perms.virtualization.add_clustertype %}
|
||||
<a href="{% url 'virtualization:clustertype_add' %}" class="btn btn-primary">
|
||||
<span class="fa fa-plus" aria-hidden="true"></span>
|
||||
Add a cluster type
|
||||
</a>
|
||||
<a href="{% url 'virtualization:clustertype_import' %}" class="btn btn-info">
|
||||
<span class="fa fa-download" aria-hidden="true"></span>
|
||||
Import cluster types
|
||||
</a>
|
||||
{% add_button 'virtualization:clustertype_add' %}
|
||||
{% import_button 'virtualization:clustertype_import' %}
|
||||
{% endif %}
|
||||
{% export_button content_type %}
|
||||
</div>
|
||||
<h1>{% block title %}Cluster Types{% endblock %}</h1>
|
||||
<div class="row">
|
||||
|
@ -1,18 +1,13 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load buttons %}
|
||||
|
||||
{% block content %}
|
||||
<div class="pull-right">
|
||||
{% if perms.virtualization.add_virtualmachine %}
|
||||
<a href="{% url 'virtualization:virtualmachine_add' %}" class="btn btn-primary">
|
||||
<span class="fa fa-plus" aria-hidden="true"></span>
|
||||
Add a virtual machine
|
||||
</a>
|
||||
<a href="{% url 'virtualization:virtualmachine_import' %}" class="btn btn-info">
|
||||
<span class="fa fa-download" aria-hidden="true"></span>
|
||||
Import virtual machines
|
||||
</a>
|
||||
{% add_button 'virtualization:virtualmachine_add' %}
|
||||
{% import_button 'virtualization:virtualmachine_import' %}
|
||||
{% endif %}
|
||||
{% include 'inc/export_button.html' with obj_type='virtual machines' %}
|
||||
{% export_button content_type %}
|
||||
</div>
|
||||
<h1>{% block title %}Virtual Machines{% endblock %}</h1>
|
||||
<div class="row">
|
||||
|
@ -27,7 +27,7 @@ class TenantGroupCSVForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = TenantGroup
|
||||
fields = ['name', 'slug']
|
||||
fields = TenantGroup.csv_headers
|
||||
help_texts = {
|
||||
'name': 'Group name',
|
||||
}
|
||||
@ -60,7 +60,7 @@ class TenantCSVForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = Tenant
|
||||
fields = ['name', 'slug', 'group', 'description', 'comments']
|
||||
fields = Tenant.csv_headers
|
||||
help_texts = {
|
||||
'name': 'Tenant name',
|
||||
'comments': 'Free-form comments'
|
||||
|
@ -7,7 +7,6 @@ from django.utils.encoding import python_2_unicode_compatible
|
||||
|
||||
from extras.models import CustomFieldModel, CustomFieldValue
|
||||
from utilities.models import CreatedUpdatedModel
|
||||
from utilities.utils import csv_format
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
@ -18,6 +17,8 @@ class TenantGroup(models.Model):
|
||||
name = models.CharField(max_length=50, unique=True)
|
||||
slug = models.SlugField(unique=True)
|
||||
|
||||
csv_headers = ['name', 'slug']
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
|
||||
@ -27,6 +28,12 @@ class TenantGroup(models.Model):
|
||||
def get_absolute_url(self):
|
||||
return "{}?group={}".format(reverse('tenancy:tenant_list'), self.slug)
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
self.name,
|
||||
self.slug,
|
||||
)
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class Tenant(CreatedUpdatedModel, CustomFieldModel):
|
||||
@ -41,7 +48,7 @@ class Tenant(CreatedUpdatedModel, CustomFieldModel):
|
||||
comments = models.TextField(blank=True)
|
||||
custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
|
||||
|
||||
csv_headers = ['name', 'slug', 'group', 'description']
|
||||
csv_headers = ['name', 'slug', 'group', 'description', 'comments']
|
||||
|
||||
class Meta:
|
||||
ordering = ['group', 'name']
|
||||
@ -53,9 +60,10 @@ class Tenant(CreatedUpdatedModel, CustomFieldModel):
|
||||
return reverse('tenancy:tenant', args=[self.slug])
|
||||
|
||||
def to_csv(self):
|
||||
return csv_format([
|
||||
return (
|
||||
self.name,
|
||||
self.slug,
|
||||
self.group.name if self.group else None,
|
||||
self.description,
|
||||
])
|
||||
self.comments,
|
||||
)
|
||||
|
@ -1,7 +1,7 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import csv
|
||||
import itertools
|
||||
from io import StringIO
|
||||
import re
|
||||
|
||||
from django import forms
|
||||
@ -245,14 +245,10 @@ class CSVDataField(forms.CharField):
|
||||
|
||||
def to_python(self, value):
|
||||
|
||||
# Python 2's csv module has problems with Unicode
|
||||
if not isinstance(value, str):
|
||||
value = value.encode('utf-8')
|
||||
|
||||
records = []
|
||||
reader = csv.reader(value.splitlines())
|
||||
reader = csv.reader(StringIO(value))
|
||||
|
||||
# Consume and valdiate the first line of CSV data as column headers
|
||||
# Consume and validate the first line of CSV data as column headers
|
||||
headers = next(reader)
|
||||
for f in self.required_fields:
|
||||
if f not in headers:
|
||||
|
3
netbox/utilities/templates/buttons/add.html
Normal file
3
netbox/utilities/templates/buttons/add.html
Normal 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>
|
19
netbox/utilities/templates/buttons/export.html
Normal file
19
netbox/utilities/templates/buttons/export.html
Normal 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 %}
|
3
netbox/utilities/templates/buttons/import.html
Normal file
3
netbox/utilities/templates/buttons/import.html
Normal 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>
|
26
netbox/utilities/templatetags/buttons.py
Normal file
26
netbox/utilities/templatetags/buttons.py
Normal 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,
|
||||
}
|
@ -1,7 +1,10 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import datetime
|
||||
import six
|
||||
|
||||
from django.http import HttpResponse
|
||||
|
||||
|
||||
def csv_format(data):
|
||||
"""
|
||||
@ -15,12 +18,16 @@ def csv_format(data):
|
||||
csv.append('')
|
||||
continue
|
||||
|
||||
# Convert dates to ISO format
|
||||
if isinstance(value, (datetime.date, datetime.datetime)):
|
||||
value = value.isoformat()
|
||||
|
||||
# Force conversion to string first so we can check for any commas
|
||||
if not isinstance(value, six.string_types):
|
||||
value = '{}'.format(value)
|
||||
|
||||
# Double-quote the value if it contains a comma
|
||||
if ',' in value:
|
||||
if ',' in value or '\n' in value:
|
||||
csv.append('"{}"'.format(value))
|
||||
else:
|
||||
csv.append('{}'.format(value))
|
||||
@ -28,6 +35,32 @@ def csv_format(data):
|
||||
return ','.join(csv)
|
||||
|
||||
|
||||
def queryset_to_csv(queryset):
|
||||
"""
|
||||
Export a queryset of objects as CSV, using the model's to_csv() method.
|
||||
"""
|
||||
output = []
|
||||
|
||||
# Start with the column headers
|
||||
headers = ','.join(queryset.model.csv_headers)
|
||||
output.append(headers)
|
||||
|
||||
# Iterate through the queryset
|
||||
for obj in queryset:
|
||||
data = csv_format(obj.to_csv())
|
||||
output.append(data)
|
||||
|
||||
# Build the HTTP response
|
||||
response = HttpResponse(
|
||||
'\n'.join(output),
|
||||
content_type='text/csv'
|
||||
)
|
||||
filename = 'netbox_{}.csv'.format(queryset.model._meta.verbose_name_plural)
|
||||
response['Content-Disposition'] = 'attachment; filename="{}"'.format(filename)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
def foreground_color(bg_color):
|
||||
"""
|
||||
Return the ideal foreground color (black or white) for a given background color in hexadecimal RGB format.
|
||||
|
@ -10,7 +10,6 @@ from django.core.exceptions import ValidationError
|
||||
from django.db import transaction, IntegrityError
|
||||
from django.db.models import ProtectedError
|
||||
from django.forms import CharField, Form, ModelMultipleChoiceField, MultipleHiddenInput, Textarea
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.template.exceptions import TemplateSyntaxError
|
||||
from django.urls import reverse
|
||||
@ -21,6 +20,7 @@ from django.views.generic import View
|
||||
from django_tables2 import RequestConfig
|
||||
|
||||
from extras.models import CustomField, CustomFieldValue, ExportTemplate, UserAction
|
||||
from utilities.utils import queryset_to_csv
|
||||
from utilities.forms import BootstrapMixin, CSVDataField
|
||||
from .constants import M2M_FIELD_TYPES
|
||||
from .error_handlers import handle_protectederror
|
||||
@ -80,7 +80,7 @@ class ObjectListView(View):
|
||||
def get(self, request):
|
||||
|
||||
model = self.queryset.model
|
||||
object_ct = ContentType.objects.get_for_model(model)
|
||||
content_type = ContentType.objects.get_for_model(model)
|
||||
|
||||
if self.filter:
|
||||
self.queryset = self.filter(request.GET, self.queryset).qs
|
||||
@ -93,27 +93,18 @@ class ObjectListView(View):
|
||||
|
||||
# Check for export template rendering
|
||||
if request.GET.get('export'):
|
||||
et = get_object_or_404(ExportTemplate, content_type=object_ct, name=request.GET.get('export'))
|
||||
et = get_object_or_404(ExportTemplate, content_type=content_type, name=request.GET.get('export'))
|
||||
queryset = CustomFieldQueryset(self.queryset, custom_fields) if custom_fields else self.queryset
|
||||
try:
|
||||
response = et.to_response(context_dict={'queryset': queryset},
|
||||
filename='netbox_{}'.format(model._meta.verbose_name_plural))
|
||||
return response
|
||||
return et.render_to_response(queryset)
|
||||
except TemplateSyntaxError:
|
||||
messages.error(request, "There was an error rendering the selected export template ({})."
|
||||
.format(et.name))
|
||||
# Fall back to built-in CSV export
|
||||
elif 'export' in request.GET and hasattr(model, 'to_csv'):
|
||||
headers = getattr(model, 'csv_headers', None)
|
||||
output = ','.join(headers) + '\n' if headers else ''
|
||||
output += '\n'.join([obj.to_csv() for obj in self.queryset])
|
||||
response = HttpResponse(
|
||||
output,
|
||||
content_type='text/csv'
|
||||
messages.error(
|
||||
request,
|
||||
"There was an error rendering the selected export template ({}).".format(et.name)
|
||||
)
|
||||
response['Content-Disposition'] = 'attachment; filename="netbox_{}.csv"'\
|
||||
.format(self.queryset.model._meta.verbose_name_plural)
|
||||
return response
|
||||
# Fall back to built-in CSV export if no template was specified
|
||||
elif 'export' in request.GET and hasattr(model, 'to_csv'):
|
||||
return queryset_to_csv(self.queryset)
|
||||
|
||||
# Provide a hook to tweak the queryset based on the request immediately prior to rendering the object list
|
||||
self.queryset = self.alter_queryset(request)
|
||||
@ -135,10 +126,10 @@ class ObjectListView(View):
|
||||
RequestConfig(request, paginate).configure(table)
|
||||
|
||||
context = {
|
||||
'content_type': content_type,
|
||||
'table': table,
|
||||
'permissions': permissions,
|
||||
'filter_form': self.filter_form(request.GET, label_suffix='') if self.filter_form else None,
|
||||
'export_templates': ExportTemplate.objects.filter(content_type=object_ct),
|
||||
}
|
||||
context.update(self.extra_context())
|
||||
|
||||
|
@ -41,7 +41,7 @@ class ClusterTypeCSVForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = ClusterType
|
||||
fields = ['name', 'slug']
|
||||
fields = ClusterType.csv_headers
|
||||
help_texts = {
|
||||
'name': 'Name of cluster type',
|
||||
}
|
||||
@ -64,7 +64,7 @@ class ClusterGroupCSVForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = ClusterGroup
|
||||
fields = ['name', 'slug']
|
||||
fields = ClusterGroup.csv_headers
|
||||
help_texts = {
|
||||
'name': 'Name of cluster group',
|
||||
}
|
||||
@ -112,7 +112,7 @@ class ClusterCSVForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = Cluster
|
||||
fields = ['name', 'type', 'group', 'site', 'comments']
|
||||
fields = Cluster.csv_headers
|
||||
|
||||
|
||||
class ClusterBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
@ -306,7 +306,7 @@ class VirtualMachineCSVForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = VirtualMachine
|
||||
fields = ['name', 'status', 'cluster', 'role', 'tenant', 'platform', 'vcpus', 'memory', 'disk', 'comments']
|
||||
fields = VirtualMachine.csv_headers
|
||||
|
||||
|
||||
class VirtualMachineBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
|
@ -10,7 +10,6 @@ from django.utils.encoding import python_2_unicode_compatible
|
||||
from dcim.models import Device
|
||||
from extras.models import CustomFieldModel, CustomFieldValue
|
||||
from utilities.models import CreatedUpdatedModel
|
||||
from utilities.utils import csv_format
|
||||
from .constants import DEVICE_STATUS_ACTIVE, VM_STATUS_CHOICES, VM_STATUS_CLASSES
|
||||
|
||||
|
||||
@ -31,6 +30,8 @@ class ClusterType(models.Model):
|
||||
unique=True
|
||||
)
|
||||
|
||||
csv_headers = ['name', 'slug']
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
|
||||
@ -40,6 +41,12 @@ class ClusterType(models.Model):
|
||||
def get_absolute_url(self):
|
||||
return "{}?type={}".format(reverse('virtualization:cluster_list'), self.slug)
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
self.name,
|
||||
self.slug,
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# Cluster groups
|
||||
@ -58,6 +65,8 @@ class ClusterGroup(models.Model):
|
||||
unique=True
|
||||
)
|
||||
|
||||
csv_headers = ['name', 'slug']
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
|
||||
@ -67,6 +76,12 @@ class ClusterGroup(models.Model):
|
||||
def get_absolute_url(self):
|
||||
return "{}?group={}".format(reverse('virtualization:cluster_list'), self.slug)
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
self.name,
|
||||
self.slug,
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# Clusters
|
||||
@ -109,9 +124,7 @@ class Cluster(CreatedUpdatedModel, CustomFieldModel):
|
||||
object_id_field='obj_id'
|
||||
)
|
||||
|
||||
csv_headers = [
|
||||
'name', 'type', 'group', 'site', 'comments',
|
||||
]
|
||||
csv_headers = ['name', 'type', 'group', 'site', 'comments']
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
@ -135,13 +148,13 @@ class Cluster(CreatedUpdatedModel, CustomFieldModel):
|
||||
})
|
||||
|
||||
def to_csv(self):
|
||||
return csv_format([
|
||||
return (
|
||||
self.name,
|
||||
self.type.name,
|
||||
self.group.name if self.group else None,
|
||||
self.site.name if self.site else None,
|
||||
self.comments,
|
||||
])
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
@ -230,7 +243,7 @@ class VirtualMachine(CreatedUpdatedModel, CustomFieldModel):
|
||||
)
|
||||
|
||||
csv_headers = [
|
||||
'name', 'status', 'cluster', 'tenant', 'platform', 'vcpus', 'memory', 'disk', 'comments',
|
||||
'name', 'status', 'role', 'cluster', 'tenant', 'platform', 'vcpus', 'memory', 'disk', 'comments',
|
||||
]
|
||||
|
||||
class Meta:
|
||||
@ -243,9 +256,10 @@ class VirtualMachine(CreatedUpdatedModel, CustomFieldModel):
|
||||
return reverse('virtualization:virtualmachine', args=[self.pk])
|
||||
|
||||
def to_csv(self):
|
||||
return csv_format([
|
||||
return (
|
||||
self.name,
|
||||
self.get_status_display(),
|
||||
self.role.name if self.role else None,
|
||||
self.cluster.name,
|
||||
self.tenant.name if self.tenant else None,
|
||||
self.platform.name if self.platform else None,
|
||||
@ -253,7 +267,7 @@ class VirtualMachine(CreatedUpdatedModel, CustomFieldModel):
|
||||
self.memory,
|
||||
self.disk,
|
||||
self.comments,
|
||||
])
|
||||
)
|
||||
|
||||
def get_status_class(self):
|
||||
return VM_STATUS_CLASSES[self.status]
|
||||
|
@ -80,8 +80,9 @@ class ClusterGroupTable(BaseTable):
|
||||
class ClusterTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.LinkColumn()
|
||||
device_count = tables.Column(verbose_name='Devices')
|
||||
vm_count = tables.Column(verbose_name='VMs')
|
||||
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')])
|
||||
device_count = tables.Column(accessor=Accessor('devices.count'), orderable=False, verbose_name='Devices')
|
||||
vm_count = tables.Column(accessor=Accessor('virtual_machines.count'), orderable=False, verbose_name='VMs')
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Cluster
|
||||
|
@ -99,10 +99,7 @@ class ClusterGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
#
|
||||
|
||||
class ClusterListView(ObjectListView):
|
||||
queryset = Cluster.objects.annotate(
|
||||
device_count=Count('devices', distinct=True),
|
||||
vm_count=Count('virtual_machines', distinct=True)
|
||||
)
|
||||
queryset = Cluster.objects.select_related('type', 'group')
|
||||
table = tables.ClusterTable
|
||||
filter = filters.ClusterFilter
|
||||
filter_form = forms.ClusterFilterForm
|
||||
@ -162,10 +159,7 @@ class ClusterBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
class ClusterBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'virtualization.delete_cluster'
|
||||
cls = Cluster
|
||||
queryset = Cluster.objects.annotate(
|
||||
device_count=Count('devices', distinct=True),
|
||||
vm_count=Count('virtual_machines', distinct=True)
|
||||
)
|
||||
queryset = Cluster.objects.all()
|
||||
table = tables.ClusterTable
|
||||
default_return_url = 'virtualization:cluster_list'
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user