mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-22 20:12:00 -06:00
commit
c4f7e8121a
@ -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
|
||||
|
||||
|
@ -5,7 +5,7 @@ Supported HTTP methods:
|
||||
* `GET`: Retrieve an object or list of objects
|
||||
* `POST`: Create a new object
|
||||
* `PUT`: Update an existing object, all mandatory fields must be specified
|
||||
* `PATCH`: Updates an existing object, only specifiying the field to be changed
|
||||
* `PATCH`: Updates an existing object, only specifying the field to be changed
|
||||
* `DELETE`: Delete an existing object
|
||||
|
||||
To authenticate a request, attach your token in an `Authorization` header:
|
||||
@ -144,4 +144,4 @@ $ curl -v -X DELETE -H "Authorization: Token d2f763479f703d80de0ec15254237bc651f
|
||||
* Closing connection 0
|
||||
```
|
||||
|
||||
The response to a successfull `DELETE` request will have code 204 (No Content); the body of the response will be empty.
|
||||
The response to a successful `DELETE` request will have code 204 (No Content); the body of the response will be empty.
|
||||
|
@ -87,7 +87,7 @@ AUTH_LDAP_USER_ATTR_MAP = {
|
||||
from django_auth_ldap.config import LDAPSearch, GroupOfNamesType
|
||||
|
||||
# This search ought to return all groups to which the user belongs. django_auth_ldap uses this to determine group
|
||||
# heirarchy.
|
||||
# hierarchy.
|
||||
AUTH_LDAP_GROUP_SEARCH = LDAPSearch("dc=example,dc=com", ldap.SCOPE_SUBTREE,
|
||||
"(objectClass=group)")
|
||||
AUTH_LDAP_GROUP_TYPE = GroupOfNamesType()
|
||||
|
@ -1,4 +1,4 @@
|
||||
NetBox includes a Python shell withing which objects can be directly queried, created, modified, and deleted. To enter the shell, run the following command:
|
||||
NetBox includes a Python shell within which objects can be directly queried, created, modified, and deleted. To enter the shell, run the following command:
|
||||
|
||||
```
|
||||
./manage.py nbshell
|
||||
@ -86,7 +86,7 @@ The `count()` method can be appended to the queryset to return a count of object
|
||||
982
|
||||
```
|
||||
|
||||
Relationships with other models can be traversed by concatenting field names with a double-underscore. For example, the following will return all devices assigned to the tenant named "Pied Piper."
|
||||
Relationships with other models can be traversed by concatenating field names with a double-underscore. For example, the following will return all devices assigned to the tenant named "Pied Piper."
|
||||
|
||||
```
|
||||
>>> Device.objects.filter(tenant__name='Pied Piper')
|
||||
|
@ -43,7 +43,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',
|
||||
@ -89,7 +89,7 @@ class CircuitTypeCSVForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = CircuitType
|
||||
fields = ['name', 'slug']
|
||||
fields = CircuitType.csv_headers
|
||||
help_texts = {
|
||||
'name': 'Name of circuit type',
|
||||
}
|
||||
|
@ -9,7 +9,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 *
|
||||
|
||||
|
||||
@ -29,7 +28,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']
|
||||
@ -41,13 +40,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
|
||||
@ -59,6 +61,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']
|
||||
|
||||
@ -68,6 +72,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):
|
||||
@ -86,7 +96,7 @@ 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', 'tenant', 'install_date', 'commit_rate', 'description']
|
||||
csv_headers = ['cid', 'provider', 'type', 'tenant', 'install_date', 'commit_rate', 'description', 'comments']
|
||||
|
||||
class Meta:
|
||||
ordering = ['provider', 'cid']
|
||||
@ -99,15 +109,16 @@ 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.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_termination(self, side):
|
||||
for ct in self.terminations.all():
|
||||
|
@ -72,9 +72,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',
|
||||
@ -136,10 +134,7 @@ class SiteCSVForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = Site
|
||||
fields = [
|
||||
'name', 'slug', 'region', 'tenant', 'facility', 'asn', 'physical_address', 'shipping_address',
|
||||
'contact_name', 'contact_phone', 'contact_email', 'comments',
|
||||
]
|
||||
fields = Site.csv_headers
|
||||
help_texts = {
|
||||
'name': 'Site name',
|
||||
'slug': 'URL-friendly slug',
|
||||
@ -196,9 +191,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',
|
||||
@ -226,7 +219,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)'
|
||||
@ -313,10 +306,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',
|
||||
@ -444,9 +434,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',
|
||||
@ -492,8 +480,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',
|
||||
@ -658,7 +645,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)'
|
||||
@ -682,7 +669,7 @@ class PlatformCSVForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = Platform
|
||||
fields = ['name', 'slug', 'napalm_driver']
|
||||
fields = Platform.csv_headers
|
||||
help_texts = {
|
||||
'name': 'Platform name',
|
||||
}
|
||||
@ -932,7 +919,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):
|
||||
@ -981,7 +968,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):
|
||||
@ -1061,6 +1048,15 @@ class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
)
|
||||
status = forms.MultipleChoiceField(choices=device_status_choices, required=False)
|
||||
mac_address = forms.CharField(required=False, label='MAC address')
|
||||
has_primary_ip = forms.NullBooleanField(
|
||||
required=False,
|
||||
label='Has a primary IP',
|
||||
widget=forms.Select(choices=[
|
||||
('', '---------'),
|
||||
('True', 'Yes'),
|
||||
('False', 'No'),
|
||||
])
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
@ -1610,7 +1606,7 @@ class InterfaceForm(BootstrapMixin, forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = Interface
|
||||
fields = ['device', 'name', 'form_factor', 'enabled', 'lag', 'mac_address', 'mtu', 'mgmt_only', 'description']
|
||||
fields = ['device', 'name', 'form_factor', 'enabled', 'lag', 'mtu', 'mac_address', 'mgmt_only', 'description']
|
||||
widgets = {
|
||||
'device': forms.HiddenInput(),
|
||||
}
|
||||
@ -1636,7 +1632,11 @@ class InterfaceCreateForm(ComponentForm):
|
||||
lag = forms.ModelChoiceField(queryset=Interface.objects.all(), required=False, label='Parent LAG')
|
||||
mtu = forms.IntegerField(required=False, min_value=1, max_value=32767, label='MTU')
|
||||
mac_address = MACAddressFormField(required=False, label='MAC Address')
|
||||
mgmt_only = forms.BooleanField(required=False, label='OOB Management')
|
||||
mgmt_only = forms.BooleanField(
|
||||
required=False,
|
||||
label='OOB Management',
|
||||
help_text='This interface is used only for out-of-band management'
|
||||
)
|
||||
description = forms.CharField(max_length=100, required=False)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@ -1808,7 +1808,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):
|
||||
|
||||
@ -1951,7 +1951,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):
|
||||
|
@ -22,7 +22,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
|
||||
@ -43,9 +42,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']
|
||||
@ -57,11 +54,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,
|
||||
])
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
@ -98,7 +95,8 @@ class Site(CreatedUpdatedModel, CustomFieldModel):
|
||||
objects = SiteManager()
|
||||
|
||||
csv_headers = [
|
||||
'name', 'slug', 'region', 'tenant', 'facility', 'asn', 'contact_name', 'contact_phone', 'contact_email',
|
||||
'name', 'slug', 'region', 'tenant', 'facility', 'asn', 'physical_address', 'shipping_address', 'contact_name',
|
||||
'contact_phone', 'contact_email', 'comments',
|
||||
]
|
||||
|
||||
class Meta:
|
||||
@ -111,17 +109,20 @@ class Site(CreatedUpdatedModel, CustomFieldModel):
|
||||
return reverse('dcim:site', args=[self.slug])
|
||||
|
||||
def to_csv(self):
|
||||
return csv_format([
|
||||
return (
|
||||
self.name,
|
||||
self.slug,
|
||||
self.region.name if self.region else None,
|
||||
self.tenant.name if self.tenant else None,
|
||||
self.facility,
|
||||
self.asn,
|
||||
self.physical_address,
|
||||
self.shipping_address,
|
||||
self.contact_name,
|
||||
self.contact_phone,
|
||||
self.contact_email,
|
||||
])
|
||||
self.comments,
|
||||
)
|
||||
|
||||
@property
|
||||
def count_prefixes(self):
|
||||
@ -164,9 +165,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']
|
||||
@ -182,11 +181,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
|
||||
@ -198,6 +197,8 @@ class RackRole(models.Model):
|
||||
slug = models.SlugField(unique=True)
|
||||
color = ColorField()
|
||||
|
||||
csv_headers = ['name', 'slug', 'color']
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
|
||||
@ -207,6 +208,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):
|
||||
|
||||
@ -242,7 +250,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:
|
||||
@ -292,7 +300,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,
|
||||
@ -304,7 +312,8 @@ class Rack(CreatedUpdatedModel, CustomFieldModel):
|
||||
self.width,
|
||||
self.u_height,
|
||||
self.desc_units,
|
||||
])
|
||||
self.comments,
|
||||
)
|
||||
|
||||
@property
|
||||
def units(self):
|
||||
@ -479,9 +488,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']
|
||||
@ -493,10 +500,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
|
||||
@ -539,7 +546,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:
|
||||
@ -562,7 +569,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,
|
||||
@ -574,7 +581,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):
|
||||
|
||||
@ -754,6 +762,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']
|
||||
|
||||
@ -763,6 +773,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):
|
||||
@ -778,6 +796,8 @@ class Platform(models.Model):
|
||||
rpc_client = models.CharField(max_length=30, choices=RPC_CLIENT_CHOICES, blank=True,
|
||||
verbose_name='Legacy RPC client')
|
||||
|
||||
csv_headers = ['name', 'slug', 'napalm_driver']
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
|
||||
@ -787,6 +807,13 @@ 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.napalm_driver,
|
||||
)
|
||||
|
||||
|
||||
class DeviceManager(NaturalOrderByManager):
|
||||
|
||||
@ -848,7 +875,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:
|
||||
@ -989,7 +1016,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,
|
||||
@ -1004,7 +1031,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):
|
||||
@ -1076,15 +1104,14 @@ class ConsolePort(models.Model):
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
# 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(),
|
||||
])
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
@ -1153,15 +1180,14 @@ class PowerPort(models.Model):
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
# 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(),
|
||||
])
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
@ -1382,15 +1408,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(),
|
||||
])
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
@ -1453,7 +1478,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:
|
||||
@ -1464,12 +1489,13 @@ class InventoryItem(models.Model):
|
||||
return self.name
|
||||
|
||||
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,
|
||||
)
|
||||
|
@ -65,6 +65,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>
|
||||
@ -83,6 +87,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>
|
||||
@ -218,12 +238,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',
|
||||
)
|
||||
|
||||
|
||||
@ -362,12 +386,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
|
||||
@ -380,10 +417,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'}},
|
||||
|
@ -276,7 +276,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
|
||||
@ -715,10 +715,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'
|
||||
|
||||
@ -756,10 +753,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'
|
||||
|
||||
|
@ -39,7 +39,7 @@ class CustomFieldChoiceAdmin(admin.TabularInline):
|
||||
@admin.register(CustomField)
|
||||
class CustomFieldAdmin(admin.ModelAdmin):
|
||||
inlines = [CustomFieldChoiceAdmin]
|
||||
list_display = ['name', 'models', 'type', 'required', 'default', 'weight', 'description']
|
||||
list_display = ['name', 'models', 'type', 'required', 'filter_logic', 'default', 'weight', 'description']
|
||||
form = CustomFieldForm
|
||||
|
||||
def models(self, obj):
|
||||
|
@ -26,6 +26,16 @@ CUSTOMFIELD_TYPE_CHOICES = (
|
||||
(CF_TYPE_SELECT, 'Selection'),
|
||||
)
|
||||
|
||||
# Custom field filter logic choices
|
||||
CF_FILTER_DISABLED = 0
|
||||
CF_FILTER_LOOSE = 1
|
||||
CF_FILTER_EXACT = 2
|
||||
CF_FILTER_CHOICES = (
|
||||
(CF_FILTER_DISABLED, 'Disabled'),
|
||||
(CF_FILTER_LOOSE, 'Loose'),
|
||||
(CF_FILTER_EXACT, 'Exact'),
|
||||
)
|
||||
|
||||
# Graph types
|
||||
GRAPH_TYPE_INTERFACE = 100
|
||||
GRAPH_TYPE_PROVIDER = 200
|
||||
@ -46,6 +56,16 @@ EXPORTTEMPLATE_MODELS = [
|
||||
'cluster', 'virtualmachine', # Virtualization
|
||||
]
|
||||
|
||||
# Topology map types
|
||||
TOPOLOGYMAP_TYPE_NETWORK = 1
|
||||
TOPOLOGYMAP_TYPE_CONSOLE = 2
|
||||
TOPOLOGYMAP_TYPE_POWER = 3
|
||||
TOPOLOGYMAP_TYPE_CHOICES = (
|
||||
(TOPOLOGYMAP_TYPE_NETWORK, 'Network'),
|
||||
(TOPOLOGYMAP_TYPE_CONSOLE, 'Console'),
|
||||
(TOPOLOGYMAP_TYPE_POWER, 'Power'),
|
||||
)
|
||||
|
||||
# User action types
|
||||
ACTION_CREATE = 1
|
||||
ACTION_IMPORT = 2
|
||||
|
@ -5,7 +5,7 @@ from django.contrib.auth.models import User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
from dcim.models import Site
|
||||
from .constants import CF_TYPE_SELECT
|
||||
from .constants import CF_FILTER_DISABLED, CF_FILTER_EXACT, CF_TYPE_BOOLEAN, CF_TYPE_SELECT
|
||||
from .models import CustomField, Graph, ExportTemplate, TopologyMap, UserAction
|
||||
|
||||
|
||||
@ -14,8 +14,9 @@ class CustomFieldFilter(django_filters.Filter):
|
||||
Filter objects by the presence of a CustomFieldValue. The filter's name is used as the CustomField name.
|
||||
"""
|
||||
|
||||
def __init__(self, cf_type, *args, **kwargs):
|
||||
self.cf_type = cf_type
|
||||
def __init__(self, custom_field, *args, **kwargs):
|
||||
self.cf_type = custom_field.type
|
||||
self.filter_logic = custom_field.filter_logic
|
||||
super(CustomFieldFilter, self).__init__(*args, **kwargs)
|
||||
|
||||
def filter(self, queryset, value):
|
||||
@ -41,10 +42,12 @@ class CustomFieldFilter(django_filters.Filter):
|
||||
except ValueError:
|
||||
return queryset.none()
|
||||
|
||||
return queryset.filter(
|
||||
custom_field_values__field__name=self.name,
|
||||
custom_field_values__serialized_value__icontains=value,
|
||||
)
|
||||
# Apply the assigned filter logic (exact or loose)
|
||||
queryset = queryset.filter(custom_field_values__field__name=self.name)
|
||||
if self.cf_type == CF_TYPE_BOOLEAN or self.filter_logic == CF_FILTER_EXACT:
|
||||
return queryset.filter(custom_field_values__serialized_value=value)
|
||||
else:
|
||||
return queryset.filter(custom_field_values__serialized_value__icontains=value)
|
||||
|
||||
|
||||
class CustomFieldFilterSet(django_filters.FilterSet):
|
||||
@ -56,9 +59,9 @@ class CustomFieldFilterSet(django_filters.FilterSet):
|
||||
super(CustomFieldFilterSet, self).__init__(*args, **kwargs)
|
||||
|
||||
obj_type = ContentType.objects.get_for_model(self._meta.model)
|
||||
custom_fields = CustomField.objects.filter(obj_type=obj_type, is_filterable=True)
|
||||
custom_fields = CustomField.objects.filter(obj_type=obj_type).exclude(filter_logic=CF_FILTER_DISABLED)
|
||||
for cf in custom_fields:
|
||||
self.filters['cf_{}'.format(cf.name)] = CustomFieldFilter(name=cf.name, cf_type=cf.type)
|
||||
self.filters['cf_{}'.format(cf.name)] = CustomFieldFilter(name=cf.name, custom_field=cf)
|
||||
|
||||
|
||||
class GraphFilter(django_filters.FilterSet):
|
||||
|
@ -6,7 +6,7 @@ from django import forms
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
from utilities.forms import BootstrapMixin, BulkEditForm, LaxURLField
|
||||
from .constants import CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_INTEGER, CF_TYPE_SELECT, CF_TYPE_URL
|
||||
from .constants import CF_FILTER_DISABLED, CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_INTEGER, CF_TYPE_SELECT, CF_TYPE_URL
|
||||
from .models import CustomField, CustomFieldValue, ImageAttachment
|
||||
|
||||
|
||||
@ -15,17 +15,17 @@ def get_custom_fields_for_model(content_type, filterable_only=False, bulk_edit=F
|
||||
Retrieve all CustomFields applicable to the given ContentType
|
||||
"""
|
||||
field_dict = OrderedDict()
|
||||
kwargs = {'obj_type': content_type}
|
||||
custom_fields = CustomField.objects.filter(obj_type=content_type)
|
||||
if filterable_only:
|
||||
kwargs['is_filterable'] = True
|
||||
custom_fields = CustomField.objects.filter(**kwargs)
|
||||
custom_fields = custom_fields.exclude(filter_logic=CF_FILTER_DISABLED)
|
||||
|
||||
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 +34,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 is not None and initial.lower() in ['true', 'yes', '1']:
|
||||
initial = 1
|
||||
elif cf.default.lower() in ['false', 'no', '0']:
|
||||
elif initial is not None and 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 +57,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()
|
||||
|
@ -4,14 +4,6 @@ from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
from extras.models import TopologyMap
|
||||
|
||||
|
||||
def commas_to_semicolons(apps, schema_editor):
|
||||
for tm in TopologyMap.objects.filter(device_patterns__contains=','):
|
||||
tm.device_patterns = tm.device_patterns.replace(',', ';')
|
||||
tm.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
@ -25,5 +17,4 @@ class Migration(migrations.Migration):
|
||||
name='device_patterns',
|
||||
field=models.TextField(help_text=b'Identify devices to include in the diagram using regular expressions, one per line. Each line will result in a new tier of the drawing. Separate multiple regexes within a line using semicolons. Devices will be rendered in the order they are defined.'),
|
||||
),
|
||||
migrations.RunPython(commas_to_semicolons),
|
||||
]
|
||||
|
20
netbox/extras/migrations/0009_topologymap_type.py
Normal file
20
netbox/extras/migrations/0009_topologymap_type.py
Normal file
@ -0,0 +1,20 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.9 on 2018-02-15 16:28
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('extras', '0008_reports'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='topologymap',
|
||||
name='type',
|
||||
field=models.PositiveSmallIntegerField(choices=[(1, 'Network'), (2, 'Console'), (3, 'Power')], default=1),
|
||||
),
|
||||
]
|
51
netbox/extras/migrations/0010_customfield_filter_logic.py
Normal file
51
netbox/extras/migrations/0010_customfield_filter_logic.py
Normal file
@ -0,0 +1,51 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.9 on 2018-02-21 19:48
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
from extras.constants import CF_FILTER_DISABLED, CF_FILTER_EXACT, CF_FILTER_LOOSE, CF_TYPE_SELECT
|
||||
|
||||
|
||||
def is_filterable_to_filter_logic(apps, schema_editor):
|
||||
CustomField = apps.get_model('extras', 'CustomField')
|
||||
CustomField.objects.filter(is_filterable=False).update(filter_logic=CF_FILTER_DISABLED)
|
||||
CustomField.objects.filter(is_filterable=True).update(filter_logic=CF_FILTER_LOOSE)
|
||||
# Select fields match on primary key only
|
||||
CustomField.objects.filter(is_filterable=True, type=CF_TYPE_SELECT).update(filter_logic=CF_FILTER_EXACT)
|
||||
|
||||
|
||||
def filter_logic_to_is_filterable(apps, schema_editor):
|
||||
CustomField = apps.get_model('extras', 'CustomField')
|
||||
CustomField.objects.filter(filter_logic=CF_FILTER_DISABLED).update(is_filterable=False)
|
||||
CustomField.objects.exclude(filter_logic=CF_FILTER_DISABLED).update(is_filterable=True)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('extras', '0009_topologymap_type'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='customfield',
|
||||
name='filter_logic',
|
||||
field=models.PositiveSmallIntegerField(choices=[(0, 'Disabled'), (1, 'Loose'), (2, 'Exact')], default=1, help_text='Loose matches any instance of a given string; exact matches the entire field.'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='customfield',
|
||||
name='required',
|
||||
field=models.BooleanField(default=False, help_text='If true, this field is required when creating new objects or editing an existing object.'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='customfield',
|
||||
name='weight',
|
||||
field=models.PositiveSmallIntegerField(default=100, help_text='Fields with higher weights appear lower in a form.'),
|
||||
),
|
||||
migrations.RunPython(is_filterable_to_filter_logic, filter_logic_to_is_filterable),
|
||||
migrations.RemoveField(
|
||||
model_name='customfield',
|
||||
name='is_filterable',
|
||||
),
|
||||
]
|
@ -16,6 +16,7 @@ from django.template import Template, Context
|
||||
from django.utils.encoding import python_2_unicode_compatible
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
from dcim.constants import CONNECTION_STATUS_CONNECTED
|
||||
from utilities.utils import foreground_color
|
||||
from .constants import *
|
||||
|
||||
@ -54,22 +55,48 @@ class CustomFieldModel(object):
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class CustomField(models.Model):
|
||||
obj_type = models.ManyToManyField(ContentType, related_name='custom_fields', verbose_name='Object(s)',
|
||||
limit_choices_to={'model__in': CUSTOMFIELD_MODELS},
|
||||
help_text="The object(s) to which this field applies.")
|
||||
type = models.PositiveSmallIntegerField(choices=CUSTOMFIELD_TYPE_CHOICES, default=CF_TYPE_TEXT)
|
||||
name = models.CharField(max_length=50, unique=True)
|
||||
label = models.CharField(max_length=50, blank=True, help_text="Name of the field as displayed to users (if not "
|
||||
"provided, the field's name will be used)")
|
||||
description = models.CharField(max_length=100, blank=True)
|
||||
required = models.BooleanField(default=False, help_text="Determines whether this field is required when creating "
|
||||
"new objects or editing an existing object.")
|
||||
is_filterable = models.BooleanField(default=True, help_text="This field can be used to filter objects.")
|
||||
default = models.CharField(max_length=100, blank=True, help_text="Default value for the field. Use \"true\" or "
|
||||
"\"false\" for booleans. N/A for selection "
|
||||
"fields.")
|
||||
weight = models.PositiveSmallIntegerField(default=100, help_text="Fields with higher weights appear lower in a "
|
||||
"form")
|
||||
obj_type = models.ManyToManyField(
|
||||
to=ContentType,
|
||||
related_name='custom_fields',
|
||||
verbose_name='Object(s)',
|
||||
limit_choices_to={'model__in': CUSTOMFIELD_MODELS},
|
||||
help_text='The object(s) to which this field applies.'
|
||||
)
|
||||
type = models.PositiveSmallIntegerField(
|
||||
choices=CUSTOMFIELD_TYPE_CHOICES,
|
||||
default=CF_TYPE_TEXT
|
||||
)
|
||||
name = models.CharField(
|
||||
max_length=50,
|
||||
unique=True
|
||||
)
|
||||
label = models.CharField(
|
||||
max_length=50,
|
||||
blank=True,
|
||||
help_text='Name of the field as displayed to users (if not provided, the field\'s name will be used)'
|
||||
)
|
||||
description = models.CharField(
|
||||
max_length=100,
|
||||
blank=True
|
||||
)
|
||||
required = models.BooleanField(
|
||||
default=False,
|
||||
help_text='If true, this field is required when creating new objects or editing an existing object.'
|
||||
)
|
||||
filter_logic = models.PositiveSmallIntegerField(
|
||||
choices=CF_FILTER_CHOICES,
|
||||
default=CF_FILTER_LOOSE,
|
||||
help_text="Loose matches any instance of a given string; exact matches the entire field."
|
||||
)
|
||||
default = models.CharField(
|
||||
max_length=100,
|
||||
blank=True,
|
||||
help_text='Default value for the field. Use "true" or "false" for booleans. N/A for selection fields.'
|
||||
)
|
||||
weight = models.PositiveSmallIntegerField(
|
||||
default=100,
|
||||
help_text='Fields with higher weights appear lower in a form.'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ['weight', 'name']
|
||||
@ -223,19 +250,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
|
||||
|
||||
|
||||
@ -247,7 +280,17 @@ class ExportTemplate(models.Model):
|
||||
class TopologyMap(models.Model):
|
||||
name = models.CharField(max_length=50, unique=True)
|
||||
slug = models.SlugField(unique=True)
|
||||
site = models.ForeignKey('dcim.Site', related_name='topology_maps', blank=True, null=True, on_delete=models.CASCADE)
|
||||
type = models.PositiveSmallIntegerField(
|
||||
choices=TOPOLOGYMAP_TYPE_CHOICES,
|
||||
default=TOPOLOGYMAP_TYPE_NETWORK
|
||||
)
|
||||
site = models.ForeignKey(
|
||||
to='dcim.Site',
|
||||
related_name='topology_maps',
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=models.CASCADE
|
||||
)
|
||||
device_patterns = models.TextField(
|
||||
help_text="Identify devices to include in the diagram using regular expressions, one per line. Each line will "
|
||||
"result in a new tier of the drawing. Separate multiple regexes within a line using semicolons. "
|
||||
@ -269,22 +312,26 @@ class TopologyMap(models.Model):
|
||||
|
||||
def render(self, img_format='png'):
|
||||
|
||||
from circuits.models import CircuitTermination
|
||||
from dcim.models import CONNECTION_STATUS_CONNECTED, Device, InterfaceConnection
|
||||
from dcim.models import Device
|
||||
|
||||
# Construct the graph
|
||||
graph = graphviz.Graph()
|
||||
graph.graph_attr['ranksep'] = '1'
|
||||
if self.type == TOPOLOGYMAP_TYPE_NETWORK:
|
||||
G = graphviz.Graph
|
||||
else:
|
||||
G = graphviz.Digraph
|
||||
self.graph = G()
|
||||
self.graph.graph_attr['ranksep'] = '1'
|
||||
seen = set()
|
||||
for i, device_set in enumerate(self.device_sets):
|
||||
|
||||
subgraph = graphviz.Graph(name='sg{}'.format(i))
|
||||
subgraph = G(name='sg{}'.format(i))
|
||||
subgraph.graph_attr['rank'] = 'same'
|
||||
subgraph.graph_attr['directed'] = 'true'
|
||||
|
||||
# Add a pseudonode for each device_set to enforce hierarchical layout
|
||||
subgraph.node('set{}'.format(i), label='', shape='none', width='0')
|
||||
if i:
|
||||
graph.edge('set{}'.format(i - 1), 'set{}'.format(i), style='invis')
|
||||
self.graph.edge('set{}'.format(i - 1), 'set{}'.format(i), style='invis')
|
||||
|
||||
# Add each device to the graph
|
||||
devices = []
|
||||
@ -302,31 +349,64 @@ class TopologyMap(models.Model):
|
||||
for j in range(0, len(devices) - 1):
|
||||
subgraph.edge(devices[j].name, devices[j + 1].name, style='invis')
|
||||
|
||||
graph.subgraph(subgraph)
|
||||
self.graph.subgraph(subgraph)
|
||||
|
||||
# Compile list of all devices
|
||||
device_superset = Q()
|
||||
for device_set in self.device_sets:
|
||||
for query in device_set.split(';'): # Split regexes on semicolons
|
||||
device_superset = device_superset | Q(name__regex=query)
|
||||
devices = Device.objects.filter(*(device_superset,))
|
||||
|
||||
# Draw edges depending on graph type
|
||||
if self.type == TOPOLOGYMAP_TYPE_NETWORK:
|
||||
self.add_network_connections(devices)
|
||||
elif self.type == TOPOLOGYMAP_TYPE_CONSOLE:
|
||||
self.add_console_connections(devices)
|
||||
elif self.type == TOPOLOGYMAP_TYPE_POWER:
|
||||
self.add_power_connections(devices)
|
||||
|
||||
return self.graph.pipe(format=img_format)
|
||||
|
||||
def add_network_connections(self, devices):
|
||||
|
||||
from circuits.models import CircuitTermination
|
||||
from dcim.models import InterfaceConnection
|
||||
|
||||
# Add all interface connections to the graph
|
||||
devices = Device.objects.filter(*(device_superset,))
|
||||
connections = InterfaceConnection.objects.filter(
|
||||
interface_a__device__in=devices, interface_b__device__in=devices
|
||||
)
|
||||
for c in connections:
|
||||
style = 'solid' if c.connection_status == CONNECTION_STATUS_CONNECTED else 'dashed'
|
||||
graph.edge(c.interface_a.device.name, c.interface_b.device.name, style=style)
|
||||
self.graph.edge(c.interface_a.device.name, c.interface_b.device.name, style=style)
|
||||
|
||||
# Add all circuits to the graph
|
||||
for termination in CircuitTermination.objects.filter(term_side='A', interface__device__in=devices):
|
||||
peer_termination = termination.get_peer_termination()
|
||||
if (peer_termination is not None and peer_termination.interface is not None and
|
||||
peer_termination.interface.device in devices):
|
||||
graph.edge(termination.interface.device.name, peer_termination.interface.device.name, color='blue')
|
||||
self.graph.edge(termination.interface.device.name, peer_termination.interface.device.name, color='blue')
|
||||
|
||||
return graph.pipe(format=img_format)
|
||||
def add_console_connections(self, devices):
|
||||
|
||||
from dcim.models import ConsolePort
|
||||
|
||||
# Add all console connections to the graph
|
||||
console_ports = ConsolePort.objects.filter(device__in=devices, cs_port__device__in=devices)
|
||||
for cp in console_ports:
|
||||
style = 'solid' if cp.connection_status == CONNECTION_STATUS_CONNECTED else 'dashed'
|
||||
self.graph.edge(cp.cs_port.device.name, cp.device.name, style=style)
|
||||
|
||||
def add_power_connections(self, devices):
|
||||
|
||||
from dcim.models import PowerPort
|
||||
|
||||
# Add all power connections to the graph
|
||||
power_ports = PowerPort.objects.filter(device__in=devices, power_outlet__device__in=devices)
|
||||
for pp in power_ports:
|
||||
style = 'solid' if pp.connection_status == CONNECTION_STATUS_CONNECTED else 'dashed'
|
||||
self.graph.edge(pp.power_outlet.device.name, pp.device.name, style=style)
|
||||
|
||||
|
||||
#
|
||||
|
@ -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',
|
||||
|
19
netbox/ipam/migrations/0021_vrf_ordering.py
Normal file
19
netbox/ipam/migrations/0021_vrf_ordering.py
Normal file
@ -0,0 +1,19 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.9 on 2018-02-07 18:37
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('ipam', '0020_ipaddress_add_role_carp'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='vrf',
|
||||
options={'ordering': ['name', 'rd'], 'verbose_name': 'VRF', 'verbose_name_plural': 'VRFs'},
|
||||
),
|
||||
]
|
@ -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
|
||||
@ -38,7 +37,7 @@ class VRF(CreatedUpdatedModel, CustomFieldModel):
|
||||
csv_headers = ['name', 'rd', 'tenant', 'enforce_unique', 'description']
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
ordering = ['name', 'rd']
|
||||
verbose_name = 'VRF'
|
||||
verbose_name_plural = 'VRFs'
|
||||
|
||||
@ -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):
|
||||
|
@ -13,7 +13,7 @@ except ImportError:
|
||||
)
|
||||
|
||||
|
||||
VERSION = '2.2.9'
|
||||
VERSION = '2.2.10'
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__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',
|
||||
|
@ -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,13 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load helpers %}
|
||||
{% load buttons %}
|
||||
|
||||
{% 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,13 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load helpers %}
|
||||
{% load buttons %}
|
||||
|
||||
{% 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,13 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load helpers %}
|
||||
{% load buttons %}
|
||||
|
||||
{% 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,13 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load helpers %}
|
||||
{% load buttons %}
|
||||
|
||||
{% 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,13 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load helpers %}
|
||||
{% load buttons %}
|
||||
|
||||
{% 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">
|
||||
|
@ -43,17 +43,23 @@
|
||||
<h1>{{ device }}</h1>
|
||||
{% include 'inc/created_updated.html' with obj=device %}
|
||||
<ul class="nav nav-tabs" style="margin-bottom: 20px">
|
||||
<li role="presentation"{% if active_tab == 'info' %} class="active"{% endif %}><a href="{% url 'dcim:device' pk=device.pk %}">Info</a></li>
|
||||
<li role="presentation"{% if active_tab == 'inventory' %} class="active"{% endif %}><a href="{% url 'dcim:device_inventory' pk=device.pk %}">Inventory</a></li>
|
||||
<li role="presentation"{% if active_tab == 'info' %} class="active"{% endif %}>
|
||||
<a href="{% url 'dcim:device' pk=device.pk %}">Info</a>
|
||||
</li>
|
||||
<li role="presentation"{% if active_tab == 'inventory' %} class="active"{% endif %}>
|
||||
<a href="{% url 'dcim:device_inventory' pk=device.pk %}">Inventory</a>
|
||||
</li>
|
||||
{% if perms.dcim.napalm_read %}
|
||||
{% if device.status == 1 and device.platform.napalm_driver and device.primary_ip %}
|
||||
<li role="presentation"{% if active_tab == 'status' %} class="active"{% endif %}><a href="{% url 'dcim:device_status' pk=device.pk %}">Status</a></li>
|
||||
<li role="presentation"{% if active_tab == 'lldp-neighbors' %} class="active"{% endif %}><a href="{% url 'dcim:device_lldp_neighbors' pk=device.pk %}">LLDP Neighbors</a></li>
|
||||
<li role="presentation"{% if active_tab == 'config' %} class="active"{% endif %}><a href="{% url 'dcim:device_config' pk=device.pk %}">Configuration</a></li>
|
||||
{% if device.status != 1 %}
|
||||
{% include 'dcim/inc/device_napalm_tabs.html' with disabled_message='Device must be in active status' %}
|
||||
{% elif not device.platform %}
|
||||
{% include 'dcim/inc/device_napalm_tabs.html' with disabled_message='No platform assigned to this device' %}
|
||||
{% elif not device.platform.napalm_driver %}
|
||||
{% include 'dcim/inc/device_napalm_tabs.html' with disabled_message='No NAPALM driver assigned for this platform' %}
|
||||
{% elif not device.primary_ip %}
|
||||
{% include 'dcim/inc/device_napalm_tabs.html' with disabled_message='No primary IP address assigned to this device' %}
|
||||
{% else %}
|
||||
<li role="presentation" class="disabled"><a href="#">Status</a></li>
|
||||
<li role="presentation" class="disabled"><a href="#">LLDP Neighbors</a></li>
|
||||
<li role="presentation" class="disabled"><a href="#">Configuration</a></li>
|
||||
{% include 'dcim/inc/device_napalm_tabs.html' %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</ul>
|
||||
|
15
netbox/templates/dcim/inc/device_napalm_tabs.html
Normal file
15
netbox/templates/dcim/inc/device_napalm_tabs.html
Normal file
@ -0,0 +1,15 @@
|
||||
{% if not disabled_message %}
|
||||
<li role="presentation"{% if active_tab == 'status' %} class="active"{% endif %}>
|
||||
<a href="{% url 'dcim:device_status' pk=device.pk %}">Status</a>
|
||||
</li>
|
||||
<li role="presentation"{% if active_tab == 'lldp-neighbors' %} class="active"{% endif %}>
|
||||
<a href="{% url 'dcim:device_lldp_neighbors' pk=device.pk %}">LLDP Neighbors</a>
|
||||
</li>
|
||||
<li role="presentation"{% if active_tab == 'config' %} class="active"{% endif %}>
|
||||
<a href="{% url 'dcim:device_config' pk=device.pk %}">Configuration</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li role="presentation" class="disabled"><a href="#" title="{{ disabled_message }}">Status</a></li>
|
||||
<li role="presentation" class="disabled"><a href="#" title="{{ disabled_message }}">LLDP Neighbors</a></li>
|
||||
<li role="presentation" class="disabled"><a href="#" title="{{ disabled_message }}">Configuration</a></li>
|
||||
{% endif %}
|
29
netbox/templates/dcim/inc/filter_rack_group.html
Normal file
29
netbox/templates/dcim/inc/filter_rack_group.html
Normal file
@ -0,0 +1,29 @@
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function() {
|
||||
|
||||
var site_list = $('#id_site');
|
||||
var rack_group_list = $('#id_group_id');
|
||||
|
||||
// Update rack group and rack options based on selected site
|
||||
site_list.change(function() {
|
||||
var selected_sites = $(this).val();
|
||||
if (selected_sites) {
|
||||
|
||||
// Update rack group options
|
||||
rack_group_list.empty();
|
||||
$.ajax({
|
||||
url: netbox_api_path + 'dcim/rack-groups/?limit=500&site=' + selected_sites.join('&site='),
|
||||
dataType: 'json',
|
||||
success: function (response, status) {
|
||||
$.each(response["results"], function (index, group) {
|
||||
var option = $("<option></option>").attr("value", group.id).text(group.name);
|
||||
rack_group_list.append(option);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
</script>
|
@ -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,13 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load helpers %}
|
||||
{% load buttons %}
|
||||
|
||||
{% 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,13 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load helpers %}
|
||||
{% load buttons %}
|
||||
|
||||
{% 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">
|
||||
|
@ -45,9 +45,10 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block javascript %}
|
||||
<script type="text/javascript">
|
||||
$(function() {
|
||||
$('[data-toggle="popover"]').popover()
|
||||
})
|
||||
</script>
|
||||
{% include 'dcim/inc/filter_rack_group.html' %}
|
||||
<script type="text/javascript">
|
||||
$(function() {
|
||||
$('[data-toggle="popover"]').popover()
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
@ -1,19 +1,13 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load helpers %}
|
||||
{% load buttons %}
|
||||
|
||||
{% 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">
|
||||
@ -27,34 +21,6 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block javascript %}
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function() {
|
||||
|
||||
var site_list = $('#id_site');
|
||||
var rack_group_list = $('#id_group_id');
|
||||
|
||||
// Update rack group and rack options based on selected site
|
||||
site_list.change(function() {
|
||||
var selected_sites = $(this).val();
|
||||
if (selected_sites) {
|
||||
|
||||
// Update rack group options
|
||||
rack_group_list.empty();
|
||||
$.ajax({
|
||||
url: netbox_api_path + 'dcim/rack-groups/?limit=500&site=' + selected_sites.join('&site='),
|
||||
dataType: 'json',
|
||||
success: function (response, status) {
|
||||
$.each(response["results"], function (index, group) {
|
||||
var option = $("<option></option>").attr("value", group.id).text(group.name);
|
||||
rack_group_list.append(option);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
</script>
|
||||
{% include 'dcim/inc/filter_rack_group.html' %}
|
||||
{% endblock %}
|
||||
|
||||
|
@ -1,19 +1,13 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load helpers %}
|
||||
{% load buttons %}
|
||||
|
||||
{% 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,13 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load helpers %}
|
||||
{% load buttons %}
|
||||
|
||||
{% 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,14 @@
|
||||
{% 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">
|
||||
|
@ -144,7 +144,7 @@
|
||||
{% if duplicate_ips_table.rows %}
|
||||
{% include 'panel_table.html' with table=duplicate_ips_table heading='Duplicate IP Addresses' panel_class='danger' %}
|
||||
{% endif %}
|
||||
{% include 'panel_table.html' with table=related_ips_table heading='Related IP Addresses' %}
|
||||
{% include 'panel_table.html' with table=related_ips_table heading='Related IP Addresses' panel_class='default' %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@ -1,19 +1,13 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load helpers %}
|
||||
{% load buttons %}
|
||||
|
||||
{% 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">
|
||||
|
@ -136,7 +136,7 @@
|
||||
{% if duplicate_prefix_table.rows %}
|
||||
{% include 'panel_table.html' with table=duplicate_prefix_table heading='Duplicate Prefixes' panel_class='danger' %}
|
||||
{% endif %}
|
||||
{% include 'panel_table.html' with table=parent_prefix_table heading='Parent Prefixes' %}
|
||||
{% include 'panel_table.html' with table=parent_prefix_table heading='Parent Prefixes' panel_class='default' %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@ -1,6 +1,6 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load buttons %}
|
||||
{% load helpers %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block content %}
|
||||
<div class="pull-right">
|
||||
@ -9,16 +9,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">
|
||||
|
@ -1,6 +1,6 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load buttons %}
|
||||
{% load humanize %}
|
||||
{% load helpers %}
|
||||
|
||||
{% block content %}
|
||||
<div class="pull-right">
|
||||
@ -16,15 +16,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,13 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load helpers %}
|
||||
{% load buttons %}
|
||||
|
||||
{% 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,13 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load helpers %}
|
||||
{% load form_helpers %}
|
||||
{% load buttons %}
|
||||
|
||||
{% 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">
|
||||
|
@ -1,18 +1,13 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load helpers %}
|
||||
{% load buttons %}
|
||||
|
||||
{% 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">
|
||||
|
@ -1,20 +1,13 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load helpers %}
|
||||
{% load form_helpers %}
|
||||
{% load buttons %}
|
||||
|
||||
{% 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">
|
||||
|
@ -1,13 +1,10 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load helpers %}
|
||||
{% load buttons %}
|
||||
|
||||
{% 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,13 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load helpers %}
|
||||
{% load buttons %}
|
||||
|
||||
{% 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,13 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load helpers %}
|
||||
{% load buttons %}
|
||||
|
||||
{% 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,13 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load helpers %}
|
||||
{% load buttons %}
|
||||
|
||||
{% 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,13 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load helpers %}
|
||||
{% load buttons %}
|
||||
|
||||
{% 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,13 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load helpers %}
|
||||
{% load buttons %}
|
||||
|
||||
{% 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">
|
||||
|
@ -6,9 +6,7 @@
|
||||
<div class="panel-heading"><strong>Virtual Machine</strong></div>
|
||||
<div class="panel-body">
|
||||
{% render_field form.name %}
|
||||
{% render_field form.status %}
|
||||
{% render_field form.role %}
|
||||
{% render_field form.platform %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel panel-default">
|
||||
@ -18,6 +16,15 @@
|
||||
{% render_field form.cluster %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><strong>Management</strong></div>
|
||||
<div class="panel-body">
|
||||
{% render_field form.status %}
|
||||
{% render_field form.platform %}
|
||||
{% render_field form.primary_ip4 %}
|
||||
{% render_field form.primary_ip6 %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><strong>Resources</strong></div>
|
||||
<div class="panel-body">
|
||||
|
@ -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, TypedChoiceField
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.template 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 .error_handlers import handle_protectederror
|
||||
from .forms import ConfirmationForm
|
||||
@ -79,7 +79,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
|
||||
@ -92,27 +92,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)
|
||||
@ -134,10 +125,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())
|
||||
|
||||
|
@ -9,6 +9,7 @@ from dcim.constants import IFACE_FF_VIRTUAL
|
||||
from dcim.formfields import MACAddressFormField
|
||||
from dcim.models import Device, DeviceRole, Interface, Platform, Rack, Region, Site
|
||||
from extras.forms import CustomFieldBulkEditForm, CustomFieldForm, CustomFieldFilterForm
|
||||
from ipam.models import IPAddress
|
||||
from tenancy.forms import TenancyForm
|
||||
from tenancy.models import Tenant
|
||||
from utilities.forms import (
|
||||
@ -41,7 +42,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 +65,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 +113,7 @@ class ClusterCSVForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = Cluster
|
||||
fields = ['name', 'type', 'group', 'site', 'comments']
|
||||
fields = Cluster.csv_headers
|
||||
|
||||
|
||||
class ClusterBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
@ -246,8 +247,8 @@ class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
||||
class Meta:
|
||||
model = VirtualMachine
|
||||
fields = [
|
||||
'name', 'status', 'cluster_group', 'cluster', 'role', 'tenant', 'platform', 'vcpus', 'memory', 'disk',
|
||||
'comments',
|
||||
'name', 'status', 'cluster_group', 'cluster', 'role', 'tenant', 'platform', 'primary_ip4', 'primary_ip6',
|
||||
'vcpus', 'memory', 'disk', 'comments',
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@ -261,6 +262,41 @@ class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
||||
|
||||
super(VirtualMachineForm, self).__init__(*args, **kwargs)
|
||||
|
||||
if self.instance.pk:
|
||||
|
||||
# Compile list of choices for primary IPv4 and IPv6 addresses
|
||||
for family in [4, 6]:
|
||||
ip_choices = [(None, '---------')]
|
||||
# Collect interface IPs
|
||||
interface_ips = IPAddress.objects.select_related('interface').filter(
|
||||
family=family, interface__virtual_machine=self.instance
|
||||
)
|
||||
if interface_ips:
|
||||
ip_choices.append(
|
||||
('Interface IPs', [
|
||||
(ip.id, '{} ({})'.format(ip.address, ip.interface)) for ip in interface_ips
|
||||
])
|
||||
)
|
||||
# Collect NAT IPs
|
||||
nat_ips = IPAddress.objects.select_related('nat_inside').filter(
|
||||
family=family, nat_inside__interface__virtual_machine=self.instance
|
||||
)
|
||||
if nat_ips:
|
||||
ip_choices.append(
|
||||
('NAT IPs', [
|
||||
(ip.id, '{} ({})'.format(ip.address, ip.nat_inside.address)) for ip in nat_ips
|
||||
])
|
||||
)
|
||||
self.fields['primary_ip{}'.format(family)].choices = ip_choices
|
||||
|
||||
else:
|
||||
|
||||
# An object that doesn't exist yet can't have any IPs assigned to it
|
||||
self.fields['primary_ip4'].choices = []
|
||||
self.fields['primary_ip4'].widget.attrs['readonly'] = True
|
||||
self.fields['primary_ip6'].choices = []
|
||||
self.fields['primary_ip6'].widget.attrs['readonly'] = True
|
||||
|
||||
|
||||
class VirtualMachineCSVForm(forms.ModelForm):
|
||||
status = CSVChoiceField(
|
||||
@ -306,7 +342,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 STATUS_ACTIVE, 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