Merge branch 'develop' into develop-2.3

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

View File

@ -10,24 +10,23 @@ We have established a Google Groups Mailing List for issues and general
discussion. This is the best forum for obtaining assistance with NetBox
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.

View File

@ -1,12 +1,18 @@
![NetBox](docs/netbox_logo.png "NetBox logo")
NetBox is an IP address management (IPAM) and data center infrastructure management (DCIM) tool. Initially conceived by the network engineering team at [DigitalOcean](https://www.digitalocean.com/), NetBox was developed specifically to address the needs of network and infrastructure engineers.
NetBox is an IP address management (IPAM) and data center infrastructure
management (DCIM) tool. Initially conceived by the network engineering team at
[DigitalOcean](https://www.digitalocean.com/), NetBox was developed specifically
to address the needs of network and infrastructure engineers.
NetBox runs as a web application atop the [Django](https://www.djangoproject.com/) Python framework with a [PostgreSQL](http://www.postgresql.org/) database. For a complete list of requirements, see `requirements.txt`. The code is available [on GitHub](https://github.com/digitalocean/netbox).
NetBox runs as a web application atop the [Django](https://www.djangoproject.com/)
Python framework with a [PostgreSQL](http://www.postgresql.org/) database. For a
complete list of requirements, see `requirements.txt`. The code is available [on GitHub](https://github.com/digitalocean/netbox).
The complete documentation for NetBox can be found at [Read the Docs](http://netbox.readthedocs.io/en/stable/).
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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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