Merge branch '7854-vdc' of https://github.com/netbox-community/netbox into 7854-vdc

This commit is contained in:
Daniel Sheppard 2022-11-04 08:21:12 -05:00
commit c9afb2895e
190 changed files with 2268 additions and 1236 deletions

View File

@ -1,5 +1,13 @@
# Security & Authentication Parameters # Security & Authentication Parameters
## ALLOW_TOKEN_RETRIEVAL
Default: True
If disabled, the values of API tokens will not be displayed after each token's initial creation. A user **must** record the value of a token immediately upon its creation, or it will be lost. Note that this affects _all_ users, regardless of assigned permissions.
---
## ALLOWED_URL_SCHEMES ## ALLOWED_URL_SCHEMES
!!! tip "Dynamic Configuration Parameter" !!! tip "Dynamic Configuration Parameter"

View File

@ -1,5 +1,5 @@
# Journaling # Journaling
All primary objects in NetBox support journaling. A journal is a collection of human-generated notes and comments about an object maintained for historical context. It supplements NetBox's change log to provide additional information about why changes have been made or to convey events which occur outside NetBox. Unlike the change log, in which records typically expire after a configurable period of time, journal entries persist for the life of their associated object. All primary and organizational models in NetBox support journaling. A journal is a collection of human-generated notes and comments about an object maintained for historical context. It supplements NetBox's change log to provide additional information about why changes have been made or to convey events which occur outside NetBox. Unlike the change log, in which records typically expire after a configurable period of time, journal entries persist for the life of their associated object.
Each journal entry has a selectable kind (info, success, warning, or danger) and a user-populated `comments` field. Each entry automatically records the date, time, and associated user upon being created. Each journal entry has a selectable kind (info, success, warning, or danger) and a user-populated `comments` field. Each entry automatically records the date, time, and associated user upon being created.

View File

@ -579,6 +579,9 @@ By default, a token can be used to perform all actions via the API that a user w
Additionally, a token can be set to expire at a specific time. This can be useful if an external client needs to be granted temporary access to NetBox. Additionally, a token can be set to expire at a specific time. This can be useful if an external client needs to be granted temporary access to NetBox.
!!! warning "Restricting Token Retrieval"
The ability to retrieve the key value of a previously-created API token can be restricted by disabling the [`ALLOW_TOKEN_RETRIEVAL`](../configuration/security.md#allow_token_retrieval) configuration parameter.
#### Client IP Restriction #### Client IP Restriction
!!! note !!! note

View File

@ -33,7 +33,7 @@ Each site can have multiple [AS numbers](../ipam/asn.md) assigned to it.
### Time Zone ### Time Zone
The site's local time zone. (Time zones are provided by the [pytz](https://pypi.org/project/pytz/) package.) The site's local time zone. (Time zones are provided by the [zoneinfo](https://docs.python.org/3/library/zoneinfo.html) library.)
### Physical Address ### Physical Address

View File

@ -117,11 +117,11 @@ NetBox looks for the `config` variable within a plugin's `__init__.py` to load i
All required settings must be configured by the user. If a configuration parameter is listed in both `required_settings` and `default_settings`, the default setting will be ignored. All required settings must be configured by the user. If a configuration parameter is listed in both `required_settings` and `default_settings`, the default setting will be ignored.
!!! tip "Accessing Config Parameters" !!! tip "Accessing Config Parameters"
Plugin configuration parameters can be accessed in `settings.PLUGINS_CONFIG`, mapped by plugin name. For example: Plugin configuration parameters can be accessed using the `get_plugin_config()` function. For example:
```python ```python
from django.conf import settings from extras.plugins import get_plugin_config
settings.PLUGINS_CONFIG['myplugin']['verbose_name'] get_plugin_config('my_plugin', 'verbose_name')
``` ```
#### Important Notes About `django_apps` #### Important Notes About `django_apps`

View File

@ -28,6 +28,8 @@ A new `PluginMenu` class has been introduced, which enables a plugin to inject a
* [#8245](https://github.com/netbox-community/netbox/issues/8245) - Enable GraphQL filtering of related objects * [#8245](https://github.com/netbox-community/netbox/issues/8245) - Enable GraphQL filtering of related objects
* [#8274](https://github.com/netbox-community/netbox/issues/8274) - Enable associating a custom link with multiple object types * [#8274](https://github.com/netbox-community/netbox/issues/8274) - Enable associating a custom link with multiple object types
* [#8485](https://github.com/netbox-community/netbox/issues/8485) - Enable journaling for all organizational models
* [#8853](https://github.com/netbox-community/netbox/issues/8853) - Introduce the `ALLOW_TOKEN_RETRIEVAL` config parameter to restrict the display of API tokens
* [#9249](https://github.com/netbox-community/netbox/issues/9249) - Device and virtual machine names are no longer case-sensitive * [#9249](https://github.com/netbox-community/netbox/issues/9249) - Device and virtual machine names are no longer case-sensitive
* [#9478](https://github.com/netbox-community/netbox/issues/9478) - Add `link_peers` field to GraphQL types for cabled objects * [#9478](https://github.com/netbox-community/netbox/issues/9478) - Add `link_peers` field to GraphQL types for cabled objects
* [#9654](https://github.com/netbox-community/netbox/issues/9654) - Add `weight` field to racks, device types, and module types * [#9654](https://github.com/netbox-community/netbox/issues/9654) - Add `weight` field to racks, device types, and module types
@ -37,6 +39,7 @@ A new `PluginMenu` class has been introduced, which enables a plugin to inject a
* [#10348](https://github.com/netbox-community/netbox/issues/10348) - Add decimal custom field type * [#10348](https://github.com/netbox-community/netbox/issues/10348) - Add decimal custom field type
* [#10556](https://github.com/netbox-community/netbox/issues/10556) - Include a `display` field in all GraphQL object types * [#10556](https://github.com/netbox-community/netbox/issues/10556) - Include a `display` field in all GraphQL object types
* [#10595](https://github.com/netbox-community/netbox/issues/10595) - Add GraphQL relationships for additional generic foreign key fields * [#10595](https://github.com/netbox-community/netbox/issues/10595) - Add GraphQL relationships for additional generic foreign key fields
* [#10698](https://github.com/netbox-community/netbox/issues/10698) - Omit app label from content type in table columns
* [#10761](https://github.com/netbox-community/netbox/issues/10761) - Enable associating an export template with multiple object types * [#10761](https://github.com/netbox-community/netbox/issues/10761) - Enable associating an export template with multiple object types
* [#10781](https://github.com/netbox-community/netbox/issues/10781) - Add support for Python v3.11 * [#10781](https://github.com/netbox-community/netbox/issues/10781) - Add support for Python v3.11
@ -48,6 +51,7 @@ A new `PluginMenu` class has been introduced, which enables a plugin to inject a
* [#9880](https://github.com/netbox-community/netbox/issues/9880) - Introduce `django_apps` plugin configuration parameter * [#9880](https://github.com/netbox-community/netbox/issues/9880) - Introduce `django_apps` plugin configuration parameter
* [#9887](https://github.com/netbox-community/netbox/issues/9887) - Inspect `docs_url` property to determine link to model documentation * [#9887](https://github.com/netbox-community/netbox/issues/9887) - Inspect `docs_url` property to determine link to model documentation
* [#10314](https://github.com/netbox-community/netbox/issues/10314) - Move `clone()` method from NetBoxModel to CloningMixin * [#10314](https://github.com/netbox-community/netbox/issues/10314) - Move `clone()` method from NetBoxModel to CloningMixin
* [#10543](https://github.com/netbox-community/netbox/issues/10543) - Introduce `get_plugin_config()` utility function
* [#10739](https://github.com/netbox-community/netbox/issues/10739) - Introduce `get_queryset()` method on generic views * [#10739](https://github.com/netbox-community/netbox/issues/10739) - Introduce `get_queryset()` method on generic views
### Other Changes ### Other Changes
@ -55,24 +59,79 @@ A new `PluginMenu` class has been introduced, which enables a plugin to inject a
* [#9045](https://github.com/netbox-community/netbox/issues/9045) - Remove legacy ASN field from provider model * [#9045](https://github.com/netbox-community/netbox/issues/9045) - Remove legacy ASN field from provider model
* [#9046](https://github.com/netbox-community/netbox/issues/9046) - Remove legacy contact fields from provider model * [#9046](https://github.com/netbox-community/netbox/issues/9046) - Remove legacy contact fields from provider model
* [#10358](https://github.com/netbox-community/netbox/issues/10358) - Raise minimum required PostgreSQL version from 10 to 11 * [#10358](https://github.com/netbox-community/netbox/issues/10358) - Raise minimum required PostgreSQL version from 10 to 11
* [#10697](https://github.com/netbox-community/netbox/issues/10697) - Move application registry into core app
* [#10699](https://github.com/netbox-community/netbox/issues/10699) - Remove custom `import_object()` function * [#10699](https://github.com/netbox-community/netbox/issues/10699) - Remove custom `import_object()` function
* [#10816](https://github.com/netbox-community/netbox/issues/10816) - Pass the current request when instantiating a FilterSet within UI views
* [#10820](https://github.com/netbox-community/netbox/issues/10820) - Switch timezone library from pytz to zoneinfo
* [#10821](https://github.com/netbox-community/netbox/issues/10821) - Enable data localization
### REST API Changes ### REST API Changes
* circuits.provider * circuits.provider
* Removed the `asn`, `noc_contact`, `admin_contact`, and `portal_url` fields * Removed the `asn`, `noc_contact`, `admin_contact`, and `portal_url` fields
* Added a `description` field
* dcim.Cable
* Added `description` and `comments` fields
* dcim.Device
* Added a `description` field
* dcim.DeviceType * dcim.DeviceType
* Added a `description` field
* Added optional `weight` and `weight_unit` fields * Added optional `weight` and `weight_unit` fields
* dcim.Module
* Added a `description` field
* dcim.ModuleType * dcim.ModuleType
* Added a `description` field
* Added optional `weight` and `weight_unit` fields * Added optional `weight` and `weight_unit` fields
* dcim.PowerFeed
* Added a `description` field
* dcim.PowerPanel
* Added `description` and `comments` fields
* dcim.Rack * dcim.Rack
* Added a `description` field
* Added optional `weight` and `weight_unit` fields * Added optional `weight` and `weight_unit` fields
* dcim.RackReservation
* Added a `comments` field
* dcim.VirtualChassis
* Added `description` and `comments` fields
* extras.CustomLink * extras.CustomLink
* Renamed `content_type` field to `content_types` * Renamed `content_type` field to `content_types`
* extras.ExportTemplate * extras.ExportTemplate
* Renamed `content_type` field to `content_types` * Renamed `content_type` field to `content_types`
* ipam.Aggregate
* Added a `comments` field
* ipam.ASN
* Added a `comments` field
* ipam.FHRPGroup * ipam.FHRPGroup
* Added a `comments` field
* Added optional `name` field * Added optional `name` field
* ipam.IPAddress
* Added a `comments` field
* ipam.IPRange
* Added a `comments` field
* ipam.L2VPN
* Added a `comments` field
* ipam.Prefix
* Added a `comments` field
* ipam.RouteTarget
* Added a `comments` field
* ipam.Service
* Added a `comments` field
* ipam.ServiceTemplate
* Added a `comments` field
* ipam.VLAN
* Added a `comments` field
* ipam.VRF
* Added a `comments` field
* tenancy.Contact
* Added a `description` field
* virtualization.Cluster
* Added a `description` field
* virtualization.VirtualMachine
* Added a `description` field
* wireless.WirelessLAN
* Added a `comments` field
* wireless.WirelessLink
* Added a `comments` field
### GraphQL API Changes ### GraphQL API Changes

View File

@ -31,8 +31,8 @@ class ProviderSerializer(NetBoxModelSerializer):
class Meta: class Meta:
model = Provider model = Provider
fields = [ fields = [
'id', 'url', 'display', 'name', 'slug', 'account', 'id', 'url', 'display', 'name', 'slug', 'account', 'description', 'comments', 'asns', 'tags',
'comments', 'asns', 'tags', 'custom_fields', 'created', 'last_updated', 'circuit_count', 'custom_fields', 'created', 'last_updated', 'circuit_count',
] ]

View File

@ -30,6 +30,10 @@ class ProviderBulkEditForm(NetBoxModelBulkEditForm):
required=False, required=False,
label='Account number' label='Account number'
) )
description = forms.CharField(
max_length=200,
required=False
)
comments = CommentField( comments = CommentField(
widget=SmallTextarea, widget=SmallTextarea,
label='Comments' label='Comments'
@ -40,7 +44,7 @@ class ProviderBulkEditForm(NetBoxModelBulkEditForm):
(None, ('asns', 'account', )), (None, ('asns', 'account', )),
) )
nullable_fields = ( nullable_fields = (
'asns', 'account', 'comments', 'asns', 'account', 'description', 'comments',
) )

View File

@ -18,7 +18,7 @@ class ProviderCSVForm(NetBoxModelCSVForm):
class Meta: class Meta:
model = Provider model = Provider
fields = ( fields = (
'name', 'slug', 'account', 'comments', 'name', 'slug', 'account', 'description', 'comments',
) )

View File

@ -20,7 +20,7 @@ __all__ = (
class ProviderFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm): class ProviderFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
model = Provider model = Provider
fieldsets = ( fieldsets = (
(None, ('q', 'tag')), (None, ('q', 'filter', 'tag')),
('Location', ('region_id', 'site_group_id', 'site_id')), ('Location', ('region_id', 'site_group_id', 'site_id')),
('ASN', ('asn',)), ('ASN', ('asn',)),
('Contacts', ('contact', 'contact_role', 'contact_group')), ('Contacts', ('contact', 'contact_role', 'contact_group')),
@ -59,7 +59,7 @@ class ProviderFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
class ProviderNetworkFilterForm(NetBoxModelFilterSetForm): class ProviderNetworkFilterForm(NetBoxModelFilterSetForm):
model = ProviderNetwork model = ProviderNetwork
fieldsets = ( fieldsets = (
(None, ('q', 'tag')), (None, ('q', 'filter', 'tag')),
('Attributes', ('provider_id', 'service_id')), ('Attributes', ('provider_id', 'service_id')),
) )
provider_id = DynamicModelMultipleChoiceField( provider_id = DynamicModelMultipleChoiceField(
@ -82,7 +82,7 @@ class CircuitTypeFilterForm(NetBoxModelFilterSetForm):
class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm): class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm):
model = Circuit model = Circuit
fieldsets = ( fieldsets = (
(None, ('q', 'tag')), (None, ('q', 'filter', 'tag')),
('Provider', ('provider_id', 'provider_network_id')), ('Provider', ('provider_id', 'provider_network_id')),
('Attributes', ('type_id', 'status', 'install_date', 'termination_date', 'commit_rate')), ('Attributes', ('type_id', 'status', 'install_date', 'termination_date', 'commit_rate')),
('Location', ('region_id', 'site_group_id', 'site_id')), ('Location', ('region_id', 'site_group_id', 'site_id')),

View File

@ -1,4 +1,3 @@
from django import forms
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from circuits.models import * from circuits.models import *
@ -7,8 +6,8 @@ from ipam.models import ASN
from netbox.forms import NetBoxModelForm from netbox.forms import NetBoxModelForm
from tenancy.forms import TenancyForm from tenancy.forms import TenancyForm
from utilities.forms import ( from utilities.forms import (
BootstrapMixin, CommentField, DatePicker, DynamicModelChoiceField, DynamicModelMultipleChoiceField, CommentField, DatePicker, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SelectSpeedWidget, SlugField,
SelectSpeedWidget, SmallTextarea, SlugField, StaticSelect, StaticSelect,
) )
__all__ = ( __all__ = (
@ -30,14 +29,14 @@ class ProviderForm(NetBoxModelForm):
comments = CommentField() comments = CommentField()
fieldsets = ( fieldsets = (
('Provider', ('name', 'slug', 'asns', 'tags')), ('Provider', ('name', 'slug', 'asns', 'description', 'tags')),
('Support Info', ('account',)), ('Support Info', ('account',)),
) )
class Meta: class Meta:
model = Provider model = Provider
fields = [ fields = [
'name', 'slug', 'account', 'asns', 'comments', 'tags', 'name', 'slug', 'account', 'asns', 'description', 'comments', 'tags',
] ]
help_texts = { help_texts = {
'name': "Full name of the provider", 'name': "Full name of the provider",

View File

@ -65,7 +65,7 @@ class Migration(migrations.Migration):
('description', models.CharField(blank=True, max_length=200)), ('description', models.CharField(blank=True, max_length=200)),
], ],
options={ options={
'ordering': ['name'], 'ordering': ('name',),
}, },
), ),
migrations.CreateModel( migrations.CreateModel(

View File

@ -0,0 +1,18 @@
# Generated by Django 4.1.2 on 2022-11-03 18:24
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('circuits', '0040_provider_remove_deprecated_fields'),
]
operations = [
migrations.AddField(
model_name='provider',
name='description',
field=models.CharField(blank=True, max_length=200),
),
]

View File

@ -7,7 +7,7 @@ from django.urls import reverse
from circuits.choices import * from circuits.choices import *
from dcim.models import CabledObjectModel from dcim.models import CabledObjectModel
from netbox.models import ( from netbox.models import (
ChangeLoggedModel, CustomFieldsMixin, CustomLinksMixin, OrganizationalModel, NetBoxModel, TagsMixin, ChangeLoggedModel, CustomFieldsMixin, CustomLinksMixin, OrganizationalModel, PrimaryModel, TagsMixin,
) )
from netbox.models.features import WebhooksMixin from netbox.models.features import WebhooksMixin
@ -23,30 +23,11 @@ class CircuitType(OrganizationalModel):
Circuits can be organized by their functional role. For example, a user might wish to define CircuitTypes named Circuits can be organized by their functional role. For example, a user might wish to define CircuitTypes named
"Long Haul," "Metro," or "Out-of-Band". "Long Haul," "Metro," or "Out-of-Band".
""" """
name = models.CharField(
max_length=100,
unique=True
)
slug = models.SlugField(
max_length=100,
unique=True
)
description = models.CharField(
max_length=200,
blank=True,
)
class Meta:
ordering = ['name']
def __str__(self):
return self.name
def get_absolute_url(self): def get_absolute_url(self):
return reverse('circuits:circuittype', args=[self.pk]) return reverse('circuits:circuittype', args=[self.pk])
class Circuit(NetBoxModel): class Circuit(PrimaryModel):
""" """
A communications circuit connects two points. Each Circuit belongs to a Provider; Providers may have multiple A communications circuit connects two points. Each Circuit belongs to a Provider; Providers may have multiple
circuits. Each circuit is also assigned a CircuitType and a Site. Circuit port speed and commit rate are measured circuits. Each circuit is also assigned a CircuitType and a Site. Circuit port speed and commit rate are measured
@ -92,13 +73,6 @@ class Circuit(NetBoxModel):
blank=True, blank=True,
null=True, null=True,
verbose_name='Commit rate (Kbps)') verbose_name='Commit rate (Kbps)')
description = models.CharField(
max_length=200,
blank=True
)
comments = models.TextField(
blank=True
)
# Generic relations # Generic relations
contacts = GenericRelation( contacts = GenericRelation(

View File

@ -2,8 +2,7 @@ from django.contrib.contenttypes.fields import GenericRelation
from django.db import models from django.db import models
from django.urls import reverse from django.urls import reverse
from dcim.fields import ASNField from netbox.models import PrimaryModel
from netbox.models import NetBoxModel
__all__ = ( __all__ = (
'ProviderNetwork', 'ProviderNetwork',
@ -11,7 +10,7 @@ __all__ = (
) )
class Provider(NetBoxModel): class Provider(PrimaryModel):
""" """
Each Circuit belongs to a Provider. This is usually a telecommunications company or similar organization. This model Each Circuit belongs to a Provider. This is usually a telecommunications company or similar organization. This model
stores information pertinent to the user's relationship with the Provider. stores information pertinent to the user's relationship with the Provider.
@ -34,9 +33,6 @@ class Provider(NetBoxModel):
blank=True, blank=True,
verbose_name='Account number' verbose_name='Account number'
) )
comments = models.TextField(
blank=True
)
# Generic relations # Generic relations
contacts = GenericRelation( contacts = GenericRelation(
@ -57,7 +53,7 @@ class Provider(NetBoxModel):
return reverse('circuits:provider', args=[self.pk]) return reverse('circuits:provider', args=[self.pk])
class ProviderNetwork(NetBoxModel): class ProviderNetwork(PrimaryModel):
""" """
This represents a provider network which exists outside of NetBox, the details of which are unknown or This represents a provider network which exists outside of NetBox, the details of which are unknown or
unimportant to the user. unimportant to the user.
@ -75,13 +71,6 @@ class ProviderNetwork(NetBoxModel):
blank=True, blank=True,
verbose_name='Service ID' verbose_name='Service ID'
) )
description = models.CharField(
max_length=200,
blank=True
)
comments = models.TextField(
blank=True
)
class Meta: class Meta:
ordering = ('provider', 'name') ordering = ('provider', 'name')

View File

@ -39,8 +39,8 @@ class ProviderTable(ContactsColumnMixin, NetBoxTable):
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = Provider model = Provider
fields = ( fields = (
'pk', 'id', 'name', 'asns', 'account', 'asn_count', 'pk', 'id', 'name', 'asns', 'account', 'asn_count', 'circuit_count', 'description', 'comments', 'contacts',
'circuit_count', 'comments', 'contacts', 'tags', 'created', 'last_updated', 'tags', 'created', 'last_updated',
) )
default_columns = ('pk', 'name', 'account', 'circuit_count') default_columns = ('pk', 'name', 'account', 'circuit_count')

View File

@ -210,8 +210,8 @@ class RackSerializer(NetBoxModelSerializer):
fields = [ fields = [
'id', 'url', 'display', 'name', 'facility_id', 'site', 'location', 'tenant', 'status', 'role', 'serial', 'id', 'url', 'display', 'name', 'facility_id', 'site', 'location', 'tenant', 'status', 'role', 'serial',
'asset_tag', 'type', 'width', 'u_height', 'weight', 'weight_unit', 'desc_units', 'outer_width', 'asset_tag', 'type', 'width', 'u_height', 'weight', 'weight_unit', 'desc_units', 'outer_width',
'outer_depth', 'outer_unit', 'mounting_depth', 'comments', 'tags', 'custom_fields', 'created', 'outer_depth', 'outer_unit', 'mounting_depth', 'description', 'comments', 'tags', 'custom_fields',
'last_updated', 'device_count', 'powerfeed_count', 'created', 'last_updated', 'device_count', 'powerfeed_count',
] ]
@ -243,8 +243,8 @@ class RackReservationSerializer(NetBoxModelSerializer):
class Meta: class Meta:
model = RackReservation model = RackReservation
fields = [ fields = [
'id', 'url', 'display', 'rack', 'units', 'created', 'last_updated', 'user', 'tenant', 'description', 'tags', 'id', 'url', 'display', 'rack', 'units', 'created', 'last_updated', 'user', 'tenant', 'description',
'custom_fields', 'comments', 'tags', 'custom_fields',
] ]
@ -324,8 +324,8 @@ class DeviceTypeSerializer(NetBoxModelSerializer):
model = DeviceType model = DeviceType
fields = [ fields = [
'id', 'url', 'display', 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'id', 'url', 'display', 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth',
'subdevice_role', 'airflow', 'weight', 'weight_unit', 'front_image', 'rear_image', 'comments', 'subdevice_role', 'airflow', 'weight', 'weight_unit', 'front_image', 'rear_image', 'description',
'tags', 'custom_fields', 'created', 'last_updated', 'device_count', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'device_count',
] ]
@ -333,13 +333,12 @@ class ModuleTypeSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:moduletype-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:moduletype-detail')
manufacturer = NestedManufacturerSerializer() manufacturer = NestedManufacturerSerializer()
weight_unit = ChoiceField(choices=WeightUnitChoices, allow_blank=True, required=False) weight_unit = ChoiceField(choices=WeightUnitChoices, allow_blank=True, required=False)
# module_count = serializers.IntegerField(read_only=True)
class Meta: class Meta:
model = ModuleType model = ModuleType
fields = [ fields = [
'id', 'url', 'display', 'manufacturer', 'model', 'part_number', 'weight', 'weight_unit', 'comments', 'tags', 'id', 'url', 'display', 'manufacturer', 'model', 'part_number', 'weight', 'weight_unit', 'description',
'custom_fields', 'created', 'last_updated', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
] ]
@ -656,8 +655,8 @@ class DeviceSerializer(NetBoxModelSerializer):
fields = [ fields = [
'id', 'url', 'display', 'name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'asset_tag', 'id', 'url', 'display', 'name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'asset_tag',
'site', 'location', 'rack', 'position', 'face', 'parent_device', 'status', 'airflow', 'primary_ip', 'site', 'location', 'rack', 'position', 'face', 'parent_device', 'status', 'airflow', 'primary_ip',
'primary_ip4', 'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'primary_ip4', 'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'description',
'local_context_data', 'tags', 'custom_fields', 'created', 'last_updated', 'comments', 'local_context_data', 'tags', 'custom_fields', 'created', 'last_updated',
] ]
@swagger_serializer_method(serializer_or_field=NestedDeviceSerializer) @swagger_serializer_method(serializer_or_field=NestedDeviceSerializer)
@ -697,8 +696,8 @@ class ModuleSerializer(NetBoxModelSerializer):
class Meta: class Meta:
model = Module model = Module
fields = [ fields = [
'id', 'url', 'display', 'device', 'module_bay', 'module_type', 'serial', 'asset_tag', 'comments', 'tags', 'id', 'url', 'display', 'device', 'module_bay', 'module_type', 'serial', 'asset_tag', 'description',
'custom_fields', 'created', 'last_updated', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
] ]
@ -1041,7 +1040,7 @@ class CableSerializer(NetBoxModelSerializer):
model = Cable model = Cable
fields = [ fields = [
'id', 'url', 'display', 'type', 'a_terminations', 'b_terminations', 'status', 'tenant', 'label', 'color', 'id', 'url', 'display', 'type', 'a_terminations', 'b_terminations', 'status', 'tenant', 'label', 'color',
'length', 'length_unit', 'tags', 'custom_fields', 'created', 'last_updated', 'length', 'length_unit', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
] ]
@ -1107,8 +1106,8 @@ class VirtualChassisSerializer(NetBoxModelSerializer):
class Meta: class Meta:
model = VirtualChassis model = VirtualChassis
fields = [ fields = [
'id', 'url', 'display', 'name', 'domain', 'master', 'tags', 'custom_fields', 'member_count', 'id', 'url', 'display', 'name', 'domain', 'master', 'description', 'comments', 'tags', 'custom_fields',
'created', 'last_updated', 'member_count', 'created', 'last_updated',
] ]
@ -1129,8 +1128,8 @@ class PowerPanelSerializer(NetBoxModelSerializer):
class Meta: class Meta:
model = PowerPanel model = PowerPanel
fields = [ fields = [
'id', 'url', 'display', 'site', 'location', 'name', 'tags', 'custom_fields', 'powerfeed_count', 'id', 'url', 'display', 'site', 'location', 'name', 'description', 'comments', 'tags', 'custom_fields',
'created', 'last_updated', 'powerfeed_count', 'created', 'last_updated',
] ]
@ -1163,7 +1162,7 @@ class PowerFeedSerializer(NetBoxModelSerializer, CabledObjectSerializer, Connect
model = PowerFeed model = PowerFeed
fields = [ fields = [
'id', 'url', 'display', 'power_panel', 'rack', 'name', 'status', 'type', 'supply', 'phase', 'voltage', 'id', 'url', 'display', 'power_panel', 'rack', 'name', 'status', 'type', 'supply', 'phase', 'voltage',
'amperage', 'max_utilization', 'comments', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'amperage', 'max_utilization', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type',
'link_peers_type', 'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable', 'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable', 'description',
'tags', 'custom_fields', 'created', 'last_updated', '_occupied', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
] ]

View File

@ -128,22 +128,26 @@ class SiteBulkEditForm(NetBoxModelBulkEditForm):
required=False, required=False,
label='Contact E-mail' label='Contact E-mail'
) )
description = forms.CharField(
max_length=100,
required=False
)
time_zone = TimeZoneFormField( time_zone = TimeZoneFormField(
choices=add_blank_choice(TimeZoneFormField().choices), choices=add_blank_choice(TimeZoneFormField().choices),
required=False, required=False,
widget=StaticSelect() widget=StaticSelect()
) )
description = forms.CharField(
max_length=200,
required=False
)
comments = CommentField(
widget=SmallTextarea,
label='Comments'
)
model = Site model = Site
fieldsets = ( fieldsets = (
(None, ('status', 'region', 'group', 'tenant', 'asns', 'time_zone', 'description')), (None, ('status', 'region', 'group', 'tenant', 'asns', 'time_zone', 'description')),
) )
nullable_fields = ( nullable_fields = (
'region', 'group', 'tenant', 'asns', 'description', 'time_zone', 'region', 'group', 'tenant', 'asns', 'time_zone', 'description', 'comments',
) )
@ -286,10 +290,6 @@ class RackBulkEditForm(NetBoxModelBulkEditForm):
required=False, required=False,
min_value=1 min_value=1
) )
comments = CommentField(
widget=SmallTextarea,
label='Comments'
)
weight = forms.DecimalField( weight = forms.DecimalField(
min_value=0, min_value=0,
required=False required=False
@ -300,10 +300,18 @@ class RackBulkEditForm(NetBoxModelBulkEditForm):
initial='', initial='',
widget=StaticSelect() widget=StaticSelect()
) )
description = forms.CharField(
max_length=200,
required=False
)
comments = CommentField(
widget=SmallTextarea,
label='Comments'
)
model = Rack model = Rack
fieldsets = ( fieldsets = (
('Rack', ('status', 'role', 'tenant', 'serial', 'asset_tag')), ('Rack', ('status', 'role', 'tenant', 'serial', 'asset_tag', 'description')),
('Location', ('region', 'site_group', 'site', 'location')), ('Location', ('region', 'site_group', 'site', 'location')),
('Hardware', ( ('Hardware', (
'type', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'mounting_depth', 'type', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'mounting_depth',
@ -311,8 +319,8 @@ class RackBulkEditForm(NetBoxModelBulkEditForm):
('Weight', ('weight', 'weight_unit')), ('Weight', ('weight', 'weight_unit')),
) )
nullable_fields = ( nullable_fields = (
'location', 'tenant', 'role', 'serial', 'asset_tag', 'outer_width', 'outer_depth', 'outer_unit', 'comments', 'location', 'tenant', 'role', 'serial', 'asset_tag', 'outer_width', 'outer_depth', 'outer_unit', 'weight',
'weight', 'weight_unit' 'weight_unit', 'description', 'comments',
) )
@ -329,14 +337,19 @@ class RackReservationBulkEditForm(NetBoxModelBulkEditForm):
required=False required=False
) )
description = forms.CharField( description = forms.CharField(
max_length=100, max_length=200,
required=False required=False
) )
comments = CommentField(
widget=SmallTextarea,
label='Comments'
)
model = RackReservation model = RackReservation
fieldsets = ( fieldsets = (
(None, ('user', 'tenant', 'description')), (None, ('user', 'tenant', 'description')),
) )
nullable_fields = ('comments',)
class ManufacturerBulkEditForm(NetBoxModelBulkEditForm): class ManufacturerBulkEditForm(NetBoxModelBulkEditForm):
@ -384,13 +397,21 @@ class DeviceTypeBulkEditForm(NetBoxModelBulkEditForm):
initial='', initial='',
widget=StaticSelect() widget=StaticSelect()
) )
description = forms.CharField(
max_length=200,
required=False
)
comments = CommentField(
widget=SmallTextarea,
label='Comments'
)
model = DeviceType model = DeviceType
fieldsets = ( fieldsets = (
('Device Type', ('manufacturer', 'part_number', 'u_height', 'is_full_depth', 'airflow')), ('Device Type', ('manufacturer', 'part_number', 'u_height', 'is_full_depth', 'airflow', 'description')),
('Weight', ('weight', 'weight_unit')), ('Weight', ('weight', 'weight_unit')),
) )
nullable_fields = ('part_number', 'airflow', 'weight', 'weight_unit') nullable_fields = ('part_number', 'airflow', 'weight', 'weight_unit', 'description', 'comments')
class ModuleTypeBulkEditForm(NetBoxModelBulkEditForm): class ModuleTypeBulkEditForm(NetBoxModelBulkEditForm):
@ -411,13 +432,21 @@ class ModuleTypeBulkEditForm(NetBoxModelBulkEditForm):
initial='', initial='',
widget=StaticSelect() widget=StaticSelect()
) )
description = forms.CharField(
max_length=200,
required=False
)
comments = CommentField(
widget=SmallTextarea,
label='Comments'
)
model = ModuleType model = ModuleType
fieldsets = ( fieldsets = (
('Module Type', ('manufacturer', 'part_number')), ('Module Type', ('manufacturer', 'part_number', 'description')),
('Weight', ('weight', 'weight_unit')), ('Weight', ('weight', 'weight_unit')),
) )
nullable_fields = ('part_number', 'weight', 'weight_unit') nullable_fields = ('part_number', 'weight', 'weight_unit', 'description', 'comments')
class DeviceRoleBulkEditForm(NetBoxModelBulkEditForm): class DeviceRoleBulkEditForm(NetBoxModelBulkEditForm):
@ -513,15 +542,23 @@ class DeviceBulkEditForm(NetBoxModelBulkEditForm):
required=False, required=False,
label='Serial Number' label='Serial Number'
) )
description = forms.CharField(
max_length=200,
required=False
)
comments = CommentField(
widget=SmallTextarea,
label='Comments'
)
model = Device model = Device
fieldsets = ( fieldsets = (
('Device', ('device_role', 'status', 'tenant', 'platform')), ('Device', ('device_role', 'status', 'tenant', 'platform', 'description')),
('Location', ('site', 'location')), ('Location', ('site', 'location')),
('Hardware', ('manufacturer', 'device_type', 'airflow', 'serial')), ('Hardware', ('manufacturer', 'device_type', 'airflow', 'serial')),
) )
nullable_fields = ( nullable_fields = (
'location', 'tenant', 'platform', 'serial', 'airflow', 'location', 'tenant', 'platform', 'serial', 'airflow', 'description', 'comments',
) )
@ -542,12 +579,20 @@ class ModuleBulkEditForm(NetBoxModelBulkEditForm):
required=False, required=False,
label='Serial Number' label='Serial Number'
) )
description = forms.CharField(
max_length=200,
required=False
)
comments = CommentField(
widget=SmallTextarea,
label='Comments'
)
model = Module model = Module
fieldsets = ( fieldsets = (
(None, ('manufacturer', 'module_type', 'serial')), (None, ('manufacturer', 'module_type', 'serial', 'description')),
) )
nullable_fields = ('serial',) nullable_fields = ('serial', 'description', 'comments')
class CableBulkEditForm(NetBoxModelBulkEditForm): class CableBulkEditForm(NetBoxModelBulkEditForm):
@ -584,14 +629,22 @@ class CableBulkEditForm(NetBoxModelBulkEditForm):
initial='', initial='',
widget=StaticSelect() widget=StaticSelect()
) )
description = forms.CharField(
max_length=200,
required=False
)
comments = CommentField(
widget=SmallTextarea,
label='Comments'
)
model = Cable model = Cable
fieldsets = ( fieldsets = (
(None, ('type', 'status', 'tenant', 'label')), (None, ('type', 'status', 'tenant', 'label', 'description')),
('Attributes', ('color', 'length', 'length_unit')), ('Attributes', ('color', 'length', 'length_unit')),
) )
nullable_fields = ( nullable_fields = (
'type', 'status', 'tenant', 'label', 'color', 'length', 'type', 'status', 'tenant', 'label', 'color', 'length', 'description', 'comments',
) )
@ -600,12 +653,20 @@ class VirtualChassisBulkEditForm(NetBoxModelBulkEditForm):
max_length=30, max_length=30,
required=False required=False
) )
description = forms.CharField(
max_length=200,
required=False
)
comments = CommentField(
widget=SmallTextarea,
label='Comments'
)
model = VirtualChassis model = VirtualChassis
fieldsets = ( fieldsets = (
(None, ('domain',)), (None, ('domain', 'description')),
) )
nullable_fields = ('domain',) nullable_fields = ('domain', 'description', 'comments')
class PowerPanelBulkEditForm(NetBoxModelBulkEditForm): class PowerPanelBulkEditForm(NetBoxModelBulkEditForm):
@ -638,12 +699,20 @@ class PowerPanelBulkEditForm(NetBoxModelBulkEditForm):
'site_id': '$site' 'site_id': '$site'
} }
) )
description = forms.CharField(
max_length=200,
required=False
)
comments = CommentField(
widget=SmallTextarea,
label='Comments'
)
model = PowerPanel model = PowerPanel
fieldsets = ( fieldsets = (
(None, ('region', 'site_group', 'site', 'location')), (None, ('region', 'site_group', 'site', 'location', 'description')),
) )
nullable_fields = ('location',) nullable_fields = ('location', 'description', 'comments')
class PowerFeedBulkEditForm(NetBoxModelBulkEditForm): class PowerFeedBulkEditForm(NetBoxModelBulkEditForm):
@ -692,6 +761,10 @@ class PowerFeedBulkEditForm(NetBoxModelBulkEditForm):
required=False, required=False,
widget=BulkEditNullBooleanSelect widget=BulkEditNullBooleanSelect
) )
description = forms.CharField(
max_length=200,
required=False
)
comments = CommentField( comments = CommentField(
widget=SmallTextarea, widget=SmallTextarea,
label='Comments' label='Comments'
@ -699,10 +772,10 @@ class PowerFeedBulkEditForm(NetBoxModelBulkEditForm):
model = PowerFeed model = PowerFeed
fieldsets = ( fieldsets = (
(None, ('power_panel', 'rack', 'status', 'type', 'mark_connected')), (None, ('power_panel', 'rack', 'status', 'type', 'mark_connected', 'description')),
('Power', ('supply', 'phase', 'voltage', 'amperage', 'max_utilization')) ('Power', ('supply', 'phase', 'voltage', 'amperage', 'max_utilization'))
) )
nullable_fields = ('location', 'comments') nullable_fields = ('location', 'description', 'comments')
# #

View File

@ -197,7 +197,8 @@ class RackCSVForm(NetBoxModelCSVForm):
model = Rack model = Rack
fields = ( fields = (
'site', 'location', 'name', 'facility_id', 'tenant', 'status', 'role', 'type', 'serial', 'asset_tag', 'site', 'location', 'name', 'facility_id', 'tenant', 'status', 'role', 'type', 'serial', 'asset_tag',
'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'mounting_depth', 'comments', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'mounting_depth',
'description', 'comments',
) )
def __init__(self, data=None, *args, **kwargs): def __init__(self, data=None, *args, **kwargs):
@ -241,7 +242,7 @@ class RackReservationCSVForm(NetBoxModelCSVForm):
class Meta: class Meta:
model = RackReservation model = RackReservation
fields = ('site', 'location', 'rack', 'units', 'tenant', 'description') fields = ('site', 'location', 'rack', 'units', 'tenant', 'description', 'comments')
def __init__(self, data=None, *args, **kwargs): def __init__(self, data=None, *args, **kwargs):
super().__init__(data, *args, **kwargs) super().__init__(data, *args, **kwargs)
@ -388,7 +389,7 @@ class DeviceCSVForm(BaseDeviceCSVForm):
fields = [ fields = [
'name', 'device_role', 'tenant', 'manufacturer', 'device_type', 'platform', 'serial', 'asset_tag', 'status', 'name', 'device_role', 'tenant', 'manufacturer', 'device_type', 'platform', 'serial', 'asset_tag', 'status',
'site', 'location', 'rack', 'position', 'face', 'airflow', 'virtual_chassis', 'vc_position', 'vc_priority', 'site', 'location', 'rack', 'position', 'face', 'airflow', 'virtual_chassis', 'vc_position', 'vc_priority',
'cluster', 'comments', 'cluster', 'description', 'comments',
] ]
def __init__(self, data=None, *args, **kwargs): def __init__(self, data=None, *args, **kwargs):
@ -425,7 +426,7 @@ class ModuleCSVForm(NetBoxModelCSVForm):
class Meta: class Meta:
model = Module model = Module
fields = ( fields = (
'device', 'module_bay', 'module_type', 'serial', 'asset_tag', 'comments', 'device', 'module_bay', 'module_type', 'serial', 'asset_tag', 'description', 'comments',
) )
def __init__(self, data=None, *args, **kwargs): def __init__(self, data=None, *args, **kwargs):
@ -928,7 +929,7 @@ class CableCSVForm(NetBoxModelCSVForm):
model = Cable model = Cable
fields = [ fields = [
'side_a_device', 'side_a_type', 'side_a_name', 'side_b_device', 'side_b_type', 'side_b_name', 'type', 'side_a_device', 'side_a_type', 'side_a_name', 'side_b_device', 'side_b_type', 'side_b_name', 'type',
'status', 'tenant', 'label', 'color', 'length', 'length_unit', 'status', 'tenant', 'label', 'color', 'length', 'length_unit', 'description', 'comments',
] ]
help_texts = { help_texts = {
'color': mark_safe('RGB color in hexadecimal (e.g. <code>00ff00</code>)'), 'color': mark_safe('RGB color in hexadecimal (e.g. <code>00ff00</code>)'),
@ -985,7 +986,7 @@ class VirtualChassisCSVForm(NetBoxModelCSVForm):
class Meta: class Meta:
model = VirtualChassis model = VirtualChassis
fields = ('name', 'domain', 'master') fields = ('name', 'domain', 'master', 'description')
# #
@ -1006,7 +1007,7 @@ class PowerPanelCSVForm(NetBoxModelCSVForm):
class Meta: class Meta:
model = PowerPanel model = PowerPanel
fields = ('site', 'location', 'name') fields = ('site', 'location', 'name', 'description', 'comments')
def __init__(self, data=None, *args, **kwargs): def __init__(self, data=None, *args, **kwargs):
super().__init__(data, *args, **kwargs) super().__init__(data, *args, **kwargs)
@ -1062,7 +1063,7 @@ class PowerFeedCSVForm(NetBoxModelCSVForm):
model = PowerFeed model = PowerFeed
fields = ( fields = (
'site', 'power_panel', 'location', 'rack', 'name', 'status', 'type', 'mark_connected', 'supply', 'phase', 'site', 'power_panel', 'location', 'rack', 'name', 'status', 'type', 'mark_connected', 'supply', 'phase',
'voltage', 'amperage', 'max_utilization', 'comments', 'voltage', 'amperage', 'max_utilization', 'description', 'comments',
) )
def __init__(self, data=None, *args, **kwargs): def __init__(self, data=None, *args, **kwargs):

View File

@ -117,7 +117,7 @@ class DeviceComponentFilterForm(NetBoxModelFilterSetForm):
class RegionFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm): class RegionFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
model = Region model = Region
fieldsets = ( fieldsets = (
(None, ('q', 'tag', 'parent_id')), (None, ('q', 'filter', 'tag', 'parent_id')),
('Contacts', ('contact', 'contact_role', 'contact_group')) ('Contacts', ('contact', 'contact_role', 'contact_group'))
) )
parent_id = DynamicModelMultipleChoiceField( parent_id = DynamicModelMultipleChoiceField(
@ -131,7 +131,7 @@ class RegionFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
class SiteGroupFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm): class SiteGroupFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
model = SiteGroup model = SiteGroup
fieldsets = ( fieldsets = (
(None, ('q', 'tag', 'parent_id')), (None, ('q', 'filter', 'tag', 'parent_id')),
('Contacts', ('contact', 'contact_role', 'contact_group')) ('Contacts', ('contact', 'contact_role', 'contact_group'))
) )
parent_id = DynamicModelMultipleChoiceField( parent_id = DynamicModelMultipleChoiceField(
@ -145,7 +145,7 @@ class SiteGroupFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
class SiteFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm): class SiteFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm):
model = Site model = Site
fieldsets = ( fieldsets = (
(None, ('q', 'tag')), (None, ('q', 'filter', 'tag')),
('Attributes', ('status', 'region_id', 'group_id', 'asn_id')), ('Attributes', ('status', 'region_id', 'group_id', 'asn_id')),
('Tenant', ('tenant_group_id', 'tenant_id')), ('Tenant', ('tenant_group_id', 'tenant_id')),
('Contacts', ('contact', 'contact_role', 'contact_group')), ('Contacts', ('contact', 'contact_role', 'contact_group')),
@ -175,7 +175,7 @@ class SiteFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilte
class LocationFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm): class LocationFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm):
model = Location model = Location
fieldsets = ( fieldsets = (
(None, ('q', 'tag')), (None, ('q', 'filter', 'tag')),
('Attributes', ('region_id', 'site_group_id', 'site_id', 'parent_id', 'status')), ('Attributes', ('region_id', 'site_group_id', 'site_id', 'parent_id', 'status')),
('Tenant', ('tenant_group_id', 'tenant_id')), ('Tenant', ('tenant_group_id', 'tenant_id')),
('Contacts', ('contact', 'contact_role', 'contact_group')), ('Contacts', ('contact', 'contact_role', 'contact_group')),
@ -223,7 +223,7 @@ class RackRoleFilterForm(NetBoxModelFilterSetForm):
class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm): class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm):
model = Rack model = Rack
fieldsets = ( fieldsets = (
(None, ('q', 'tag')), (None, ('q', 'filter', 'tag')),
('Location', ('region_id', 'site_group_id', 'site_id', 'location_id')), ('Location', ('region_id', 'site_group_id', 'site_id', 'location_id')),
('Function', ('status', 'role_id')), ('Function', ('status', 'role_id')),
('Hardware', ('type', 'width', 'serial', 'asset_tag')), ('Hardware', ('type', 'width', 'serial', 'asset_tag')),
@ -307,7 +307,7 @@ class RackElevationFilterForm(RackFilterForm):
class RackReservationFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): class RackReservationFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
model = RackReservation model = RackReservation
fieldsets = ( fieldsets = (
(None, ('q', 'tag')), (None, ('q', 'filter', 'tag')),
('User', ('user_id',)), ('User', ('user_id',)),
('Rack', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')), ('Rack', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
('Tenant', ('tenant_group_id', 'tenant_id')), ('Tenant', ('tenant_group_id', 'tenant_id')),
@ -363,7 +363,7 @@ class RackReservationFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
class ManufacturerFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm): class ManufacturerFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
model = Manufacturer model = Manufacturer
fieldsets = ( fieldsets = (
(None, ('q', 'tag')), (None, ('q', 'filter', 'tag')),
('Contacts', ('contact', 'contact_role', 'contact_group')) ('Contacts', ('contact', 'contact_role', 'contact_group'))
) )
tag = TagFilterField(model) tag = TagFilterField(model)
@ -372,7 +372,7 @@ class ManufacturerFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
class DeviceTypeFilterForm(NetBoxModelFilterSetForm): class DeviceTypeFilterForm(NetBoxModelFilterSetForm):
model = DeviceType model = DeviceType
fieldsets = ( fieldsets = (
(None, ('q', 'tag')), (None, ('q', 'filter', 'tag')),
('Hardware', ('manufacturer_id', 'part_number', 'subdevice_role', 'airflow')), ('Hardware', ('manufacturer_id', 'part_number', 'subdevice_role', 'airflow')),
('Images', ('has_front_image', 'has_rear_image')), ('Images', ('has_front_image', 'has_rear_image')),
('Components', ( ('Components', (
@ -487,7 +487,7 @@ class DeviceTypeFilterForm(NetBoxModelFilterSetForm):
class ModuleTypeFilterForm(NetBoxModelFilterSetForm): class ModuleTypeFilterForm(NetBoxModelFilterSetForm):
model = ModuleType model = ModuleType
fieldsets = ( fieldsets = (
(None, ('q', 'tag')), (None, ('q', 'filter', 'tag')),
('Hardware', ('manufacturer_id', 'part_number')), ('Hardware', ('manufacturer_id', 'part_number')),
('Components', ( ('Components', (
'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces', 'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces',
@ -579,7 +579,7 @@ class DeviceFilterForm(
): ):
model = Device model = Device
fieldsets = ( fieldsets = (
(None, ('q', 'tag')), (None, ('q', 'filter', 'tag')),
('Location', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')), ('Location', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
('Operation', ('status', 'role_id', 'airflow', 'serial', 'asset_tag', 'mac_address')), ('Operation', ('status', 'role_id', 'airflow', 'serial', 'asset_tag', 'mac_address')),
('Hardware', ('manufacturer_id', 'device_type_id', 'platform_id')), ('Hardware', ('manufacturer_id', 'device_type_id', 'platform_id')),
@ -763,7 +763,7 @@ class VirtualDeviceContextFilterForm(
class ModuleFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, NetBoxModelFilterSetForm): class ModuleFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, NetBoxModelFilterSetForm):
model = Module model = Module
fieldsets = ( fieldsets = (
(None, ('q', 'tag')), (None, ('q', 'filter', 'tag')),
('Hardware', ('manufacturer_id', 'module_type_id', 'serial', 'asset_tag')), ('Hardware', ('manufacturer_id', 'module_type_id', 'serial', 'asset_tag')),
) )
manufacturer_id = DynamicModelMultipleChoiceField( manufacturer_id = DynamicModelMultipleChoiceField(
@ -793,7 +793,7 @@ class ModuleFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, NetBoxMo
class VirtualChassisFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): class VirtualChassisFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
model = VirtualChassis model = VirtualChassis
fieldsets = ( fieldsets = (
(None, ('q', 'tag')), (None, ('q', 'filter', 'tag')),
('Location', ('region_id', 'site_group_id', 'site_id')), ('Location', ('region_id', 'site_group_id', 'site_id')),
('Tenant', ('tenant_group_id', 'tenant_id')), ('Tenant', ('tenant_group_id', 'tenant_id')),
) )
@ -822,7 +822,7 @@ class VirtualChassisFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
class CableFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): class CableFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
model = Cable model = Cable
fieldsets = ( fieldsets = (
(None, ('q', 'tag')), (None, ('q', 'filter', 'tag')),
('Location', ('site_id', 'location_id', 'rack_id', 'device_id')), ('Location', ('site_id', 'location_id', 'rack_id', 'device_id')),
('Attributes', ('type', 'status', 'color', 'length', 'length_unit')), ('Attributes', ('type', 'status', 'color', 'length', 'length_unit')),
('Tenant', ('tenant_group_id', 'tenant_id')), ('Tenant', ('tenant_group_id', 'tenant_id')),
@ -894,7 +894,7 @@ class CableFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
class PowerPanelFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm): class PowerPanelFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
model = PowerPanel model = PowerPanel
fieldsets = ( fieldsets = (
(None, ('q', 'tag')), (None, ('q', 'filter', 'tag')),
('Location', ('region_id', 'site_group_id', 'site_id', 'location_id')), ('Location', ('region_id', 'site_group_id', 'site_id', 'location_id')),
('Contacts', ('contact', 'contact_role', 'contact_group')), ('Contacts', ('contact', 'contact_role', 'contact_group')),
) )
@ -932,7 +932,7 @@ class PowerPanelFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
class PowerFeedFilterForm(NetBoxModelFilterSetForm): class PowerFeedFilterForm(NetBoxModelFilterSetForm):
model = PowerFeed model = PowerFeed
fieldsets = ( fieldsets = (
(None, ('q', 'tag')), (None, ('q', 'filter', 'tag')),
('Location', ('region_id', 'site_group_id', 'site_id', 'power_panel_id', 'rack_id')), ('Location', ('region_id', 'site_group_id', 'site_id', 'power_panel_id', 'rack_id')),
('Attributes', ('status', 'type', 'supply', 'phase', 'voltage', 'amperage', 'max_utilization')), ('Attributes', ('status', 'type', 'supply', 'phase', 'voltage', 'amperage', 'max_utilization')),
) )
@ -1034,7 +1034,7 @@ class PathEndpointFilterForm(CabledFilterForm):
class ConsolePortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm): class ConsolePortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
model = ConsolePort model = ConsolePort
fieldsets = ( fieldsets = (
(None, ('q', 'tag')), (None, ('q', 'filter', 'tag')),
('Attributes', ('name', 'label', 'type', 'speed')), ('Attributes', ('name', 'label', 'type', 'speed')),
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')), ('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
('Connection', ('cabled', 'connected', 'occupied')), ('Connection', ('cabled', 'connected', 'occupied')),
@ -1053,7 +1053,7 @@ class ConsolePortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
class ConsoleServerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm): class ConsoleServerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
model = ConsoleServerPort model = ConsoleServerPort
fieldsets = ( fieldsets = (
(None, ('q', 'tag')), (None, ('q', 'filter', 'tag')),
('Attributes', ('name', 'label', 'type', 'speed')), ('Attributes', ('name', 'label', 'type', 'speed')),
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')), ('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
('Connection', ('cabled', 'connected', 'occupied')), ('Connection', ('cabled', 'connected', 'occupied')),
@ -1072,7 +1072,7 @@ class ConsoleServerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterF
class PowerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm): class PowerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
model = PowerPort model = PowerPort
fieldsets = ( fieldsets = (
(None, ('q', 'tag')), (None, ('q', 'filter', 'tag')),
('Attributes', ('name', 'label', 'type')), ('Attributes', ('name', 'label', 'type')),
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')), ('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
('Connection', ('cabled', 'connected', 'occupied')), ('Connection', ('cabled', 'connected', 'occupied')),
@ -1087,7 +1087,7 @@ class PowerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
class PowerOutletFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm): class PowerOutletFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
model = PowerOutlet model = PowerOutlet
fieldsets = ( fieldsets = (
(None, ('q', 'tag')), (None, ('q', 'filter', 'tag')),
('Attributes', ('name', 'label', 'type')), ('Attributes', ('name', 'label', 'type')),
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')), ('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
('Connection', ('cabled', 'connected', 'occupied')), ('Connection', ('cabled', 'connected', 'occupied')),
@ -1102,7 +1102,7 @@ class PowerOutletFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm): class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
model = Interface model = Interface
fieldsets = ( fieldsets = (
(None, ('q', 'tag')), (None, ('q', 'filter', 'tag')),
('Attributes', ('name', 'label', 'kind', 'type', 'speed', 'duplex', 'enabled', 'mgmt_only')), ('Attributes', ('name', 'label', 'kind', 'type', 'speed', 'duplex', 'enabled', 'mgmt_only')),
('Addressing', ('vrf_id', 'mac_address', 'wwn')), ('Addressing', ('vrf_id', 'mac_address', 'wwn')),
('PoE', ('poe_mode', 'poe_type')), ('PoE', ('poe_mode', 'poe_type')),
@ -1200,7 +1200,7 @@ class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
class FrontPortFilterForm(CabledFilterForm, DeviceComponentFilterForm): class FrontPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
fieldsets = ( fieldsets = (
(None, ('q', 'tag')), (None, ('q', 'filter', 'tag')),
('Attributes', ('name', 'label', 'type', 'color')), ('Attributes', ('name', 'label', 'type', 'color')),
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')), ('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
('Cable', ('cabled', 'occupied')), ('Cable', ('cabled', 'occupied')),
@ -1219,7 +1219,7 @@ class FrontPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
class RearPortFilterForm(CabledFilterForm, DeviceComponentFilterForm): class RearPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
model = RearPort model = RearPort
fieldsets = ( fieldsets = (
(None, ('q', 'tag')), (None, ('q', 'filter', 'tag')),
('Attributes', ('name', 'label', 'type', 'color')), ('Attributes', ('name', 'label', 'type', 'color')),
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')), ('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
('Cable', ('cabled', 'occupied')), ('Cable', ('cabled', 'occupied')),
@ -1237,7 +1237,7 @@ class RearPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
class ModuleBayFilterForm(DeviceComponentFilterForm): class ModuleBayFilterForm(DeviceComponentFilterForm):
model = ModuleBay model = ModuleBay
fieldsets = ( fieldsets = (
(None, ('q', 'tag')), (None, ('q', 'filter', 'tag')),
('Attributes', ('name', 'label', 'position')), ('Attributes', ('name', 'label', 'position')),
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')), ('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
) )
@ -1250,7 +1250,7 @@ class ModuleBayFilterForm(DeviceComponentFilterForm):
class DeviceBayFilterForm(DeviceComponentFilterForm): class DeviceBayFilterForm(DeviceComponentFilterForm):
model = DeviceBay model = DeviceBay
fieldsets = ( fieldsets = (
(None, ('q', 'tag')), (None, ('q', 'filter', 'tag')),
('Attributes', ('name', 'label')), ('Attributes', ('name', 'label')),
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')), ('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
) )
@ -1260,7 +1260,7 @@ class DeviceBayFilterForm(DeviceComponentFilterForm):
class InventoryItemFilterForm(DeviceComponentFilterForm): class InventoryItemFilterForm(DeviceComponentFilterForm):
model = InventoryItem model = InventoryItem
fieldsets = ( fieldsets = (
(None, ('q', 'tag')), (None, ('q', 'filter', 'tag')),
('Attributes', ('name', 'label', 'role_id', 'manufacturer_id', 'serial', 'asset_tag', 'discovered')), ('Attributes', ('name', 'label', 'role_id', 'manufacturer_id', 'serial', 'asset_tag', 'discovered')),
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')), ('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
) )

View File

@ -279,7 +279,7 @@ class RackForm(TenancyForm, NetBoxModelForm):
fields = [ fields = [
'region', 'site_group', 'site', 'location', 'name', 'facility_id', 'tenant_group', 'tenant', 'status', 'region', 'site_group', 'site', 'location', 'name', 'facility_id', 'tenant_group', 'tenant', 'status',
'role', 'serial', 'asset_tag', 'type', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'role', 'serial', 'asset_tag', 'type', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth',
'outer_unit', 'mounting_depth', 'weight', 'weight_unit', 'comments', 'tags', 'outer_unit', 'mounting_depth', 'weight', 'weight_unit', 'description', 'comments', 'tags',
] ]
help_texts = { help_texts = {
'site': "The site at which the rack exists", 'site': "The site at which the rack exists",
@ -343,6 +343,7 @@ class RackReservationForm(TenancyForm, NetBoxModelForm):
), ),
widget=StaticSelect() widget=StaticSelect()
) )
comments = CommentField()
fieldsets = ( fieldsets = (
('Reservation', ('region', 'site_group', 'site', 'location', 'rack', 'units', 'user', 'description', 'tags')), ('Reservation', ('region', 'site_group', 'site', 'location', 'rack', 'units', 'user', 'description', 'tags')),
@ -353,7 +354,7 @@ class RackReservationForm(TenancyForm, NetBoxModelForm):
model = RackReservation model = RackReservation
fields = [ fields = [
'region', 'site_group', 'site', 'location', 'rack', 'units', 'user', 'tenant_group', 'tenant', 'region', 'site_group', 'site', 'location', 'rack', 'units', 'user', 'tenant_group', 'tenant',
'description', 'tags', 'description', 'comments', 'tags',
] ]
@ -384,10 +385,10 @@ class DeviceTypeForm(NetBoxModelForm):
fieldsets = ( fieldsets = (
('Device Type', ( ('Device Type', (
'manufacturer', 'model', 'slug', 'part_number', 'tags', 'manufacturer', 'model', 'slug', 'description', 'tags',
)), )),
('Chassis', ( ('Chassis', (
'u_height', 'is_full_depth', 'subdevice_role', 'airflow', 'u_height', 'is_full_depth', 'part_number', 'subdevice_role', 'airflow',
)), )),
('Attributes', ('weight', 'weight_unit')), ('Attributes', ('weight', 'weight_unit')),
('Images', ('front_image', 'rear_image')), ('Images', ('front_image', 'rear_image')),
@ -397,7 +398,7 @@ class DeviceTypeForm(NetBoxModelForm):
model = DeviceType model = DeviceType
fields = [ fields = [
'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role', 'airflow', 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role', 'airflow',
'weight', 'weight_unit', 'front_image', 'rear_image', 'comments', 'tags', 'weight', 'weight_unit', 'front_image', 'rear_image', 'description', 'comments', 'tags',
] ]
widgets = { widgets = {
'airflow': StaticSelect(), 'airflow': StaticSelect(),
@ -419,15 +420,14 @@ class ModuleTypeForm(NetBoxModelForm):
comments = CommentField() comments = CommentField()
fieldsets = ( fieldsets = (
('Module Type', ( ('Module Type', ('manufacturer', 'model', 'part_number', 'description', 'tags')),
'manufacturer', 'model', 'part_number', 'tags', 'weight', 'weight_unit' ('Weight', ('weight', 'weight_unit'))
)),
) )
class Meta: class Meta:
model = ModuleType model = ModuleType
fields = [ fields = [
'manufacturer', 'model', 'part_number', 'weight', 'weight_unit', 'comments', 'tags', 'manufacturer', 'model', 'part_number', 'weight', 'weight_unit', 'description', 'comments', 'tags',
] ]
widgets = { widgets = {
@ -592,7 +592,7 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
'name', 'device_role', 'device_type', 'serial', 'asset_tag', 'region', 'site_group', 'site', 'rack', 'name', 'device_role', 'device_type', 'serial', 'asset_tag', 'region', 'site_group', 'site', 'rack',
'location', 'position', 'face', 'status', 'airflow', 'platform', 'primary_ip4', 'primary_ip6', 'location', 'position', 'face', 'status', 'airflow', 'platform', 'primary_ip4', 'primary_ip6',
'cluster_group', 'cluster', 'tenant_group', 'tenant', 'virtual_chassis', 'vc_position', 'vc_priority', 'cluster_group', 'cluster', 'tenant_group', 'tenant', 'virtual_chassis', 'vc_position', 'vc_priority',
'comments', 'tags', 'local_context_data' 'description', 'comments', 'tags', 'local_context_data'
] ]
help_texts = { help_texts = {
'device_role': "The function this device serves", 'device_role': "The function this device serves",
@ -706,7 +706,7 @@ class ModuleForm(NetBoxModelForm):
fieldsets = ( fieldsets = (
('Module', ( ('Module', (
'device', 'module_bay', 'manufacturer', 'module_type', 'tags', 'device', 'module_bay', 'manufacturer', 'module_type', 'description', 'tags',
)), )),
('Hardware', ( ('Hardware', (
'serial', 'asset_tag', 'replicate_components', 'adopt_components', 'serial', 'asset_tag', 'replicate_components', 'adopt_components',
@ -717,7 +717,7 @@ class ModuleForm(NetBoxModelForm):
model = Module model = Module
fields = [ fields = [
'device', 'module_bay', 'manufacturer', 'module_type', 'serial', 'asset_tag', 'tags', 'device', 'module_bay', 'manufacturer', 'module_type', 'serial', 'asset_tag', 'tags',
'replicate_components', 'adopt_components', 'comments', 'replicate_components', 'adopt_components', 'description', 'comments',
] ]
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -794,11 +794,13 @@ class ModuleForm(NetBoxModelForm):
class CableForm(TenancyForm, NetBoxModelForm): class CableForm(TenancyForm, NetBoxModelForm):
comments = CommentField()
class Meta: class Meta:
model = Cable model = Cable
fields = [ fields = [
'type', 'status', 'tenant_group', 'tenant', 'label', 'color', 'length', 'length_unit', 'tags', 'type', 'status', 'tenant_group', 'tenant', 'label', 'color', 'length', 'length_unit', 'description',
'comments', 'tags',
] ]
widgets = { widgets = {
'status': StaticSelect, 'status': StaticSelect,
@ -841,15 +843,16 @@ class PowerPanelForm(NetBoxModelForm):
'site_id': '$site' 'site_id': '$site'
} }
) )
comments = CommentField()
fieldsets = ( fieldsets = (
('Power Panel', ('region', 'site_group', 'site', 'location', 'name', 'tags')), ('Power Panel', ('region', 'site_group', 'site', 'location', 'name', 'description', 'tags')),
) )
class Meta: class Meta:
model = PowerPanel model = PowerPanel
fields = [ fields = [
'region', 'site_group', 'site', 'location', 'name', 'tags', 'region', 'site_group', 'site', 'location', 'name', 'description', 'comments', 'tags',
] ]
@ -895,7 +898,7 @@ class PowerFeedForm(NetBoxModelForm):
comments = CommentField() comments = CommentField()
fieldsets = ( fieldsets = (
('Power Panel', ('region', 'site', 'power_panel')), ('Power Panel', ('region', 'site', 'power_panel', 'description')),
('Power Feed', ('rack', 'name', 'status', 'type', 'mark_connected', 'tags')), ('Power Feed', ('rack', 'name', 'status', 'type', 'mark_connected', 'tags')),
('Characteristics', ('supply', 'voltage', 'amperage', 'phase', 'max_utilization')), ('Characteristics', ('supply', 'voltage', 'amperage', 'phase', 'max_utilization')),
) )
@ -904,7 +907,7 @@ class PowerFeedForm(NetBoxModelForm):
model = PowerFeed model = PowerFeed
fields = [ fields = [
'region', 'site_group', 'site', 'power_panel', 'rack', 'name', 'status', 'type', 'mark_connected', 'supply', 'region', 'site_group', 'site', 'power_panel', 'rack', 'name', 'status', 'type', 'mark_connected', 'supply',
'phase', 'voltage', 'amperage', 'max_utilization', 'comments', 'tags', 'phase', 'voltage', 'amperage', 'max_utilization', 'description', 'comments', 'tags',
] ]
widgets = { widgets = {
'status': StaticSelect(), 'status': StaticSelect(),
@ -923,11 +926,12 @@ class VirtualChassisForm(NetBoxModelForm):
queryset=Device.objects.all(), queryset=Device.objects.all(),
required=False, required=False,
) )
comments = CommentField()
class Meta: class Meta:
model = VirtualChassis model = VirtualChassis
fields = [ fields = [
'name', 'domain', 'master', 'tags', 'name', 'domain', 'master', 'description', 'comments', 'tags',
] ]
widgets = { widgets = {
'master': SelectWithPK(), 'master': SelectWithPK(),

View File

@ -30,7 +30,7 @@ class DeviceTypeImportForm(BootstrapMixin, forms.ModelForm):
model = DeviceType model = DeviceType
fields = [ fields = [
'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role', 'airflow', 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role', 'airflow',
'comments', 'description', 'comments',
] ]
@ -42,7 +42,7 @@ class ModuleTypeImportForm(BootstrapMixin, forms.ModelForm):
class Meta: class Meta:
model = ModuleType model = ModuleType
fields = ['manufacturer', 'model', 'part_number', 'comments'] fields = ['manufacturer', 'model', 'part_number', 'description', 'comments']
# #

View File

@ -195,7 +195,7 @@ class Migration(migrations.Migration):
('description', models.CharField(blank=True, max_length=200)), ('description', models.CharField(blank=True, max_length=200)),
], ],
options={ options={
'ordering': ['name'], 'ordering': ('name',),
}, },
), ),
migrations.CreateModel( migrations.CreateModel(
@ -352,7 +352,7 @@ class Migration(migrations.Migration):
('description', models.CharField(blank=True, max_length=200)), ('description', models.CharField(blank=True, max_length=200)),
], ],
options={ options={
'ordering': ['name'], 'ordering': ('name',),
}, },
), ),
migrations.CreateModel( migrations.CreateModel(
@ -369,7 +369,7 @@ class Migration(migrations.Migration):
('description', models.CharField(blank=True, max_length=200)), ('description', models.CharField(blank=True, max_length=200)),
], ],
options={ options={
'ordering': ['name'], 'ordering': ('name',),
}, },
), ),
migrations.CreateModel( migrations.CreateModel(
@ -538,7 +538,7 @@ class Migration(migrations.Migration):
('description', models.CharField(blank=True, max_length=200)), ('description', models.CharField(blank=True, max_length=200)),
], ],
options={ options={
'ordering': ['name'], 'ordering': ('name',),
}, },
), ),
migrations.CreateModel( migrations.CreateModel(

View File

@ -27,7 +27,7 @@ class Migration(migrations.Migration):
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')), ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
], ],
options={ options={
'ordering': ['name'], 'ordering': ('name',),
}, },
), ),
migrations.AddField( migrations.AddField(

View File

@ -0,0 +1,78 @@
# Generated by Django 4.1.2 on 2022-11-03 18:24
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dcim', '0164_rack_mounting_depth'),
]
operations = [
migrations.AddField(
model_name='cable',
name='comments',
field=models.TextField(blank=True),
),
migrations.AddField(
model_name='cable',
name='description',
field=models.CharField(blank=True, max_length=200),
),
migrations.AddField(
model_name='device',
name='description',
field=models.CharField(blank=True, max_length=200),
),
migrations.AddField(
model_name='devicetype',
name='description',
field=models.CharField(blank=True, max_length=200),
),
migrations.AddField(
model_name='module',
name='description',
field=models.CharField(blank=True, max_length=200),
),
migrations.AddField(
model_name='moduletype',
name='description',
field=models.CharField(blank=True, max_length=200),
),
migrations.AddField(
model_name='powerfeed',
name='description',
field=models.CharField(blank=True, max_length=200),
),
migrations.AddField(
model_name='powerpanel',
name='comments',
field=models.TextField(blank=True),
),
migrations.AddField(
model_name='powerpanel',
name='description',
field=models.CharField(blank=True, max_length=200),
),
migrations.AddField(
model_name='rack',
name='description',
field=models.CharField(blank=True, max_length=200),
),
migrations.AddField(
model_name='rackreservation',
name='comments',
field=models.TextField(blank=True),
),
migrations.AddField(
model_name='virtualchassis',
name='comments',
field=models.TextField(blank=True),
),
migrations.AddField(
model_name='virtualchassis',
name='description',
field=models.CharField(blank=True, max_length=200),
),
]

View File

@ -1,4 +1,4 @@
# Generated by Django 4.1.2 on 2022-11-02 13:24 # Generated by Django 4.1.2 on 2022-11-04 12:46
from django.db import migrations, models from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion
@ -9,10 +9,10 @@ import utilities.json
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('ipam', '0062_unique_constraints'), ('extras', '0083_savedfilter'),
('extras', '0082_exporttemplate_content_types'), ('ipam', '0063_standardize_description_comments'),
('tenancy', '0008_unique_constraints'), ('tenancy', '0009_standardize_description_comments'),
('dcim', '0164_rack_mounting_depth'), ('dcim', '0165_standardize_description_comments'),
] ]
operations = [ operations = [
@ -23,6 +23,7 @@ class Migration(migrations.Migration):
('created', models.DateTimeField(auto_now_add=True, null=True)), ('created', models.DateTimeField(auto_now_add=True, null=True)),
('last_updated', models.DateTimeField(auto_now=True, null=True)), ('last_updated', models.DateTimeField(auto_now=True, null=True)),
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder)), ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder)),
('description', models.CharField(blank=True, max_length=200)),
('name', models.CharField(max_length=64)), ('name', models.CharField(max_length=64)),
('status', models.CharField(blank=True, max_length=50)), ('status', models.CharField(blank=True, max_length=50)),
('identifier', models.PositiveSmallIntegerField(blank=True, null=True)), ('identifier', models.PositiveSmallIntegerField(blank=True, null=True)),

View File

@ -12,8 +12,8 @@ from django.urls import reverse
from dcim.choices import * from dcim.choices import *
from dcim.constants import * from dcim.constants import *
from dcim.fields import PathField from dcim.fields import PathField
from dcim.utils import decompile_path_node, object_to_path_node, path_node_to_object from dcim.utils import decompile_path_node, object_to_path_node
from netbox.models import NetBoxModel from netbox.models import PrimaryModel
from utilities.fields import ColorField from utilities.fields import ColorField
from utilities.querysets import RestrictedQuerySet from utilities.querysets import RestrictedQuerySet
from utilities.utils import to_meters from utilities.utils import to_meters
@ -34,7 +34,7 @@ trace_paths = Signal()
# Cables # Cables
# #
class Cable(NetBoxModel): class Cable(PrimaryModel):
""" """
A physical connection between two endpoints. A physical connection between two endpoints.
""" """

View File

@ -1029,27 +1029,9 @@ class InventoryItemRole(OrganizationalModel):
""" """
Inventory items may optionally be assigned a functional role. Inventory items may optionally be assigned a functional role.
""" """
name = models.CharField(
max_length=100,
unique=True
)
slug = models.SlugField(
max_length=100,
unique=True
)
color = ColorField( color = ColorField(
default=ColorChoices.COLOR_GREY default=ColorChoices.COLOR_GREY
) )
description = models.CharField(
max_length=200,
blank=True,
)
class Meta:
ordering = ['name']
def __str__(self):
return self.name
def get_absolute_url(self): def get_absolute_url(self):
return reverse('dcim:inventoryitemrole', args=[self.pk]) return reverse('dcim:inventoryitemrole', args=[self.pk])

View File

@ -18,7 +18,7 @@ from dcim.constants import *
from extras.models import ConfigContextModel from extras.models import ConfigContextModel
from extras.querysets import ConfigContextModelQuerySet from extras.querysets import ConfigContextModelQuerySet
from netbox.config import ConfigItem from netbox.config import ConfigItem
from netbox.models import OrganizationalModel, NetBoxModel from netbox.models import OrganizationalModel, PrimaryModel
from utilities.choices import ColorChoices from utilities.choices import ColorChoices
from utilities.fields import ColorField, NaturalOrderingField from utilities.fields import ColorField, NaturalOrderingField
from .device_components import * from .device_components import *
@ -46,35 +46,16 @@ class Manufacturer(OrganizationalModel):
""" """
A Manufacturer represents a company which produces hardware devices; for example, Juniper or Dell. A Manufacturer represents a company which produces hardware devices; for example, Juniper or Dell.
""" """
name = models.CharField(
max_length=100,
unique=True
)
slug = models.SlugField(
max_length=100,
unique=True
)
description = models.CharField(
max_length=200,
blank=True
)
# Generic relations # Generic relations
contacts = GenericRelation( contacts = GenericRelation(
to='tenancy.ContactAssignment' to='tenancy.ContactAssignment'
) )
class Meta:
ordering = ['name']
def __str__(self):
return self.name
def get_absolute_url(self): def get_absolute_url(self):
return reverse('dcim:manufacturer', args=[self.pk]) return reverse('dcim:manufacturer', args=[self.pk])
class DeviceType(NetBoxModel, WeightMixin): class DeviceType(PrimaryModel, WeightMixin):
""" """
A DeviceType represents a particular make (Manufacturer) and model of device. It specifies rack height and depth, as A DeviceType represents a particular make (Manufacturer) and model of device. It specifies rack height and depth, as
well as high-level functional role(s). well as high-level functional role(s).
@ -137,9 +118,6 @@ class DeviceType(NetBoxModel, WeightMixin):
upload_to='devicetype-images', upload_to='devicetype-images',
blank=True blank=True
) )
comments = models.TextField(
blank=True
)
clone_fields = ( clone_fields = (
'manufacturer', 'u_height', 'is_full_depth', 'subdevice_role', 'airflow', 'weight', 'weight_unit' 'manufacturer', 'u_height', 'is_full_depth', 'subdevice_role', 'airflow', 'weight', 'weight_unit'
@ -318,7 +296,7 @@ class DeviceType(NetBoxModel, WeightMixin):
return self.subdevice_role == SubdeviceRoleChoices.ROLE_CHILD return self.subdevice_role == SubdeviceRoleChoices.ROLE_CHILD
class ModuleType(NetBoxModel, WeightMixin): class ModuleType(PrimaryModel, WeightMixin):
""" """
A ModuleType represents a hardware element that can be installed within a device and which houses additional A ModuleType represents a hardware element that can be installed within a device and which houses additional
components; for example, a line card within a chassis-based switch such as the Cisco Catalyst 6500. Like a components; for example, a line card within a chassis-based switch such as the Cisco Catalyst 6500. Like a
@ -338,9 +316,6 @@ class ModuleType(NetBoxModel, WeightMixin):
blank=True, blank=True,
help_text='Discrete part number (optional)' help_text='Discrete part number (optional)'
) )
comments = models.TextField(
blank=True
)
# Generic relations # Generic relations
images = GenericRelation( images = GenericRelation(
@ -419,14 +394,6 @@ class DeviceRole(OrganizationalModel):
color to be used when displaying rack elevations. The vm_role field determines whether the role is applicable to color to be used when displaying rack elevations. The vm_role field determines whether the role is applicable to
virtual machines as well. virtual machines as well.
""" """
name = models.CharField(
max_length=100,
unique=True
)
slug = models.SlugField(
max_length=100,
unique=True
)
color = ColorField( color = ColorField(
default=ColorChoices.COLOR_GREY default=ColorChoices.COLOR_GREY
) )
@ -435,16 +402,6 @@ class DeviceRole(OrganizationalModel):
verbose_name='VM Role', verbose_name='VM Role',
help_text='Virtual machines may be assigned to this role' help_text='Virtual machines may be assigned to this role'
) )
description = models.CharField(
max_length=200,
blank=True,
)
class Meta:
ordering = ['name']
def __str__(self):
return self.name
def get_absolute_url(self): def get_absolute_url(self):
return reverse('dcim:devicerole', args=[self.pk]) return reverse('dcim:devicerole', args=[self.pk])
@ -456,14 +413,6 @@ class Platform(OrganizationalModel):
NetBox uses Platforms to determine how to interact with devices when pulling inventory data or other information by NetBox uses Platforms to determine how to interact with devices when pulling inventory data or other information by
specifying a NAPALM driver. specifying a NAPALM driver.
""" """
name = models.CharField(
max_length=100,
unique=True
)
slug = models.SlugField(
max_length=100,
unique=True
)
manufacturer = models.ForeignKey( manufacturer = models.ForeignKey(
to='dcim.Manufacturer', to='dcim.Manufacturer',
on_delete=models.PROTECT, on_delete=models.PROTECT,
@ -484,22 +433,12 @@ class Platform(OrganizationalModel):
verbose_name='NAPALM arguments', verbose_name='NAPALM arguments',
help_text='Additional arguments to pass when initiating the NAPALM driver (JSON format)' help_text='Additional arguments to pass when initiating the NAPALM driver (JSON format)'
) )
description = models.CharField(
max_length=200,
blank=True
)
class Meta:
ordering = ['name']
def __str__(self):
return self.name
def get_absolute_url(self): def get_absolute_url(self):
return reverse('dcim:platform', args=[self.pk]) return reverse('dcim:platform', args=[self.pk])
class Device(NetBoxModel, ConfigContextModel): class Device(PrimaryModel, ConfigContextModel):
""" """
A Device represents a piece of physical hardware mounted within a Rack. Each Device is assigned a DeviceType, A Device represents a piece of physical hardware mounted within a Rack. Each Device is assigned a DeviceType,
DeviceRole, and (optionally) a Platform. Device names are not required, however if one is set it must be unique. DeviceRole, and (optionally) a Platform. Device names are not required, however if one is set it must be unique.
@ -643,9 +582,6 @@ class Device(NetBoxModel, ConfigContextModel):
null=True, null=True,
validators=[MaxValueValidator(255)] validators=[MaxValueValidator(255)]
) )
comments = models.TextField(
blank=True
)
# Generic relations # Generic relations
contacts = GenericRelation( contacts = GenericRelation(
@ -962,7 +898,7 @@ class Device(NetBoxModel, ConfigContextModel):
return round(total_weight / 1000, 2) return round(total_weight / 1000, 2)
class Module(NetBoxModel, ConfigContextModel): class Module(PrimaryModel, ConfigContextModel):
""" """
A Module represents a field-installable component within a Device which may itself hold multiple device components A Module represents a field-installable component within a Device which may itself hold multiple device components
(for example, a line card within a chassis switch). Modules are instantiated from ModuleTypes. (for example, a line card within a chassis switch). Modules are instantiated from ModuleTypes.
@ -995,9 +931,6 @@ class Module(NetBoxModel, ConfigContextModel):
verbose_name='Asset tag', verbose_name='Asset tag',
help_text='A unique tag used to identify this device' help_text='A unique tag used to identify this device'
) )
comments = models.TextField(
blank=True
)
clone_fields = ('device', 'module_type') clone_fields = ('device', 'module_type')
@ -1075,7 +1008,7 @@ class Module(NetBoxModel, ConfigContextModel):
# Virtual chassis # Virtual chassis
# #
class VirtualChassis(NetBoxModel): class VirtualChassis(PrimaryModel):
""" """
A collection of Devices which operate with a shared control plane (e.g. a switch stack). A collection of Devices which operate with a shared control plane (e.g. a switch stack).
""" """
@ -1132,7 +1065,7 @@ class VirtualChassis(NetBoxModel):
return super().delete(*args, **kwargs) return super().delete(*args, **kwargs)
class VirtualDeviceContext(NetBoxModel): class VirtualDeviceContext(PrimaryModel):
device = models.ForeignKey( device = models.ForeignKey(
to='Device', to='Device',
on_delete=models.PROTECT, on_delete=models.PROTECT,

View File

@ -6,9 +6,8 @@ from django.db import models
from django.urls import reverse from django.urls import reverse
from dcim.choices import * from dcim.choices import *
from dcim.constants import *
from netbox.config import ConfigItem from netbox.config import ConfigItem
from netbox.models import NetBoxModel from netbox.models import PrimaryModel
from utilities.validators import ExclusionValidator from utilities.validators import ExclusionValidator
from .device_components import CabledObjectModel, PathEndpoint from .device_components import CabledObjectModel, PathEndpoint
@ -22,7 +21,7 @@ __all__ = (
# Power # Power
# #
class PowerPanel(NetBoxModel): class PowerPanel(PrimaryModel):
""" """
A distribution point for electrical power; e.g. a data center RPP. A distribution point for electrical power; e.g. a data center RPP.
""" """
@ -77,7 +76,7 @@ class PowerPanel(NetBoxModel):
) )
class PowerFeed(NetBoxModel, PathEndpoint, CabledObjectModel): class PowerFeed(PrimaryModel, PathEndpoint, CabledObjectModel):
""" """
An electrical circuit delivered from a PowerPanel. An electrical circuit delivered from a PowerPanel.
""" """
@ -132,9 +131,6 @@ class PowerFeed(NetBoxModel, PathEndpoint, CabledObjectModel):
default=0, default=0,
editable=False editable=False
) )
comments = models.TextField(
blank=True
)
clone_fields = ( clone_fields = (
'power_panel', 'rack', 'status', 'type', 'mark_connected', 'supply', 'phase', 'voltage', 'amperage', 'power_panel', 'rack', 'status', 'type', 'mark_connected', 'supply', 'phase', 'voltage', 'amperage',

View File

@ -14,7 +14,7 @@ from django.urls import reverse
from dcim.choices import * from dcim.choices import *
from dcim.constants import * from dcim.constants import *
from dcim.svg import RackElevationSVG from dcim.svg import RackElevationSVG
from netbox.models import OrganizationalModel, NetBoxModel from netbox.models import OrganizationalModel, PrimaryModel
from utilities.choices import ColorChoices from utilities.choices import ColorChoices
from utilities.fields import ColorField, NaturalOrderingField from utilities.fields import ColorField, NaturalOrderingField
from utilities.utils import array_to_string, drange from utilities.utils import array_to_string, drange
@ -38,33 +38,15 @@ class RackRole(OrganizationalModel):
""" """
Racks can be organized by functional role, similar to Devices. Racks can be organized by functional role, similar to Devices.
""" """
name = models.CharField(
max_length=100,
unique=True
)
slug = models.SlugField(
max_length=100,
unique=True
)
color = ColorField( color = ColorField(
default=ColorChoices.COLOR_GREY default=ColorChoices.COLOR_GREY
) )
description = models.CharField(
max_length=200,
blank=True,
)
class Meta:
ordering = ['name']
def __str__(self):
return self.name
def get_absolute_url(self): def get_absolute_url(self):
return reverse('dcim:rackrole', args=[self.pk]) return reverse('dcim:rackrole', args=[self.pk])
class Rack(NetBoxModel, WeightMixin): class Rack(PrimaryModel, WeightMixin):
""" """
Devices are housed within Racks. Each rack has a defined height measured in rack units, and a front and rear face. Devices are housed within Racks. Each rack has a defined height measured in rack units, and a front and rear face.
Each Rack is assigned to a Site and (optionally) a Location. Each Rack is assigned to a Site and (optionally) a Location.
@ -175,9 +157,6 @@ class Rack(NetBoxModel, WeightMixin):
'distance between the front and rear rails.' 'distance between the front and rear rails.'
) )
) )
comments = models.TextField(
blank=True
)
# Generic relations # Generic relations
vlan_groups = GenericRelation( vlan_groups = GenericRelation(
@ -481,7 +460,7 @@ class Rack(NetBoxModel, WeightMixin):
return round(total_weight / 1000, 2) return round(total_weight / 1000, 2)
class RackReservation(NetBoxModel): class RackReservation(PrimaryModel):
""" """
One or more reserved units within a Rack. One or more reserved units within a Rack.
""" """

View File

@ -2,12 +2,11 @@ from django.contrib.contenttypes.fields import GenericRelation
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import models from django.db import models
from django.urls import reverse from django.urls import reverse
from mptt.models import TreeForeignKey
from timezone_field import TimeZoneField from timezone_field import TimeZoneField
from dcim.choices import * from dcim.choices import *
from dcim.constants import * from dcim.constants import *
from netbox.models import NestedGroupModel, NetBoxModel from netbox.models import NestedGroupModel, PrimaryModel
from utilities.fields import NaturalOrderingField from utilities.fields import NaturalOrderingField
__all__ = ( __all__ = (
@ -28,25 +27,6 @@ class Region(NestedGroupModel):
states, and/or cities. Regions are recursively nested into a hierarchy: all sites belonging to a child region are states, and/or cities. Regions are recursively nested into a hierarchy: all sites belonging to a child region are
also considered to be members of its parent and ancestor region(s). also considered to be members of its parent and ancestor region(s).
""" """
parent = TreeForeignKey(
to='self',
on_delete=models.CASCADE,
related_name='children',
blank=True,
null=True,
db_index=True
)
name = models.CharField(
max_length=100
)
slug = models.SlugField(
max_length=100
)
description = models.CharField(
max_length=200,
blank=True
)
# Generic relations # Generic relations
vlan_groups = GenericRelation( vlan_groups = GenericRelation(
to='ipam.VLANGroup', to='ipam.VLANGroup',
@ -102,25 +82,6 @@ class SiteGroup(NestedGroupModel):
within corporate sites you might distinguish between offices and data centers. Like regions, site groups can be within corporate sites you might distinguish between offices and data centers. Like regions, site groups can be
nested recursively to form a hierarchy. nested recursively to form a hierarchy.
""" """
parent = TreeForeignKey(
to='self',
on_delete=models.CASCADE,
related_name='children',
blank=True,
null=True,
db_index=True
)
name = models.CharField(
max_length=100
)
slug = models.SlugField(
max_length=100
)
description = models.CharField(
max_length=200,
blank=True
)
# Generic relations # Generic relations
vlan_groups = GenericRelation( vlan_groups = GenericRelation(
to='ipam.VLANGroup', to='ipam.VLANGroup',
@ -170,7 +131,7 @@ class SiteGroup(NestedGroupModel):
# Sites # Sites
# #
class Site(NetBoxModel): class Site(PrimaryModel):
""" """
A Site represents a geographic location within a network; typically a building or campus. The optional facility A Site represents a geographic location within a network; typically a building or campus. The optional facility
field can be used to include an external designation, such as a data center name (e.g. Equinix SV6). field can be used to include an external designation, such as a data center name (e.g. Equinix SV6).
@ -227,10 +188,6 @@ class Site(NetBoxModel):
time_zone = TimeZoneField( time_zone = TimeZoneField(
blank=True blank=True
) )
description = models.CharField(
max_length=200,
blank=True
)
physical_address = models.CharField( physical_address = models.CharField(
max_length=200, max_length=200,
blank=True blank=True
@ -253,9 +210,6 @@ class Site(NetBoxModel):
null=True, null=True,
help_text='GPS coordinate (longitude)' help_text='GPS coordinate (longitude)'
) )
comments = models.TextField(
blank=True
)
# Generic relations # Generic relations
vlan_groups = GenericRelation( vlan_groups = GenericRelation(
@ -298,25 +252,11 @@ class Location(NestedGroupModel):
A Location represents a subgroup of Racks and/or Devices within a Site. A Location may represent a building within a A Location represents a subgroup of Racks and/or Devices within a Site. A Location may represent a building within a
site, or a room within a building, for example. site, or a room within a building, for example.
""" """
name = models.CharField(
max_length=100
)
slug = models.SlugField(
max_length=100
)
site = models.ForeignKey( site = models.ForeignKey(
to='dcim.Site', to='dcim.Site',
on_delete=models.CASCADE, on_delete=models.CASCADE,
related_name='locations' related_name='locations'
) )
parent = TreeForeignKey(
to='self',
on_delete=models.CASCADE,
related_name='children',
blank=True,
null=True,
db_index=True
)
status = models.CharField( status = models.CharField(
max_length=50, max_length=50,
choices=LocationStatusChoices, choices=LocationStatusChoices,
@ -329,10 +269,6 @@ class Location(NestedGroupModel):
blank=True, blank=True,
null=True null=True
) )
description = models.CharField(
max_length=200,
blank=True
)
# Generic relations # Generic relations
vlan_groups = GenericRelation( vlan_groups = GenericRelation(

View File

@ -111,6 +111,7 @@ class CableTable(TenancyColumnsMixin, NetBoxTable):
order_by=('_abs_length', 'length_unit') order_by=('_abs_length', 'length_unit')
) )
color = columns.ColorColumn() color = columns.ColorColumn()
comments = columns.MarkdownColumn()
tags = columns.TagColumn( tags = columns.TagColumn(
url_name='dcim:cable_list' url_name='dcim:cable_list'
) )
@ -120,7 +121,7 @@ class CableTable(TenancyColumnsMixin, NetBoxTable):
fields = ( fields = (
'pk', 'id', 'label', 'a_terminations', 'b_terminations', 'device_a', 'device_b', 'rack_a', 'rack_b', 'pk', 'id', 'label', 'a_terminations', 'b_terminations', 'device_a', 'device_b', 'rack_a', 'rack_b',
'location_a', 'location_b', 'site_a', 'site_b', 'status', 'type', 'tenant', 'tenant_group', 'color', 'location_a', 'location_b', 'site_a', 'site_b', 'status', 'type', 'tenant', 'tenant_group', 'color',
'length', 'tags', 'created', 'last_updated', 'length', 'description', 'comments', 'tags', 'created', 'last_updated',
) )
default_columns = ( default_columns = (
'pk', 'id', 'label', 'a_terminations', 'b_terminations', 'status', 'type', 'pk', 'id', 'label', 'a_terminations', 'b_terminations', 'status', 'type',

View File

@ -1,22 +1,5 @@
import django_tables2 as tables import django_tables2 as tables
from dcim.models import ( from dcim import models
ConsolePort,
ConsoleServerPort,
Device,
DeviceBay,
DeviceRole,
FrontPort,
Interface,
InventoryItem,
InventoryItemRole,
ModuleBay,
Platform,
PowerOutlet,
PowerPort,
RearPort,
VirtualChassis,
VirtualDeviceContext,
)
from django_tables2.utils import Accessor from django_tables2.utils import Accessor
from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin
@ -108,7 +91,7 @@ class DeviceRoleTable(NetBoxTable):
) )
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = DeviceRole model = models.DeviceRole
fields = ( fields = (
'pk', 'id', 'name', 'device_count', 'vm_count', 'color', 'vm_role', 'description', 'slug', 'tags', 'pk', 'id', 'name', 'device_count', 'vm_count', 'color', 'vm_role', 'description', 'slug', 'tags',
'actions', 'created', 'last_updated', 'actions', 'created', 'last_updated',
@ -139,7 +122,7 @@ class PlatformTable(NetBoxTable):
) )
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = Platform model = models.Platform
fields = ( fields = (
'pk', 'id', 'name', 'manufacturer', 'device_count', 'vm_count', 'slug', 'napalm_driver', 'napalm_args', 'pk', 'id', 'name', 'manufacturer', 'device_count', 'vm_count', 'slug', 'napalm_driver', 'napalm_args',
'description', 'tags', 'actions', 'created', 'last_updated', 'description', 'tags', 'actions', 'created', 'last_updated',
@ -222,12 +205,12 @@ class DeviceTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
) )
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = Device model = models.Device
fields = ( fields = (
'pk', 'id', 'name', 'status', 'tenant', 'tenant_group', 'device_role', 'manufacturer', 'device_type', 'pk', 'id', 'name', 'status', 'tenant', 'tenant_group', 'device_role', 'manufacturer', 'device_type',
'platform', 'serial', 'asset_tag', 'region', 'site_group', 'site', 'location', 'rack', 'position', 'face', 'platform', 'serial', 'asset_tag', 'region', 'site_group', 'site', 'location', 'rack', 'position', 'face',
'airflow', 'primary_ip', 'primary_ip4', 'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'airflow', 'primary_ip', 'primary_ip4', 'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position',
'vc_priority', 'comments', 'contacts', 'tags', 'created', 'last_updated', 'vc_priority', 'description', 'comments', 'contacts', 'tags', 'created', 'last_updated',
) )
default_columns = ( default_columns = (
'pk', 'name', 'status', 'tenant', 'site', 'location', 'rack', 'device_role', 'manufacturer', 'device_type', 'pk', 'name', 'status', 'tenant', 'site', 'location', 'rack', 'device_role', 'manufacturer', 'device_type',
@ -254,7 +237,7 @@ class DeviceImportTable(TenancyColumnsMixin, NetBoxTable):
) )
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = Device model = models.Device
fields = ('id', 'name', 'status', 'tenant', 'tenant_group', 'site', 'rack', 'position', 'device_role', 'device_type') fields = ('id', 'name', 'status', 'tenant', 'tenant_group', 'site', 'rack', 'position', 'device_role', 'device_type')
empty_text = False empty_text = False
@ -328,7 +311,7 @@ class ConsolePortTable(ModularDeviceComponentTable, PathEndpointTable):
) )
class Meta(DeviceComponentTable.Meta): class Meta(DeviceComponentTable.Meta):
model = ConsolePort model = models.ConsolePort
fields = ( fields = (
'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'type', 'speed', 'description', 'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'type', 'speed', 'description',
'mark_connected', 'cable', 'cable_color', 'link_peer', 'connection', 'tags', 'created', 'last_updated', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'connection', 'tags', 'created', 'last_updated',
@ -347,7 +330,7 @@ class DeviceConsolePortTable(ConsolePortTable):
) )
class Meta(DeviceComponentTable.Meta): class Meta(DeviceComponentTable.Meta):
model = ConsolePort model = models.ConsolePort
fields = ( fields = (
'pk', 'id', 'name', 'module_bay', 'module', 'label', 'type', 'speed', 'description', 'mark_connected', 'pk', 'id', 'name', 'module_bay', 'module', 'label', 'type', 'speed', 'description', 'mark_connected',
'cable', 'cable_color', 'link_peer', 'connection', 'tags', 'actions' 'cable', 'cable_color', 'link_peer', 'connection', 'tags', 'actions'
@ -370,7 +353,7 @@ class ConsoleServerPortTable(ModularDeviceComponentTable, PathEndpointTable):
) )
class Meta(DeviceComponentTable.Meta): class Meta(DeviceComponentTable.Meta):
model = ConsoleServerPort model = models.ConsoleServerPort
fields = ( fields = (
'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'type', 'speed', 'description', 'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'type', 'speed', 'description',
'mark_connected', 'cable', 'cable_color', 'link_peer', 'connection', 'tags', 'created', 'last_updated', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'connection', 'tags', 'created', 'last_updated',
@ -390,7 +373,7 @@ class DeviceConsoleServerPortTable(ConsoleServerPortTable):
) )
class Meta(DeviceComponentTable.Meta): class Meta(DeviceComponentTable.Meta):
model = ConsoleServerPort model = models.ConsoleServerPort
fields = ( fields = (
'pk', 'id', 'name', 'module_bay', 'module', 'label', 'type', 'speed', 'description', 'mark_connected', 'pk', 'id', 'name', 'module_bay', 'module', 'label', 'type', 'speed', 'description', 'mark_connected',
'cable', 'cable_color', 'link_peer', 'connection', 'tags', 'actions', 'cable', 'cable_color', 'link_peer', 'connection', 'tags', 'actions',
@ -413,7 +396,7 @@ class PowerPortTable(ModularDeviceComponentTable, PathEndpointTable):
) )
class Meta(DeviceComponentTable.Meta): class Meta(DeviceComponentTable.Meta):
model = PowerPort model = models.PowerPort
fields = ( fields = (
'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'type', 'description', 'mark_connected', 'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'type', 'description', 'mark_connected',
'maximum_draw', 'allocated_draw', 'cable', 'cable_color', 'link_peer', 'connection', 'tags', 'created', 'maximum_draw', 'allocated_draw', 'cable', 'cable_color', 'link_peer', 'connection', 'tags', 'created',
@ -434,7 +417,7 @@ class DevicePowerPortTable(PowerPortTable):
) )
class Meta(DeviceComponentTable.Meta): class Meta(DeviceComponentTable.Meta):
model = PowerPort model = models.PowerPort
fields = ( fields = (
'pk', 'id', 'name', 'module_bay', 'module', 'label', 'type', 'maximum_draw', 'allocated_draw', 'pk', 'id', 'name', 'module_bay', 'module', 'label', 'type', 'maximum_draw', 'allocated_draw',
'description', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'connection', 'tags', 'actions', 'description', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'connection', 'tags', 'actions',
@ -462,7 +445,7 @@ class PowerOutletTable(ModularDeviceComponentTable, PathEndpointTable):
) )
class Meta(DeviceComponentTable.Meta): class Meta(DeviceComponentTable.Meta):
model = PowerOutlet model = models.PowerOutlet
fields = ( fields = (
'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'type', 'description', 'power_port', 'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'type', 'description', 'power_port',
'feed_leg', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'connection', 'tags', 'created', 'feed_leg', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'connection', 'tags', 'created',
@ -482,7 +465,7 @@ class DevicePowerOutletTable(PowerOutletTable):
) )
class Meta(DeviceComponentTable.Meta): class Meta(DeviceComponentTable.Meta):
model = PowerOutlet model = models.PowerOutlet
fields = ( fields = (
'pk', 'id', 'name', 'module_bay', 'module', 'label', 'type', 'power_port', 'feed_leg', 'description', 'pk', 'id', 'name', 'module_bay', 'module', 'label', 'type', 'power_port', 'feed_leg', 'description',
'mark_connected', 'cable', 'cable_color', 'link_peer', 'connection', 'tags', 'actions', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'connection', 'tags', 'actions',
@ -546,7 +529,7 @@ class InterfaceTable(ModularDeviceComponentTable, BaseInterfaceTable, PathEndpoi
) )
class Meta(DeviceComponentTable.Meta): class Meta(DeviceComponentTable.Meta):
model = Interface model = models.Interface
fields = ( fields = (
'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'enabled', 'type', 'mgmt_only', 'mtu', 'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'enabled', 'type', 'mgmt_only', 'mtu',
'speed', 'duplex', 'mode', 'mac_address', 'wwn', 'poe_mode', 'poe_type', 'rf_role', 'rf_channel', 'speed', 'duplex', 'mode', 'mac_address', 'wwn', 'poe_mode', 'poe_type', 'rf_role', 'rf_channel',
@ -580,7 +563,7 @@ class DeviceInterfaceTable(InterfaceTable):
) )
class Meta(DeviceComponentTable.Meta): class Meta(DeviceComponentTable.Meta):
model = Interface model = models.Interface
fields = ( fields = (
'pk', 'id', 'name', 'module_bay', 'module', 'label', 'enabled', 'type', 'parent', 'bridge', 'lag', 'pk', 'id', 'name', 'module_bay', 'module', 'label', 'enabled', 'type', 'parent', 'bridge', 'lag',
'mgmt_only', 'mtu', 'mode', 'mac_address', 'wwn', 'rf_role', 'rf_channel', 'rf_channel_frequency', 'mgmt_only', 'mtu', 'mode', 'mac_address', 'wwn', 'rf_role', 'rf_channel', 'rf_channel_frequency',
@ -619,7 +602,7 @@ class FrontPortTable(ModularDeviceComponentTable, CableTerminationTable):
) )
class Meta(DeviceComponentTable.Meta): class Meta(DeviceComponentTable.Meta):
model = FrontPort model = models.FrontPort
fields = ( fields = (
'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'type', 'color', 'rear_port', 'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'type', 'color', 'rear_port',
'rear_port_position', 'description', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'tags', 'rear_port_position', 'description', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'tags',
@ -642,7 +625,7 @@ class DeviceFrontPortTable(FrontPortTable):
) )
class Meta(DeviceComponentTable.Meta): class Meta(DeviceComponentTable.Meta):
model = FrontPort model = models.FrontPort
fields = ( fields = (
'pk', 'id', 'name', 'module_bay', 'module', 'label', 'type', 'rear_port', 'rear_port_position', 'pk', 'id', 'name', 'module_bay', 'module', 'label', 'type', 'rear_port', 'rear_port_position',
'description', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'tags', 'actions', 'description', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'tags', 'actions',
@ -668,7 +651,7 @@ class RearPortTable(ModularDeviceComponentTable, CableTerminationTable):
) )
class Meta(DeviceComponentTable.Meta): class Meta(DeviceComponentTable.Meta):
model = RearPort model = models.RearPort
fields = ( fields = (
'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'type', 'color', 'positions', 'description', 'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'type', 'color', 'positions', 'description',
'mark_connected', 'cable', 'cable_color', 'link_peer', 'tags', 'created', 'last_updated', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'tags', 'created', 'last_updated',
@ -688,7 +671,7 @@ class DeviceRearPortTable(RearPortTable):
) )
class Meta(DeviceComponentTable.Meta): class Meta(DeviceComponentTable.Meta):
model = RearPort model = models.RearPort
fields = ( fields = (
'pk', 'id', 'name', 'module_bay', 'module', 'label', 'type', 'positions', 'description', 'mark_connected', 'pk', 'id', 'name', 'module_bay', 'module', 'label', 'type', 'positions', 'description', 'mark_connected',
'cable', 'cable_color', 'link_peer', 'tags', 'actions', 'cable', 'cable_color', 'link_peer', 'tags', 'actions',
@ -729,7 +712,7 @@ class DeviceBayTable(DeviceComponentTable):
) )
class Meta(DeviceComponentTable.Meta): class Meta(DeviceComponentTable.Meta):
model = DeviceBay model = models.DeviceBay
fields = ( fields = (
'pk', 'id', 'name', 'device', 'label', 'status', 'device_role', 'device_type', 'installed_device', 'description', 'tags', 'pk', 'id', 'name', 'device', 'label', 'status', 'device_role', 'device_type', 'installed_device', 'description', 'tags',
'created', 'last_updated', 'created', 'last_updated',
@ -750,7 +733,7 @@ class DeviceDeviceBayTable(DeviceBayTable):
) )
class Meta(DeviceComponentTable.Meta): class Meta(DeviceComponentTable.Meta):
model = DeviceBay model = models.DeviceBay
fields = ( fields = (
'pk', 'id', 'name', 'label', 'status', 'installed_device', 'description', 'tags', 'actions', 'pk', 'id', 'name', 'label', 'status', 'installed_device', 'description', 'tags', 'actions',
) )
@ -779,7 +762,7 @@ class ModuleBayTable(DeviceComponentTable):
) )
class Meta(DeviceComponentTable.Meta): class Meta(DeviceComponentTable.Meta):
model = ModuleBay model = models.ModuleBay
fields = ( fields = (
'pk', 'id', 'name', 'device', 'label', 'position', 'installed_module', 'module_serial', 'module_asset_tag', 'pk', 'id', 'name', 'device', 'label', 'position', 'installed_module', 'module_serial', 'module_asset_tag',
'description', 'tags', 'description', 'tags',
@ -793,7 +776,7 @@ class DeviceModuleBayTable(ModuleBayTable):
) )
class Meta(DeviceComponentTable.Meta): class Meta(DeviceComponentTable.Meta):
model = ModuleBay model = models.ModuleBay
fields = ( fields = (
'pk', 'id', 'name', 'label', 'position', 'installed_module', 'module_serial', 'module_asset_tag', 'pk', 'id', 'name', 'label', 'position', 'installed_module', 'module_serial', 'module_asset_tag',
'description', 'tags', 'actions', 'description', 'tags', 'actions',
@ -823,7 +806,7 @@ class InventoryItemTable(DeviceComponentTable):
cable = None # Override DeviceComponentTable cable = None # Override DeviceComponentTable
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = InventoryItem model = models.InventoryItem
fields = ( fields = (
'pk', 'id', 'name', 'device', 'component', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'pk', 'id', 'name', 'device', 'component', 'label', 'role', 'manufacturer', 'part_id', 'serial',
'asset_tag', 'description', 'discovered', 'tags', 'created', 'last_updated', 'asset_tag', 'description', 'discovered', 'tags', 'created', 'last_updated',
@ -842,7 +825,7 @@ class DeviceInventoryItemTable(InventoryItemTable):
) )
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = InventoryItem model = models.InventoryItem
fields = ( fields = (
'pk', 'id', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'component', 'pk', 'id', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'component',
'description', 'discovered', 'tags', 'actions', 'description', 'discovered', 'tags', 'actions',
@ -867,7 +850,7 @@ class InventoryItemRoleTable(NetBoxTable):
) )
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = InventoryItemRole model = models.InventoryItemRole
fields = ( fields = (
'pk', 'id', 'name', 'inventoryitem_count', 'color', 'description', 'slug', 'tags', 'actions', 'pk', 'id', 'name', 'inventoryitem_count', 'color', 'description', 'slug', 'tags', 'actions',
) )
@ -890,13 +873,17 @@ class VirtualChassisTable(NetBoxTable):
url_params={'virtual_chassis_id': 'pk'}, url_params={'virtual_chassis_id': 'pk'},
verbose_name='Members' verbose_name='Members'
) )
comments = columns.MarkdownColumn()
tags = columns.TagColumn( tags = columns.TagColumn(
url_name='dcim:virtualchassis_list' url_name='dcim:virtualchassis_list'
) )
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = VirtualChassis model = models.VirtualChassis
fields = ('pk', 'id', 'name', 'domain', 'master', 'member_count', 'tags', 'created', 'last_updated',) fields = (
'pk', 'id', 'name', 'domain', 'master', 'member_count', 'description', 'comments', 'tags', 'created',
'last_updated',
)
default_columns = ('pk', 'name', 'domain', 'master', 'member_count') default_columns = ('pk', 'name', 'domain', 'master', 'member_count')
@ -931,7 +918,7 @@ class VirtualDeviceContextTable(TenancyColumnsMixin, NetBoxTable):
) )
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = VirtualDeviceContext model = models.VirtualDeviceContext
fields = ( fields = (
'pk', 'id', 'name', 'identifier', 'tenant', 'tenant_group', 'pk', 'id', 'name', 'identifier', 'tenant', 'tenant_group',
'primary_ip', 'primary_ip4', 'primary_ip6', 'comments', 'tags', 'created', 'last_updated', 'primary_ip', 'primary_ip4', 'primary_ip6', 'comments', 'tags', 'created', 'last_updated',

View File

@ -1,19 +1,6 @@
import django_tables2 as tables import django_tables2 as tables
from dcim.models import ( from dcim import models
ConsolePortTemplate,
ConsoleServerPortTemplate,
DeviceBayTemplate,
DeviceType,
FrontPortTemplate,
InterfaceTemplate,
InventoryItemTemplate,
Manufacturer,
ModuleBayTemplate,
PowerOutletTemplate,
PowerPortTemplate,
RearPortTemplate,
)
from netbox.tables import NetBoxTable, columns from netbox.tables import NetBoxTable, columns
from tenancy.tables import ContactsColumnMixin from tenancy.tables import ContactsColumnMixin
from .template_code import MODULAR_COMPONENT_TEMPLATE_BUTTONS, DEVICE_WEIGHT from .template_code import MODULAR_COMPONENT_TEMPLATE_BUTTONS, DEVICE_WEIGHT
@ -59,7 +46,7 @@ class ManufacturerTable(ContactsColumnMixin, NetBoxTable):
) )
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = Manufacturer model = models.Manufacturer
fields = ( fields = (
'pk', 'id', 'name', 'devicetype_count', 'inventoryitem_count', 'platform_count', 'description', 'slug', 'pk', 'id', 'name', 'devicetype_count', 'inventoryitem_count', 'platform_count', 'description', 'slug',
'contacts', 'actions', 'created', 'last_updated', 'contacts', 'actions', 'created', 'last_updated',
@ -100,15 +87,12 @@ class DeviceTypeTable(NetBoxTable):
template_code=DEVICE_WEIGHT, template_code=DEVICE_WEIGHT,
order_by=('_abs_weight', 'weight_unit') order_by=('_abs_weight', 'weight_unit')
) )
u_height = columns.TemplateColumn(
template_code='{{ value|floatformat }}'
)
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = DeviceType model = models.DeviceType
fields = ( fields = (
'pk', 'id', 'model', 'manufacturer', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role', 'pk', 'id', 'model', 'manufacturer', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role',
'airflow', 'weight', 'comments', 'instance_count', 'tags', 'created', 'last_updated', 'airflow', 'weight', 'description', 'comments', 'instance_count', 'tags', 'created', 'last_updated',
) )
default_columns = ( default_columns = (
'pk', 'model', 'manufacturer', 'part_number', 'u_height', 'is_full_depth', 'instance_count', 'pk', 'model', 'manufacturer', 'part_number', 'u_height', 'is_full_depth', 'instance_count',
@ -138,7 +122,7 @@ class ConsolePortTemplateTable(ComponentTemplateTable):
) )
class Meta(ComponentTemplateTable.Meta): class Meta(ComponentTemplateTable.Meta):
model = ConsolePortTemplate model = models.ConsolePortTemplate
fields = ('pk', 'name', 'label', 'type', 'description', 'actions') fields = ('pk', 'name', 'label', 'type', 'description', 'actions')
empty_text = "None" empty_text = "None"
@ -150,7 +134,7 @@ class ConsoleServerPortTemplateTable(ComponentTemplateTable):
) )
class Meta(ComponentTemplateTable.Meta): class Meta(ComponentTemplateTable.Meta):
model = ConsoleServerPortTemplate model = models.ConsoleServerPortTemplate
fields = ('pk', 'name', 'label', 'type', 'description', 'actions') fields = ('pk', 'name', 'label', 'type', 'description', 'actions')
empty_text = "None" empty_text = "None"
@ -162,7 +146,7 @@ class PowerPortTemplateTable(ComponentTemplateTable):
) )
class Meta(ComponentTemplateTable.Meta): class Meta(ComponentTemplateTable.Meta):
model = PowerPortTemplate model = models.PowerPortTemplate
fields = ('pk', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description', 'actions') fields = ('pk', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description', 'actions')
empty_text = "None" empty_text = "None"
@ -174,7 +158,7 @@ class PowerOutletTemplateTable(ComponentTemplateTable):
) )
class Meta(ComponentTemplateTable.Meta): class Meta(ComponentTemplateTable.Meta):
model = PowerOutletTemplate model = models.PowerOutletTemplate
fields = ('pk', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description', 'actions') fields = ('pk', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description', 'actions')
empty_text = "None" empty_text = "None"
@ -189,7 +173,7 @@ class InterfaceTemplateTable(ComponentTemplateTable):
) )
class Meta(ComponentTemplateTable.Meta): class Meta(ComponentTemplateTable.Meta):
model = InterfaceTemplate model = models.InterfaceTemplate
fields = ('pk', 'name', 'label', 'mgmt_only', 'type', 'description', 'poe_mode', 'poe_type', 'actions') fields = ('pk', 'name', 'label', 'mgmt_only', 'type', 'description', 'poe_mode', 'poe_type', 'actions')
empty_text = "None" empty_text = "None"
@ -205,7 +189,7 @@ class FrontPortTemplateTable(ComponentTemplateTable):
) )
class Meta(ComponentTemplateTable.Meta): class Meta(ComponentTemplateTable.Meta):
model = FrontPortTemplate model = models.FrontPortTemplate
fields = ('pk', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'description', 'actions') fields = ('pk', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'description', 'actions')
empty_text = "None" empty_text = "None"
@ -218,7 +202,7 @@ class RearPortTemplateTable(ComponentTemplateTable):
) )
class Meta(ComponentTemplateTable.Meta): class Meta(ComponentTemplateTable.Meta):
model = RearPortTemplate model = models.RearPortTemplate
fields = ('pk', 'name', 'label', 'type', 'color', 'positions', 'description', 'actions') fields = ('pk', 'name', 'label', 'type', 'color', 'positions', 'description', 'actions')
empty_text = "None" empty_text = "None"
@ -229,7 +213,7 @@ class ModuleBayTemplateTable(ComponentTemplateTable):
) )
class Meta(ComponentTemplateTable.Meta): class Meta(ComponentTemplateTable.Meta):
model = ModuleBayTemplate model = models.ModuleBayTemplate
fields = ('pk', 'name', 'label', 'position', 'description', 'actions') fields = ('pk', 'name', 'label', 'position', 'description', 'actions')
empty_text = "None" empty_text = "None"
@ -240,7 +224,7 @@ class DeviceBayTemplateTable(ComponentTemplateTable):
) )
class Meta(ComponentTemplateTable.Meta): class Meta(ComponentTemplateTable.Meta):
model = DeviceBayTemplate model = models.DeviceBayTemplate
fields = ('pk', 'name', 'label', 'description', 'actions') fields = ('pk', 'name', 'label', 'description', 'actions')
empty_text = "None" empty_text = "None"
@ -260,7 +244,7 @@ class InventoryItemTemplateTable(ComponentTemplateTable):
) )
class Meta(ComponentTemplateTable.Meta): class Meta(ComponentTemplateTable.Meta):
model = InventoryItemTemplate model = models.InventoryItemTemplate
fields = ( fields = (
'pk', 'name', 'label', 'parent', 'role', 'manufacturer', 'part_id', 'component', 'description', 'actions', 'pk', 'name', 'label', 'parent', 'role', 'manufacturer', 'part_id', 'component', 'description', 'actions',
) )

View File

@ -35,7 +35,7 @@ class ModuleTypeTable(NetBoxTable):
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = ModuleType model = ModuleType
fields = ( fields = (
'pk', 'id', 'model', 'manufacturer', 'part_number', 'weight', 'comments', 'tags', 'pk', 'id', 'model', 'manufacturer', 'part_number', 'weight', 'description', 'comments', 'tags',
) )
default_columns = ( default_columns = (
'pk', 'model', 'manufacturer', 'part_number', 'pk', 'model', 'manufacturer', 'part_number',
@ -64,8 +64,8 @@ class ModuleTable(NetBoxTable):
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = Module model = Module
fields = ( fields = (
'pk', 'id', 'device', 'module_bay', 'manufacturer', 'module_type', 'serial', 'asset_tag', 'comments', 'pk', 'id', 'device', 'module_bay', 'manufacturer', 'module_type', 'serial', 'asset_tag', 'description',
'tags', 'comments', 'tags',
) )
default_columns = ( default_columns = (
'pk', 'id', 'device', 'module_bay', 'manufacturer', 'module_type', 'serial', 'asset_tag', 'pk', 'id', 'device', 'module_bay', 'manufacturer', 'module_type', 'serial', 'asset_tag',

View File

@ -31,6 +31,7 @@ class PowerPanelTable(ContactsColumnMixin, NetBoxTable):
url_params={'power_panel_id': 'pk'}, url_params={'power_panel_id': 'pk'},
verbose_name='Feeds' verbose_name='Feeds'
) )
comments = columns.MarkdownColumn()
tags = columns.TagColumn( tags = columns.TagColumn(
url_name='dcim:powerpanel_list' url_name='dcim:powerpanel_list'
) )
@ -38,7 +39,8 @@ class PowerPanelTable(ContactsColumnMixin, NetBoxTable):
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = PowerPanel model = PowerPanel
fields = ( fields = (
'pk', 'id', 'name', 'site', 'location', 'powerfeed_count', 'contacts', 'tags', 'created', 'last_updated', 'pk', 'id', 'name', 'site', 'location', 'powerfeed_count', 'contacts', 'description', 'comments', 'tags',
'created', 'last_updated',
) )
default_columns = ('pk', 'name', 'site', 'location', 'powerfeed_count') default_columns = ('pk', 'name', 'site', 'location', 'powerfeed_count')
@ -77,7 +79,7 @@ class PowerFeedTable(CableTerminationTable):
fields = ( fields = (
'pk', 'id', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase', 'pk', 'id', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase',
'max_utilization', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'connection', 'available_power', 'max_utilization', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'connection', 'available_power',
'comments', 'tags', 'created', 'last_updated', 'description', 'comments', 'tags', 'created', 'last_updated',
) )
default_columns = ( default_columns = (
'pk', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase', 'cable', 'pk', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase', 'cable',

View File

@ -90,8 +90,8 @@ class RackTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
fields = ( fields = (
'pk', 'id', 'name', 'site', 'location', 'status', 'facility_id', 'tenant', 'tenant_group', 'role', 'serial', 'pk', 'id', 'name', 'site', 'location', 'status', 'facility_id', 'tenant', 'tenant_group', 'role', 'serial',
'asset_tag', 'type', 'u_height', 'width', 'outer_width', 'outer_depth', 'mounting_depth', 'weight', 'asset_tag', 'type', 'u_height', 'width', 'outer_width', 'outer_depth', 'mounting_depth', 'weight',
'comments', 'device_count', 'get_utilization', 'get_power_utilization', 'contacts', 'tags', 'created', 'comments', 'device_count', 'get_utilization', 'get_power_utilization', 'description', 'contacts', 'tags',
'last_updated', 'created', 'last_updated',
) )
default_columns = ( default_columns = (
'pk', 'name', 'site', 'location', 'status', 'facility_id', 'tenant', 'role', 'u_height', 'device_count', 'pk', 'name', 'site', 'location', 'status', 'facility_id', 'tenant', 'role', 'u_height', 'device_count',
@ -123,6 +123,7 @@ class RackReservationTable(TenancyColumnsMixin, NetBoxTable):
orderable=False, orderable=False,
verbose_name='Units' verbose_name='Units'
) )
comments = columns.MarkdownColumn()
tags = columns.TagColumn( tags = columns.TagColumn(
url_name='dcim:rackreservation_list' url_name='dcim:rackreservation_list'
) )
@ -130,7 +131,7 @@ class RackReservationTable(TenancyColumnsMixin, NetBoxTable):
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = RackReservation model = RackReservation
fields = ( fields = (
'pk', 'id', 'reservation', 'site', 'location', 'rack', 'unit_list', 'user', 'created', 'tenant', 'tenant_group', 'description', 'tags', 'pk', 'id', 'reservation', 'site', 'location', 'rack', 'unit_list', 'user', 'created', 'tenant',
'actions', 'created', 'last_updated', 'tenant_group', 'description', 'comments', 'tags', 'actions', 'created', 'last_updated',
) )
default_columns = ('pk', 'reservation', 'site', 'rack', 'unit_list', 'user', 'description') default_columns = ('pk', 'reservation', 'site', 'rack', 'unit_list', 'user', 'description')

View File

@ -1,6 +1,10 @@
from decimal import Decimal from decimal import Decimal
try:
from zoneinfo import ZoneInfo
except ImportError:
# Python 3.8
from backports.zoneinfo import ZoneInfo
import pytz
import yaml import yaml
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
@ -12,7 +16,6 @@ from dcim.choices import *
from dcim.constants import * from dcim.constants import *
from dcim.models import * from dcim.models import *
from ipam.models import ASN, RIR, VLAN, VRF from ipam.models import ASN, RIR, VLAN, VRF
from netbox.api.serializers import GenericObjectSerializer
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.testing import ViewTestCases, create_tags, create_test_device, post_data from utilities.testing import ViewTestCases, create_tags, create_test_device, post_data
from wireless.models import WirelessLAN from wireless.models import WirelessLAN
@ -153,7 +156,7 @@ class SiteTestCase(ViewTestCases.PrimaryObjectViewTestCase):
'tenant': None, 'tenant': None,
'facility': 'Facility X', 'facility': 'Facility X',
'asns': [asns[6].pk, asns[7].pk], 'asns': [asns[6].pk, asns[7].pk],
'time_zone': pytz.UTC, 'time_zone': ZoneInfo('UTC'),
'description': 'Site description', 'description': 'Site description',
'physical_address': '742 Evergreen Terrace, Springfield, USA', 'physical_address': '742 Evergreen Terrace, Springfield, USA',
'shipping_address': '742 Evergreen Terrace, Springfield, USA', 'shipping_address': '742 Evergreen Terrace, Springfield, USA',
@ -182,7 +185,7 @@ class SiteTestCase(ViewTestCases.PrimaryObjectViewTestCase):
'region': regions[1].pk, 'region': regions[1].pk,
'group': groups[1].pk, 'group': groups[1].pk,
'tenant': None, 'tenant': None,
'time_zone': pytz.timezone('US/Eastern'), 'time_zone': ZoneInfo('US/Eastern'),
'description': 'New description', 'description': 'New description',
} }

View File

@ -13,6 +13,7 @@ __all__ = [
'NestedImageAttachmentSerializer', 'NestedImageAttachmentSerializer',
'NestedJobResultSerializer', 'NestedJobResultSerializer',
'NestedJournalEntrySerializer', 'NestedJournalEntrySerializer',
'NestedSavedFilterSerializer',
'NestedTagSerializer', # Defined in netbox.api.serializers 'NestedTagSerializer', # Defined in netbox.api.serializers
'NestedWebhookSerializer', 'NestedWebhookSerializer',
] ]
@ -58,6 +59,14 @@ class NestedExportTemplateSerializer(WritableNestedSerializer):
fields = ['id', 'url', 'display', 'name'] fields = ['id', 'url', 'display', 'name']
class NestedSavedFilterSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:savedfilter-detail')
class Meta:
model = models.SavedFilter
fields = ['id', 'url', 'display', 'name']
class NestedImageAttachmentSerializer(WritableNestedSerializer): class NestedImageAttachmentSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:imageattachment-detail') url = serializers.HyperlinkedIdentityField(view_name='extras-api:imageattachment-detail')

View File

@ -39,6 +39,7 @@ __all__ = (
'ReportDetailSerializer', 'ReportDetailSerializer',
'ReportSerializer', 'ReportSerializer',
'ReportInputSerializer', 'ReportInputSerializer',
'SavedFilterSerializer',
'ScriptDetailSerializer', 'ScriptDetailSerializer',
'ScriptInputSerializer', 'ScriptInputSerializer',
'ScriptLogMessageSerializer', 'ScriptLogMessageSerializer',
@ -149,6 +150,25 @@ class ExportTemplateSerializer(ValidatedModelSerializer):
] ]
#
# Saved filters
#
class SavedFilterSerializer(ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:savedfilter-detail')
content_types = ContentTypeField(
queryset=ContentType.objects.all(),
many=True
)
class Meta:
model = SavedFilter
fields = [
'id', 'url', 'display', 'content_types', 'name', 'description', 'user', 'weight',
'enabled', 'shared', 'parameters', 'created', 'last_updated',
]
# #
# Tags # Tags
# #

View File

@ -5,43 +5,19 @@ from . import views
router = NetBoxRouter() router = NetBoxRouter()
router.APIRootView = views.ExtrasRootView router.APIRootView = views.ExtrasRootView
# Webhooks
router.register('webhooks', views.WebhookViewSet) router.register('webhooks', views.WebhookViewSet)
# Custom fields
router.register('custom-fields', views.CustomFieldViewSet) router.register('custom-fields', views.CustomFieldViewSet)
# Custom links
router.register('custom-links', views.CustomLinkViewSet) router.register('custom-links', views.CustomLinkViewSet)
# Export templates
router.register('export-templates', views.ExportTemplateViewSet) router.register('export-templates', views.ExportTemplateViewSet)
router.register('saved-filters', views.SavedFilterViewSet)
# Tags
router.register('tags', views.TagViewSet) router.register('tags', views.TagViewSet)
# Image attachments
router.register('image-attachments', views.ImageAttachmentViewSet) router.register('image-attachments', views.ImageAttachmentViewSet)
# Journal entries
router.register('journal-entries', views.JournalEntryViewSet) router.register('journal-entries', views.JournalEntryViewSet)
# Config contexts
router.register('config-contexts', views.ConfigContextViewSet) router.register('config-contexts', views.ConfigContextViewSet)
# Reports
router.register('reports', views.ReportViewSet, basename='report') router.register('reports', views.ReportViewSet, basename='report')
# Scripts
router.register('scripts', views.ScriptViewSet, basename='script') router.register('scripts', views.ScriptViewSet, basename='script')
# Change logging
router.register('object-changes', views.ObjectChangeViewSet) router.register('object-changes', views.ObjectChangeViewSet)
# Job Results
router.register('job-results', views.JobResultViewSet) router.register('job-results', views.JobResultViewSet)
# ContentTypes
router.register('content-types', views.ContentTypeViewSet) router.register('content-types', views.ContentTypeViewSet)
app_name = 'extras-api' app_name = 'extras-api'

View File

@ -1,4 +1,5 @@
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.db.models import Q
from django.http import Http404 from django.http import Http404
from django_rq.queues import get_connection from django_rq.queues import get_connection
from rest_framework import status from rest_framework import status
@ -98,6 +99,17 @@ class ExportTemplateViewSet(NetBoxModelViewSet):
filterset_class = filtersets.ExportTemplateFilterSet filterset_class = filtersets.ExportTemplateFilterSet
#
# Saved filters
#
class SavedFilterViewSet(NetBoxModelViewSet):
metadata_class = ContentTypeMetadata
queryset = SavedFilter.objects.all()
serializer_class = serializers.SavedFilterSerializer
filterset_class = filtersets.SavedFilterFilterSet
# #
# Tags # Tags
# #

View File

@ -23,6 +23,7 @@ __all__ = (
'JournalEntryFilterSet', 'JournalEntryFilterSet',
'LocalConfigContextFilterSet', 'LocalConfigContextFilterSet',
'ObjectChangeFilterSet', 'ObjectChangeFilterSet',
'SavedFilterFilterSet',
'TagFilterSet', 'TagFilterSet',
'WebhookFilterSet', 'WebhookFilterSet',
) )
@ -138,6 +139,55 @@ class ExportTemplateFilterSet(BaseFilterSet):
) )
class SavedFilterFilterSet(BaseFilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',
)
content_type_id = MultiValueNumberFilter(
field_name='content_types__id'
)
content_types = ContentTypeFilter()
user_id = django_filters.ModelMultipleChoiceFilter(
queryset=User.objects.all(),
label='User (ID)',
)
user = django_filters.ModelMultipleChoiceFilter(
field_name='user__username',
queryset=User.objects.all(),
to_field_name='username',
label='User (name)',
)
usable = django_filters.BooleanFilter(
method='_usable'
)
class Meta:
model = SavedFilter
fields = ['id', 'content_types', 'name', 'description', 'enabled', 'shared', 'weight']
def search(self, queryset, name, value):
if not value.strip():
return queryset
return queryset.filter(
Q(name__icontains=value) |
Q(description__icontains=value)
)
def _usable(self, queryset, name, value):
"""
Return only SavedFilters that are both enabled and are shared (or belong to the current user).
"""
user = self.request.user if self.request else None
if not user or user.is_anonymous:
if value:
return queryset.filter(enabled=True, shared=True)
return queryset.filter(Q(enabled=False) | Q(shared=False))
if value:
return queryset.filter(enabled=True).filter(Q(shared=True) | Q(user=user))
return queryset.filter(Q(enabled=False) | Q(Q(shared=False) & ~Q(user=user)))
class ImageAttachmentFilterSet(BaseFilterSet): class ImageAttachmentFilterSet(BaseFilterSet):
q = django_filters.CharFilter( q = django_filters.CharFilter(
method='search', method='search',

View File

@ -2,6 +2,6 @@ from .model_forms import *
from .filtersets import * from .filtersets import *
from .bulk_edit import * from .bulk_edit import *
from .bulk_import import * from .bulk_import import *
from .customfields import * from .mixins import *
from .config import * from .config import *
from .scripts import * from .scripts import *

View File

@ -1,11 +1,9 @@
from django import forms from django import forms
from django.contrib.contenttypes.models import ContentType
from extras.choices import * from extras.choices import *
from extras.models import * from extras.models import *
from extras.utils import FeatureQuery
from utilities.forms import ( from utilities.forms import (
add_blank_choice, BulkEditForm, BulkEditNullBooleanSelect, ColorField, ContentTypeChoiceField, StaticSelect, add_blank_choice, BulkEditForm, BulkEditNullBooleanSelect, ColorField, StaticSelect,
) )
__all__ = ( __all__ = (
@ -14,6 +12,7 @@ __all__ = (
'CustomLinkBulkEditForm', 'CustomLinkBulkEditForm',
'ExportTemplateBulkEditForm', 'ExportTemplateBulkEditForm',
'JournalEntryBulkEditForm', 'JournalEntryBulkEditForm',
'SavedFilterBulkEditForm',
'TagBulkEditForm', 'TagBulkEditForm',
'WebhookBulkEditForm', 'WebhookBulkEditForm',
) )
@ -96,6 +95,30 @@ class ExportTemplateBulkEditForm(BulkEditForm):
nullable_fields = ('description', 'mime_type', 'file_extension') nullable_fields = ('description', 'mime_type', 'file_extension')
class SavedFilterBulkEditForm(BulkEditForm):
pk = forms.ModelMultipleChoiceField(
queryset=SavedFilter.objects.all(),
widget=forms.MultipleHiddenInput
)
description = forms.CharField(
max_length=200,
required=False
)
weight = forms.IntegerField(
required=False
)
enabled = forms.NullBooleanField(
required=False,
widget=BulkEditNullBooleanSelect()
)
shared = forms.NullBooleanField(
required=False,
widget=BulkEditNullBooleanSelect()
)
nullable_fields = ('description',)
class WebhookBulkEditForm(BulkEditForm): class WebhookBulkEditForm(BulkEditForm):
pk = forms.ModelMultipleChoiceField( pk = forms.ModelMultipleChoiceField(
queryset=Webhook.objects.all(), queryset=Webhook.objects.all(),

View File

@ -12,6 +12,7 @@ __all__ = (
'CustomFieldCSVForm', 'CustomFieldCSVForm',
'CustomLinkCSVForm', 'CustomLinkCSVForm',
'ExportTemplateCSVForm', 'ExportTemplateCSVForm',
'SavedFilterCSVForm',
'TagCSVForm', 'TagCSVForm',
'WebhookCSVForm', 'WebhookCSVForm',
) )
@ -81,6 +82,19 @@ class ExportTemplateCSVForm(CSVModelForm):
) )
class SavedFilterCSVForm(CSVModelForm):
content_types = CSVMultipleContentTypeField(
queryset=ContentType.objects.all(),
help_text="One or more assigned object types"
)
class Meta:
model = SavedFilter
fields = (
'name', 'content_types', 'description', 'weight', 'enabled', 'shared', 'parameters',
)
class WebhookCSVForm(CSVModelForm): class WebhookCSVForm(CSVModelForm):
content_types = CSVMultipleContentTypeField( content_types = CSVMultipleContentTypeField(
queryset=ContentType.objects.all(), queryset=ContentType.objects.all(),

View File

@ -15,6 +15,7 @@ from utilities.forms import (
StaticSelect, TagFilterField, StaticSelect, TagFilterField,
) )
from virtualization.models import Cluster, ClusterGroup, ClusterType from virtualization.models import Cluster, ClusterGroup, ClusterType
from .mixins import SavedFiltersMixin
__all__ = ( __all__ = (
'ConfigContextFilterForm', 'ConfigContextFilterForm',
@ -25,14 +26,15 @@ __all__ = (
'JournalEntryFilterForm', 'JournalEntryFilterForm',
'LocalConfigContextFilterForm', 'LocalConfigContextFilterForm',
'ObjectChangeFilterForm', 'ObjectChangeFilterForm',
'SavedFilterFilterForm',
'TagFilterForm', 'TagFilterForm',
'WebhookFilterForm', 'WebhookFilterForm',
) )
class CustomFieldFilterForm(FilterForm): class CustomFieldFilterForm(SavedFiltersMixin, FilterForm):
fieldsets = ( fieldsets = (
(None, ('q',)), (None, ('q', 'filter')),
('Attributes', ('type', 'content_type_id', 'group_name', 'weight', 'required', 'ui_visibility')), ('Attributes', ('type', 'content_type_id', 'group_name', 'weight', 'required', 'ui_visibility')),
) )
content_type_id = ContentTypeMultipleChoiceField( content_type_id = ContentTypeMultipleChoiceField(
@ -66,9 +68,9 @@ class CustomFieldFilterForm(FilterForm):
) )
class JobResultFilterForm(FilterForm): class JobResultFilterForm(SavedFiltersMixin, FilterForm):
fieldsets = ( fieldsets = (
(None, ('q',)), (None, ('q', 'filter')),
('Attributes', ('obj_type', 'status')), ('Attributes', ('obj_type', 'status')),
('Creation', ('created__before', 'created__after', 'completed__before', 'completed__after', ('Creation', ('created__before', 'created__after', 'completed__before', 'completed__after',
'scheduled_time__before', 'scheduled_time__after', 'user')), 'scheduled_time__before', 'scheduled_time__after', 'user')),
@ -118,9 +120,9 @@ class JobResultFilterForm(FilterForm):
) )
class CustomLinkFilterForm(FilterForm): class CustomLinkFilterForm(SavedFiltersMixin, FilterForm):
fieldsets = ( fieldsets = (
(None, ('q',)), (None, ('q', 'filter')),
('Attributes', ('content_types', 'enabled', 'new_window', 'weight')), ('Attributes', ('content_types', 'enabled', 'new_window', 'weight')),
) )
content_types = ContentTypeMultipleChoiceField( content_types = ContentTypeMultipleChoiceField(
@ -145,9 +147,9 @@ class CustomLinkFilterForm(FilterForm):
) )
class ExportTemplateFilterForm(FilterForm): class ExportTemplateFilterForm(SavedFiltersMixin, FilterForm):
fieldsets = ( fieldsets = (
(None, ('q',)), (None, ('q', 'filter')),
('Attributes', ('content_types', 'mime_type', 'file_extension', 'as_attachment')), ('Attributes', ('content_types', 'mime_type', 'file_extension', 'as_attachment')),
) )
content_types = ContentTypeMultipleChoiceField( content_types = ContentTypeMultipleChoiceField(
@ -170,9 +172,36 @@ class ExportTemplateFilterForm(FilterForm):
) )
class WebhookFilterForm(FilterForm): class SavedFilterFilterForm(SavedFiltersMixin, FilterForm):
fieldsets = ( fieldsets = (
(None, ('q',)), (None, ('q', 'filter')),
('Attributes', ('content_types', 'enabled', 'shared', 'weight')),
)
content_types = ContentTypeMultipleChoiceField(
queryset=ContentType.objects.all(),
limit_choices_to=FeatureQuery('export_templates'),
required=False
)
enabled = forms.NullBooleanField(
required=False,
widget=StaticSelect(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
shared = forms.NullBooleanField(
required=False,
widget=StaticSelect(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
weight = forms.IntegerField(
required=False
)
class WebhookFilterForm(SavedFiltersMixin, FilterForm):
fieldsets = (
(None, ('q', 'filter')),
('Attributes', ('content_type_id', 'http_method', 'enabled')), ('Attributes', ('content_type_id', 'http_method', 'enabled')),
('Events', ('type_create', 'type_update', 'type_delete')), ('Events', ('type_create', 'type_update', 'type_delete')),
) )
@ -213,7 +242,7 @@ class WebhookFilterForm(FilterForm):
) )
class TagFilterForm(FilterForm): class TagFilterForm(SavedFiltersMixin, FilterForm):
model = Tag model = Tag
content_type_id = ContentTypeMultipleChoiceField( content_type_id = ContentTypeMultipleChoiceField(
queryset=ContentType.objects.filter(FeatureQuery('tags').get_query()), queryset=ContentType.objects.filter(FeatureQuery('tags').get_query()),
@ -222,9 +251,9 @@ class TagFilterForm(FilterForm):
) )
class ConfigContextFilterForm(FilterForm): class ConfigContextFilterForm(SavedFiltersMixin, FilterForm):
fieldsets = ( fieldsets = (
(None, ('q', 'tag_id')), (None, ('q', 'filter', 'tag_id')),
('Location', ('region_id', 'site_group_id', 'site_id', 'location_id')), ('Location', ('region_id', 'site_group_id', 'site_id', 'location_id')),
('Device', ('device_type_id', 'platform_id', 'role_id')), ('Device', ('device_type_id', 'platform_id', 'role_id')),
('Cluster', ('cluster_type_id', 'cluster_group_id', 'cluster_id')), ('Cluster', ('cluster_type_id', 'cluster_group_id', 'cluster_id')),
@ -311,7 +340,7 @@ class LocalConfigContextFilterForm(forms.Form):
class JournalEntryFilterForm(NetBoxModelFilterSetForm): class JournalEntryFilterForm(NetBoxModelFilterSetForm):
model = JournalEntry model = JournalEntry
fieldsets = ( fieldsets = (
(None, ('q', 'tag')), (None, ('q', 'filter', 'tag')),
('Creation', ('created_before', 'created_after', 'created_by_id')), ('Creation', ('created_before', 'created_after', 'created_by_id')),
('Attributes', ('assigned_object_type_id', 'kind')) ('Attributes', ('assigned_object_type_id', 'kind'))
) )
@ -349,10 +378,10 @@ class JournalEntryFilterForm(NetBoxModelFilterSetForm):
tag = TagFilterField(model) tag = TagFilterField(model)
class ObjectChangeFilterForm(FilterForm): class ObjectChangeFilterForm(SavedFiltersMixin, FilterForm):
model = ObjectChange model = ObjectChange
fieldsets = ( fieldsets = (
(None, ('q',)), (None, ('q', 'filter')),
('Time', ('time_before', 'time_after')), ('Time', ('time_before', 'time_after')),
('Attributes', ('action', 'user_id', 'changed_object_type_id')), ('Attributes', ('action', 'user_id', 'changed_object_type_id')),
) )

View File

@ -1,10 +1,13 @@
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django import forms
from extras.models import * from extras.models import *
from extras.choices import CustomFieldVisibilityChoices from extras.choices import CustomFieldVisibilityChoices
from utilities.forms.fields import DynamicModelMultipleChoiceField
__all__ = ( __all__ = (
'CustomFieldsMixin', 'CustomFieldsMixin',
'SavedFiltersMixin',
) )
@ -57,3 +60,14 @@ class CustomFieldsMixin:
if customfield.group_name not in self.custom_field_groups: if customfield.group_name not in self.custom_field_groups:
self.custom_field_groups[customfield.group_name] = [] self.custom_field_groups[customfield.group_name] = []
self.custom_field_groups[customfield.group_name].append(field_name) self.custom_field_groups[customfield.group_name].append(field_name)
class SavedFiltersMixin(forms.Form):
filter = DynamicModelMultipleChoiceField(
queryset=SavedFilter.objects.all(),
required=False,
label='Saved Filter',
query_params={
'usable': True,
}
)

View File

@ -1,5 +1,6 @@
from django import forms from django import forms
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.http import QueryDict
from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup
from extras.choices import * from extras.choices import *
@ -20,6 +21,7 @@ __all__ = (
'ExportTemplateForm', 'ExportTemplateForm',
'ImageAttachmentForm', 'ImageAttachmentForm',
'JournalEntryForm', 'JournalEntryForm',
'SavedFilterForm',
'TagForm', 'TagForm',
'WebhookForm', 'WebhookForm',
) )
@ -108,6 +110,34 @@ class ExportTemplateForm(BootstrapMixin, forms.ModelForm):
} }
class SavedFilterForm(BootstrapMixin, forms.ModelForm):
content_types = ContentTypeMultipleChoiceField(
queryset=ContentType.objects.all()
)
fieldsets = (
('Saved Filter', ('name', 'content_types', 'description', 'weight', 'enabled', 'shared')),
('Parameters', ('parameters',)),
)
class Meta:
model = SavedFilter
exclude = ('user',)
widgets = {
'parameters': forms.Textarea(attrs={'class': 'font-monospace'}),
}
def __init__(self, *args, initial=None, **kwargs):
# Convert any parameters delivered via initial data to a dictionary
if initial and 'parameters' in initial:
if type(initial['parameters']) is str:
# TODO: Make a utility function for this
initial['parameters'] = dict(QueryDict(initial['parameters']).lists())
super().__init__(*args, initial=initial, **kwargs)
class WebhookForm(BootstrapMixin, forms.ModelForm): class WebhookForm(BootstrapMixin, forms.ModelForm):
content_types = ContentTypeMultipleChoiceField( content_types = ContentTypeMultipleChoiceField(
queryset=ContentType.objects.all(), queryset=ContentType.objects.all(),

View File

@ -20,6 +20,9 @@ class ExtrasQuery(graphene.ObjectType):
image_attachment = ObjectField(ImageAttachmentType) image_attachment = ObjectField(ImageAttachmentType)
image_attachment_list = ObjectListField(ImageAttachmentType) image_attachment_list = ObjectListField(ImageAttachmentType)
saved_filter = ObjectField(SavedFilterType)
saved_filter_list = ObjectListField(SavedFilterType)
journal_entry = ObjectField(JournalEntryType) journal_entry = ObjectField(JournalEntryType)
journal_entry_list = ObjectListField(JournalEntryType) journal_entry_list = ObjectListField(JournalEntryType)

View File

@ -10,6 +10,7 @@ __all__ = (
'ImageAttachmentType', 'ImageAttachmentType',
'JournalEntryType', 'JournalEntryType',
'ObjectChangeType', 'ObjectChangeType',
'SavedFilterType',
'TagType', 'TagType',
'WebhookType', 'WebhookType',
) )
@ -71,6 +72,14 @@ class ObjectChangeType(BaseObjectType):
filterset_class = filtersets.ObjectChangeFilterSet filterset_class = filtersets.ObjectChangeFilterSet
class SavedFilterType(ObjectType):
class Meta:
model = models.SavedFilter
exclude = ('content_types', )
filterset_class = filtersets.SavedFilterFilterSet
class TagType(ObjectType): class TagType(ObjectType):
class Meta: class Meta:

View File

@ -1,7 +1,7 @@
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.core.management.base import BaseCommand, CommandError from django.core.management.base import BaseCommand, CommandError
from extras.registry import registry from netbox.registry import registry
from netbox.search.backends import search_backend from netbox.search.backends import search_backend

View File

@ -0,0 +1,36 @@
# Generated by Django 4.1.1 on 2022-10-27 18:18
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('contenttypes', '0002_remove_content_type_name'),
('extras', '0082_exporttemplate_content_types'),
]
operations = [
migrations.CreateModel(
name='SavedFilter',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
('created', models.DateTimeField(auto_now_add=True, null=True)),
('last_updated', models.DateTimeField(auto_now=True, null=True)),
('name', models.CharField(max_length=100, unique=True)),
('description', models.CharField(blank=True, max_length=200)),
('weight', models.PositiveSmallIntegerField(default=100)),
('enabled', models.BooleanField(default=True)),
('shared', models.BooleanField(default=True)),
('parameters', models.JSONField()),
('content_types', models.ManyToManyField(related_name='saved_filters', to='contenttypes.contenttype')),
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
],
options={
'ordering': ('weight', 'name'),
},
),
]

View File

@ -18,6 +18,7 @@ __all__ = (
'JournalEntry', 'JournalEntry',
'ObjectChange', 'ObjectChange',
'Report', 'Report',
'SavedFilter',
'Script', 'Script',
'Tag', 'Tag',
'TaggedItem', 'TaggedItem',

View File

@ -8,7 +8,7 @@ from django.contrib.contenttypes.models import ContentType
from django.core.cache import cache from django.core.cache import cache
from django.core.validators import ValidationError from django.core.validators import ValidationError
from django.db import models from django.db import models
from django.http import HttpResponse from django.http import HttpResponse, QueryDict
from django.urls import reverse from django.urls import reverse
from django.utils import timezone from django.utils import timezone
from django.utils.formats import date_format from django.utils.formats import date_format
@ -34,6 +34,7 @@ __all__ = (
'JobResult', 'JobResult',
'JournalEntry', 'JournalEntry',
'Report', 'Report',
'SavedFilter',
'Script', 'Script',
'Webhook', 'Webhook',
) )
@ -350,6 +351,69 @@ class ExportTemplate(ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel):
return response return response
class SavedFilter(CloningMixin, ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel):
"""
A set of predefined keyword parameters that can be reused to filter for specific objects.
"""
content_types = models.ManyToManyField(
to=ContentType,
related_name='saved_filters',
help_text='The object type(s) to which this filter applies.'
)
name = models.CharField(
max_length=100,
unique=True
)
description = models.CharField(
max_length=200,
blank=True
)
user = models.ForeignKey(
to=User,
on_delete=models.SET_NULL,
blank=True,
null=True
)
weight = models.PositiveSmallIntegerField(
default=100
)
enabled = models.BooleanField(
default=True
)
shared = models.BooleanField(
default=True
)
parameters = models.JSONField()
clone_fields = (
'enabled', 'weight',
)
class Meta:
ordering = ('weight', 'name')
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse('extras:savedfilter', args=[self.pk])
def clean(self):
super().clean()
# Verify that `parameters` is a JSON object
if type(self.parameters) is not dict:
raise ValidationError(
{'parameters': 'Filter parameters must be stored as a dictionary of keyword arguments.'}
)
@property
def url_params(self):
qd = QueryDict(mutable=True)
qd.update(self.parameters)
return qd.urlencode()
class ImageAttachment(WebhooksMixin, ChangeLoggedModel): class ImageAttachment(WebhooksMixin, ChangeLoggedModel):
""" """
An uploaded image which is associated with an object. An uploaded image which is associated with an object.

View File

@ -1,17 +1,16 @@
import collections import collections
import inspect
from packaging import version
from django.apps import AppConfig from django.apps import AppConfig
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.template.loader import get_template
from django.utils.module_loading import import_string from django.utils.module_loading import import_string
from packaging import version
from extras.registry import registry from netbox.registry import registry
from netbox.navigation import MenuGroup
from netbox.search import register_search from netbox.search import register_search
from utilities.choices import ButtonColorChoices from .navigation import *
from .registration import *
from .templates import *
# Initialize plugin registry # Initialize plugin registry
registry['plugins'] = { registry['plugins'] = {
@ -145,185 +144,20 @@ class PluginConfig(AppConfig):
# #
# Template content injection # Utilities
# #
class PluginTemplateExtension: def get_plugin_config(plugin_name, parameter, default=None):
""" """
This class is used to register plugin content to be injected into core NetBox templates. It contains methods Return the value of the specified plugin configuration parameter.
that are overridden by plugin authors to return template content.
The `model` attribute on the class defines the which model detail page this class renders content for. It Args:
should be set as a string in the form '<app_label>.<model_name>'. render() provides the following context data: plugin_name: The name of the plugin
parameter: The name of the configuration parameter
* object - The object being viewed default: The value to return if the parameter is not defined (default: None)
* request - The current request
* settings - Global NetBox settings
* config - Plugin-specific configuration parameters
""" """
model = None try:
plugin_config = settings.PLUGINS_CONFIG[plugin_name]
def __init__(self, context): return plugin_config.get(parameter, default)
self.context = context except KeyError:
raise ImproperlyConfigured(f"Plugin {plugin_name} is not registered.")
def render(self, template_name, extra_context=None):
"""
Convenience method for rendering the specified Django template using the default context data. An additional
context dictionary may be passed as `extra_context`.
"""
if extra_context is None:
extra_context = {}
elif not isinstance(extra_context, dict):
raise TypeError("extra_context must be a dictionary")
return get_template(template_name).render({**self.context, **extra_context})
def left_page(self):
"""
Content that will be rendered on the left of the detail page view. Content should be returned as an
HTML string. Note that content does not need to be marked as safe because this is automatically handled.
"""
raise NotImplementedError
def right_page(self):
"""
Content that will be rendered on the right of the detail page view. Content should be returned as an
HTML string. Note that content does not need to be marked as safe because this is automatically handled.
"""
raise NotImplementedError
def full_width_page(self):
"""
Content that will be rendered within the full width of the detail page view. Content should be returned as an
HTML string. Note that content does not need to be marked as safe because this is automatically handled.
"""
raise NotImplementedError
def buttons(self):
"""
Buttons that will be rendered and added to the existing list of buttons on the detail page view. Content
should be returned as an HTML string. Note that content does not need to be marked as safe because this is
automatically handled.
"""
raise NotImplementedError
def register_template_extensions(class_list):
"""
Register a list of PluginTemplateExtension classes
"""
# Validation
for template_extension in class_list:
if not inspect.isclass(template_extension):
raise TypeError(f"PluginTemplateExtension class {template_extension} was passed as an instance!")
if not issubclass(template_extension, PluginTemplateExtension):
raise TypeError(f"{template_extension} is not a subclass of extras.plugins.PluginTemplateExtension!")
if template_extension.model is None:
raise TypeError(f"PluginTemplateExtension class {template_extension} does not define a valid model!")
registry['plugins']['template_extensions'][template_extension.model].append(template_extension)
#
# Navigation menu links
#
class PluginMenu:
icon_class = 'mdi mdi-puzzle'
def __init__(self, label, groups, icon_class=None):
self.label = label
self.groups = [
MenuGroup(label, items) for label, items in groups
]
if icon_class is not None:
self.icon_class = icon_class
class PluginMenuItem:
"""
This class represents a navigation menu item. This constitutes primary link and its text, but also allows for
specifying additional link buttons that appear to the right of the item in the van menu.
Links are specified as Django reverse URL strings.
Buttons are each specified as a list of PluginMenuButton instances.
"""
permissions = []
buttons = []
def __init__(self, link, link_text, permissions=None, buttons=None):
self.link = link
self.link_text = link_text
if permissions is not None:
if type(permissions) not in (list, tuple):
raise TypeError("Permissions must be passed as a tuple or list.")
self.permissions = permissions
if buttons is not None:
if type(buttons) not in (list, tuple):
raise TypeError("Buttons must be passed as a tuple or list.")
self.buttons = buttons
class PluginMenuButton:
"""
This class represents a button within a PluginMenuItem. Note that button colors should come from
ButtonColorChoices.
"""
color = ButtonColorChoices.DEFAULT
permissions = []
def __init__(self, link, title, icon_class, color=None, permissions=None):
self.link = link
self.title = title
self.icon_class = icon_class
if permissions is not None:
if type(permissions) not in (list, tuple):
raise TypeError("Permissions must be passed as a tuple or list.")
self.permissions = permissions
if color is not None:
if color not in ButtonColorChoices.values():
raise ValueError("Button color must be a choice within ButtonColorChoices.")
self.color = color
def register_menu(menu):
if not isinstance(menu, PluginMenu):
raise TypeError(f"{menu} must be an instance of extras.plugins.PluginMenu")
registry['plugins']['menus'].append(menu)
def register_menu_items(section_name, class_list):
"""
Register a list of PluginMenuItem instances for a given menu section (e.g. plugin name)
"""
# Validation
for menu_link in class_list:
if not isinstance(menu_link, PluginMenuItem):
raise TypeError(f"{menu_link} must be an instance of extras.plugins.PluginMenuItem")
for button in menu_link.buttons:
if not isinstance(button, PluginMenuButton):
raise TypeError(f"{button} must be an instance of extras.plugins.PluginMenuButton")
registry['plugins']['menu_items'][section_name] = class_list
#
# GraphQL schemas
#
def register_graphql_schema(graphql_schema):
"""
Register a GraphQL schema class for inclusion in NetBox's GraphQL API.
"""
registry['plugins']['graphql_schemas'].append(graphql_schema)
#
# User preferences
#
def register_user_preferences(plugin_name, preferences):
"""
Register a list of user preferences defined by a plugin.
"""
registry['plugins']['preferences'][plugin_name] = preferences

View File

@ -0,0 +1,66 @@
from netbox.navigation import MenuGroup
from utilities.choices import ButtonColorChoices
__all__ = (
'PluginMenu',
'PluginMenuButton',
'PluginMenuItem',
)
class PluginMenu:
icon_class = 'mdi mdi-puzzle'
def __init__(self, label, groups, icon_class=None):
self.label = label
self.groups = [
MenuGroup(label, items) for label, items in groups
]
if icon_class is not None:
self.icon_class = icon_class
class PluginMenuItem:
"""
This class represents a navigation menu item. This constitutes primary link and its text, but also allows for
specifying additional link buttons that appear to the right of the item in the van menu.
Links are specified as Django reverse URL strings.
Buttons are each specified as a list of PluginMenuButton instances.
"""
permissions = []
buttons = []
def __init__(self, link, link_text, permissions=None, buttons=None):
self.link = link
self.link_text = link_text
if permissions is not None:
if type(permissions) not in (list, tuple):
raise TypeError("Permissions must be passed as a tuple or list.")
self.permissions = permissions
if buttons is not None:
if type(buttons) not in (list, tuple):
raise TypeError("Buttons must be passed as a tuple or list.")
self.buttons = buttons
class PluginMenuButton:
"""
This class represents a button within a PluginMenuItem. Note that button colors should come from
ButtonColorChoices.
"""
color = ButtonColorChoices.DEFAULT
permissions = []
def __init__(self, link, title, icon_class, color=None, permissions=None):
self.link = link
self.title = title
self.icon_class = icon_class
if permissions is not None:
if type(permissions) not in (list, tuple):
raise TypeError("Permissions must be passed as a tuple or list.")
self.permissions = permissions
if color is not None:
if color not in ButtonColorChoices.values():
raise ValueError("Button color must be a choice within ButtonColorChoices.")
self.color = color

View File

@ -0,0 +1,64 @@
import inspect
from netbox.registry import registry
from .navigation import PluginMenu, PluginMenuButton, PluginMenuItem
from .templates import PluginTemplateExtension
__all__ = (
'register_graphql_schema',
'register_menu',
'register_menu_items',
'register_template_extensions',
'register_user_preferences',
)
def register_template_extensions(class_list):
"""
Register a list of PluginTemplateExtension classes
"""
# Validation
for template_extension in class_list:
if not inspect.isclass(template_extension):
raise TypeError(f"PluginTemplateExtension class {template_extension} was passed as an instance!")
if not issubclass(template_extension, PluginTemplateExtension):
raise TypeError(f"{template_extension} is not a subclass of extras.plugins.PluginTemplateExtension!")
if template_extension.model is None:
raise TypeError(f"PluginTemplateExtension class {template_extension} does not define a valid model!")
registry['plugins']['template_extensions'][template_extension.model].append(template_extension)
def register_menu(menu):
if not isinstance(menu, PluginMenu):
raise TypeError(f"{menu} must be an instance of extras.plugins.PluginMenu")
registry['plugins']['menus'].append(menu)
def register_menu_items(section_name, class_list):
"""
Register a list of PluginMenuItem instances for a given menu section (e.g. plugin name)
"""
# Validation
for menu_link in class_list:
if not isinstance(menu_link, PluginMenuItem):
raise TypeError(f"{menu_link} must be an instance of extras.plugins.PluginMenuItem")
for button in menu_link.buttons:
if not isinstance(button, PluginMenuButton):
raise TypeError(f"{button} must be an instance of extras.plugins.PluginMenuButton")
registry['plugins']['menu_items'][section_name] = class_list
def register_graphql_schema(graphql_schema):
"""
Register a GraphQL schema class for inclusion in NetBox's GraphQL API.
"""
registry['plugins']['graphql_schemas'].append(graphql_schema)
def register_user_preferences(plugin_name, preferences):
"""
Register a list of user preferences defined by a plugin.
"""
registry['plugins']['preferences'][plugin_name] = preferences

View File

@ -0,0 +1,65 @@
from django.template.loader import get_template
__all__ = (
'PluginTemplateExtension',
)
class PluginTemplateExtension:
"""
This class is used to register plugin content to be injected into core NetBox templates. It contains methods
that are overridden by plugin authors to return template content.
The `model` attribute on the class defines the which model detail page this class renders content for. It
should be set as a string in the form '<app_label>.<model_name>'. render() provides the following context data:
* object - The object being viewed
* request - The current request
* settings - Global NetBox settings
* config - Plugin-specific configuration parameters
"""
model = None
def __init__(self, context):
self.context = context
def render(self, template_name, extra_context=None):
"""
Convenience method for rendering the specified Django template using the default context data. An additional
context dictionary may be passed as `extra_context`.
"""
if extra_context is None:
extra_context = {}
elif not isinstance(extra_context, dict):
raise TypeError("extra_context must be a dictionary")
return get_template(template_name).render({**self.context, **extra_context})
def left_page(self):
"""
Content that will be rendered on the left of the detail page view. Content should be returned as an
HTML string. Note that content does not need to be marked as safe because this is automatically handled.
"""
raise NotImplementedError
def right_page(self):
"""
Content that will be rendered on the right of the detail page view. Content should be returned as an
HTML string. Note that content does not need to be marked as safe because this is automatically handled.
"""
raise NotImplementedError
def full_width_page(self):
"""
Content that will be rendered within the full width of the detail page view. Content should be returned as an
HTML string. Note that content does not need to be marked as safe because this is automatically handled.
"""
raise NotImplementedError
def buttons(self):
"""
Buttons that will be rendered and added to the existing list of buttons on the detail page view. Content
should be returned as an HTML string. Note that content does not need to be marked as safe because this is
automatically handled.
"""
raise NotImplementedError

View File

@ -13,16 +13,13 @@ __all__ = (
'ExportTemplateTable', 'ExportTemplateTable',
'JournalEntryTable', 'JournalEntryTable',
'ObjectChangeTable', 'ObjectChangeTable',
'SavedFilterTable',
'TaggedItemTable', 'TaggedItemTable',
'TagTable', 'TagTable',
'WebhookTable', 'WebhookTable',
) )
#
# Custom fields
#
class CustomFieldTable(NetBoxTable): class CustomFieldTable(NetBoxTable):
name = tables.Column( name = tables.Column(
linkify=True linkify=True
@ -40,10 +37,6 @@ class CustomFieldTable(NetBoxTable):
default_columns = ('pk', 'name', 'content_types', 'label', 'group_name', 'type', 'required', 'description') default_columns = ('pk', 'name', 'content_types', 'label', 'group_name', 'type', 'required', 'description')
#
# Custom fields
#
class JobResultTable(NetBoxTable): class JobResultTable(NetBoxTable):
name = tables.Column( name = tables.Column(
linkify=True linkify=True
@ -61,10 +54,6 @@ class JobResultTable(NetBoxTable):
default_columns = ('pk', 'id', 'name', 'obj_type', 'status', 'created', 'completed', 'user',) default_columns = ('pk', 'id', 'name', 'obj_type', 'status', 'created', 'completed', 'user',)
#
# Custom links
#
class CustomLinkTable(NetBoxTable): class CustomLinkTable(NetBoxTable):
name = tables.Column( name = tables.Column(
linkify=True linkify=True
@ -82,10 +71,6 @@ class CustomLinkTable(NetBoxTable):
default_columns = ('pk', 'name', 'content_types', 'enabled', 'group_name', 'button_class', 'new_window') default_columns = ('pk', 'name', 'content_types', 'enabled', 'group_name', 'button_class', 'new_window')
#
# Export templates
#
class ExportTemplateTable(NetBoxTable): class ExportTemplateTable(NetBoxTable):
name = tables.Column( name = tables.Column(
linkify=True linkify=True
@ -104,9 +89,24 @@ class ExportTemplateTable(NetBoxTable):
) )
# class SavedFilterTable(NetBoxTable):
# Webhooks name = tables.Column(
# linkify=True
)
content_types = columns.ContentTypesColumn()
enabled = columns.BooleanColumn()
shared = columns.BooleanColumn()
class Meta(NetBoxTable.Meta):
model = SavedFilter
fields = (
'pk', 'id', 'name', 'content_types', 'description', 'user', 'weight', 'enabled', 'shared',
'created', 'last_updated',
)
default_columns = (
'pk', 'name', 'content_types', 'user', 'description', 'enabled', 'shared',
)
class WebhookTable(NetBoxTable): class WebhookTable(NetBoxTable):
name = tables.Column( name = tables.Column(
@ -139,10 +139,6 @@ class WebhookTable(NetBoxTable):
) )
#
# Tags
#
class TagTable(NetBoxTable): class TagTable(NetBoxTable):
name = tables.Column( name = tables.Column(
linkify=True linkify=True

View File

@ -3,7 +3,7 @@ from django.conf import settings
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from extras.plugins import PluginTemplateExtension from extras.plugins import PluginTemplateExtension
from extras.registry import registry from netbox.registry import registry
register = template_.Library() register = template_.Library()

View File

@ -3,7 +3,6 @@ from unittest import skipIf
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.test import override_settings
from django.urls import reverse from django.urls import reverse
from django.utils.timezone import make_aware from django.utils.timezone import make_aware
from django_rq.queues import get_connection from django_rq.queues import get_connection
@ -17,7 +16,6 @@ from extras.reports import Report
from extras.scripts import BooleanVar, IntegerVar, Script, StringVar from extras.scripts import BooleanVar, IntegerVar, Script, StringVar
from utilities.testing import APITestCase, APIViewTestCases from utilities.testing import APITestCase, APIViewTestCases
rq_worker_running = Worker.count(get_connection('default')) rq_worker_running = Worker.count(get_connection('default'))
@ -192,6 +190,73 @@ class CustomLinkTest(APIViewTestCases.APIViewTestCase):
custom_link.content_types.set([site_ct]) custom_link.content_types.set([site_ct])
class SavedFilterTest(APIViewTestCases.APIViewTestCase):
model = SavedFilter
brief_fields = ['display', 'id', 'name', 'url']
create_data = [
{
'content_types': ['dcim.site'],
'name': 'Saved Filter 4',
'weight': 100,
'enabled': True,
'shared': True,
'parameters': {'status': ['active']},
},
{
'content_types': ['dcim.site'],
'name': 'Saved Filter 5',
'weight': 200,
'enabled': True,
'shared': True,
'parameters': {'status': ['planned']},
},
{
'content_types': ['dcim.site'],
'name': 'Saved Filter 6',
'weight': 300,
'enabled': True,
'shared': True,
'parameters': {'status': ['retired']},
},
]
bulk_update_data = {
'weight': 1000,
'enabled': False,
'shared': False,
}
@classmethod
def setUpTestData(cls):
site_ct = ContentType.objects.get_for_model(Site)
saved_filters = (
SavedFilter(
name='Saved Filter 1',
weight=100,
enabled=True,
shared=True,
parameters={'status': ['active']}
),
SavedFilter(
name='Saved Filter 2',
weight=200,
enabled=True,
shared=True,
parameters={'status': ['planned']}
),
SavedFilter(
name='Saved Filter 3',
weight=300,
enabled=True,
shared=True,
parameters={'status': ['retired']}
),
)
SavedFilter.objects.bulk_create(saved_filters)
for i, savedfilter in enumerate(saved_filters):
savedfilter.content_types.set([site_ct])
class ExportTemplateTest(APIViewTestCases.APIViewTestCase): class ExportTemplateTest(APIViewTestCases.APIViewTestCase):
model = ExportTemplate model = ExportTemplate
brief_fields = ['display', 'id', 'name', 'url'] brief_fields = ['display', 'id', 'name', 'url']

View File

@ -222,6 +222,92 @@ class CustomLinkTestCase(TestCase, BaseFilterSetTests):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
class SavedFilterTestCase(TestCase, BaseFilterSetTests):
queryset = SavedFilter.objects.all()
filterset = SavedFilterFilterSet
@classmethod
def setUpTestData(cls):
content_types = ContentType.objects.filter(model__in=['site', 'rack', 'device'])
users = (
User(username='User 1'),
User(username='User 2'),
User(username='User 3'),
)
User.objects.bulk_create(users)
saved_filters = (
SavedFilter(
name='Saved Filter 1',
user=users[0],
weight=100,
enabled=True,
shared=True,
parameters={'status': ['active']}
),
SavedFilter(
name='Saved Filter 2',
user=users[1],
weight=200,
enabled=True,
shared=True,
parameters={'status': ['planned']}
),
SavedFilter(
name='Saved Filter 3',
user=users[2],
weight=300,
enabled=False,
shared=False,
parameters={'status': ['retired']}
),
)
SavedFilter.objects.bulk_create(saved_filters)
for i, savedfilter in enumerate(saved_filters):
savedfilter.content_types.set([content_types[i]])
def test_name(self):
params = {'name': ['Saved Filter 1', 'Saved Filter 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_content_types(self):
params = {'content_types': 'dcim.site'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
params = {'content_type_id': [ContentType.objects.get_for_model(Site).pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_user(self):
users = User.objects.filter(username__startswith='User')
params = {'user': [users[0].username, users[1].username]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'user_id': [users[0].pk, users[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_weight(self):
params = {'weight': [100, 200]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_enabled(self):
params = {'enabled': True}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'enabled': False}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_shared(self):
params = {'enabled': True}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'enabled': False}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_usable(self):
# Filtering for an anonymous user
params = {'usable': True}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'usable': False}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
class ExportTemplateTestCase(TestCase, BaseFilterSetTests): class ExportTemplateTestCase(TestCase, BaseFilterSetTests):
queryset = ExportTemplate.objects.all() queryset = ExportTemplate.objects.all()
filterset = ExportTemplateFilterSet filterset = ExportTemplateFilterSet

View File

@ -5,10 +5,10 @@ from django.core.exceptions import ImproperlyConfigured
from django.test import Client, TestCase, override_settings from django.test import Client, TestCase, override_settings
from django.urls import reverse from django.urls import reverse
from extras.plugins import PluginMenu from extras.plugins import PluginMenu, get_plugin_config
from extras.registry import registry
from extras.tests.dummy_plugin import config as dummy_config from extras.tests.dummy_plugin import config as dummy_config
from netbox.graphql.schema import Query from netbox.graphql.schema import Query
from netbox.registry import registry
@skipIf('extras.tests.dummy_plugin' not in settings.PLUGINS, "dummy_plugin not in settings.PLUGINS") @skipIf('extras.tests.dummy_plugin' not in settings.PLUGINS, "dummy_plugin not in settings.PLUGINS")
@ -173,3 +173,13 @@ class PluginTest(TestCase):
self.assertIn(DummyQuery, registry['plugins']['graphql_schemas']) self.assertIn(DummyQuery, registry['plugins']['graphql_schemas'])
self.assertTrue(issubclass(Query, DummyQuery)) self.assertTrue(issubclass(Query, DummyQuery))
@override_settings(PLUGINS_CONFIG={'extras.tests.dummy_plugin': {'foo': 123}})
def test_get_plugin_config(self):
"""
Validate that get_plugin_config() returns config parameters correctly.
"""
plugin = 'extras.tests.dummy_plugin'
self.assertEqual(get_plugin_config(plugin, 'foo'), 123)
self.assertEqual(get_plugin_config(plugin, 'bar'), None)
self.assertEqual(get_plugin_config(plugin, 'bar', default=456), 456)

View File

@ -107,6 +107,58 @@ class CustomLinkTestCase(ViewTestCases.PrimaryObjectViewTestCase):
} }
class SavedFilterTestCase(ViewTestCases.PrimaryObjectViewTestCase):
model = SavedFilter
@classmethod
def setUpTestData(cls):
site_ct = ContentType.objects.get_for_model(Site)
users = (
User(username='User 1'),
User(username='User 2'),
User(username='User 3'),
)
User.objects.bulk_create(users)
saved_filters = (
SavedFilter(name='Saved Filter 1', user=users[0], weight=100, parameters={'status': ['active']}),
SavedFilter(name='Saved Filter 2', user=users[1], weight=200, parameters={'status': ['planned']}),
SavedFilter(name='Saved Filter 3', user=users[2], weight=300, parameters={'status': ['retired']}),
)
SavedFilter.objects.bulk_create(saved_filters)
for i, savedfilter in enumerate(saved_filters):
savedfilter.content_types.set([site_ct])
cls.form_data = {
'name': 'Saved Filter X',
'content_types': [site_ct.pk],
'description': 'Foo',
'weight': 1000,
'enabled': True,
'shared': True,
'parameters': '{"foo": 123}',
}
cls.csv_data = (
'name,content_types,weight,enabled,shared,parameters',
'Saved Filter 4,dcim.device,400,True,True,{"foo": "a"}',
'Saved Filter 5,dcim.device,500,True,True,{"foo": "b"}',
'Saved Filter 6,dcim.device,600,True,True,{"foo": "c"}',
)
cls.csv_update_data = (
"id,name",
f"{saved_filters[0].pk},Saved Filter 7",
f"{saved_filters[1].pk},Saved Filter 8",
f"{saved_filters[2].pk},Saved Filter 9",
)
cls.bulk_edit_data = {
'weight': 999,
}
class ExportTemplateTestCase(ViewTestCases.PrimaryObjectViewTestCase): class ExportTemplateTestCase(ViewTestCases.PrimaryObjectViewTestCase):
model = ExportTemplate model = ExportTemplate

View File

@ -31,6 +31,14 @@ urlpatterns = [
path('export-templates/delete/', views.ExportTemplateBulkDeleteView.as_view(), name='exporttemplate_bulk_delete'), path('export-templates/delete/', views.ExportTemplateBulkDeleteView.as_view(), name='exporttemplate_bulk_delete'),
path('export-templates/<int:pk>/', include(get_model_urls('extras', 'exporttemplate'))), path('export-templates/<int:pk>/', include(get_model_urls('extras', 'exporttemplate'))),
# Saved filters
path('saved-filters/', views.SavedFilterListView.as_view(), name='savedfilter_list'),
path('saved-filters/add/', views.SavedFilterEditView.as_view(), name='savedfilter_add'),
path('saved-filters/import/', views.SavedFilterBulkImportView.as_view(), name='savedfilter_import'),
path('saved-filters/edit/', views.SavedFilterBulkEditView.as_view(), name='savedfilter_bulk_edit'),
path('saved-filters/delete/', views.SavedFilterBulkDeleteView.as_view(), name='savedfilter_bulk_delete'),
path('saved-filters/<int:pk>/', include(get_model_urls('extras', 'savedfilter'))),
# Webhooks # Webhooks
path('webhooks/', views.WebhookListView.as_view(), name='webhook_list'), path('webhooks/', views.WebhookListView.as_view(), name='webhook_list'),
path('webhooks/add/', views.WebhookEditView.as_view(), name='webhook_add'), path('webhooks/add/', views.WebhookEditView.as_view(), name='webhook_add'),

View File

@ -3,7 +3,7 @@ from django.utils.deconstruct import deconstructible
from taggit.managers import _TaggableManager from taggit.managers import _TaggableManager
from extras.constants import EXTRAS_FEATURES from extras.constants import EXTRAS_FEATURES
from extras.registry import registry from netbox.registry import registry
def is_taggable(obj): def is_taggable(obj):

View File

@ -9,7 +9,6 @@ from django_rq.queues import get_connection
from rq import Worker from rq import Worker
from netbox.views import generic from netbox.views import generic
from utilities.forms import ConfirmationForm
from utilities.htmx import is_htmx from utilities.htmx import is_htmx
from utilities.utils import copy_safe_request, count_related, get_viewname, normalize_querydict, shallow_compare_dict from utilities.utils import copy_safe_request, count_related, get_viewname, normalize_querydict, shallow_compare_dict
from utilities.views import ContentTypePermissionRequiredMixin, register_model_view from utilities.views import ContentTypePermissionRequiredMixin, register_model_view
@ -159,6 +158,74 @@ class ExportTemplateBulkDeleteView(generic.BulkDeleteView):
table = tables.ExportTemplateTable table = tables.ExportTemplateTable
#
# Saved filters
#
class SavedFilterMixin:
def get_queryset(self, request):
"""
Return only shared SavedFilters, or those owned by the current user, unless
this is a superuser.
"""
queryset = SavedFilter.objects.all()
user = request.user
if user.is_superuser:
return queryset
if user.is_anonymous:
return queryset.filter(shared=True)
return queryset.filter(
Q(shared=True) | Q(user=user)
)
class SavedFilterListView(SavedFilterMixin, generic.ObjectListView):
filterset = filtersets.SavedFilterFilterSet
filterset_form = forms.SavedFilterFilterForm
table = tables.SavedFilterTable
@register_model_view(SavedFilter)
class SavedFilterView(SavedFilterMixin, generic.ObjectView):
queryset = SavedFilter.objects.all()
@register_model_view(SavedFilter, 'edit')
class SavedFilterEditView(SavedFilterMixin, generic.ObjectEditView):
queryset = SavedFilter.objects.all()
form = forms.SavedFilterForm
def alter_object(self, obj, request, url_args, url_kwargs):
if not obj.pk:
obj.user = request.user
return obj
@register_model_view(SavedFilter, 'delete')
class SavedFilterDeleteView(SavedFilterMixin, generic.ObjectDeleteView):
queryset = SavedFilter.objects.all()
class SavedFilterBulkImportView(SavedFilterMixin, generic.BulkImportView):
queryset = SavedFilter.objects.all()
model_form = forms.SavedFilterCSVForm
table = tables.SavedFilterTable
class SavedFilterBulkEditView(SavedFilterMixin, generic.BulkEditView):
queryset = SavedFilter.objects.all()
filterset = filtersets.SavedFilterFilterSet
table = tables.SavedFilterTable
form = forms.SavedFilterBulkEditForm
class SavedFilterBulkDeleteView(SavedFilterMixin, generic.BulkDeleteView):
queryset = SavedFilter.objects.all()
filterset = filtersets.SavedFilterFilterSet
table = tables.SavedFilterTable
# #
# Webhooks # Webhooks
# #

View File

@ -5,11 +5,11 @@ from django.contrib.contenttypes.models import ContentType
from django.utils import timezone from django.utils import timezone
from django_rq import get_queue from django_rq import get_queue
from netbox.registry import registry
from utilities.api import get_serializer_for_model from utilities.api import get_serializer_for_model
from utilities.utils import serialize_object from utilities.utils import serialize_object
from .choices import * from .choices import *
from .models import Webhook from .models import Webhook
from .registry import registry
def serialize_for_webhook(instance): def serialize_for_webhook(instance):

View File

@ -31,8 +31,8 @@ class ASNSerializer(NetBoxModelSerializer):
class Meta: class Meta:
model = ASN model = ASN
fields = [ fields = [
'id', 'url', 'display', 'asn', 'rir', 'tenant', 'description', 'site_count', 'provider_count', 'tags', 'id', 'url', 'display', 'asn', 'rir', 'tenant', 'description', 'comments', 'tags', 'custom_fields',
'custom_fields', 'created', 'last_updated', 'created', 'last_updated', 'site_count', 'provider_count',
] ]
@ -61,8 +61,9 @@ class VRFSerializer(NetBoxModelSerializer):
class Meta: class Meta:
model = VRF model = VRF
fields = [ fields = [
'id', 'url', 'display', 'name', 'rd', 'tenant', 'enforce_unique', 'description', 'import_targets', 'id', 'url', 'display', 'name', 'rd', 'tenant', 'enforce_unique', 'description', 'comments',
'export_targets', 'tags', 'custom_fields', 'created', 'last_updated', 'ipaddress_count', 'prefix_count', 'import_targets', 'export_targets', 'tags', 'custom_fields', 'created', 'last_updated', 'ipaddress_count',
'prefix_count',
] ]
@ -77,7 +78,8 @@ class RouteTargetSerializer(NetBoxModelSerializer):
class Meta: class Meta:
model = RouteTarget model = RouteTarget
fields = [ fields = [
'id', 'url', 'display', 'name', 'tenant', 'description', 'tags', 'custom_fields', 'created', 'last_updated', 'id', 'url', 'display', 'name', 'tenant', 'description', 'comments', 'tags', 'custom_fields', 'created',
'last_updated',
] ]
@ -106,8 +108,8 @@ class AggregateSerializer(NetBoxModelSerializer):
class Meta: class Meta:
model = Aggregate model = Aggregate
fields = [ fields = [
'id', 'url', 'display', 'family', 'prefix', 'rir', 'tenant', 'date_added', 'description', 'tags', 'id', 'url', 'display', 'family', 'prefix', 'rir', 'tenant', 'date_added', 'description', 'comments',
'custom_fields', 'created', 'last_updated', 'tags', 'custom_fields', 'created', 'last_updated',
] ]
read_only_fields = ['family'] read_only_fields = ['family']
@ -123,8 +125,8 @@ class FHRPGroupSerializer(NetBoxModelSerializer):
class Meta: class Meta:
model = FHRPGroup model = FHRPGroup
fields = [ fields = [
'id', 'name', 'url', 'display', 'protocol', 'group_id', 'auth_type', 'auth_key', 'description', 'ip_addresses', 'id', 'name', 'url', 'display', 'protocol', 'group_id', 'auth_type', 'auth_key', 'description', 'comments',
'tags', 'custom_fields', 'created', 'last_updated', 'tags', 'custom_fields', 'created', 'last_updated', 'ip_addresses',
] ]
@ -215,7 +217,7 @@ class VLANSerializer(NetBoxModelSerializer):
model = VLAN model = VLAN
fields = [ fields = [
'id', 'url', 'display', 'site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description', 'id', 'url', 'display', 'site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description',
'l2vpn_termination', 'tags', 'custom_fields', 'created', 'last_updated', 'prefix_count', 'comments', 'l2vpn_termination', 'tags', 'custom_fields', 'created', 'last_updated', 'prefix_count',
] ]
@ -273,7 +275,8 @@ class PrefixSerializer(NetBoxModelSerializer):
model = Prefix model = Prefix
fields = [ fields = [
'id', 'url', 'display', 'family', 'prefix', 'site', 'vrf', 'tenant', 'vlan', 'status', 'role', 'is_pool', 'id', 'url', 'display', 'family', 'prefix', 'site', 'vrf', 'tenant', 'vlan', 'status', 'role', 'is_pool',
'mark_utilized', 'description', 'tags', 'custom_fields', 'created', 'last_updated', 'children', '_depth', 'mark_utilized', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'children',
'_depth',
] ]
read_only_fields = ['family'] read_only_fields = ['family']
@ -342,7 +345,7 @@ class IPRangeSerializer(NetBoxModelSerializer):
model = IPRange model = IPRange
fields = [ fields = [
'id', 'url', 'display', 'family', 'start_address', 'end_address', 'size', 'vrf', 'tenant', 'status', 'role', 'id', 'url', 'display', 'family', 'start_address', 'end_address', 'size', 'vrf', 'tenant', 'status', 'role',
'description', 'tags', 'custom_fields', 'created', 'last_updated', 'children', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'children',
] ]
read_only_fields = ['family'] read_only_fields = ['family']
@ -371,8 +374,8 @@ class IPAddressSerializer(NetBoxModelSerializer):
model = IPAddress model = IPAddress
fields = [ fields = [
'id', 'url', 'display', 'family', 'address', 'vrf', 'tenant', 'status', 'role', 'assigned_object_type', 'id', 'url', 'display', 'family', 'address', 'vrf', 'tenant', 'status', 'role', 'assigned_object_type',
'assigned_object_id', 'assigned_object', 'nat_inside', 'nat_outside', 'dns_name', 'description', 'tags', 'assigned_object_id', 'assigned_object', 'nat_inside', 'nat_outside', 'dns_name', 'description', 'comments',
'custom_fields', 'created', 'last_updated', 'tags', 'custom_fields', 'created', 'last_updated',
] ]
@swagger_serializer_method(serializer_or_field=serializers.JSONField) @swagger_serializer_method(serializer_or_field=serializers.JSONField)
@ -415,8 +418,8 @@ class ServiceTemplateSerializer(NetBoxModelSerializer):
class Meta: class Meta:
model = ServiceTemplate model = ServiceTemplate
fields = [ fields = [
'id', 'url', 'display', 'name', 'ports', 'protocol', 'description', 'tags', 'custom_fields', 'created', 'id', 'url', 'display', 'name', 'ports', 'protocol', 'description', 'comments', 'tags', 'custom_fields',
'last_updated', 'created', 'last_updated',
] ]
@ -436,7 +439,7 @@ class ServiceSerializer(NetBoxModelSerializer):
model = Service model = Service
fields = [ fields = [
'id', 'url', 'display', 'device', 'virtual_machine', 'name', 'ports', 'protocol', 'ipaddresses', 'id', 'url', 'display', 'device', 'virtual_machine', 'name', 'ports', 'protocol', 'ipaddresses',
'description', 'tags', 'custom_fields', 'created', 'last_updated', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
] ]
# #
@ -465,7 +468,7 @@ class L2VPNSerializer(NetBoxModelSerializer):
model = L2VPN model = L2VPN
fields = [ fields = [
'id', 'url', 'display', 'identifier', 'name', 'slug', 'type', 'import_targets', 'export_targets', 'id', 'url', 'display', 'identifier', 'name', 'slug', 'type', 'import_targets', 'export_targets',
'description', 'tenant', 'tags', 'custom_fields', 'created', 'last_updated' 'description', 'comments', 'tenant', 'tags', 'custom_fields', 'created', 'last_updated'
] ]

View File

@ -8,8 +8,8 @@ from ipam.models import ASN
from netbox.forms import NetBoxModelBulkEditForm from netbox.forms import NetBoxModelBulkEditForm
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.forms import ( from utilities.forms import (
add_blank_choice, BulkEditNullBooleanSelect, DynamicModelChoiceField, NumericArrayField, StaticSelect, add_blank_choice, BulkEditNullBooleanSelect, CommentField, DynamicModelChoiceField, NumericArrayField,
DynamicModelMultipleChoiceField, SmallTextarea, StaticSelect, DynamicModelMultipleChoiceField,
) )
__all__ = ( __all__ = (
@ -43,15 +43,19 @@ class VRFBulkEditForm(NetBoxModelBulkEditForm):
label='Enforce unique space' label='Enforce unique space'
) )
description = forms.CharField( description = forms.CharField(
max_length=100, max_length=200,
required=False required=False
) )
comments = CommentField(
widget=SmallTextarea,
label='Comments'
)
model = VRF model = VRF
fieldsets = ( fieldsets = (
(None, ('tenant', 'enforce_unique', 'description')), (None, ('tenant', 'enforce_unique', 'description')),
) )
nullable_fields = ('tenant', 'description') nullable_fields = ('tenant', 'description', 'comments')
class RouteTargetBulkEditForm(NetBoxModelBulkEditForm): class RouteTargetBulkEditForm(NetBoxModelBulkEditForm):
@ -63,12 +67,16 @@ class RouteTargetBulkEditForm(NetBoxModelBulkEditForm):
max_length=200, max_length=200,
required=False required=False
) )
comments = CommentField(
widget=SmallTextarea,
label='Comments'
)
model = RouteTarget model = RouteTarget
fieldsets = ( fieldsets = (
(None, ('tenant', 'description')), (None, ('tenant', 'description')),
) )
nullable_fields = ('tenant', 'description') nullable_fields = ('tenant', 'description', 'comments')
class RIRBulkEditForm(NetBoxModelBulkEditForm): class RIRBulkEditForm(NetBoxModelBulkEditForm):
@ -103,15 +111,19 @@ class ASNBulkEditForm(NetBoxModelBulkEditForm):
required=False required=False
) )
description = forms.CharField( description = forms.CharField(
max_length=100, max_length=200,
required=False required=False
) )
comments = CommentField(
widget=SmallTextarea,
label='Comments'
)
model = ASN model = ASN
fieldsets = ( fieldsets = (
(None, ('sites', 'rir', 'tenant', 'description')), (None, ('sites', 'rir', 'tenant', 'description')),
) )
nullable_fields = ('date_added', 'description') nullable_fields = ('date_added', 'description', 'comments')
class AggregateBulkEditForm(NetBoxModelBulkEditForm): class AggregateBulkEditForm(NetBoxModelBulkEditForm):
@ -128,15 +140,19 @@ class AggregateBulkEditForm(NetBoxModelBulkEditForm):
required=False required=False
) )
description = forms.CharField( description = forms.CharField(
max_length=100, max_length=200,
required=False required=False
) )
comments = CommentField(
widget=SmallTextarea,
label='Comments'
)
model = Aggregate model = Aggregate
fieldsets = ( fieldsets = (
(None, ('rir', 'tenant', 'date_added', 'description')), (None, ('rir', 'tenant', 'date_added', 'description')),
) )
nullable_fields = ('date_added', 'description') nullable_fields = ('date_added', 'description', 'comments')
class RoleBulkEditForm(NetBoxModelBulkEditForm): class RoleBulkEditForm(NetBoxModelBulkEditForm):
@ -206,9 +222,13 @@ class PrefixBulkEditForm(NetBoxModelBulkEditForm):
label='Treat as 100% utilized' label='Treat as 100% utilized'
) )
description = forms.CharField( description = forms.CharField(
max_length=100, max_length=200,
required=False required=False
) )
comments = CommentField(
widget=SmallTextarea,
label='Comments'
)
model = Prefix model = Prefix
fieldsets = ( fieldsets = (
@ -217,7 +237,7 @@ class PrefixBulkEditForm(NetBoxModelBulkEditForm):
('Addressing', ('vrf', 'prefix_length', 'is_pool', 'mark_utilized')), ('Addressing', ('vrf', 'prefix_length', 'is_pool', 'mark_utilized')),
) )
nullable_fields = ( nullable_fields = (
'site', 'vrf', 'tenant', 'role', 'description', 'site', 'vrf', 'tenant', 'role', 'description', 'comments',
) )
@ -241,16 +261,20 @@ class IPRangeBulkEditForm(NetBoxModelBulkEditForm):
required=False required=False
) )
description = forms.CharField( description = forms.CharField(
max_length=100, max_length=200,
required=False required=False
) )
comments = CommentField(
widget=SmallTextarea,
label='Comments'
)
model = IPRange model = IPRange
fieldsets = ( fieldsets = (
(None, ('status', 'role', 'vrf', 'tenant', 'description')), (None, ('status', 'role', 'vrf', 'tenant', 'description')),
) )
nullable_fields = ( nullable_fields = (
'vrf', 'tenant', 'role', 'description', 'vrf', 'tenant', 'role', 'description', 'comments',
) )
@ -285,9 +309,13 @@ class IPAddressBulkEditForm(NetBoxModelBulkEditForm):
label='DNS name' label='DNS name'
) )
description = forms.CharField( description = forms.CharField(
max_length=100, max_length=200,
required=False required=False
) )
comments = CommentField(
widget=SmallTextarea,
label='Comments'
)
model = IPAddress model = IPAddress
fieldsets = ( fieldsets = (
@ -295,7 +323,7 @@ class IPAddressBulkEditForm(NetBoxModelBulkEditForm):
('Addressing', ('vrf', 'mask_length', 'dns_name')), ('Addressing', ('vrf', 'mask_length', 'dns_name')),
) )
nullable_fields = ( nullable_fields = (
'vrf', 'role', 'tenant', 'dns_name', 'description', 'vrf', 'role', 'tenant', 'dns_name', 'description', 'comments',
) )
@ -329,13 +357,17 @@ class FHRPGroupBulkEditForm(NetBoxModelBulkEditForm):
max_length=200, max_length=200,
required=False required=False
) )
comments = CommentField(
widget=SmallTextarea,
label='Comments'
)
model = FHRPGroup model = FHRPGroup
fieldsets = ( fieldsets = (
(None, ('protocol', 'group_id', 'name', 'description')), (None, ('protocol', 'group_id', 'name', 'description')),
('Authentication', ('auth_type', 'auth_key')), ('Authentication', ('auth_type', 'auth_key')),
) )
nullable_fields = ('auth_type', 'auth_key', 'name', 'description') nullable_fields = ('auth_type', 'auth_key', 'name', 'description', 'comments')
class VLANGroupBulkEditForm(NetBoxModelBulkEditForm): class VLANGroupBulkEditForm(NetBoxModelBulkEditForm):
@ -405,9 +437,13 @@ class VLANBulkEditForm(NetBoxModelBulkEditForm):
required=False required=False
) )
description = forms.CharField( description = forms.CharField(
max_length=100, max_length=200,
required=False required=False
) )
comments = CommentField(
widget=SmallTextarea,
label='Comments'
)
model = VLAN model = VLAN
fieldsets = ( fieldsets = (
@ -415,7 +451,7 @@ class VLANBulkEditForm(NetBoxModelBulkEditForm):
('Site & Group', ('region', 'site_group', 'site', 'group')), ('Site & Group', ('region', 'site_group', 'site', 'group')),
) )
nullable_fields = ( nullable_fields = (
'site', 'group', 'tenant', 'role', 'description', 'site', 'group', 'tenant', 'role', 'description', 'comments',
) )
@ -433,15 +469,19 @@ class ServiceTemplateBulkEditForm(NetBoxModelBulkEditForm):
required=False required=False
) )
description = forms.CharField( description = forms.CharField(
max_length=100, max_length=200,
required=False required=False
) )
comments = CommentField(
widget=SmallTextarea,
label='Comments'
)
model = ServiceTemplate model = ServiceTemplate
fieldsets = ( fieldsets = (
(None, ('protocol', 'ports', 'description')), (None, ('protocol', 'ports', 'description')),
) )
nullable_fields = ('description',) nullable_fields = ('description', 'comments')
class ServiceBulkEditForm(ServiceTemplateBulkEditForm): class ServiceBulkEditForm(ServiceTemplateBulkEditForm):
@ -459,15 +499,19 @@ class L2VPNBulkEditForm(NetBoxModelBulkEditForm):
required=False required=False
) )
description = forms.CharField( description = forms.CharField(
max_length=100, max_length=200,
required=False required=False
) )
comments = CommentField(
widget=SmallTextarea,
label='Comments'
)
model = L2VPN model = L2VPN
fieldsets = ( fieldsets = (
(None, ('type', 'description', 'tenant')), (None, ('type', 'tenant', 'description')),
) )
nullable_fields = ('tenant', 'description',) nullable_fields = ('tenant', 'description', 'comments')
class L2VPNTerminationBulkEditForm(NetBoxModelBulkEditForm): class L2VPNTerminationBulkEditForm(NetBoxModelBulkEditForm):

View File

@ -41,7 +41,7 @@ class VRFCSVForm(NetBoxModelCSVForm):
class Meta: class Meta:
model = VRF model = VRF
fields = ('name', 'rd', 'tenant', 'enforce_unique', 'description') fields = ('name', 'rd', 'tenant', 'enforce_unique', 'description', 'comments')
class RouteTargetCSVForm(NetBoxModelCSVForm): class RouteTargetCSVForm(NetBoxModelCSVForm):
@ -54,7 +54,7 @@ class RouteTargetCSVForm(NetBoxModelCSVForm):
class Meta: class Meta:
model = RouteTarget model = RouteTarget
fields = ('name', 'description', 'tenant') fields = ('name', 'tenant', 'description', 'comments')
class RIRCSVForm(NetBoxModelCSVForm): class RIRCSVForm(NetBoxModelCSVForm):
@ -83,7 +83,7 @@ class AggregateCSVForm(NetBoxModelCSVForm):
class Meta: class Meta:
model = Aggregate model = Aggregate
fields = ('prefix', 'rir', 'tenant', 'date_added', 'description') fields = ('prefix', 'rir', 'tenant', 'date_added', 'description', 'comments')
class ASNCSVForm(NetBoxModelCSVForm): class ASNCSVForm(NetBoxModelCSVForm):
@ -101,7 +101,7 @@ class ASNCSVForm(NetBoxModelCSVForm):
class Meta: class Meta:
model = ASN model = ASN
fields = ('asn', 'rir', 'tenant', 'description') fields = ('asn', 'rir', 'tenant', 'description', 'comments')
help_texts = {} help_texts = {}
@ -159,7 +159,7 @@ class PrefixCSVForm(NetBoxModelCSVForm):
model = Prefix model = Prefix
fields = ( fields = (
'prefix', 'vrf', 'tenant', 'site', 'vlan_group', 'vlan', 'status', 'role', 'is_pool', 'mark_utilized', 'prefix', 'vrf', 'tenant', 'site', 'vlan_group', 'vlan', 'status', 'role', 'is_pool', 'mark_utilized',
'description', 'description', 'comments',
) )
def __init__(self, data=None, *args, **kwargs): def __init__(self, data=None, *args, **kwargs):
@ -204,7 +204,7 @@ class IPRangeCSVForm(NetBoxModelCSVForm):
class Meta: class Meta:
model = IPRange model = IPRange
fields = ( fields = (
'start_address', 'end_address', 'vrf', 'tenant', 'status', 'role', 'description', 'start_address', 'end_address', 'vrf', 'tenant', 'status', 'role', 'description', 'comments',
) )
@ -257,7 +257,7 @@ class IPAddressCSVForm(NetBoxModelCSVForm):
model = IPAddress model = IPAddress
fields = [ fields = [
'address', 'vrf', 'tenant', 'status', 'role', 'device', 'virtual_machine', 'interface', 'is_primary', 'address', 'vrf', 'tenant', 'status', 'role', 'device', 'virtual_machine', 'interface', 'is_primary',
'dns_name', 'description', 'dns_name', 'description', 'comments',
] ]
def __init__(self, data=None, *args, **kwargs): def __init__(self, data=None, *args, **kwargs):
@ -326,7 +326,7 @@ class FHRPGroupCSVForm(NetBoxModelCSVForm):
class Meta: class Meta:
model = FHRPGroup model = FHRPGroup
fields = ('protocol', 'group_id', 'auth_type', 'auth_key', 'name', 'description') fields = ('protocol', 'group_id', 'auth_type', 'auth_key', 'name', 'description', 'comments')
class VLANGroupCSVForm(NetBoxModelCSVForm): class VLANGroupCSVForm(NetBoxModelCSVForm):
@ -389,7 +389,7 @@ class VLANCSVForm(NetBoxModelCSVForm):
class Meta: class Meta:
model = VLAN model = VLAN
fields = ('site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description') fields = ('site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description', 'comments')
help_texts = { help_texts = {
'vid': 'Numeric VLAN ID (1-4094)', 'vid': 'Numeric VLAN ID (1-4094)',
'name': 'VLAN name', 'name': 'VLAN name',
@ -404,7 +404,7 @@ class ServiceTemplateCSVForm(NetBoxModelCSVForm):
class Meta: class Meta:
model = ServiceTemplate model = ServiceTemplate
fields = ('name', 'protocol', 'ports', 'description') fields = ('name', 'protocol', 'ports', 'description', 'comments')
class ServiceCSVForm(NetBoxModelCSVForm): class ServiceCSVForm(NetBoxModelCSVForm):
@ -427,7 +427,7 @@ class ServiceCSVForm(NetBoxModelCSVForm):
class Meta: class Meta:
model = Service model = Service
fields = ('device', 'virtual_machine', 'name', 'protocol', 'ports', 'description') fields = ('device', 'virtual_machine', 'name', 'protocol', 'ports', 'description', 'comments')
class L2VPNCSVForm(NetBoxModelCSVForm): class L2VPNCSVForm(NetBoxModelCSVForm):
@ -443,7 +443,7 @@ class L2VPNCSVForm(NetBoxModelCSVForm):
class Meta: class Meta:
model = L2VPN model = L2VPN
fields = ('identifier', 'name', 'slug', 'type', 'description') fields = ('identifier', 'name', 'slug', 'type', 'description', 'comments')
class L2VPNTerminationCSVForm(NetBoxModelCSVForm): class L2VPNTerminationCSVForm(NetBoxModelCSVForm):

View File

@ -1,6 +1,5 @@
from django import forms from django import forms
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.db.models import Q
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from dcim.models import Location, Rack, Region, Site, SiteGroup, Device from dcim.models import Location, Rack, Region, Site, SiteGroup, Device
@ -11,7 +10,7 @@ from netbox.forms import NetBoxModelFilterSetForm
from tenancy.forms import TenancyFilterForm from tenancy.forms import TenancyFilterForm
from utilities.forms import ( from utilities.forms import (
add_blank_choice, ContentTypeMultipleChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, add_blank_choice, ContentTypeMultipleChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField,
MultipleChoiceField, StaticSelect, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES, APISelectMultiple, MultipleChoiceField, StaticSelect, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES,
) )
from virtualization.models import VirtualMachine from virtualization.models import VirtualMachine
@ -46,7 +45,7 @@ IPADDRESS_MASK_LENGTH_CHOICES = add_blank_choice([
class VRFFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): class VRFFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
model = VRF model = VRF
fieldsets = ( fieldsets = (
(None, ('q', 'tag')), (None, ('q', 'filter', 'tag')),
('Route Targets', ('import_target_id', 'export_target_id')), ('Route Targets', ('import_target_id', 'export_target_id')),
('Tenant', ('tenant_group_id', 'tenant_id')), ('Tenant', ('tenant_group_id', 'tenant_id')),
) )
@ -66,7 +65,7 @@ class VRFFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
class RouteTargetFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): class RouteTargetFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
model = RouteTarget model = RouteTarget
fieldsets = ( fieldsets = (
(None, ('q', 'tag')), (None, ('q', 'filter', 'tag')),
('VRF', ('importing_vrf_id', 'exporting_vrf_id')), ('VRF', ('importing_vrf_id', 'exporting_vrf_id')),
('Tenant', ('tenant_group_id', 'tenant_id')), ('Tenant', ('tenant_group_id', 'tenant_id')),
) )
@ -98,7 +97,7 @@ class RIRFilterForm(NetBoxModelFilterSetForm):
class AggregateFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): class AggregateFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
model = Aggregate model = Aggregate
fieldsets = ( fieldsets = (
(None, ('q', 'tag')), (None, ('q', 'filter', 'tag')),
('Attributes', ('family', 'rir_id')), ('Attributes', ('family', 'rir_id')),
('Tenant', ('tenant_group_id', 'tenant_id')), ('Tenant', ('tenant_group_id', 'tenant_id')),
) )
@ -119,7 +118,7 @@ class AggregateFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
class ASNFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): class ASNFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
model = ASN model = ASN
fieldsets = ( fieldsets = (
(None, ('q', 'tag')), (None, ('q', 'filter', 'tag')),
('Assignment', ('rir_id', 'site_id')), ('Assignment', ('rir_id', 'site_id')),
('Tenant', ('tenant_group_id', 'tenant_id')), ('Tenant', ('tenant_group_id', 'tenant_id')),
) )
@ -144,7 +143,7 @@ class RoleFilterForm(NetBoxModelFilterSetForm):
class PrefixFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): class PrefixFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
model = Prefix model = Prefix
fieldsets = ( fieldsets = (
(None, ('q', 'tag')), (None, ('q', 'filter', 'tag')),
('Addressing', ('within_include', 'family', 'status', 'role_id', 'mask_length', 'is_pool', 'mark_utilized')), ('Addressing', ('within_include', 'family', 'status', 'role_id', 'mask_length', 'is_pool', 'mark_utilized')),
('VRF', ('vrf_id', 'present_in_vrf_id')), ('VRF', ('vrf_id', 'present_in_vrf_id')),
('Location', ('region_id', 'site_group_id', 'site_id')), ('Location', ('region_id', 'site_group_id', 'site_id')),
@ -233,7 +232,7 @@ class PrefixFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
class IPRangeFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): class IPRangeFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
model = IPRange model = IPRange
fieldsets = ( fieldsets = (
(None, ('q', 'tag')), (None, ('q', 'filter', 'tag')),
('Attriubtes', ('family', 'vrf_id', 'status', 'role_id')), ('Attriubtes', ('family', 'vrf_id', 'status', 'role_id')),
('Tenant', ('tenant_group_id', 'tenant_id')), ('Tenant', ('tenant_group_id', 'tenant_id')),
) )
@ -265,7 +264,7 @@ class IPRangeFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
class IPAddressFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): class IPAddressFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
model = IPAddress model = IPAddress
fieldsets = ( fieldsets = (
(None, ('q', 'tag')), (None, ('q', 'filter', 'tag')),
('Attributes', ('parent', 'family', 'status', 'role', 'mask_length', 'assigned_to_interface')), ('Attributes', ('parent', 'family', 'status', 'role', 'mask_length', 'assigned_to_interface')),
('VRF', ('vrf_id', 'present_in_vrf_id')), ('VRF', ('vrf_id', 'present_in_vrf_id')),
('Tenant', ('tenant_group_id', 'tenant_id')), ('Tenant', ('tenant_group_id', 'tenant_id')),
@ -334,7 +333,7 @@ class IPAddressFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
class FHRPGroupFilterForm(NetBoxModelFilterSetForm): class FHRPGroupFilterForm(NetBoxModelFilterSetForm):
model = FHRPGroup model = FHRPGroup
fieldsets = ( fieldsets = (
(None, ('q', 'tag')), (None, ('q', 'filter', 'tag')),
('Attributes', ('name', 'protocol', 'group_id')), ('Attributes', ('name', 'protocol', 'group_id')),
('Authentication', ('auth_type', 'auth_key')), ('Authentication', ('auth_type', 'auth_key')),
) )
@ -364,7 +363,7 @@ class FHRPGroupFilterForm(NetBoxModelFilterSetForm):
class VLANGroupFilterForm(NetBoxModelFilterSetForm): class VLANGroupFilterForm(NetBoxModelFilterSetForm):
fieldsets = ( fieldsets = (
(None, ('q', 'tag')), (None, ('q', 'filter', 'tag')),
('Location', ('region', 'sitegroup', 'site', 'location', 'rack')), ('Location', ('region', 'sitegroup', 'site', 'location', 'rack')),
('VLAN ID', ('min_vid', 'max_vid')), ('VLAN ID', ('min_vid', 'max_vid')),
) )
@ -412,7 +411,7 @@ class VLANGroupFilterForm(NetBoxModelFilterSetForm):
class VLANFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): class VLANFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
model = VLAN model = VLAN
fieldsets = ( fieldsets = (
(None, ('q', 'tag')), (None, ('q', 'filter', 'tag')),
('Location', ('region_id', 'site_group_id', 'site_id')), ('Location', ('region_id', 'site_group_id', 'site_id')),
('Attributes', ('group_id', 'status', 'role_id', 'vid')), ('Attributes', ('group_id', 'status', 'role_id', 'vid')),
('Tenant', ('tenant_group_id', 'tenant_id')), ('Tenant', ('tenant_group_id', 'tenant_id')),
@ -465,7 +464,7 @@ class VLANFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
class ServiceTemplateFilterForm(NetBoxModelFilterSetForm): class ServiceTemplateFilterForm(NetBoxModelFilterSetForm):
model = ServiceTemplate model = ServiceTemplate
fieldsets = ( fieldsets = (
(None, ('q', 'tag')), (None, ('q', 'filter', 'tag')),
('Attributes', ('protocol', 'port')), ('Attributes', ('protocol', 'port')),
) )
protocol = forms.ChoiceField( protocol = forms.ChoiceField(
@ -486,7 +485,7 @@ class ServiceFilterForm(ServiceTemplateFilterForm):
class L2VPNFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): class L2VPNFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
model = L2VPN model = L2VPN
fieldsets = ( fieldsets = (
(None, ('q', 'tag')), (None, ('q', 'filter', 'tag')),
('Attributes', ('type', 'import_target_id', 'export_target_id')), ('Attributes', ('type', 'import_target_id', 'export_target_id')),
('Tenant', ('tenant_group_id', 'tenant_id')), ('Tenant', ('tenant_group_id', 'tenant_id')),
) )
@ -511,8 +510,10 @@ class L2VPNFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
class L2VPNTerminationFilterForm(NetBoxModelFilterSetForm): class L2VPNTerminationFilterForm(NetBoxModelFilterSetForm):
model = L2VPNTermination model = L2VPNTermination
fieldsets = ( fieldsets = (
(None, ('l2vpn_id', )), (None, ('filter', 'l2vpn_id',)),
('Assigned Object', ('assigned_object_type_id', 'region_id', 'site_id', 'device_id', 'virtual_machine_id', 'vlan_id')), ('Assigned Object', (
'assigned_object_type_id', 'region_id', 'site_id', 'device_id', 'virtual_machine_id', 'vlan_id',
)),
) )
l2vpn_id = DynamicModelChoiceField( l2vpn_id = DynamicModelChoiceField(
queryset=L2VPN.objects.all(), queryset=L2VPN.objects.all(),

View File

@ -11,7 +11,7 @@ from netbox.forms import NetBoxModelForm
from tenancy.forms import TenancyForm from tenancy.forms import TenancyForm
from utilities.exceptions import PermissionsViolation from utilities.exceptions import PermissionsViolation
from utilities.forms import ( from utilities.forms import (
add_blank_choice, BootstrapMixin, ContentTypeChoiceField, DatePicker, DynamicModelChoiceField, add_blank_choice, BootstrapMixin, CommentField, ContentTypeChoiceField, DatePicker, DynamicModelChoiceField,
DynamicModelMultipleChoiceField, NumericArrayField, SlugField, StaticSelect, StaticSelectMultiple, DynamicModelMultipleChoiceField, NumericArrayField, SlugField, StaticSelect, StaticSelectMultiple,
) )
from virtualization.models import Cluster, ClusterGroup, VirtualMachine, VMInterface from virtualization.models import Cluster, ClusterGroup, VirtualMachine, VMInterface
@ -49,6 +49,7 @@ class VRFForm(TenancyForm, NetBoxModelForm):
queryset=RouteTarget.objects.all(), queryset=RouteTarget.objects.all(),
required=False required=False
) )
comments = CommentField()
fieldsets = ( fieldsets = (
('VRF', ('name', 'rd', 'enforce_unique', 'description', 'tags')), ('VRF', ('name', 'rd', 'enforce_unique', 'description', 'tags')),
@ -59,8 +60,8 @@ class VRFForm(TenancyForm, NetBoxModelForm):
class Meta: class Meta:
model = VRF model = VRF
fields = [ fields = [
'name', 'rd', 'enforce_unique', 'description', 'import_targets', 'export_targets', 'tenant_group', 'tenant', 'name', 'rd', 'enforce_unique', 'import_targets', 'export_targets', 'tenant_group', 'tenant', 'description',
'tags', 'comments', 'tags',
] ]
labels = { labels = {
'rd': "RD", 'rd': "RD",
@ -75,11 +76,12 @@ class RouteTargetForm(TenancyForm, NetBoxModelForm):
('Route Target', ('name', 'description', 'tags')), ('Route Target', ('name', 'description', 'tags')),
('Tenancy', ('tenant_group', 'tenant')), ('Tenancy', ('tenant_group', 'tenant')),
) )
comments = CommentField()
class Meta: class Meta:
model = RouteTarget model = RouteTarget
fields = [ fields = [
'name', 'description', 'tenant_group', 'tenant', 'tags', 'name', 'tenant_group', 'tenant', 'description', 'comments', 'tags',
] ]
@ -104,6 +106,7 @@ class AggregateForm(TenancyForm, NetBoxModelForm):
queryset=RIR.objects.all(), queryset=RIR.objects.all(),
label='RIR' label='RIR'
) )
comments = CommentField()
fieldsets = ( fieldsets = (
('Aggregate', ('prefix', 'rir', 'date_added', 'description', 'tags')), ('Aggregate', ('prefix', 'rir', 'date_added', 'description', 'tags')),
@ -113,7 +116,7 @@ class AggregateForm(TenancyForm, NetBoxModelForm):
class Meta: class Meta:
model = Aggregate model = Aggregate
fields = [ fields = [
'prefix', 'rir', 'date_added', 'description', 'tenant_group', 'tenant', 'tags', 'prefix', 'rir', 'date_added', 'tenant_group', 'tenant', 'description', 'comments', 'tags',
] ]
help_texts = { help_texts = {
'prefix': "IPv4 or IPv6 network", 'prefix': "IPv4 or IPv6 network",
@ -134,6 +137,7 @@ class ASNForm(TenancyForm, NetBoxModelForm):
label='Sites', label='Sites',
required=False required=False
) )
comments = CommentField()
fieldsets = ( fieldsets = (
('ASN', ('asn', 'rir', 'sites', 'description', 'tags')), ('ASN', ('asn', 'rir', 'sites', 'description', 'tags')),
@ -143,7 +147,7 @@ class ASNForm(TenancyForm, NetBoxModelForm):
class Meta: class Meta:
model = ASN model = ASN
fields = [ fields = [
'asn', 'rir', 'sites', 'tenant_group', 'tenant', 'description', 'tags' 'asn', 'rir', 'sites', 'tenant_group', 'tenant', 'description', 'comments', 'tags'
] ]
help_texts = { help_texts = {
'asn': "AS number", 'asn': "AS number",
@ -235,6 +239,7 @@ class PrefixForm(TenancyForm, NetBoxModelForm):
queryset=Role.objects.all(), queryset=Role.objects.all(),
required=False required=False
) )
comments = CommentField()
fieldsets = ( fieldsets = (
('Prefix', ('prefix', 'status', 'vrf', 'role', 'is_pool', 'mark_utilized', 'description', 'tags')), ('Prefix', ('prefix', 'status', 'vrf', 'role', 'is_pool', 'mark_utilized', 'description', 'tags')),
@ -245,8 +250,8 @@ class PrefixForm(TenancyForm, NetBoxModelForm):
class Meta: class Meta:
model = Prefix model = Prefix
fields = [ fields = [
'prefix', 'vrf', 'site', 'vlan', 'status', 'role', 'is_pool', 'mark_utilized', 'description', 'prefix', 'vrf', 'site', 'vlan', 'status', 'role', 'is_pool', 'mark_utilized', 'tenant_group', 'tenant',
'tenant_group', 'tenant', 'tags', 'description', 'comments', 'tags',
] ]
widgets = { widgets = {
'status': StaticSelect(), 'status': StaticSelect(),
@ -263,6 +268,7 @@ class IPRangeForm(TenancyForm, NetBoxModelForm):
queryset=Role.objects.all(), queryset=Role.objects.all(),
required=False required=False
) )
comments = CommentField()
fieldsets = ( fieldsets = (
('IP Range', ('vrf', 'start_address', 'end_address', 'role', 'status', 'description', 'tags')), ('IP Range', ('vrf', 'start_address', 'end_address', 'role', 'status', 'description', 'tags')),
@ -272,7 +278,8 @@ class IPRangeForm(TenancyForm, NetBoxModelForm):
class Meta: class Meta:
model = IPRange model = IPRange
fields = [ fields = [
'vrf', 'start_address', 'end_address', 'status', 'role', 'description', 'tenant_group', 'tenant', 'tags', 'vrf', 'start_address', 'end_address', 'status', 'role', 'tenant_group', 'tenant', 'description',
'comments', 'tags',
] ]
widgets = { widgets = {
'status': StaticSelect(), 'status': StaticSelect(),
@ -394,13 +401,14 @@ class IPAddressForm(TenancyForm, NetBoxModelForm):
required=False, required=False,
label='Make this the primary IP for the device/VM' label='Make this the primary IP for the device/VM'
) )
comments = CommentField()
class Meta: class Meta:
model = IPAddress model = IPAddress
fields = [ fields = [
'address', 'vrf', 'status', 'role', 'dns_name', 'description', 'primary_for_parent', 'nat_site', 'nat_rack', 'address', 'vrf', 'status', 'role', 'dns_name', 'primary_for_parent', 'nat_site', 'nat_rack', 'nat_device',
'nat_device', 'nat_cluster', 'nat_virtual_machine', 'nat_vrf', 'nat_inside', 'tenant_group', 'tenant', 'nat_cluster', 'nat_virtual_machine', 'nat_vrf', 'nat_inside', 'tenant_group', 'tenant', 'description',
'tags', 'comments', 'tags',
] ]
widgets = { widgets = {
'status': StaticSelect(), 'status': StaticSelect(),
@ -535,6 +543,7 @@ class FHRPGroupForm(NetBoxModelForm):
required=False, required=False,
label='Status' label='Status'
) )
comments = CommentField()
fieldsets = ( fieldsets = (
('FHRP Group', ('protocol', 'group_id', 'name', 'description', 'tags')), ('FHRP Group', ('protocol', 'group_id', 'name', 'description', 'tags')),
@ -545,7 +554,8 @@ class FHRPGroupForm(NetBoxModelForm):
class Meta: class Meta:
model = FHRPGroup model = FHRPGroup
fields = ( fields = (
'protocol', 'group_id', 'auth_type', 'auth_key', 'name', 'description', 'ip_vrf', 'ip_address', 'ip_status', 'tags', 'protocol', 'group_id', 'auth_type', 'auth_key', 'name', 'ip_vrf', 'ip_address', 'ip_status', 'description',
'comments', 'tags',
) )
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
@ -767,11 +777,13 @@ class VLANForm(TenancyForm, NetBoxModelForm):
queryset=Role.objects.all(), queryset=Role.objects.all(),
required=False required=False
) )
comments = CommentField()
class Meta: class Meta:
model = VLAN model = VLAN
fields = [ fields = [
'site', 'group', 'vid', 'name', 'status', 'role', 'description', 'tenant_group', 'tenant', 'tags', 'site', 'group', 'vid', 'name', 'status', 'role', 'tenant_group', 'tenant', 'description', 'comments',
'tags',
] ]
help_texts = { help_texts = {
'site': "Leave blank if this VLAN spans multiple sites", 'site': "Leave blank if this VLAN spans multiple sites",
@ -794,6 +806,7 @@ class ServiceTemplateForm(NetBoxModelForm):
), ),
help_text="Comma-separated list of one or more port numbers. A range may be specified using a hyphen." help_text="Comma-separated list of one or more port numbers. A range may be specified using a hyphen."
) )
comments = CommentField()
fieldsets = ( fieldsets = (
('Service Template', ( ('Service Template', (
@ -803,7 +816,7 @@ class ServiceTemplateForm(NetBoxModelForm):
class Meta: class Meta:
model = ServiceTemplate model = ServiceTemplate
fields = ('name', 'protocol', 'ports', 'description', 'tags') fields = ('name', 'protocol', 'ports', 'description', 'comments', 'tags')
widgets = { widgets = {
'protocol': StaticSelect(), 'protocol': StaticSelect(),
} }
@ -834,11 +847,12 @@ class ServiceForm(NetBoxModelForm):
'virtual_machine_id': '$virtual_machine', 'virtual_machine_id': '$virtual_machine',
} }
) )
comments = CommentField()
class Meta: class Meta:
model = Service model = Service
fields = [ fields = [
'device', 'virtual_machine', 'name', 'protocol', 'ports', 'ipaddresses', 'description', 'tags', 'device', 'virtual_machine', 'name', 'protocol', 'ports', 'ipaddresses', 'description', 'comments', 'tags',
] ]
help_texts = { help_texts = {
'ipaddresses': "IP address assignment is optional. If no IPs are selected, the service is assumed to be " 'ipaddresses': "IP address assignment is optional. If no IPs are selected, the service is assumed to be "
@ -899,6 +913,7 @@ class L2VPNForm(TenancyForm, NetBoxModelForm):
queryset=RouteTarget.objects.all(), queryset=RouteTarget.objects.all(),
required=False required=False
) )
comments = CommentField()
fieldsets = ( fieldsets = (
('L2VPN', ('name', 'slug', 'type', 'identifier', 'description', 'tags')), ('L2VPN', ('name', 'slug', 'type', 'identifier', 'description', 'tags')),
@ -909,7 +924,8 @@ class L2VPNForm(TenancyForm, NetBoxModelForm):
class Meta: class Meta:
model = L2VPN model = L2VPN
fields = ( fields = (
'name', 'slug', 'type', 'identifier', 'description', 'import_targets', 'export_targets', 'tenant', 'tags' 'name', 'slug', 'type', 'identifier', 'import_targets', 'export_targets', 'tenant', 'description',
'comments', 'tags'
) )
widgets = { widgets = {
'type': StaticSelect(), 'type': StaticSelect(),

View File

@ -91,7 +91,7 @@ class Migration(migrations.Migration):
options={ options={
'verbose_name': 'RIR', 'verbose_name': 'RIR',
'verbose_name_plural': 'RIRs', 'verbose_name_plural': 'RIRs',
'ordering': ['name'], 'ordering': ('name',),
}, },
), ),
migrations.CreateModel( migrations.CreateModel(
@ -107,7 +107,7 @@ class Migration(migrations.Migration):
('description', models.CharField(blank=True, max_length=200)), ('description', models.CharField(blank=True, max_length=200)),
], ],
options={ options={
'ordering': ['weight', 'name'], 'ordering': ('weight', 'name'),
}, },
), ),
migrations.CreateModel( migrations.CreateModel(

View File

@ -0,0 +1,73 @@
# Generated by Django 4.1.2 on 2022-11-03 18:24
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ipam', '0062_unique_constraints'),
]
operations = [
migrations.AddField(
model_name='aggregate',
name='comments',
field=models.TextField(blank=True),
),
migrations.AddField(
model_name='asn',
name='comments',
field=models.TextField(blank=True),
),
migrations.AddField(
model_name='fhrpgroup',
name='comments',
field=models.TextField(blank=True),
),
migrations.AddField(
model_name='ipaddress',
name='comments',
field=models.TextField(blank=True),
),
migrations.AddField(
model_name='iprange',
name='comments',
field=models.TextField(blank=True),
),
migrations.AddField(
model_name='l2vpn',
name='comments',
field=models.TextField(blank=True),
),
migrations.AddField(
model_name='prefix',
name='comments',
field=models.TextField(blank=True),
),
migrations.AddField(
model_name='routetarget',
name='comments',
field=models.TextField(blank=True),
),
migrations.AddField(
model_name='service',
name='comments',
field=models.TextField(blank=True),
),
migrations.AddField(
model_name='servicetemplate',
name='comments',
field=models.TextField(blank=True),
),
migrations.AddField(
model_name='vlan',
name='comments',
field=models.TextField(blank=True),
),
migrations.AddField(
model_name='vrf',
name='comments',
field=models.TextField(blank=True),
),
]

View File

@ -4,7 +4,7 @@ from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models from django.db import models
from django.urls import reverse from django.urls import reverse
from netbox.models import ChangeLoggedModel, NetBoxModel from netbox.models import ChangeLoggedModel, PrimaryModel
from netbox.models.features import WebhooksMixin from netbox.models.features import WebhooksMixin
from ipam.choices import * from ipam.choices import *
from ipam.constants import * from ipam.constants import *
@ -15,7 +15,7 @@ __all__ = (
) )
class FHRPGroup(NetBoxModel): class FHRPGroup(PrimaryModel):
""" """
A grouping of next hope resolution protocol (FHRP) peers. (For instance, VRRP or HSRP.) A grouping of next hope resolution protocol (FHRP) peers. (For instance, VRRP or HSRP.)
""" """
@ -41,10 +41,6 @@ class FHRPGroup(NetBoxModel):
blank=True, blank=True,
verbose_name='Authentication key' verbose_name='Authentication key'
) )
description = models.CharField(
max_length=200,
blank=True
)
ip_addresses = GenericRelation( ip_addresses = GenericRelation(
to='ipam.IPAddress', to='ipam.IPAddress',
content_type_field='assigned_object_type', content_type_field='assigned_object_type',

View File

@ -9,7 +9,7 @@ from django.utils.functional import cached_property
from dcim.fields import ASNField from dcim.fields import ASNField
from dcim.models import Device from dcim.models import Device
from netbox.models import OrganizationalModel, NetBoxModel from netbox.models import OrganizationalModel, PrimaryModel
from ipam.choices import * from ipam.choices import *
from ipam.constants import * from ipam.constants import *
from ipam.fields import IPNetworkField, IPAddressField from ipam.fields import IPNetworkField, IPAddressField
@ -61,37 +61,22 @@ class RIR(OrganizationalModel):
A Regional Internet Registry (RIR) is responsible for the allocation of a large portion of the global IP address A Regional Internet Registry (RIR) is responsible for the allocation of a large portion of the global IP address
space. This can be an organization like ARIN or RIPE, or a governing standard such as RFC 1918. space. This can be an organization like ARIN or RIPE, or a governing standard such as RFC 1918.
""" """
name = models.CharField(
max_length=100,
unique=True
)
slug = models.SlugField(
max_length=100,
unique=True
)
is_private = models.BooleanField( is_private = models.BooleanField(
default=False, default=False,
verbose_name='Private', verbose_name='Private',
help_text='IP space managed by this RIR is considered private' help_text='IP space managed by this RIR is considered private'
) )
description = models.CharField(
max_length=200,
blank=True
)
class Meta: class Meta:
ordering = ['name'] ordering = ('name',)
verbose_name = 'RIR' verbose_name = 'RIR'
verbose_name_plural = 'RIRs' verbose_name_plural = 'RIRs'
def __str__(self):
return self.name
def get_absolute_url(self): def get_absolute_url(self):
return reverse('ipam:rir', args=[self.pk]) return reverse('ipam:rir', args=[self.pk])
class ASN(NetBoxModel): class ASN(PrimaryModel):
""" """
An autonomous system (AS) number is typically used to represent an independent routing domain. A site can have An autonomous system (AS) number is typically used to represent an independent routing domain. A site can have
one or more ASNs assigned to it. one or more ASNs assigned to it.
@ -101,10 +86,6 @@ class ASN(NetBoxModel):
verbose_name='ASN', verbose_name='ASN',
help_text='32-bit autonomous system number' help_text='32-bit autonomous system number'
) )
description = models.CharField(
max_length=200,
blank=True
)
rir = models.ForeignKey( rir = models.ForeignKey(
to='ipam.RIR', to='ipam.RIR',
on_delete=models.PROTECT, on_delete=models.PROTECT,
@ -154,7 +135,7 @@ class ASN(NetBoxModel):
return self.asn return self.asn
class Aggregate(GetAvailablePrefixesMixin, NetBoxModel): class Aggregate(GetAvailablePrefixesMixin, PrimaryModel):
""" """
An aggregate exists at the root level of the IP address space hierarchy in NetBox. Aggregates are used to organize An aggregate exists at the root level of the IP address space hierarchy in NetBox. Aggregates are used to organize
the hierarchy and track the overall utilization of available address space. Each Aggregate is assigned to a RIR. the hierarchy and track the overall utilization of available address space. Each Aggregate is assigned to a RIR.
@ -177,10 +158,6 @@ class Aggregate(GetAvailablePrefixesMixin, NetBoxModel):
blank=True, blank=True,
null=True null=True
) )
description = models.CharField(
max_length=200,
blank=True
)
clone_fields = ( clone_fields = (
'rir', 'tenant', 'date_added', 'description', 'rir', 'tenant', 'date_added', 'description',
@ -265,24 +242,12 @@ class Role(OrganizationalModel):
A Role represents the functional role of a Prefix or VLAN; for example, "Customer," "Infrastructure," or A Role represents the functional role of a Prefix or VLAN; for example, "Customer," "Infrastructure," or
"Management." "Management."
""" """
name = models.CharField(
max_length=100,
unique=True
)
slug = models.SlugField(
max_length=100,
unique=True
)
weight = models.PositiveSmallIntegerField( weight = models.PositiveSmallIntegerField(
default=1000 default=1000
) )
description = models.CharField(
max_length=200,
blank=True,
)
class Meta: class Meta:
ordering = ['weight', 'name'] ordering = ('weight', 'name')
def __str__(self): def __str__(self):
return self.name return self.name
@ -291,7 +256,7 @@ class Role(OrganizationalModel):
return reverse('ipam:role', args=[self.pk]) return reverse('ipam:role', args=[self.pk])
class Prefix(GetAvailablePrefixesMixin, NetBoxModel): class Prefix(GetAvailablePrefixesMixin, PrimaryModel):
""" """
A Prefix represents an IPv4 or IPv6 network, including mask length. Prefixes can optionally be assigned to Sites and A Prefix represents an IPv4 or IPv6 network, including mask length. Prefixes can optionally be assigned to Sites and
VRFs. A Prefix must be assigned a status and may optionally be assigned a used-define Role. A Prefix can also be VRFs. A Prefix must be assigned a status and may optionally be assigned a used-define Role. A Prefix can also be
@ -354,10 +319,6 @@ class Prefix(GetAvailablePrefixesMixin, NetBoxModel):
default=False, default=False,
help_text="Treat as 100% utilized" help_text="Treat as 100% utilized"
) )
description = models.CharField(
max_length=200,
blank=True
)
# Cached depth & child counts # Cached depth & child counts
_depth = models.PositiveSmallIntegerField( _depth = models.PositiveSmallIntegerField(
@ -572,7 +533,7 @@ class Prefix(GetAvailablePrefixesMixin, NetBoxModel):
return min(utilization, 100) return min(utilization, 100)
class IPRange(NetBoxModel): class IPRange(PrimaryModel):
""" """
A range of IP addresses, defined by start and end addresses. A range of IP addresses, defined by start and end addresses.
""" """
@ -614,10 +575,6 @@ class IPRange(NetBoxModel):
null=True, null=True,
help_text='The primary function of this range' help_text='The primary function of this range'
) )
description = models.CharField(
max_length=200,
blank=True
)
clone_fields = ( clone_fields = (
'vrf', 'tenant', 'status', 'role', 'description', 'vrf', 'tenant', 'status', 'role', 'description',
@ -767,7 +724,7 @@ class IPRange(NetBoxModel):
return int(float(child_count) / self.size * 100) return int(float(child_count) / self.size * 100)
class IPAddress(NetBoxModel): class IPAddress(PrimaryModel):
""" """
An IPAddress represents an individual IPv4 or IPv6 address and its mask. The mask length should match what is An IPAddress represents an individual IPv4 or IPv6 address and its mask. The mask length should match what is
configured in the real world. (Typically, only loopback interfaces are configured with /32 or /128 masks.) Like configured in the real world. (Typically, only loopback interfaces are configured with /32 or /128 masks.) Like
@ -840,10 +797,6 @@ class IPAddress(NetBoxModel):
verbose_name='DNS Name', verbose_name='DNS Name',
help_text='Hostname or FQDN (not case-sensitive)' help_text='Hostname or FQDN (not case-sensitive)'
) )
description = models.CharField(
max_length=200,
blank=True
)
objects = IPAddressManager() objects = IPAddressManager()

View File

@ -8,7 +8,7 @@ from django.utils.functional import cached_property
from ipam.choices import L2VPNTypeChoices from ipam.choices import L2VPNTypeChoices
from ipam.constants import L2VPN_ASSIGNMENT_MODELS from ipam.constants import L2VPN_ASSIGNMENT_MODELS
from netbox.models import NetBoxModel from netbox.models import NetBoxModel, PrimaryModel
__all__ = ( __all__ = (
'L2VPN', 'L2VPN',
@ -16,7 +16,7 @@ __all__ = (
) )
class L2VPN(NetBoxModel): class L2VPN(PrimaryModel):
name = models.CharField( name = models.CharField(
max_length=100, max_length=100,
unique=True unique=True
@ -43,10 +43,6 @@ class L2VPN(NetBoxModel):
related_name='exporting_l2vpns', related_name='exporting_l2vpns',
blank=True blank=True
) )
description = models.CharField(
max_length=200,
blank=True
)
tenant = models.ForeignKey( tenant = models.ForeignKey(
to='tenancy.Tenant', to='tenancy.Tenant',
on_delete=models.PROTECT, on_delete=models.PROTECT,

View File

@ -6,7 +6,7 @@ from django.urls import reverse
from ipam.choices import * from ipam.choices import *
from ipam.constants import * from ipam.constants import *
from netbox.models import NetBoxModel from netbox.models import PrimaryModel
from utilities.utils import array_to_string from utilities.utils import array_to_string
@ -30,10 +30,6 @@ class ServiceBase(models.Model):
), ),
verbose_name='Port numbers' verbose_name='Port numbers'
) )
description = models.CharField(
max_length=200,
blank=True
)
class Meta: class Meta:
abstract = True abstract = True
@ -46,7 +42,7 @@ class ServiceBase(models.Model):
return array_to_string(self.ports) return array_to_string(self.ports)
class ServiceTemplate(ServiceBase, NetBoxModel): class ServiceTemplate(ServiceBase, PrimaryModel):
""" """
A template for a Service to be applied to a device or virtual machine. A template for a Service to be applied to a device or virtual machine.
""" """
@ -62,7 +58,7 @@ class ServiceTemplate(ServiceBase, NetBoxModel):
return reverse('ipam:servicetemplate', args=[self.pk]) return reverse('ipam:servicetemplate', args=[self.pk])
class Service(ServiceBase, NetBoxModel): class Service(ServiceBase, PrimaryModel):
""" """
A Service represents a layer-four service (e.g. HTTP or SSH) running on a Device or VirtualMachine. A Service may A Service represents a layer-four service (e.g. HTTP or SSH) running on a Device or VirtualMachine. A Service may
optionally be tied to one or more specific IPAddresses belonging to its parent. optionally be tied to one or more specific IPAddresses belonging to its parent.

View File

@ -8,12 +8,10 @@ from django.urls import reverse
from dcim.models import Interface from dcim.models import Interface
from ipam.choices import * from ipam.choices import *
from ipam.constants import * from ipam.constants import *
from ipam.models import L2VPNTermination
from ipam.querysets import VLANQuerySet from ipam.querysets import VLANQuerySet
from netbox.models import OrganizationalModel, NetBoxModel from netbox.models import OrganizationalModel, PrimaryModel
from virtualization.models import VMInterface from virtualization.models import VMInterface
__all__ = ( __all__ = (
'VLAN', 'VLAN',
'VLANGroup', 'VLANGroup',
@ -63,10 +61,6 @@ class VLANGroup(OrganizationalModel):
), ),
help_text='Highest permissible ID of a child VLAN' help_text='Highest permissible ID of a child VLAN'
) )
description = models.CharField(
max_length=200,
blank=True
)
class Meta: class Meta:
ordering = ('name', 'pk') # Name may be non-unique ordering = ('name', 'pk') # Name may be non-unique
@ -83,9 +77,6 @@ class VLANGroup(OrganizationalModel):
verbose_name = 'VLAN group' verbose_name = 'VLAN group'
verbose_name_plural = 'VLAN groups' verbose_name_plural = 'VLAN groups'
def __str__(self):
return self.name
def get_absolute_url(self): def get_absolute_url(self):
return reverse('ipam:vlangroup', args=[self.pk]) return reverse('ipam:vlangroup', args=[self.pk])
@ -123,7 +114,7 @@ class VLANGroup(OrganizationalModel):
return None return None
class VLAN(NetBoxModel): class VLAN(PrimaryModel):
""" """
A VLAN is a distinct layer two forwarding domain identified by a 12-bit integer (1-4094). Each VLAN must be assigned A VLAN is a distinct layer two forwarding domain identified by a 12-bit integer (1-4094). Each VLAN must be assigned
to a Site, however VLAN IDs need not be unique within a Site. A VLAN may optionally be assigned to a VLANGroup, to a Site, however VLAN IDs need not be unique within a Site. A VLAN may optionally be assigned to a VLANGroup,
@ -175,10 +166,6 @@ class VLAN(NetBoxModel):
blank=True, blank=True,
null=True null=True
) )
description = models.CharField(
max_length=200,
blank=True
)
l2vpn_terminations = GenericRelation( l2vpn_terminations = GenericRelation(
to='ipam.L2VPNTermination', to='ipam.L2VPNTermination',

View File

@ -2,7 +2,7 @@ from django.db import models
from django.urls import reverse from django.urls import reverse
from ipam.constants import * from ipam.constants import *
from netbox.models import NetBoxModel from netbox.models import PrimaryModel
__all__ = ( __all__ = (
@ -11,7 +11,7 @@ __all__ = (
) )
class VRF(NetBoxModel): class VRF(PrimaryModel):
""" """
A virtual routing and forwarding (VRF) table represents a discrete layer three forwarding domain (e.g. a routing A virtual routing and forwarding (VRF) table represents a discrete layer three forwarding domain (e.g. a routing
table). Prefixes and IPAddresses can optionally be assigned to VRFs. (Prefixes and IPAddresses not assigned to a VRF table). Prefixes and IPAddresses can optionally be assigned to VRFs. (Prefixes and IPAddresses not assigned to a VRF
@ -40,10 +40,6 @@ class VRF(NetBoxModel):
verbose_name='Enforce unique space', verbose_name='Enforce unique space',
help_text='Prevent duplicate prefixes/IP addresses within this VRF' help_text='Prevent duplicate prefixes/IP addresses within this VRF'
) )
description = models.CharField(
max_length=200,
blank=True
)
import_targets = models.ManyToManyField( import_targets = models.ManyToManyField(
to='ipam.RouteTarget', to='ipam.RouteTarget',
related_name='importing_vrfs', related_name='importing_vrfs',
@ -73,7 +69,7 @@ class VRF(NetBoxModel):
return reverse('ipam:vrf', args=[self.pk]) return reverse('ipam:vrf', args=[self.pk])
class RouteTarget(NetBoxModel): class RouteTarget(PrimaryModel):
""" """
A BGP extended community used to control the redistribution of routes among VRFs, as defined in RFC 4364. A BGP extended community used to control the redistribution of routes among VRFs, as defined in RFC 4364.
""" """
@ -82,10 +78,6 @@ class RouteTarget(NetBoxModel):
unique=True, unique=True,
help_text='Route target value (formatted in accordance with RFC 4360)' help_text='Route target value (formatted in accordance with RFC 4360)'
) )
description = models.CharField(
max_length=200,
blank=True
)
tenant = models.ForeignKey( tenant = models.ForeignKey(
to='tenancy.Tenant', to='tenancy.Tenant',
on_delete=models.PROTECT, on_delete=models.PROTECT,

View File

@ -20,7 +20,6 @@ class FHRPGroupTable(NetBoxTable):
group_id = tables.Column( group_id = tables.Column(
linkify=True linkify=True
) )
comments = columns.MarkdownColumn()
ip_addresses = tables.TemplateColumn( ip_addresses = tables.TemplateColumn(
template_code=IPADDRESSES, template_code=IPADDRESSES,
orderable=False, orderable=False,
@ -29,6 +28,7 @@ class FHRPGroupTable(NetBoxTable):
member_count = tables.Column( member_count = tables.Column(
verbose_name='Members' verbose_name='Members'
) )
comments = columns.MarkdownColumn()
tags = columns.TagColumn( tags = columns.TagColumn(
url_name='ipam:fhrpgroup_list' url_name='ipam:fhrpgroup_list'
) )
@ -36,7 +36,7 @@ class FHRPGroupTable(NetBoxTable):
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = FHRPGroup model = FHRPGroup
fields = ( fields = (
'pk', 'group_id', 'protocol', 'name', 'auth_type', 'auth_key', 'description', 'ip_addresses', 'pk', 'group_id', 'protocol', 'name', 'auth_type', 'auth_key', 'description', 'comments', 'ip_addresses',
'member_count', 'tags', 'created', 'last_updated', 'member_count', 'tags', 'created', 'last_updated',
) )
default_columns = ( default_columns = (

View File

@ -120,6 +120,7 @@ class ASNTable(TenancyColumnsMixin, NetBoxTable):
linkify_item=True, linkify_item=True,
verbose_name='Sites' verbose_name='Sites'
) )
comments = columns.MarkdownColumn()
tags = columns.TagColumn( tags = columns.TagColumn(
url_name='ipam:asn_list' url_name='ipam:asn_list'
) )
@ -127,8 +128,8 @@ class ASNTable(TenancyColumnsMixin, NetBoxTable):
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = ASN model = ASN
fields = ( fields = (
'pk', 'asn', 'asn_asdot', 'rir', 'site_count', 'provider_count', 'tenant', 'tenant_group', 'description', 'sites', 'tags', 'pk', 'asn', 'asn_asdot', 'rir', 'site_count', 'provider_count', 'tenant', 'tenant_group', 'description',
'created', 'last_updated', 'actions', 'comments', 'sites', 'tags', 'created', 'last_updated', 'actions',
) )
default_columns = ('pk', 'asn', 'rir', 'site_count', 'provider_count', 'sites', 'description', 'tenant') default_columns = ('pk', 'asn', 'rir', 'site_count', 'provider_count', 'sites', 'description', 'tenant')
@ -153,6 +154,7 @@ class AggregateTable(TenancyColumnsMixin, NetBoxTable):
accessor='get_utilization', accessor='get_utilization',
orderable=False orderable=False
) )
comments = columns.MarkdownColumn()
tags = columns.TagColumn( tags = columns.TagColumn(
url_name='ipam:aggregate_list' url_name='ipam:aggregate_list'
) )
@ -160,8 +162,8 @@ class AggregateTable(TenancyColumnsMixin, NetBoxTable):
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = Aggregate model = Aggregate
fields = ( fields = (
'pk', 'id', 'prefix', 'rir', 'tenant', 'tenant_group', 'child_count', 'utilization', 'date_added', 'description', 'tags', 'pk', 'id', 'prefix', 'rir', 'tenant', 'tenant_group', 'child_count', 'utilization', 'date_added',
'created', 'last_updated', 'description', 'comments', 'tags', 'created', 'last_updated',
) )
default_columns = ('pk', 'prefix', 'rir', 'tenant', 'child_count', 'utilization', 'date_added', 'description') default_columns = ('pk', 'prefix', 'rir', 'tenant', 'child_count', 'utilization', 'date_added', 'description')
@ -278,6 +280,7 @@ class PrefixTable(TenancyColumnsMixin, NetBoxTable):
accessor='get_utilization', accessor='get_utilization',
orderable=False orderable=False
) )
comments = columns.MarkdownColumn()
tags = columns.TagColumn( tags = columns.TagColumn(
url_name='ipam:prefix_list' url_name='ipam:prefix_list'
) )
@ -285,8 +288,9 @@ class PrefixTable(TenancyColumnsMixin, NetBoxTable):
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = Prefix model = Prefix
fields = ( fields = (
'pk', 'id', 'prefix', 'prefix_flat', 'status', 'children', 'vrf', 'utilization', 'tenant', 'tenant_group', 'site', 'pk', 'id', 'prefix', 'prefix_flat', 'status', 'children', 'vrf', 'utilization', 'tenant', 'tenant_group',
'vlan_group', 'vlan', 'role', 'is_pool', 'mark_utilized', 'description', 'tags', 'created', 'last_updated', 'site', 'vlan_group', 'vlan', 'role', 'is_pool', 'mark_utilized', 'description', 'comments', 'tags',
'created', 'last_updated',
) )
default_columns = ( default_columns = (
'pk', 'prefix', 'status', 'children', 'vrf', 'utilization', 'tenant', 'site', 'vlan', 'role', 'description', 'pk', 'prefix', 'status', 'children', 'vrf', 'utilization', 'tenant', 'site', 'vlan', 'role', 'description',
@ -317,6 +321,7 @@ class IPRangeTable(TenancyColumnsMixin, NetBoxTable):
accessor='utilization', accessor='utilization',
orderable=False orderable=False
) )
comments = columns.MarkdownColumn()
tags = columns.TagColumn( tags = columns.TagColumn(
url_name='ipam:iprange_list' url_name='ipam:iprange_list'
) )
@ -324,8 +329,8 @@ class IPRangeTable(TenancyColumnsMixin, NetBoxTable):
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = IPRange model = IPRange
fields = ( fields = (
'pk', 'id', 'start_address', 'end_address', 'size', 'vrf', 'status', 'role', 'tenant', 'tenant_group', 'description', 'pk', 'id', 'start_address', 'end_address', 'size', 'vrf', 'status', 'role', 'tenant', 'tenant_group',
'utilization', 'tags', 'created', 'last_updated', 'utilization', 'description', 'comments', 'tags', 'created', 'last_updated',
) )
default_columns = ( default_columns = (
'pk', 'start_address', 'end_address', 'size', 'vrf', 'status', 'role', 'tenant', 'description', 'pk', 'start_address', 'end_address', 'size', 'vrf', 'status', 'role', 'tenant', 'description',
@ -378,6 +383,7 @@ class IPAddressTable(TenancyColumnsMixin, NetBoxTable):
linkify=lambda record: record.assigned_object.get_absolute_url(), linkify=lambda record: record.assigned_object.get_absolute_url(),
verbose_name='Assigned' verbose_name='Assigned'
) )
comments = columns.MarkdownColumn()
tags = columns.TagColumn( tags = columns.TagColumn(
url_name='ipam:ipaddress_list' url_name='ipam:ipaddress_list'
) )
@ -385,8 +391,8 @@ class IPAddressTable(TenancyColumnsMixin, NetBoxTable):
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = IPAddress model = IPAddress
fields = ( fields = (
'pk', 'id', 'address', 'vrf', 'status', 'role', 'tenant', 'tenant_group', 'nat_inside', 'nat_outside', 'assigned', 'dns_name', 'description', 'pk', 'id', 'address', 'vrf', 'status', 'role', 'tenant', 'tenant_group', 'nat_inside', 'nat_outside',
'tags', 'created', 'last_updated', 'assigned', 'dns_name', 'description', 'comments', 'tags', 'created', 'last_updated',
) )
default_columns = ( default_columns = (
'pk', 'address', 'vrf', 'status', 'role', 'tenant', 'assigned', 'dns_name', 'description', 'pk', 'address', 'vrf', 'status', 'role', 'tenant', 'assigned', 'dns_name', 'description',

View File

@ -29,12 +29,16 @@ class L2VPNTable(TenancyColumnsMixin, NetBoxTable):
template_code=L2VPN_TARGETS, template_code=L2VPN_TARGETS,
orderable=False orderable=False
) )
comments = columns.MarkdownColumn()
tags = columns.TagColumn(
url_name='ipam:prefix_list'
)
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = L2VPN model = L2VPN
fields = ( fields = (
'pk', 'name', 'slug', 'identifier', 'type', 'description', 'import_targets', 'export_targets', 'tenant', 'tenant_group', 'pk', 'name', 'slug', 'identifier', 'type', 'import_targets', 'export_targets', 'tenant', 'tenant_group',
'actions', 'description', 'comments', 'tags', 'created', 'last_updated', 'actions',
) )
default_columns = ('pk', 'name', 'identifier', 'type', 'description', 'actions') default_columns = ('pk', 'name', 'identifier', 'type', 'description', 'actions')

View File

@ -17,13 +17,16 @@ class ServiceTemplateTable(NetBoxTable):
accessor=tables.A('port_list'), accessor=tables.A('port_list'),
order_by=tables.A('ports'), order_by=tables.A('ports'),
) )
comments = columns.MarkdownColumn()
tags = columns.TagColumn( tags = columns.TagColumn(
url_name='ipam:servicetemplate_list' url_name='ipam:servicetemplate_list'
) )
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = ServiceTemplate model = ServiceTemplate
fields = ('pk', 'id', 'name', 'protocol', 'ports', 'description', 'tags') fields = (
'pk', 'id', 'name', 'protocol', 'ports', 'description', 'comments', 'tags', 'created', 'last_updated',
)
default_columns = ('pk', 'name', 'protocol', 'ports', 'description') default_columns = ('pk', 'name', 'protocol', 'ports', 'description')
@ -39,6 +42,7 @@ class ServiceTable(NetBoxTable):
accessor=tables.A('port_list'), accessor=tables.A('port_list'),
order_by=tables.A('ports'), order_by=tables.A('ports'),
) )
comments = columns.MarkdownColumn()
tags = columns.TagColumn( tags = columns.TagColumn(
url_name='ipam:service_list' url_name='ipam:service_list'
) )
@ -46,7 +50,7 @@ class ServiceTable(NetBoxTable):
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = Service model = Service
fields = ( fields = (
'pk', 'id', 'name', 'parent', 'protocol', 'ports', 'ipaddresses', 'description', 'tags', 'created', 'pk', 'id', 'name', 'parent', 'protocol', 'ports', 'ipaddresses', 'description', 'comments', 'tags',
'last_updated', 'created', 'last_updated',
) )
default_columns = ('pk', 'name', 'parent', 'protocol', 'ports', 'description') default_columns = ('pk', 'name', 'parent', 'protocol', 'ports', 'description')

View File

@ -121,6 +121,7 @@ class VLANTable(TenancyColumnsMixin, NetBoxTable):
orderable=False, orderable=False,
verbose_name='Prefixes' verbose_name='Prefixes'
) )
comments = columns.MarkdownColumn()
tags = columns.TagColumn( tags = columns.TagColumn(
url_name='ipam:vlan_list' url_name='ipam:vlan_list'
) )
@ -129,7 +130,7 @@ class VLANTable(TenancyColumnsMixin, NetBoxTable):
model = VLAN model = VLAN
fields = ( fields = (
'pk', 'id', 'vid', 'name', 'site', 'group', 'prefixes', 'tenant', 'tenant_group', 'status', 'role', 'pk', 'id', 'vid', 'name', 'site', 'group', 'prefixes', 'tenant', 'tenant_group', 'status', 'role',
'description', 'tags', 'l2vpn', 'created', 'last_updated', 'description', 'comments', 'tags', 'l2vpn', 'created', 'last_updated',
) )
default_columns = ('pk', 'vid', 'name', 'site', 'group', 'prefixes', 'tenant', 'status', 'role', 'description') default_columns = ('pk', 'vid', 'name', 'site', 'group', 'prefixes', 'tenant', 'status', 'role', 'description')
row_attrs = { row_attrs = {

View File

@ -38,6 +38,7 @@ class VRFTable(TenancyColumnsMixin, NetBoxTable):
template_code=VRF_TARGETS, template_code=VRF_TARGETS,
orderable=False orderable=False
) )
comments = columns.MarkdownColumn()
tags = columns.TagColumn( tags = columns.TagColumn(
url_name='ipam:vrf_list' url_name='ipam:vrf_list'
) )
@ -45,8 +46,8 @@ class VRFTable(TenancyColumnsMixin, NetBoxTable):
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = VRF model = VRF
fields = ( fields = (
'pk', 'id', 'name', 'rd', 'tenant', 'tenant_group', 'enforce_unique', 'description', 'import_targets', 'export_targets', 'pk', 'id', 'name', 'rd', 'tenant', 'tenant_group', 'enforce_unique', 'import_targets', 'export_targets',
'tags', 'created', 'last_updated', 'description', 'comments', 'tags', 'created', 'last_updated',
) )
default_columns = ('pk', 'name', 'rd', 'tenant', 'description') default_columns = ('pk', 'name', 'rd', 'tenant', 'description')
@ -59,11 +60,14 @@ class RouteTargetTable(TenancyColumnsMixin, NetBoxTable):
name = tables.Column( name = tables.Column(
linkify=True linkify=True
) )
comments = columns.MarkdownColumn()
tags = columns.TagColumn( tags = columns.TagColumn(
url_name='ipam:vrf_list' url_name='ipam:vrf_list'
) )
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = RouteTarget model = RouteTarget
fields = ('pk', 'id', 'name', 'tenant', 'tenant_group', 'description', 'tags', 'created', 'last_updated',) fields = (
'pk', 'id', 'name', 'tenant', 'tenant_group', 'description', 'comments', 'tags', 'created', 'last_updated',
)
default_columns = ('pk', 'name', 'tenant', 'description') default_columns = ('pk', 'name', 'tenant', 'description')

View File

@ -72,6 +72,9 @@ ADMINS = [
# ('John Doe', 'jdoe@example.com'), # ('John Doe', 'jdoe@example.com'),
] ]
# Permit the retrieval of API tokens after their creation.
ALLOW_TOKEN_RETRIEVAL = False
# Enable any desired validators for local account passwords below. For a list of included validators, please see the # Enable any desired validators for local account passwords below. For a list of included validators, please see the
# Django documentation at https://docs.djangoproject.com/en/stable/topics/auth/passwords/#password-validation. # Django documentation at https://docs.djangoproject.com/en/stable/topics/auth/passwords/#password-validation.
AUTH_PASSWORD_VALIDATORS = [ AUTH_PASSWORD_VALIDATORS = [

View File

@ -1,7 +1,7 @@
from django.conf import settings as django_settings from django.conf import settings as django_settings
from extras.registry import registry
from netbox.config import get_config from netbox.config import get_config
from netbox.registry import registry
def settings_and_registry(request): def settings_and_registry(request):

View File

@ -3,7 +3,7 @@ import logging
from django.db.models.signals import post_save from django.db.models.signals import post_save
from django.dispatch import receiver from django.dispatch import receiver
from extras.registry import registry from netbox.registry import registry
logger = logging.getLogger('netbox.denormalized') logger = logging.getLogger('netbox.denormalized')

View File

@ -4,10 +4,11 @@ from django.contrib.contenttypes.models import ContentType
from django.db import models from django.db import models
from django_filters.exceptions import FieldLookupError from django_filters.exceptions import FieldLookupError
from django_filters.utils import get_model_field, resolve_field from django_filters.utils import get_model_field, resolve_field
from django.shortcuts import get_object_or_404
from extras.choices import CustomFieldFilterLogicChoices from extras.choices import CustomFieldFilterLogicChoices
from extras.filters import TagFilter from extras.filters import TagFilter
from extras.models import CustomField from extras.models import CustomField, SavedFilter
from utilities.constants import ( from utilities.constants import (
FILTER_CHAR_BASED_LOOKUP_MAP, FILTER_NEGATION_LOOKUP_MAP, FILTER_TREENODE_NEGATION_LOOKUP_MAP, FILTER_CHAR_BASED_LOOKUP_MAP, FILTER_NEGATION_LOOKUP_MAP, FILTER_TREENODE_NEGATION_LOOKUP_MAP,
FILTER_NUMERIC_BASED_LOOKUP_MAP FILTER_NUMERIC_BASED_LOOKUP_MAP
@ -80,12 +81,28 @@ class BaseFilterSet(django_filters.FilterSet):
}, },
}) })
def __init__(self, *args, **kwargs): def __init__(self, data=None, *args, **kwargs):
# bit of a hack for #9231 - extras.lookup.Empty is registered in apps.ready # bit of a hack for #9231 - extras.lookup.Empty is registered in apps.ready
# however FilterSet Factory is setup before this which creates the # however FilterSet Factory is setup before this which creates the
# initial filters. This recreates the filters so Empty is picked up correctly. # initial filters. This recreates the filters so Empty is picked up correctly.
self.base_filters = self.__class__.get_filters() self.base_filters = self.__class__.get_filters()
super().__init__(*args, **kwargs)
# Apply any referenced SavedFilters
if data and 'filter' in data:
data = data.copy() # Get a mutable copy
saved_filters = SavedFilter.objects.filter(pk__in=data.pop('filter'))
for sf in saved_filters:
for key, value in sf.parameters.items():
# QueryDicts are... fun
if type(value) not in (list, tuple):
value = [value]
if key in data:
for v in value:
data.appendlist(key, v)
else:
data.setlist(key, value)
super().__init__(data, *args, **kwargs)
@staticmethod @staticmethod
def _get_filter_lookup_dict(existing_filter): def _get_filter_lookup_dict(existing_filter):

View File

@ -3,7 +3,7 @@ from django.contrib.contenttypes.models import ContentType
from django.db.models import Q from django.db.models import Q
from extras.choices import CustomFieldFilterLogicChoices, CustomFieldTypeChoices, CustomFieldVisibilityChoices from extras.choices import CustomFieldFilterLogicChoices, CustomFieldTypeChoices, CustomFieldVisibilityChoices
from extras.forms.customfields import CustomFieldsMixin from extras.forms.mixins import CustomFieldsMixin, SavedFiltersMixin
from extras.models import CustomField, Tag from extras.models import CustomField, Tag
from utilities.forms import BootstrapMixin, CSVModelForm from utilities.forms import BootstrapMixin, CSVModelForm
from utilities.forms.fields import DynamicModelMultipleChoiceField from utilities.forms.fields import DynamicModelMultipleChoiceField
@ -114,7 +114,7 @@ class NetBoxModelBulkEditForm(BootstrapMixin, CustomFieldsMixin, forms.Form):
self.nullable_fields = (*self.nullable_fields, *nullable_custom_fields) self.nullable_fields = (*self.nullable_fields, *nullable_custom_fields)
class NetBoxModelFilterSetForm(BootstrapMixin, CustomFieldsMixin, forms.Form): class NetBoxModelFilterSetForm(BootstrapMixin, CustomFieldsMixin, SavedFiltersMixin, forms.Form):
""" """
Base form for FilerSet forms. These are used to filter object lists in the NetBox UI. Note that the Base form for FilerSet forms. These are used to filter object lists in the NetBox UI. Note that the
corresponding FilterSet *must* provide a `q` filter. corresponding FilterSet *must* provide a `q` filter.
@ -129,6 +129,15 @@ class NetBoxModelFilterSetForm(BootstrapMixin, CustomFieldsMixin, forms.Form):
label='Search' label='Search'
) )
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Limit saved filters to those applicable to the form's model
content_type = ContentType.objects.get_for_model(self.model)
self.fields['filter'].widget.add_query_params({
'content_type_id': content_type.pk,
})
def _get_custom_fields(self, content_type): def _get_custom_fields(self, content_type):
return super()._get_custom_fields(content_type).exclude( return super()._get_custom_fields(content_type).exclude(
Q(filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED) | Q(filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED) |

View File

@ -3,8 +3,8 @@ import graphene
from circuits.graphql.schema import CircuitsQuery from circuits.graphql.schema import CircuitsQuery
from dcim.graphql.schema import DCIMQuery from dcim.graphql.schema import DCIMQuery
from extras.graphql.schema import ExtrasQuery from extras.graphql.schema import ExtrasQuery
from extras.registry import registry
from ipam.graphql.schema import IPAMQuery from ipam.graphql.schema import IPAMQuery
from netbox.registry import registry
from tenancy.graphql.schema import TenancyQuery from tenancy.graphql.schema import TenancyQuery
from users.graphql.schema import UsersQuery from users.graphql.schema import UsersQuery
from virtualization.graphql.schema import VirtualizationQuery from virtualization.graphql.schema import VirtualizationQuery

View File

@ -10,8 +10,9 @@ from netbox.models.features import *
__all__ = ( __all__ = (
'ChangeLoggedModel', 'ChangeLoggedModel',
'NestedGroupModel', 'NestedGroupModel',
'OrganizationalModel',
'NetBoxModel', 'NetBoxModel',
'OrganizationalModel',
'PrimaryModel',
) )
@ -21,6 +22,7 @@ class NetBoxFeatureSet(
CustomLinksMixin, CustomLinksMixin,
CustomValidationMixin, CustomValidationMixin,
ExportTemplatesMixin, ExportTemplatesMixin,
JournalingMixin,
TagsMixin, TagsMixin,
WebhooksMixin WebhooksMixin
): ):
@ -55,11 +57,27 @@ class ChangeLoggedModel(ChangeLoggingMixin, CustomValidationMixin, models.Model)
abstract = True abstract = True
class NetBoxModel(CloningMixin, JournalingMixin, NetBoxFeatureSet, models.Model): class NetBoxModel(CloningMixin, NetBoxFeatureSet, models.Model):
"""
Base model for most object types. Suitable for use by plugins.
"""
objects = RestrictedQuerySet.as_manager()
class Meta:
abstract = True
class PrimaryModel(NetBoxModel):
""" """
Primary models represent real objects within the infrastructure being modeled. Primary models represent real objects within the infrastructure being modeled.
""" """
objects = RestrictedQuerySet.as_manager() description = models.CharField(
max_length=200,
blank=True
)
comments = models.TextField(
blank=True
)
class Meta: class Meta:
abstract = True abstract = True
@ -81,6 +99,9 @@ class NestedGroupModel(NetBoxFeatureSet, MPTTModel):
name = models.CharField( name = models.CharField(
max_length=100 max_length=100
) )
slug = models.SlugField(
max_length=100
)
description = models.CharField( description = models.CharField(
max_length=200, max_length=200,
blank=True blank=True
@ -134,3 +155,6 @@ class OrganizationalModel(NetBoxFeatureSet, models.Model):
class Meta: class Meta:
abstract = True abstract = True
ordering = ('name',) ordering = ('name',)
def __str__(self):
return self.name

View File

@ -1,4 +1,4 @@
from extras.registry import registry from netbox.registry import registry
from . import * from . import *
@ -279,6 +279,7 @@ OTHER_MENU = Menu(
get_model_item('extras', 'customfield', 'Custom Fields'), get_model_item('extras', 'customfield', 'Custom Fields'),
get_model_item('extras', 'customlink', 'Custom Links'), get_model_item('extras', 'customlink', 'Custom Links'),
get_model_item('extras', 'exporttemplate', 'Export Templates'), get_model_item('extras', 'exporttemplate', 'Export Templates'),
get_model_item('extras', 'savedfilter', 'Saved Filters'),
), ),
), ),
MenuGroup( MenuGroup(

View File

@ -1,4 +1,4 @@
from extras.registry import registry from netbox.registry import registry
from users.preferences import UserPreference from users.preferences import UserPreference
from utilities.paginator import EnhancedPaginator from utilities.paginator import EnhancedPaginator

View File

@ -2,7 +2,7 @@ from collections import namedtuple
from django.db import models from django.db import models
from extras.registry import registry from netbox.registry import registry
ObjectFieldValue = namedtuple('ObjectFieldValue', ('name', 'type', 'weight', 'value')) ObjectFieldValue = namedtuple('ObjectFieldValue', ('name', 'type', 'weight', 'value'))

Some files were not shown because too many files have changed in this diff Show More