mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-14 09:51:22 -06:00
Merge branch 'feature' into 3979-wireless
This commit is contained in:
commit
3a3ed8bf64
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@ -17,7 +17,7 @@ body:
|
|||||||
What version of NetBox are you currently running? (If you don't have access to the most
|
What version of NetBox are you currently running? (If you don't have access to the most
|
||||||
recent NetBox release, consider testing on our [demo instance](https://demo.netbox.dev/)
|
recent NetBox release, consider testing on our [demo instance](https://demo.netbox.dev/)
|
||||||
before opening a bug report to see if your issue has already been addressed.)
|
before opening a bug report to see if your issue has already been addressed.)
|
||||||
placeholder: v3.0.7
|
placeholder: v3.0.8
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
|
2
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
2
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
@ -14,7 +14,7 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: NetBox version
|
label: NetBox version
|
||||||
description: What version of NetBox are you currently running?
|
description: What version of NetBox are you currently running?
|
||||||
placeholder: v3.0.7
|
placeholder: v3.0.8
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
|
5
docs/core-functionality/contacts.md
Normal file
5
docs/core-functionality/contacts.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# Contacts
|
||||||
|
|
||||||
|
{!models/tenancy/contact.md!}
|
||||||
|
{!models/tenancy/contactgroup.md!}
|
||||||
|
{!models/tenancy/contactrole.md!}
|
@ -1 +0,0 @@
|
|||||||
{!models/extras/customlink.md!}
|
|
@ -19,8 +19,8 @@ The Django [content types](https://docs.djangoproject.com/en/stable/ref/contrib/
|
|||||||
| Type | Change Logging | Webhooks | Custom Fields | Export Templates | Tags | Journaling | Nesting |
|
| Type | Change Logging | Webhooks | Custom Fields | Export Templates | Tags | Journaling | Nesting |
|
||||||
| ------------------ | ---------------- | ---------------- | ---------------- | ---------------- | ---------------- | ---------------- | ---------------- |
|
| ------------------ | ---------------- | ---------------- | ---------------- | ---------------- | ---------------- | ---------------- | ---------------- |
|
||||||
| Primary | :material-check: | :material-check: | :material-check: | :material-check: | :material-check: | :material-check: | |
|
| Primary | :material-check: | :material-check: | :material-check: | :material-check: | :material-check: | :material-check: | |
|
||||||
| Organizational | :material-check: | :material-check: | :material-check: | :material-check: | | | |
|
| Organizational | :material-check: | :material-check: | :material-check: | :material-check: | :material-check: | | |
|
||||||
| Nested Group | :material-check: | :material-check: | :material-check: | :material-check: | | | :material-check: |
|
| Nested Group | :material-check: | :material-check: | :material-check: | :material-check: | :material-check: | | :material-check: |
|
||||||
| Component | :material-check: | :material-check: | :material-check: | :material-check: | :material-check: | | |
|
| Component | :material-check: | :material-check: | :material-check: | :material-check: | :material-check: | | |
|
||||||
| Component Template | :material-check: | :material-check: | :material-check: | | | | |
|
| Component Template | :material-check: | :material-check: | :material-check: | | | | |
|
||||||
|
|
||||||
|
@ -2,4 +2,5 @@
|
|||||||
|
|
||||||
Racks and devices can be grouped by location within a site. A location may represent a floor, room, cage, or similar organizational unit. Locations can be nested to form a hierarchy. For example, you may have floors within a site, and rooms within a floor.
|
Racks and devices can be grouped by location within a site. A location may represent a floor, room, cage, or similar organizational unit. Locations can be nested to form a hierarchy. For example, you may have floors within a site, and rooms within a floor.
|
||||||
|
|
||||||
The name and facility ID of each rack within a location must be unique. (Racks not assigned to the same location may have identical names and/or facility IDs.)
|
Each location must have a name that is unique within its parent site and location, if any.
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# Racks
|
# Racks
|
||||||
|
|
||||||
The rack model represents a physical two- or four-post equipment rack in which devices can be installed. Each rack must be assigned to a site, and may optionally be assigned to a location and/or tenant. Racks can also be organized by user-defined functional roles.
|
The rack model represents a physical two- or four-post equipment rack in which devices can be installed. Each rack must be assigned to a site, and may optionally be assigned to a location and/or tenant. Racks can also be organized by user-defined functional roles. The name and facility ID of each rack within a location must be unique.
|
||||||
|
|
||||||
Rack height is measured in *rack units* (U); racks are commonly between 42U and 48U tall, but NetBox allows you to define racks of arbitrary height. A toggle is provided to indicate whether rack units are in ascending (from the ground up) or descending order.
|
Rack height is measured in *rack units* (U); racks are commonly between 42U and 48U tall, but NetBox allows you to define racks of arbitrary height. A toggle is provided to indicate whether rack units are in ascending (from the ground up) or descending order.
|
||||||
|
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
# Regions
|
# Regions
|
||||||
|
|
||||||
Sites can be arranged geographically using regions. A region might represent a continent, country, city, campus, or other area depending on your use case. Regions can be nested recursively to construct a hierarchy. For example, you might define several country regions, and within each of those several state or city regions to which sites are assigned.
|
Sites can be arranged geographically using regions. A region might represent a continent, country, city, campus, or other area depending on your use case. Regions can be nested recursively to construct a hierarchy. For example, you might define several country regions, and within each of those several state or city regions to which sites are assigned.
|
||||||
|
|
||||||
|
Each region must have a name that is unique within its parent region, if any.
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
# Site Groups
|
# Site Groups
|
||||||
|
|
||||||
Like regions, site groups can be used to organize sites. Whereas regions are intended to provide geographic organization, site groups can be used to classify sites by role or function. Also like regions, site groups can be nested to form a hierarchy. Sites which belong to a child group are also considered to be members of any of its parent groups.
|
Like regions, site groups can be used to organize sites. Whereas regions are intended to provide geographic organization, site groups can be used to classify sites by role or function. Also like regions, site groups can be nested to form a hierarchy. Sites which belong to a child group are also considered to be members of any of its parent groups.
|
||||||
|
|
||||||
|
Each site group must have a name that is unique within its parent group, if any.
|
||||||
|
@ -15,6 +15,3 @@ The `tag` filter can be specified multiple times to match only objects which hav
|
|||||||
```no-highlight
|
```no-highlight
|
||||||
GET /api/dcim/devices/?tag=monitored&tag=deprecated
|
GET /api/dcim/devices/?tag=monitored&tag=deprecated
|
||||||
```
|
```
|
||||||
|
|
||||||
!!! note
|
|
||||||
Tags have changed substantially in NetBox v2.9. They are no longer created on-demand when editing an object, and their representation in the REST API now includes a complete depiction of the tag rather than only its label.
|
|
||||||
|
31
docs/models/tenancy/contact.md
Normal file
31
docs/models/tenancy/contact.md
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# Contacts
|
||||||
|
|
||||||
|
A contact represent an individual or group that has been associated with an object in NetBox for administrative reasons. For example, you might assign one or more operational contacts to each site. Contacts can be arranged within nested contact groups.
|
||||||
|
|
||||||
|
Each contact must include a name, which is unique to its parent group (if any). The following optional descriptors are also available:
|
||||||
|
|
||||||
|
* Title
|
||||||
|
* Phone
|
||||||
|
* Email
|
||||||
|
* Address
|
||||||
|
|
||||||
|
## Contact Assignment
|
||||||
|
|
||||||
|
Each contact can be assigned to one or more objects, allowing for the efficient reuse of contact information. When assigning a contact to an object, the user may optionally specify a role and/or priority (primary, secondary, tertiary, or inactive) to better convey the nature of the contact's relationship to the assigned object.
|
||||||
|
|
||||||
|
The following models support the assignment of contacts:
|
||||||
|
|
||||||
|
* circuits.Circuit
|
||||||
|
* circuits.Provider
|
||||||
|
* dcim.Device
|
||||||
|
* dcim.Location
|
||||||
|
* dcim.Manufacturer
|
||||||
|
* dcim.PowerPanel
|
||||||
|
* dcim.Rack
|
||||||
|
* dcim.Region
|
||||||
|
* dcim.Site
|
||||||
|
* dcim.SiteGroup
|
||||||
|
* tenancy.Tenant
|
||||||
|
* virtualization.Cluster
|
||||||
|
* virtualization.ClusterGroup
|
||||||
|
* virtualization.VirtualMachine
|
3
docs/models/tenancy/contactgroup.md
Normal file
3
docs/models/tenancy/contactgroup.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# Contact Groups
|
||||||
|
|
||||||
|
Contacts can be organized into arbitrary groups. These groups can be recursively nested for convenience. Each contact within a group must have a unique name, but other attributes can be repeated.
|
3
docs/models/tenancy/contactrole.md
Normal file
3
docs/models/tenancy/contactrole.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# Contact Roles
|
||||||
|
|
||||||
|
Contacts can be organized by functional roles, which are fully customizable by the user. For example, you might create roles for administrative, operational, or emergency contacts.
|
@ -1,5 +1,5 @@
|
|||||||
# Clusters
|
# Clusters
|
||||||
|
|
||||||
A cluster is a logical grouping of physical resources within which virtual machines run. A cluster must be assigned a type (technological classification), and may optionally be assigned to a cluster group, site, and/or tenant.
|
A cluster is a logical grouping of physical resources within which virtual machines run. A cluster must be assigned a type (technological classification), and may optionally be assigned to a cluster group, site, and/or tenant. Each cluster must have a unique name within its assigned group and/or site, if any.
|
||||||
|
|
||||||
Physical devices may be associated with clusters as hosts. This allows users to track on which host(s) a particular virtual machine may reside. However, NetBox does not support pinning a specific VM within a cluster to a particular host device.
|
Physical devices may be associated with clusters as hosts. This allows users to track on which host(s) a particular virtual machine may reside. However, NetBox does not support pinning a specific VM within a cluster to a particular host device.
|
||||||
|
@ -1,6 +1,27 @@
|
|||||||
# NetBox v3.0
|
# NetBox v3.0
|
||||||
|
|
||||||
## v3.0.8 (FUTURE)
|
## v3.0.9 (FUTURE)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v3.0.8 (2021-10-20)
|
||||||
|
|
||||||
|
### Enhancements
|
||||||
|
|
||||||
|
* [#7551](https://github.com/netbox-community/netbox/issues/7551) - Add UI field to filter interfaces by kind
|
||||||
|
* [#7561](https://github.com/netbox-community/netbox/issues/7561) - Add a utilization column to the IP ranges table
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* [#7300](https://github.com/netbox-community/netbox/issues/7300) - Fix incorrect Device LLDP interface row coloring
|
||||||
|
* [#7495](https://github.com/netbox-community/netbox/issues/7495) - Fix navigation UI issue that caused improper element overlap
|
||||||
|
* [#7529](https://github.com/netbox-community/netbox/issues/7529) - Restore horizontal scrolling for tables in narrow viewports
|
||||||
|
* [#7534](https://github.com/netbox-community/netbox/issues/7534) - Avoid exception when utilizing "create and add another" twice in succession
|
||||||
|
* [#7544](https://github.com/netbox-community/netbox/issues/7544) - Fix multi-value filtering of custom field objects
|
||||||
|
* [#7545](https://github.com/netbox-community/netbox/issues/7545) - Fix incorrect display of update/delete events for webhooks
|
||||||
|
* [#7550](https://github.com/netbox-community/netbox/issues/7550) - Fix rendering of UTF8-encoded data in change records
|
||||||
|
* [#7556](https://github.com/netbox-community/netbox/issues/7556) - Fix display of version when new release is available
|
||||||
|
* [#7584](https://github.com/netbox-community/netbox/issues/7584) - Fix alignment of object identifier under object view
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -3,13 +3,65 @@
|
|||||||
!!! warning "PostgreSQL 10 Required"
|
!!! warning "PostgreSQL 10 Required"
|
||||||
NetBox v3.1 requires PostgreSQL 10 or later.
|
NetBox v3.1 requires PostgreSQL 10 or later.
|
||||||
|
|
||||||
|
### Breaking Changes
|
||||||
|
|
||||||
|
* The `tenant` and `tenant_id` filters for the Cable model now filter on the tenant assigned directly to each cable, rather than on the parent object of either termination.
|
||||||
|
|
||||||
|
#### Contacts ([#1344](https://github.com/netbox-community/netbox/issues/1344))
|
||||||
|
|
||||||
|
A set of new models for tracking contact information has been introduced within the tenancy app. Users may now create individual contact objects to be associated with various models within NetBox. Each contact has a name, title, email address, etc. Contacts can be arranged in hierarchical groups for ease of management.
|
||||||
|
|
||||||
|
When assigning a contact to an object, the user must select a predefined role (e.g. "billing" or "technical") and may optionally indicate a priority relative to other contacts associated with the object. There is no limit on how many contacts can be assigned to an object, nor on how many objects to which a contact can be assigned.
|
||||||
|
|
||||||
|
####
|
||||||
|
|
||||||
### Enhancements
|
### Enhancements
|
||||||
|
|
||||||
* [#1337](https://github.com/netbox-community/netbox/issues/1337) - Add WWN field to interfaces
|
* [#1337](https://github.com/netbox-community/netbox/issues/1337) - Add WWN field to interfaces
|
||||||
|
* [#1943](https://github.com/netbox-community/netbox/issues/1943) - Relax uniqueness constraint on cluster names
|
||||||
* [#3839](https://github.com/netbox-community/netbox/issues/3839) - Add `airflow` field for devices types and devices
|
* [#3839](https://github.com/netbox-community/netbox/issues/3839) - Add `airflow` field for devices types and devices
|
||||||
|
* [#6497](https://github.com/netbox-community/netbox/issues/6497) - Extend tag support to organizational models
|
||||||
* [#6711](https://github.com/netbox-community/netbox/issues/6711) - Add `longtext` custom field type with Markdown support
|
* [#6711](https://github.com/netbox-community/netbox/issues/6711) - Add `longtext` custom field type with Markdown support
|
||||||
|
* [#6715](https://github.com/netbox-community/netbox/issues/6715) - Add tenant assignment for cables
|
||||||
* [#6874](https://github.com/netbox-community/netbox/issues/6874) - Add tenant assignment for locations
|
* [#6874](https://github.com/netbox-community/netbox/issues/6874) - Add tenant assignment for locations
|
||||||
|
* [#7354](https://github.com/netbox-community/netbox/issues/7354) - Relax uniqueness constraints on region, site group, and location names
|
||||||
|
* [#7530](https://github.com/netbox-community/netbox/issues/7530) - Move device type component lists to separate views
|
||||||
|
|
||||||
### Other Changes
|
### Other Changes
|
||||||
|
|
||||||
* [#7318](https://github.com/netbox-community/netbox/issues/7318) - Raise minimum required PostgreSQL version from 9.6 to 10
|
* [#7318](https://github.com/netbox-community/netbox/issues/7318) - Raise minimum required PostgreSQL version from 9.6 to 10
|
||||||
|
|
||||||
|
### REST API Changes
|
||||||
|
|
||||||
|
* Added the following endpoints for contacts:
|
||||||
|
* `/api/tenancy/contact-assignments/`
|
||||||
|
* `/api/tenancy/contact-groups/`
|
||||||
|
* `/api/tenancy/contact-roles/`
|
||||||
|
* `/api/tenancy/contacts/`
|
||||||
|
* Added `tags` field to the following models:
|
||||||
|
* circuits.CircuitType
|
||||||
|
* dcim.DeviceRole
|
||||||
|
* dcim.Location
|
||||||
|
* dcim.Manufacturer
|
||||||
|
* dcim.Platform
|
||||||
|
* dcim.RackRole
|
||||||
|
* dcim.Region
|
||||||
|
* dcim.SiteGroup
|
||||||
|
* ipam.RIR
|
||||||
|
* ipam.Role
|
||||||
|
* ipam.VLANGroup
|
||||||
|
* tenancy.ContactGroup
|
||||||
|
* tenancy.ContactRole
|
||||||
|
* tenancy.TenantGroup
|
||||||
|
* virtualization.ClusterGroup
|
||||||
|
* virtualization.ClusterType
|
||||||
|
* dcim.Cable
|
||||||
|
* Added `tenant` field
|
||||||
|
* dcim.Device
|
||||||
|
* Added `airflow` field
|
||||||
|
* dcim.DeviceType
|
||||||
|
* Added `airflow` field
|
||||||
|
* dcim.Interface
|
||||||
|
* Added `wwn` field
|
||||||
|
* dcim.Location
|
||||||
|
* Added `tenant` field
|
||||||
|
@ -63,10 +63,11 @@ nav:
|
|||||||
- Wireless: 'core-functionality/wireless.md'
|
- Wireless: 'core-functionality/wireless.md'
|
||||||
- Power Tracking: 'core-functionality/power.md'
|
- Power Tracking: 'core-functionality/power.md'
|
||||||
- Tenancy: 'core-functionality/tenancy.md'
|
- Tenancy: 'core-functionality/tenancy.md'
|
||||||
|
- Contacts: 'core-functionality/contacts.md'
|
||||||
- Customization:
|
- Customization:
|
||||||
- Custom Fields: 'customization/custom-fields.md'
|
- Custom Fields: 'customization/custom-fields.md'
|
||||||
- Custom Validation: 'customization/custom-validation.md'
|
- Custom Validation: 'customization/custom-validation.md'
|
||||||
- Custom Links: 'customization/custom-links.md'
|
- Custom Links: 'models/extras/customlink.md'
|
||||||
- Export Templates: 'customization/export-templates.md'
|
- Export Templates: 'customization/export-templates.md'
|
||||||
- Custom Scripts: 'customization/custom-scripts.md'
|
- Custom Scripts: 'customization/custom-scripts.md'
|
||||||
- Reports: 'customization/reports.md'
|
- Reports: 'customization/reports.md'
|
||||||
|
@ -5,9 +5,7 @@ from circuits.models import *
|
|||||||
from dcim.api.nested_serializers import NestedCableSerializer, NestedSiteSerializer
|
from dcim.api.nested_serializers import NestedCableSerializer, NestedSiteSerializer
|
||||||
from dcim.api.serializers import LinkTerminationSerializer
|
from dcim.api.serializers import LinkTerminationSerializer
|
||||||
from netbox.api import ChoiceField
|
from netbox.api import ChoiceField
|
||||||
from netbox.api.serializers import (
|
from netbox.api.serializers import PrimaryModelSerializer, ValidatedModelSerializer, WritableNestedSerializer
|
||||||
OrganizationalModelSerializer, PrimaryModelSerializer, ValidatedModelSerializer, WritableNestedSerializer
|
|
||||||
)
|
|
||||||
from tenancy.api.nested_serializers import NestedTenantSerializer
|
from tenancy.api.nested_serializers import NestedTenantSerializer
|
||||||
from .nested_serializers import *
|
from .nested_serializers import *
|
||||||
|
|
||||||
@ -48,14 +46,14 @@ class ProviderNetworkSerializer(PrimaryModelSerializer):
|
|||||||
# Circuits
|
# Circuits
|
||||||
#
|
#
|
||||||
|
|
||||||
class CircuitTypeSerializer(OrganizationalModelSerializer):
|
class CircuitTypeSerializer(PrimaryModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittype-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittype-detail')
|
||||||
circuit_count = serializers.IntegerField(read_only=True)
|
circuit_count = serializers.IntegerField(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = CircuitType
|
model = CircuitType
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'name', 'slug', 'description', 'custom_fields', 'created', 'last_updated',
|
'id', 'url', 'display', 'name', 'slug', 'description', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||||
'circuit_count',
|
'circuit_count',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -34,7 +34,7 @@ class ProviderViewSet(CustomFieldModelViewSet):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class CircuitTypeViewSet(CustomFieldModelViewSet):
|
class CircuitTypeViewSet(CustomFieldModelViewSet):
|
||||||
queryset = CircuitType.objects.annotate(
|
queryset = CircuitType.objects.prefetch_related('tags').annotate(
|
||||||
circuit_count=count_related(Circuit, 'type')
|
circuit_count=count_related(Circuit, 'type')
|
||||||
)
|
)
|
||||||
serializer_class = serializers.CircuitTypeSerializer
|
serializer_class = serializers.CircuitTypeSerializer
|
||||||
|
@ -79,7 +79,7 @@ class ProviderNetworkBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomField
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class CircuitTypeBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm):
|
class CircuitTypeBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||||
pk = forms.ModelMultipleChoiceField(
|
pk = forms.ModelMultipleChoiceField(
|
||||||
queryset=CircuitType.objects.all(),
|
queryset=CircuitType.objects.all(),
|
||||||
widget=forms.MultipleHiddenInput
|
widget=forms.MultipleHiddenInput
|
||||||
|
@ -75,11 +75,15 @@ class ProviderNetworkForm(BootstrapMixin, CustomFieldModelForm):
|
|||||||
|
|
||||||
class CircuitTypeForm(BootstrapMixin, CustomFieldModelForm):
|
class CircuitTypeForm(BootstrapMixin, CustomFieldModelForm):
|
||||||
slug = SlugField()
|
slug = SlugField()
|
||||||
|
tags = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=Tag.objects.all(),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = CircuitType
|
model = CircuitType
|
||||||
fields = [
|
fields = [
|
||||||
'name', 'slug', 'description',
|
'name', 'slug', 'description', 'tags',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
20
netbox/circuits/migrations/0003_extend_tag_support.py
Normal file
20
netbox/circuits/migrations/0003_extend_tag_support.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
# Generated by Django 3.2.8 on 2021-10-21 14:50
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
import taggit.managers
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('extras', '0062_clear_secrets_changelog'),
|
||||||
|
('circuits', '0002_squashed_0029'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='circuittype',
|
||||||
|
name='tags',
|
||||||
|
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
|
||||||
|
),
|
||||||
|
]
|
@ -4,7 +4,7 @@ from django.db import migrations
|
|||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('circuits', '0002_squashed_0029'),
|
('circuits', '0003_extend_tag_support'),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
@ -62,6 +62,11 @@ class Provider(PrimaryModel):
|
|||||||
blank=True
|
blank=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Generic relations
|
||||||
|
contacts = GenericRelation(
|
||||||
|
to='tenancy.ContactAssignment'
|
||||||
|
)
|
||||||
|
|
||||||
objects = RestrictedQuerySet.as_manager()
|
objects = RestrictedQuerySet.as_manager()
|
||||||
|
|
||||||
clone_fields = [
|
clone_fields = [
|
||||||
@ -123,7 +128,7 @@ class ProviderNetwork(PrimaryModel):
|
|||||||
return reverse('circuits:providernetwork', args=[self.pk])
|
return reverse('circuits:providernetwork', args=[self.pk])
|
||||||
|
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||||
class CircuitType(OrganizationalModel):
|
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
|
||||||
@ -203,6 +208,11 @@ class Circuit(PrimaryModel):
|
|||||||
comments = models.TextField(
|
comments = models.TextField(
|
||||||
blank=True
|
blank=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Generic relations
|
||||||
|
contacts = GenericRelation(
|
||||||
|
to='tenancy.ContactAssignment'
|
||||||
|
)
|
||||||
images = GenericRelation(
|
images = GenericRelation(
|
||||||
to='extras.ImageAttachment'
|
to='extras.ImageAttachment'
|
||||||
)
|
)
|
||||||
|
@ -82,6 +82,9 @@ class CircuitTypeTable(BaseTable):
|
|||||||
name = tables.Column(
|
name = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
|
tags = TagColumn(
|
||||||
|
url_name='circuits:circuittype_list'
|
||||||
|
)
|
||||||
circuit_count = tables.Column(
|
circuit_count = tables.Column(
|
||||||
verbose_name='Circuits'
|
verbose_name='Circuits'
|
||||||
)
|
)
|
||||||
@ -89,7 +92,7 @@ class CircuitTypeTable(BaseTable):
|
|||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = CircuitType
|
model = CircuitType
|
||||||
fields = ('pk', 'name', 'circuit_count', 'description', 'slug', 'actions')
|
fields = ('pk', 'name', 'circuit_count', 'description', 'slug', 'tags', 'actions')
|
||||||
default_columns = ('pk', 'name', 'circuit_count', 'description', 'slug', 'actions')
|
default_columns = ('pk', 'name', 'circuit_count', 'description', 'slug', 'actions')
|
||||||
|
|
||||||
|
|
||||||
|
@ -64,10 +64,13 @@ class CircuitTypeTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
|||||||
CircuitType(name='Circuit Type 3', slug='circuit-type-3'),
|
CircuitType(name='Circuit Type 3', slug='circuit-type-3'),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||||
|
|
||||||
cls.form_data = {
|
cls.form_data = {
|
||||||
'name': 'Circuit Type X',
|
'name': 'Circuit Type X',
|
||||||
'slug': 'circuit-type-x',
|
'slug': 'circuit-type-x',
|
||||||
'description': 'A new circuit type',
|
'description': 'A new circuit type',
|
||||||
|
'tags': [t.pk for t in tags],
|
||||||
}
|
}
|
||||||
|
|
||||||
cls.csv_data = (
|
cls.csv_data = (
|
||||||
|
@ -2,7 +2,6 @@ from django.conf import settings
|
|||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from drf_yasg.utils import swagger_serializer_method
|
from drf_yasg.utils import swagger_serializer_method
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.validators import UniqueTogetherValidator
|
|
||||||
from timezone_field.rest_framework import TimeZoneSerializerField
|
from timezone_field.rest_framework import TimeZoneSerializerField
|
||||||
|
|
||||||
from dcim.choices import *
|
from dcim.choices import *
|
||||||
@ -12,8 +11,7 @@ from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedVLANSer
|
|||||||
from ipam.models import VLAN
|
from ipam.models import VLAN
|
||||||
from netbox.api import ChoiceField, ContentTypeField, SerializedPKRelatedField
|
from netbox.api import ChoiceField, ContentTypeField, SerializedPKRelatedField
|
||||||
from netbox.api.serializers import (
|
from netbox.api.serializers import (
|
||||||
NestedGroupModelSerializer, OrganizationalModelSerializer, PrimaryModelSerializer, ValidatedModelSerializer,
|
NestedGroupModelSerializer, PrimaryModelSerializer, ValidatedModelSerializer, WritableNestedSerializer,
|
||||||
WritableNestedSerializer,
|
|
||||||
)
|
)
|
||||||
from tenancy.api.nested_serializers import NestedTenantSerializer
|
from tenancy.api.nested_serializers import NestedTenantSerializer
|
||||||
from users.api.nested_serializers import NestedUserSerializer
|
from users.api.nested_serializers import NestedUserSerializer
|
||||||
@ -83,27 +81,27 @@ class ConnectedEndpointSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
class RegionSerializer(NestedGroupModelSerializer):
|
class RegionSerializer(NestedGroupModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:region-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:region-detail')
|
||||||
parent = NestedRegionSerializer(required=False, allow_null=True)
|
parent = NestedRegionSerializer(required=False, allow_null=True, default=None)
|
||||||
site_count = serializers.IntegerField(read_only=True)
|
site_count = serializers.IntegerField(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Region
|
model = Region
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'custom_fields', 'created', 'last_updated',
|
'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields', 'created',
|
||||||
'site_count', '_depth',
|
'last_updated', 'site_count', '_depth',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class SiteGroupSerializer(NestedGroupModelSerializer):
|
class SiteGroupSerializer(NestedGroupModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:sitegroup-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:sitegroup-detail')
|
||||||
parent = NestedSiteGroupSerializer(required=False, allow_null=True)
|
parent = NestedSiteGroupSerializer(required=False, allow_null=True, default=None)
|
||||||
site_count = serializers.IntegerField(read_only=True)
|
site_count = serializers.IntegerField(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = SiteGroup
|
model = SiteGroup
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'custom_fields', 'created', 'last_updated',
|
'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields', 'created',
|
||||||
'site_count', '_depth',
|
'last_updated', 'site_count', '_depth',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -146,20 +144,20 @@ class LocationSerializer(NestedGroupModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Location
|
model = Location
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'name', 'slug', 'site', 'parent', 'tenant', 'description', 'custom_fields',
|
'id', 'url', 'display', 'name', 'slug', 'site', 'parent', 'tenant', 'description', 'tags', 'custom_fields',
|
||||||
'created', 'last_updated', 'rack_count', 'device_count', '_depth',
|
'created', 'last_updated', 'rack_count', 'device_count', '_depth',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class RackRoleSerializer(OrganizationalModelSerializer):
|
class RackRoleSerializer(PrimaryModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackrole-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackrole-detail')
|
||||||
rack_count = serializers.IntegerField(read_only=True)
|
rack_count = serializers.IntegerField(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = RackRole
|
model = RackRole
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'name', 'slug', 'color', 'description', 'custom_fields', 'created', 'last_updated',
|
'id', 'url', 'display', 'name', 'slug', 'color', 'description', 'tags', 'custom_fields', 'created',
|
||||||
'rack_count',
|
'last_updated', 'rack_count',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -171,6 +169,8 @@ class RackSerializer(PrimaryModelSerializer):
|
|||||||
status = ChoiceField(choices=RackStatusChoices, required=False)
|
status = ChoiceField(choices=RackStatusChoices, required=False)
|
||||||
role = NestedRackRoleSerializer(required=False, allow_null=True)
|
role = NestedRackRoleSerializer(required=False, allow_null=True)
|
||||||
type = ChoiceField(choices=RackTypeChoices, allow_blank=True, required=False)
|
type = ChoiceField(choices=RackTypeChoices, allow_blank=True, required=False)
|
||||||
|
facility_id = serializers.CharField(max_length=50, allow_blank=True, allow_null=True, label='Facility ID',
|
||||||
|
default=None)
|
||||||
width = ChoiceField(choices=RackWidthChoices, required=False)
|
width = ChoiceField(choices=RackWidthChoices, required=False)
|
||||||
outer_unit = ChoiceField(choices=RackDimensionUnitChoices, allow_blank=True, required=False)
|
outer_unit = ChoiceField(choices=RackDimensionUnitChoices, allow_blank=True, required=False)
|
||||||
device_count = serializers.IntegerField(read_only=True)
|
device_count = serializers.IntegerField(read_only=True)
|
||||||
@ -183,23 +183,6 @@ class RackSerializer(PrimaryModelSerializer):
|
|||||||
'asset_tag', 'type', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit',
|
'asset_tag', 'type', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit',
|
||||||
'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'device_count', 'powerfeed_count',
|
'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'device_count', 'powerfeed_count',
|
||||||
]
|
]
|
||||||
# Omit the UniqueTogetherValidator that would be automatically added to validate (location, facility_id). This
|
|
||||||
# prevents facility_id from being interpreted as a required field.
|
|
||||||
validators = [
|
|
||||||
UniqueTogetherValidator(queryset=Rack.objects.all(), fields=('location', 'name'))
|
|
||||||
]
|
|
||||||
|
|
||||||
def validate(self, data):
|
|
||||||
|
|
||||||
# Validate uniqueness of (location, facility_id) since we omitted the automatically-created validator from Meta.
|
|
||||||
if data.get('facility_id', None):
|
|
||||||
validator = UniqueTogetherValidator(queryset=Rack.objects.all(), fields=('location', 'facility_id'))
|
|
||||||
validator(data, self)
|
|
||||||
|
|
||||||
# Enforce model validation
|
|
||||||
super().validate(data)
|
|
||||||
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
class RackUnitSerializer(serializers.Serializer):
|
class RackUnitSerializer(serializers.Serializer):
|
||||||
@ -271,7 +254,7 @@ class RackElevationDetailFilterSerializer(serializers.Serializer):
|
|||||||
# Device types
|
# Device types
|
||||||
#
|
#
|
||||||
|
|
||||||
class ManufacturerSerializer(OrganizationalModelSerializer):
|
class ManufacturerSerializer(PrimaryModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:manufacturer-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:manufacturer-detail')
|
||||||
devicetype_count = serializers.IntegerField(read_only=True)
|
devicetype_count = serializers.IntegerField(read_only=True)
|
||||||
inventoryitem_count = serializers.IntegerField(read_only=True)
|
inventoryitem_count = serializers.IntegerField(read_only=True)
|
||||||
@ -280,7 +263,7 @@ class ManufacturerSerializer(OrganizationalModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Manufacturer
|
model = Manufacturer
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'name', 'slug', 'description', 'custom_fields', 'created', 'last_updated',
|
'id', 'url', 'display', 'name', 'slug', 'description', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||||
'devicetype_count', 'inventoryitem_count', 'platform_count',
|
'devicetype_count', 'inventoryitem_count', 'platform_count',
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -428,7 +411,7 @@ class DeviceBayTemplateSerializer(ValidatedModelSerializer):
|
|||||||
# Devices
|
# Devices
|
||||||
#
|
#
|
||||||
|
|
||||||
class DeviceRoleSerializer(OrganizationalModelSerializer):
|
class DeviceRoleSerializer(PrimaryModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicerole-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicerole-detail')
|
||||||
device_count = serializers.IntegerField(read_only=True)
|
device_count = serializers.IntegerField(read_only=True)
|
||||||
virtualmachine_count = serializers.IntegerField(read_only=True)
|
virtualmachine_count = serializers.IntegerField(read_only=True)
|
||||||
@ -436,12 +419,12 @@ class DeviceRoleSerializer(OrganizationalModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = DeviceRole
|
model = DeviceRole
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'name', 'slug', 'color', 'vm_role', 'description', 'custom_fields', 'created',
|
'id', 'url', 'display', 'name', 'slug', 'color', 'vm_role', 'description', 'tags', 'custom_fields',
|
||||||
'last_updated', 'device_count', 'virtualmachine_count',
|
'created', 'last_updated', 'device_count', 'virtualmachine_count',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class PlatformSerializer(OrganizationalModelSerializer):
|
class PlatformSerializer(PrimaryModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:platform-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:platform-detail')
|
||||||
manufacturer = NestedManufacturerSerializer(required=False, allow_null=True)
|
manufacturer = NestedManufacturerSerializer(required=False, allow_null=True)
|
||||||
device_count = serializers.IntegerField(read_only=True)
|
device_count = serializers.IntegerField(read_only=True)
|
||||||
@ -451,7 +434,7 @@ class PlatformSerializer(OrganizationalModelSerializer):
|
|||||||
model = Platform
|
model = Platform
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args', 'description',
|
'id', 'url', 'display', 'name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args', 'description',
|
||||||
'custom_fields', 'created', 'last_updated', 'device_count', 'virtualmachine_count',
|
'tags', 'custom_fields', 'created', 'last_updated', 'device_count', 'virtualmachine_count',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -459,12 +442,13 @@ class DeviceSerializer(PrimaryModelSerializer):
|
|||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:device-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:device-detail')
|
||||||
device_type = NestedDeviceTypeSerializer()
|
device_type = NestedDeviceTypeSerializer()
|
||||||
device_role = NestedDeviceRoleSerializer()
|
device_role = NestedDeviceRoleSerializer()
|
||||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
tenant = NestedTenantSerializer(required=False, allow_null=True, default=None)
|
||||||
platform = NestedPlatformSerializer(required=False, allow_null=True)
|
platform = NestedPlatformSerializer(required=False, allow_null=True)
|
||||||
site = NestedSiteSerializer()
|
site = NestedSiteSerializer()
|
||||||
location = NestedLocationSerializer(required=False, allow_null=True, default=None)
|
location = NestedLocationSerializer(required=False, allow_null=True, default=None)
|
||||||
rack = NestedRackSerializer(required=False, allow_null=True)
|
rack = NestedRackSerializer(required=False, allow_null=True, default=None)
|
||||||
face = ChoiceField(choices=DeviceFaceChoices, allow_blank=True, required=False)
|
face = ChoiceField(choices=DeviceFaceChoices, allow_blank=True, default='')
|
||||||
|
position = serializers.IntegerField(allow_null=True, label='Position (U)', min_value=1, default=None)
|
||||||
status = ChoiceField(choices=DeviceStatusChoices, required=False)
|
status = ChoiceField(choices=DeviceStatusChoices, required=False)
|
||||||
airflow = ChoiceField(choices=DeviceAirflowChoices, allow_blank=True, required=False)
|
airflow = ChoiceField(choices=DeviceAirflowChoices, allow_blank=True, required=False)
|
||||||
primary_ip = NestedIPAddressSerializer(read_only=True)
|
primary_ip = NestedIPAddressSerializer(read_only=True)
|
||||||
@ -472,7 +456,8 @@ class DeviceSerializer(PrimaryModelSerializer):
|
|||||||
primary_ip6 = NestedIPAddressSerializer(required=False, allow_null=True)
|
primary_ip6 = NestedIPAddressSerializer(required=False, allow_null=True)
|
||||||
parent_device = serializers.SerializerMethodField()
|
parent_device = serializers.SerializerMethodField()
|
||||||
cluster = NestedClusterSerializer(required=False, allow_null=True)
|
cluster = NestedClusterSerializer(required=False, allow_null=True)
|
||||||
virtual_chassis = NestedVirtualChassisSerializer(required=False, allow_null=True)
|
virtual_chassis = NestedVirtualChassisSerializer(required=False, allow_null=True, default=None)
|
||||||
|
vc_position = serializers.IntegerField(allow_null=True, max_value=255, min_value=0, default=None)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Device
|
model = Device
|
||||||
@ -482,19 +467,6 @@ class DeviceSerializer(PrimaryModelSerializer):
|
|||||||
'primary_ip4', 'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments',
|
'primary_ip4', 'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments',
|
||||||
'local_context_data', 'tags', 'custom_fields', 'created', 'last_updated',
|
'local_context_data', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||||
]
|
]
|
||||||
validators = []
|
|
||||||
|
|
||||||
def validate(self, data):
|
|
||||||
|
|
||||||
# Validate uniqueness of (rack, position, face) since we omitted the automatically-created validator from Meta.
|
|
||||||
if data.get('rack') and data.get('position') and data.get('face'):
|
|
||||||
validator = UniqueTogetherValidator(queryset=Device.objects.all(), fields=('rack', 'position', 'face'))
|
|
||||||
validator(data, self)
|
|
||||||
|
|
||||||
# Enforce model validation
|
|
||||||
super().validate(data)
|
|
||||||
|
|
||||||
return data
|
|
||||||
|
|
||||||
@swagger_serializer_method(serializer_or_field=NestedDeviceSerializer)
|
@swagger_serializer_method(serializer_or_field=NestedDeviceSerializer)
|
||||||
def get_parent_device(self, obj):
|
def get_parent_device(self, obj):
|
||||||
@ -733,7 +705,6 @@ class DeviceBaySerializer(PrimaryModelSerializer):
|
|||||||
class InventoryItemSerializer(PrimaryModelSerializer):
|
class InventoryItemSerializer(PrimaryModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitem-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitem-detail')
|
||||||
device = NestedDeviceSerializer()
|
device = NestedDeviceSerializer()
|
||||||
# Provide a default value to satisfy UniqueTogetherValidator
|
|
||||||
parent = serializers.PrimaryKeyRelatedField(queryset=InventoryItem.objects.all(), allow_null=True, default=None)
|
parent = serializers.PrimaryKeyRelatedField(queryset=InventoryItem.objects.all(), allow_null=True, default=None)
|
||||||
manufacturer = NestedManufacturerSerializer(required=False, allow_null=True, default=None)
|
manufacturer = NestedManufacturerSerializer(required=False, allow_null=True, default=None)
|
||||||
_depth = serializers.IntegerField(source='level', read_only=True)
|
_depth = serializers.IntegerField(source='level', read_only=True)
|
||||||
@ -761,14 +732,15 @@ class CableSerializer(PrimaryModelSerializer):
|
|||||||
termination_a = serializers.SerializerMethodField(read_only=True)
|
termination_a = serializers.SerializerMethodField(read_only=True)
|
||||||
termination_b = serializers.SerializerMethodField(read_only=True)
|
termination_b = serializers.SerializerMethodField(read_only=True)
|
||||||
status = ChoiceField(choices=LinkStatusChoices, required=False)
|
status = ChoiceField(choices=LinkStatusChoices, required=False)
|
||||||
|
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||||
length_unit = ChoiceField(choices=CableLengthUnitChoices, allow_blank=True, required=False)
|
length_unit = ChoiceField(choices=CableLengthUnitChoices, allow_blank=True, required=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Cable
|
model = Cable
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'termination_a_type', 'termination_a_id', 'termination_a', 'termination_b_type',
|
'id', 'url', 'display', 'termination_a_type', 'termination_a_id', 'termination_a', 'termination_b_type',
|
||||||
'termination_b_id', 'termination_b', 'type', 'status', 'label', 'color', 'length', 'length_unit', 'tags',
|
'termination_b_id', 'termination_b', 'type', 'status', 'tenant', 'label', 'color', 'length', 'length_unit',
|
||||||
'custom_fields',
|
'tags', 'custom_fields',
|
||||||
]
|
]
|
||||||
|
|
||||||
def _get_termination(self, obj, side):
|
def _get_termination(self, obj, side):
|
||||||
|
@ -110,7 +110,7 @@ class RegionViewSet(CustomFieldModelViewSet):
|
|||||||
'region',
|
'region',
|
||||||
'site_count',
|
'site_count',
|
||||||
cumulative=True
|
cumulative=True
|
||||||
)
|
).prefetch_related('tags')
|
||||||
serializer_class = serializers.RegionSerializer
|
serializer_class = serializers.RegionSerializer
|
||||||
filterset_class = filtersets.RegionFilterSet
|
filterset_class = filtersets.RegionFilterSet
|
||||||
|
|
||||||
@ -126,7 +126,7 @@ class SiteGroupViewSet(CustomFieldModelViewSet):
|
|||||||
'group',
|
'group',
|
||||||
'site_count',
|
'site_count',
|
||||||
cumulative=True
|
cumulative=True
|
||||||
)
|
).prefetch_related('tags')
|
||||||
serializer_class = serializers.SiteGroupSerializer
|
serializer_class = serializers.SiteGroupSerializer
|
||||||
filterset_class = filtersets.SiteGroupFilterSet
|
filterset_class = filtersets.SiteGroupFilterSet
|
||||||
|
|
||||||
@ -167,7 +167,7 @@ class LocationViewSet(CustomFieldModelViewSet):
|
|||||||
'location',
|
'location',
|
||||||
'rack_count',
|
'rack_count',
|
||||||
cumulative=True
|
cumulative=True
|
||||||
).prefetch_related('site')
|
).prefetch_related('site', 'tags')
|
||||||
serializer_class = serializers.LocationSerializer
|
serializer_class = serializers.LocationSerializer
|
||||||
filterset_class = filtersets.LocationFilterSet
|
filterset_class = filtersets.LocationFilterSet
|
||||||
|
|
||||||
@ -177,7 +177,7 @@ class LocationViewSet(CustomFieldModelViewSet):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class RackRoleViewSet(CustomFieldModelViewSet):
|
class RackRoleViewSet(CustomFieldModelViewSet):
|
||||||
queryset = RackRole.objects.annotate(
|
queryset = RackRole.objects.prefetch_related('tags').annotate(
|
||||||
rack_count=count_related(Rack, 'role')
|
rack_count=count_related(Rack, 'role')
|
||||||
)
|
)
|
||||||
serializer_class = serializers.RackRoleSerializer
|
serializer_class = serializers.RackRoleSerializer
|
||||||
@ -261,7 +261,7 @@ class RackReservationViewSet(ModelViewSet):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class ManufacturerViewSet(CustomFieldModelViewSet):
|
class ManufacturerViewSet(CustomFieldModelViewSet):
|
||||||
queryset = Manufacturer.objects.annotate(
|
queryset = Manufacturer.objects.prefetch_related('tags').annotate(
|
||||||
devicetype_count=count_related(DeviceType, 'manufacturer'),
|
devicetype_count=count_related(DeviceType, 'manufacturer'),
|
||||||
inventoryitem_count=count_related(InventoryItem, 'manufacturer'),
|
inventoryitem_count=count_related(InventoryItem, 'manufacturer'),
|
||||||
platform_count=count_related(Platform, 'manufacturer')
|
platform_count=count_related(Platform, 'manufacturer')
|
||||||
@ -340,7 +340,7 @@ class DeviceBayTemplateViewSet(ModelViewSet):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class DeviceRoleViewSet(CustomFieldModelViewSet):
|
class DeviceRoleViewSet(CustomFieldModelViewSet):
|
||||||
queryset = DeviceRole.objects.annotate(
|
queryset = DeviceRole.objects.prefetch_related('tags').annotate(
|
||||||
device_count=count_related(Device, 'device_role'),
|
device_count=count_related(Device, 'device_role'),
|
||||||
virtualmachine_count=count_related(VirtualMachine, 'role')
|
virtualmachine_count=count_related(VirtualMachine, 'role')
|
||||||
)
|
)
|
||||||
@ -353,7 +353,7 @@ class DeviceRoleViewSet(CustomFieldModelViewSet):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class PlatformViewSet(CustomFieldModelViewSet):
|
class PlatformViewSet(CustomFieldModelViewSet):
|
||||||
queryset = Platform.objects.annotate(
|
queryset = Platform.objects.prefetch_related('tags').annotate(
|
||||||
device_count=count_related(Device, 'platform'),
|
device_count=count_related(Device, 'platform'),
|
||||||
virtualmachine_count=count_related(VirtualMachine, 'platform')
|
virtualmachine_count=count_related(VirtualMachine, 'platform')
|
||||||
)
|
)
|
||||||
|
@ -704,6 +704,18 @@ class PowerOutletFeedLegChoices(ChoiceSet):
|
|||||||
# Interfaces
|
# Interfaces
|
||||||
#
|
#
|
||||||
|
|
||||||
|
class InterfaceKindChoices(ChoiceSet):
|
||||||
|
KIND_PHYSICAL = 'physical'
|
||||||
|
KIND_VIRTUAL = 'virtual'
|
||||||
|
KIND_WIRELESS = 'wireless'
|
||||||
|
|
||||||
|
CHOICES = (
|
||||||
|
(KIND_PHYSICAL, 'Physical'),
|
||||||
|
(KIND_VIRTUAL, 'Virtual'),
|
||||||
|
(KIND_WIRELESS, 'Wireless'),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class InterfaceTypeChoices(ChoiceSet):
|
class InterfaceTypeChoices(ChoiceSet):
|
||||||
|
|
||||||
# Virtual
|
# Virtual
|
||||||
|
@ -1199,7 +1199,7 @@ class VirtualChassisFilterSet(PrimaryModelFilterSet):
|
|||||||
return queryset.filter(qs_filter).distinct()
|
return queryset.filter(qs_filter).distinct()
|
||||||
|
|
||||||
|
|
||||||
class CableFilterSet(PrimaryModelFilterSet):
|
class CableFilterSet(TenancyFilterSet, PrimaryModelFilterSet):
|
||||||
q = django_filters.CharFilter(
|
q = django_filters.CharFilter(
|
||||||
method='search',
|
method='search',
|
||||||
label='Search',
|
label='Search',
|
||||||
@ -1240,14 +1240,6 @@ class CableFilterSet(PrimaryModelFilterSet):
|
|||||||
method='filter_device',
|
method='filter_device',
|
||||||
field_name='device__site__slug'
|
field_name='device__site__slug'
|
||||||
)
|
)
|
||||||
tenant_id = MultiValueNumberFilter(
|
|
||||||
method='filter_device',
|
|
||||||
field_name='device__tenant_id'
|
|
||||||
)
|
|
||||||
tenant = MultiValueNumberFilter(
|
|
||||||
method='filter_device',
|
|
||||||
field_name='device__tenant__slug'
|
|
||||||
)
|
|
||||||
tag = TagFilter()
|
tag = TagFilter()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -51,7 +51,7 @@ __all__ = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class RegionBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm):
|
class RegionBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||||
pk = forms.ModelMultipleChoiceField(
|
pk = forms.ModelMultipleChoiceField(
|
||||||
queryset=Region.objects.all(),
|
queryset=Region.objects.all(),
|
||||||
widget=forms.MultipleHiddenInput
|
widget=forms.MultipleHiddenInput
|
||||||
@ -69,7 +69,7 @@ class RegionBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm):
|
|||||||
nullable_fields = ['parent', 'description']
|
nullable_fields = ['parent', 'description']
|
||||||
|
|
||||||
|
|
||||||
class SiteGroupBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm):
|
class SiteGroupBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||||
pk = forms.ModelMultipleChoiceField(
|
pk = forms.ModelMultipleChoiceField(
|
||||||
queryset=SiteGroup.objects.all(),
|
queryset=SiteGroup.objects.all(),
|
||||||
widget=forms.MultipleHiddenInput
|
widget=forms.MultipleHiddenInput
|
||||||
@ -132,7 +132,7 @@ class SiteBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEd
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class LocationBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm):
|
class LocationBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||||
pk = forms.ModelMultipleChoiceField(
|
pk = forms.ModelMultipleChoiceField(
|
||||||
queryset=Location.objects.all(),
|
queryset=Location.objects.all(),
|
||||||
widget=forms.MultipleHiddenInput
|
widget=forms.MultipleHiddenInput
|
||||||
@ -161,7 +161,7 @@ class LocationBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm):
|
|||||||
nullable_fields = ['parent', 'tenant', 'description']
|
nullable_fields = ['parent', 'tenant', 'description']
|
||||||
|
|
||||||
|
|
||||||
class RackRoleBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm):
|
class RackRoleBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||||
pk = forms.ModelMultipleChoiceField(
|
pk = forms.ModelMultipleChoiceField(
|
||||||
queryset=RackRole.objects.all(),
|
queryset=RackRole.objects.all(),
|
||||||
widget=forms.MultipleHiddenInput
|
widget=forms.MultipleHiddenInput
|
||||||
@ -303,7 +303,7 @@ class RackReservationBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomField
|
|||||||
nullable_fields = []
|
nullable_fields = []
|
||||||
|
|
||||||
|
|
||||||
class ManufacturerBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm):
|
class ManufacturerBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||||
pk = forms.ModelMultipleChoiceField(
|
pk = forms.ModelMultipleChoiceField(
|
||||||
queryset=Manufacturer.objects.all(),
|
queryset=Manufacturer.objects.all(),
|
||||||
widget=forms.MultipleHiddenInput
|
widget=forms.MultipleHiddenInput
|
||||||
@ -345,7 +345,7 @@ class DeviceTypeBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModel
|
|||||||
nullable_fields = ['airflow']
|
nullable_fields = ['airflow']
|
||||||
|
|
||||||
|
|
||||||
class DeviceRoleBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm):
|
class DeviceRoleBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||||
pk = forms.ModelMultipleChoiceField(
|
pk = forms.ModelMultipleChoiceField(
|
||||||
queryset=DeviceRole.objects.all(),
|
queryset=DeviceRole.objects.all(),
|
||||||
widget=forms.MultipleHiddenInput
|
widget=forms.MultipleHiddenInput
|
||||||
@ -367,7 +367,7 @@ class DeviceRoleBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm):
|
|||||||
nullable_fields = ['color', 'description']
|
nullable_fields = ['color', 'description']
|
||||||
|
|
||||||
|
|
||||||
class PlatformBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm):
|
class PlatformBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||||
pk = forms.ModelMultipleChoiceField(
|
pk = forms.ModelMultipleChoiceField(
|
||||||
queryset=Platform.objects.all(),
|
queryset=Platform.objects.all(),
|
||||||
widget=forms.MultipleHiddenInput
|
widget=forms.MultipleHiddenInput
|
||||||
@ -468,6 +468,10 @@ class CableBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkE
|
|||||||
widget=StaticSelect(),
|
widget=StaticSelect(),
|
||||||
initial=''
|
initial=''
|
||||||
)
|
)
|
||||||
|
tenant = DynamicModelChoiceField(
|
||||||
|
queryset=Tenant.objects.all(),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
label = forms.CharField(
|
label = forms.CharField(
|
||||||
max_length=100,
|
max_length=100,
|
||||||
required=False
|
required=False
|
||||||
@ -488,7 +492,7 @@ class CableBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkE
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
nullable_fields = [
|
nullable_fields = [
|
||||||
'type', 'status', 'label', 'color', 'length',
|
'type', 'status', 'tenant', 'label', 'color', 'length',
|
||||||
]
|
]
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
|
@ -828,6 +828,12 @@ class CableCSVForm(CustomFieldModelCSVForm):
|
|||||||
required=False,
|
required=False,
|
||||||
help_text='Physical medium classification'
|
help_text='Physical medium classification'
|
||||||
)
|
)
|
||||||
|
tenant = CSVModelChoiceField(
|
||||||
|
queryset=Tenant.objects.all(),
|
||||||
|
required=False,
|
||||||
|
to_field_name='name',
|
||||||
|
help_text='Assigned tenant'
|
||||||
|
)
|
||||||
length_unit = CSVChoiceField(
|
length_unit = CSVChoiceField(
|
||||||
choices=CableLengthUnitChoices,
|
choices=CableLengthUnitChoices,
|
||||||
required=False,
|
required=False,
|
||||||
@ -838,7 +844,7 @@ class CableCSVForm(CustomFieldModelCSVForm):
|
|||||||
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', 'label', 'color', 'length', 'length_unit',
|
'status', 'tenant', 'label', 'color', 'length', 'length_unit',
|
||||||
]
|
]
|
||||||
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>)'),
|
||||||
|
@ -2,6 +2,7 @@ from circuits.models import Circuit, CircuitTermination, Provider
|
|||||||
from dcim.models import *
|
from dcim.models import *
|
||||||
from extras.forms import CustomFieldModelForm
|
from extras.forms import CustomFieldModelForm
|
||||||
from extras.models import Tag
|
from extras.models import Tag
|
||||||
|
from tenancy.forms import TenancyForm
|
||||||
from utilities.forms import BootstrapMixin, DynamicModelChoiceField, DynamicModelMultipleChoiceField, StaticSelect
|
from utilities.forms import BootstrapMixin, DynamicModelChoiceField, DynamicModelMultipleChoiceField, StaticSelect
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
@ -17,7 +18,7 @@ __all__ = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ConnectCableToDeviceForm(BootstrapMixin, CustomFieldModelForm):
|
class ConnectCableToDeviceForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||||
"""
|
"""
|
||||||
Base form for connecting a Cable to a Device component
|
Base form for connecting a Cable to a Device component
|
||||||
"""
|
"""
|
||||||
@ -78,7 +79,8 @@ class ConnectCableToDeviceForm(BootstrapMixin, CustomFieldModelForm):
|
|||||||
model = Cable
|
model = Cable
|
||||||
fields = [
|
fields = [
|
||||||
'termination_b_region', 'termination_b_site', 'termination_b_rack', 'termination_b_device',
|
'termination_b_region', 'termination_b_site', 'termination_b_rack', 'termination_b_device',
|
||||||
'termination_b_id', 'type', 'status', 'label', 'color', 'length', 'length_unit', 'tags',
|
'termination_b_id', 'type', 'status', 'tenant_group', 'tenant', 'label', 'color', 'length', 'length_unit',
|
||||||
|
'tags',
|
||||||
]
|
]
|
||||||
widgets = {
|
widgets = {
|
||||||
'status': StaticSelect,
|
'status': StaticSelect,
|
||||||
@ -169,7 +171,7 @@ class ConnectCableToRearPortForm(ConnectCableToDeviceForm):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ConnectCableToCircuitTerminationForm(BootstrapMixin, CustomFieldModelForm):
|
class ConnectCableToCircuitTerminationForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||||
termination_b_provider = DynamicModelChoiceField(
|
termination_b_provider = DynamicModelChoiceField(
|
||||||
queryset=Provider.objects.all(),
|
queryset=Provider.objects.all(),
|
||||||
label='Provider',
|
label='Provider',
|
||||||
@ -219,7 +221,8 @@ class ConnectCableToCircuitTerminationForm(BootstrapMixin, CustomFieldModelForm)
|
|||||||
model = Cable
|
model = Cable
|
||||||
fields = [
|
fields = [
|
||||||
'termination_b_provider', 'termination_b_region', 'termination_b_site', 'termination_b_circuit',
|
'termination_b_provider', 'termination_b_region', 'termination_b_site', 'termination_b_circuit',
|
||||||
'termination_b_id', 'type', 'status', 'label', 'color', 'length', 'length_unit', 'tags',
|
'termination_b_id', 'type', 'status', 'tenant_group', 'tenant', 'label', 'color', 'length', 'length_unit',
|
||||||
|
'tags',
|
||||||
]
|
]
|
||||||
|
|
||||||
def clean_termination_b_id(self):
|
def clean_termination_b_id(self):
|
||||||
@ -227,7 +230,7 @@ class ConnectCableToCircuitTerminationForm(BootstrapMixin, CustomFieldModelForm)
|
|||||||
return getattr(self.cleaned_data['termination_b_id'], 'pk', None)
|
return getattr(self.cleaned_data['termination_b_id'], 'pk', None)
|
||||||
|
|
||||||
|
|
||||||
class ConnectCableToPowerFeedForm(BootstrapMixin, CustomFieldModelForm):
|
class ConnectCableToPowerFeedForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||||
termination_b_region = DynamicModelChoiceField(
|
termination_b_region = DynamicModelChoiceField(
|
||||||
queryset=Region.objects.all(),
|
queryset=Region.objects.all(),
|
||||||
label='Region',
|
label='Region',
|
||||||
@ -280,8 +283,8 @@ class ConnectCableToPowerFeedForm(BootstrapMixin, CustomFieldModelForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Cable
|
model = Cable
|
||||||
fields = [
|
fields = [
|
||||||
'termination_b_location', 'termination_b_powerpanel', 'termination_b_id', 'type', 'status', 'label',
|
'termination_b_location', 'termination_b_powerpanel', 'termination_b_id', 'type', 'status', 'tenant_group',
|
||||||
'color', 'length', 'length_unit', 'tags',
|
'tenant', 'label', 'color', 'length', 'length_unit', 'tags',
|
||||||
]
|
]
|
||||||
|
|
||||||
def clean_termination_b_id(self):
|
def clean_termination_b_id(self):
|
||||||
|
@ -7,7 +7,6 @@ from dcim.constants import *
|
|||||||
from dcim.models import *
|
from dcim.models import *
|
||||||
from extras.forms import CustomFieldModelFilterForm, LocalConfigContextFilterForm
|
from extras.forms import CustomFieldModelFilterForm, LocalConfigContextFilterForm
|
||||||
from tenancy.forms import TenancyFilterForm
|
from tenancy.forms import TenancyFilterForm
|
||||||
from tenancy.models import Tenant
|
|
||||||
from utilities.forms import (
|
from utilities.forms import (
|
||||||
APISelectMultiple, add_blank_choice, BootstrapMixin, ColorField, DynamicModelMultipleChoiceField, StaticSelect,
|
APISelectMultiple, add_blank_choice, BootstrapMixin, ColorField, DynamicModelMultipleChoiceField, StaticSelect,
|
||||||
StaticSelectMultiple, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES,
|
StaticSelectMultiple, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES,
|
||||||
@ -692,13 +691,13 @@ class VirtualChassisFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldMod
|
|||||||
tag = TagFilterField(model)
|
tag = TagFilterField(model)
|
||||||
|
|
||||||
|
|
||||||
class CableFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
|
class CableFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterForm):
|
||||||
model = Cable
|
model = Cable
|
||||||
field_groups = [
|
field_groups = [
|
||||||
['q', 'tag'],
|
['q', 'tag'],
|
||||||
['site_id', 'rack_id', 'device_id'],
|
['site_id', 'rack_id', 'device_id'],
|
||||||
['type', 'status', 'color'],
|
['type', 'status', 'color'],
|
||||||
['tenant_id'],
|
['tenant_group_id', 'tenant_id'],
|
||||||
]
|
]
|
||||||
q = forms.CharField(
|
q = forms.CharField(
|
||||||
required=False,
|
required=False,
|
||||||
@ -720,12 +719,6 @@ class CableFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
|
|||||||
label=_('Site'),
|
label=_('Site'),
|
||||||
fetch_trigger='open'
|
fetch_trigger='open'
|
||||||
)
|
)
|
||||||
tenant_id = DynamicModelMultipleChoiceField(
|
|
||||||
queryset=Tenant.objects.all(),
|
|
||||||
required=False,
|
|
||||||
label=_('Tenant'),
|
|
||||||
fetch_trigger='open'
|
|
||||||
)
|
|
||||||
rack_id = DynamicModelMultipleChoiceField(
|
rack_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=Rack.objects.all(),
|
queryset=Rack.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
@ -973,10 +966,15 @@ class InterfaceFilterForm(DeviceComponentFilterForm):
|
|||||||
model = Interface
|
model = Interface
|
||||||
field_groups = [
|
field_groups = [
|
||||||
['q', 'tag'],
|
['q', 'tag'],
|
||||||
['name', 'label', 'type', 'enabled', 'mgmt_only', 'mac_address', 'wwn'],
|
['name', 'label', 'kind', 'type', 'enabled', 'mgmt_only', 'mac_address', 'wwn'],
|
||||||
['rf_role', 'rf_channel', 'rf_channel_width'],
|
['rf_role', 'rf_channel', 'rf_channel_width'],
|
||||||
['region_id', 'site_group_id', 'site_id', 'location_id', 'device_id'],
|
['region_id', 'site_group_id', 'site_id', 'location_id', 'device_id'],
|
||||||
]
|
]
|
||||||
|
kind = forms.MultipleChoiceField(
|
||||||
|
choices=InterfaceKindChoices,
|
||||||
|
required=False,
|
||||||
|
widget=StaticSelectMultiple()
|
||||||
|
)
|
||||||
type = forms.MultipleChoiceField(
|
type = forms.MultipleChoiceField(
|
||||||
choices=InterfaceTypeChoices,
|
choices=InterfaceTypeChoices,
|
||||||
required=False,
|
required=False,
|
||||||
|
@ -71,11 +71,15 @@ class RegionForm(BootstrapMixin, CustomFieldModelForm):
|
|||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
slug = SlugField()
|
slug = SlugField()
|
||||||
|
tags = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=Tag.objects.all(),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Region
|
model = Region
|
||||||
fields = (
|
fields = (
|
||||||
'parent', 'name', 'slug', 'description',
|
'parent', 'name', 'slug', 'description', 'tags',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -85,11 +89,15 @@ class SiteGroupForm(BootstrapMixin, CustomFieldModelForm):
|
|||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
slug = SlugField()
|
slug = SlugField()
|
||||||
|
tags = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=Tag.objects.all(),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = SiteGroup
|
model = SiteGroup
|
||||||
fields = (
|
fields = (
|
||||||
'parent', 'name', 'slug', 'description',
|
'parent', 'name', 'slug', 'description', 'tags',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -188,15 +196,19 @@ class LocationForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
slug = SlugField()
|
slug = SlugField()
|
||||||
|
tags = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=Tag.objects.all(),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Location
|
model = Location
|
||||||
fields = (
|
fields = (
|
||||||
'region', 'site_group', 'site', 'parent', 'name', 'slug', 'description', 'tenant_group', 'tenant',
|
'region', 'site_group', 'site', 'parent', 'name', 'slug', 'description', 'tenant_group', 'tenant', 'tags',
|
||||||
)
|
)
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
('Location', (
|
('Location', (
|
||||||
'region', 'site_group', 'site', 'parent', 'name', 'slug', 'description',
|
'region', 'site_group', 'site', 'parent', 'name', 'slug', 'description', 'tags',
|
||||||
)),
|
)),
|
||||||
('Tenancy', ('tenant_group', 'tenant')),
|
('Tenancy', ('tenant_group', 'tenant')),
|
||||||
)
|
)
|
||||||
@ -204,11 +216,15 @@ class LocationForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
|||||||
|
|
||||||
class RackRoleForm(BootstrapMixin, CustomFieldModelForm):
|
class RackRoleForm(BootstrapMixin, CustomFieldModelForm):
|
||||||
slug = SlugField()
|
slug = SlugField()
|
||||||
|
tags = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=Tag.objects.all(),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = RackRole
|
model = RackRole
|
||||||
fields = [
|
fields = [
|
||||||
'name', 'slug', 'color', 'description',
|
'name', 'slug', 'color', 'description', 'tags',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -344,11 +360,15 @@ class RackReservationForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
|||||||
|
|
||||||
class ManufacturerForm(BootstrapMixin, CustomFieldModelForm):
|
class ManufacturerForm(BootstrapMixin, CustomFieldModelForm):
|
||||||
slug = SlugField()
|
slug = SlugField()
|
||||||
|
tags = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=Tag.objects.all(),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Manufacturer
|
model = Manufacturer
|
||||||
fields = [
|
fields = [
|
||||||
'name', 'slug', 'description',
|
'name', 'slug', 'description', 'tags',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -393,11 +413,15 @@ class DeviceTypeForm(BootstrapMixin, CustomFieldModelForm):
|
|||||||
|
|
||||||
class DeviceRoleForm(BootstrapMixin, CustomFieldModelForm):
|
class DeviceRoleForm(BootstrapMixin, CustomFieldModelForm):
|
||||||
slug = SlugField()
|
slug = SlugField()
|
||||||
|
tags = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=Tag.objects.all(),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = DeviceRole
|
model = DeviceRole
|
||||||
fields = [
|
fields = [
|
||||||
'name', 'slug', 'color', 'vm_role', 'description',
|
'name', 'slug', 'color', 'vm_role', 'description', 'tags',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -409,11 +433,15 @@ class PlatformForm(BootstrapMixin, CustomFieldModelForm):
|
|||||||
slug = SlugField(
|
slug = SlugField(
|
||||||
max_length=64
|
max_length=64
|
||||||
)
|
)
|
||||||
|
tags = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=Tag.objects.all(),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Platform
|
model = Platform
|
||||||
fields = [
|
fields = [
|
||||||
'name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args', 'description',
|
'name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args', 'description', 'tags',
|
||||||
]
|
]
|
||||||
widgets = {
|
widgets = {
|
||||||
'napalm_args': SmallTextarea(),
|
'napalm_args': SmallTextarea(),
|
||||||
@ -602,7 +630,7 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
|||||||
self.fields['position'].widget.choices = [(position, f'U{position}')]
|
self.fields['position'].widget.choices = [(position, f'U{position}')]
|
||||||
|
|
||||||
|
|
||||||
class CableForm(BootstrapMixin, CustomFieldModelForm):
|
class CableForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||||
tags = DynamicModelMultipleChoiceField(
|
tags = DynamicModelMultipleChoiceField(
|
||||||
queryset=Tag.objects.all(),
|
queryset=Tag.objects.all(),
|
||||||
required=False
|
required=False
|
||||||
@ -611,7 +639,7 @@ class CableForm(BootstrapMixin, CustomFieldModelForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Cable
|
model = Cable
|
||||||
fields = [
|
fields = [
|
||||||
'type', 'status', 'label', 'color', 'length', 'length_unit', 'tags',
|
'type', 'status', 'tenant_group', 'tenant', 'label', 'color', 'length', 'length_unit', 'tags',
|
||||||
]
|
]
|
||||||
widgets = {
|
widgets = {
|
||||||
'status': StaticSelect,
|
'status': StaticSelect,
|
||||||
|
@ -15,4 +15,9 @@ class Migration(migrations.Migration):
|
|||||||
name='tenant',
|
name='tenant',
|
||||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='locations', to='tenancy.tenant'),
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='locations', to='tenancy.tenant'),
|
||||||
),
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='cable',
|
||||||
|
name='tenant',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='cables', to='tenancy.tenant'),
|
||||||
|
),
|
||||||
]
|
]
|
@ -4,7 +4,7 @@ from django.db import migrations, models
|
|||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('dcim', '0135_location_tenant'),
|
('dcim', '0135_tenancy_extensions'),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
|
45
netbox/dcim/migrations/0137_relax_uniqueness_constraints.py
Normal file
45
netbox/dcim/migrations/0137_relax_uniqueness_constraints.py
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
# Generated by Django 3.2.8 on 2021-10-19 17:41
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('dcim', '0136_device_airflow'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='region',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(max_length=100),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='region',
|
||||||
|
name='slug',
|
||||||
|
field=models.SlugField(max_length=100),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='sitegroup',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(max_length=100),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='sitegroup',
|
||||||
|
name='slug',
|
||||||
|
field=models.SlugField(max_length=100),
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='location',
|
||||||
|
unique_together={('site', 'parent', 'name'), ('site', 'parent', 'slug')},
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='region',
|
||||||
|
unique_together={('parent', 'slug'), ('parent', 'name')},
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='sitegroup',
|
||||||
|
unique_together={('parent', 'slug'), ('parent', 'name')},
|
||||||
|
),
|
||||||
|
]
|
50
netbox/dcim/migrations/0138_extend_tag_support.py
Normal file
50
netbox/dcim/migrations/0138_extend_tag_support.py
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
# Generated by Django 3.2.8 on 2021-10-21 14:50
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
import taggit.managers
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('extras', '0062_clear_secrets_changelog'),
|
||||||
|
('dcim', '0137_relax_uniqueness_constraints'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='devicerole',
|
||||||
|
name='tags',
|
||||||
|
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='location',
|
||||||
|
name='tags',
|
||||||
|
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='manufacturer',
|
||||||
|
name='tags',
|
||||||
|
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='platform',
|
||||||
|
name='tags',
|
||||||
|
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='rackrole',
|
||||||
|
name='tags',
|
||||||
|
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='region',
|
||||||
|
name='tags',
|
||||||
|
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='sitegroup',
|
||||||
|
name='tags',
|
||||||
|
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
|
||||||
|
),
|
||||||
|
]
|
@ -4,7 +4,7 @@ from django.db import migrations
|
|||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('dcim', '0136_device_airflow'),
|
('dcim', '0138_extend_tag_support'),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
@ -5,7 +5,7 @@ import django.db.models.deletion
|
|||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('dcim', '0137_rename_cable_peer'),
|
('dcim', '0139_rename_cable_peer'),
|
||||||
('wireless', '0001_wireless'),
|
('wireless', '0001_wireless'),
|
||||||
]
|
]
|
||||||
|
|
@ -67,6 +67,13 @@ class Cable(PrimaryModel):
|
|||||||
choices=LinkStatusChoices,
|
choices=LinkStatusChoices,
|
||||||
default=LinkStatusChoices.STATUS_CONNECTED
|
default=LinkStatusChoices.STATUS_CONNECTED
|
||||||
)
|
)
|
||||||
|
tenant = models.ForeignKey(
|
||||||
|
to='tenancy.Tenant',
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
related_name='cables',
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
label = models.CharField(
|
label = models.CharField(
|
||||||
max_length=100,
|
max_length=100,
|
||||||
blank=True
|
blank=True
|
||||||
|
@ -36,7 +36,7 @@ __all__ = (
|
|||||||
# Device Types
|
# Device Types
|
||||||
#
|
#
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||||
class Manufacturer(OrganizationalModel):
|
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.
|
||||||
@ -54,6 +54,11 @@ class Manufacturer(OrganizationalModel):
|
|||||||
blank=True
|
blank=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Generic relations
|
||||||
|
contacts = GenericRelation(
|
||||||
|
to='tenancy.ContactAssignment'
|
||||||
|
)
|
||||||
|
|
||||||
objects = RestrictedQuerySet.as_manager()
|
objects = RestrictedQuerySet.as_manager()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -346,7 +351,7 @@ class DeviceType(PrimaryModel):
|
|||||||
# Devices
|
# Devices
|
||||||
#
|
#
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||||
class DeviceRole(OrganizationalModel):
|
class DeviceRole(OrganizationalModel):
|
||||||
"""
|
"""
|
||||||
Devices are organized by functional role; for example, "Core Switch" or "File Server". Each DeviceRole is assigned a
|
Devices are organized by functional role; for example, "Core Switch" or "File Server". Each DeviceRole is assigned a
|
||||||
@ -386,7 +391,7 @@ class DeviceRole(OrganizationalModel):
|
|||||||
return reverse('dcim:devicerole', args=[self.pk])
|
return reverse('dcim:devicerole', args=[self.pk])
|
||||||
|
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||||
class Platform(OrganizationalModel):
|
class Platform(OrganizationalModel):
|
||||||
"""
|
"""
|
||||||
Platform refers to the software or firmware running on a Device. For example, "Cisco IOS-XR" or "Juniper Junos".
|
Platform refers to the software or firmware running on a Device. For example, "Cisco IOS-XR" or "Juniper Junos".
|
||||||
@ -584,6 +589,11 @@ class Device(PrimaryModel, ConfigContextModel):
|
|||||||
comments = models.TextField(
|
comments = models.TextField(
|
||||||
blank=True
|
blank=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Generic relations
|
||||||
|
contacts = GenericRelation(
|
||||||
|
to='tenancy.ContactAssignment'
|
||||||
|
)
|
||||||
images = GenericRelation(
|
images = GenericRelation(
|
||||||
to='extras.ImageAttachment'
|
to='extras.ImageAttachment'
|
||||||
)
|
)
|
||||||
|
@ -40,6 +40,11 @@ class PowerPanel(PrimaryModel):
|
|||||||
name = models.CharField(
|
name = models.CharField(
|
||||||
max_length=100
|
max_length=100
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Generic relations
|
||||||
|
contacts = GenericRelation(
|
||||||
|
to='tenancy.ContactAssignment'
|
||||||
|
)
|
||||||
images = GenericRelation(
|
images = GenericRelation(
|
||||||
to='extras.ImageAttachment'
|
to='extras.ImageAttachment'
|
||||||
)
|
)
|
||||||
|
@ -35,7 +35,7 @@ __all__ = (
|
|||||||
# Racks
|
# Racks
|
||||||
#
|
#
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||||
class RackRole(OrganizationalModel):
|
class RackRole(OrganizationalModel):
|
||||||
"""
|
"""
|
||||||
Racks can be organized by functional role, similar to Devices.
|
Racks can be organized by functional role, similar to Devices.
|
||||||
@ -175,12 +175,17 @@ class Rack(PrimaryModel):
|
|||||||
comments = models.TextField(
|
comments = models.TextField(
|
||||||
blank=True
|
blank=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Generic relations
|
||||||
vlan_groups = GenericRelation(
|
vlan_groups = GenericRelation(
|
||||||
to='ipam.VLANGroup',
|
to='ipam.VLANGroup',
|
||||||
content_type_field='scope_type',
|
content_type_field='scope_type',
|
||||||
object_id_field='scope_id',
|
object_id_field='scope_id',
|
||||||
related_query_name='rack'
|
related_query_name='rack'
|
||||||
)
|
)
|
||||||
|
contacts = GenericRelation(
|
||||||
|
to='tenancy.ContactAssignment'
|
||||||
|
)
|
||||||
images = GenericRelation(
|
images = GenericRelation(
|
||||||
to='extras.ImageAttachment'
|
to='extras.ImageAttachment'
|
||||||
)
|
)
|
||||||
|
@ -25,7 +25,7 @@ __all__ = (
|
|||||||
# Regions
|
# Regions
|
||||||
#
|
#
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||||
class Region(NestedGroupModel):
|
class Region(NestedGroupModel):
|
||||||
"""
|
"""
|
||||||
A region represents a geographic collection of sites. For example, you might create regions representing countries,
|
A region represents a geographic collection of sites. For example, you might create regions representing countries,
|
||||||
@ -41,23 +41,32 @@ class Region(NestedGroupModel):
|
|||||||
db_index=True
|
db_index=True
|
||||||
)
|
)
|
||||||
name = models.CharField(
|
name = models.CharField(
|
||||||
max_length=100,
|
max_length=100
|
||||||
unique=True
|
|
||||||
)
|
)
|
||||||
slug = models.SlugField(
|
slug = models.SlugField(
|
||||||
max_length=100,
|
max_length=100
|
||||||
unique=True
|
|
||||||
)
|
)
|
||||||
description = models.CharField(
|
description = models.CharField(
|
||||||
max_length=200,
|
max_length=200,
|
||||||
blank=True
|
blank=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Generic relations
|
||||||
vlan_groups = GenericRelation(
|
vlan_groups = GenericRelation(
|
||||||
to='ipam.VLANGroup',
|
to='ipam.VLANGroup',
|
||||||
content_type_field='scope_type',
|
content_type_field='scope_type',
|
||||||
object_id_field='scope_id',
|
object_id_field='scope_id',
|
||||||
related_query_name='region'
|
related_query_name='region'
|
||||||
)
|
)
|
||||||
|
contacts = GenericRelation(
|
||||||
|
to='tenancy.ContactAssignment'
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = (
|
||||||
|
('parent', 'name'),
|
||||||
|
('parent', 'slug'),
|
||||||
|
)
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse('dcim:region', args=[self.pk])
|
return reverse('dcim:region', args=[self.pk])
|
||||||
@ -73,7 +82,7 @@ class Region(NestedGroupModel):
|
|||||||
# Site groups
|
# Site groups
|
||||||
#
|
#
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||||
class SiteGroup(NestedGroupModel):
|
class SiteGroup(NestedGroupModel):
|
||||||
"""
|
"""
|
||||||
A site group is an arbitrary grouping of sites. For example, you might have corporate sites and customer sites; and
|
A site group is an arbitrary grouping of sites. For example, you might have corporate sites and customer sites; and
|
||||||
@ -89,23 +98,32 @@ class SiteGroup(NestedGroupModel):
|
|||||||
db_index=True
|
db_index=True
|
||||||
)
|
)
|
||||||
name = models.CharField(
|
name = models.CharField(
|
||||||
max_length=100,
|
max_length=100
|
||||||
unique=True
|
|
||||||
)
|
)
|
||||||
slug = models.SlugField(
|
slug = models.SlugField(
|
||||||
max_length=100,
|
max_length=100
|
||||||
unique=True
|
|
||||||
)
|
)
|
||||||
description = models.CharField(
|
description = models.CharField(
|
||||||
max_length=200,
|
max_length=200,
|
||||||
blank=True
|
blank=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Generic relations
|
||||||
vlan_groups = GenericRelation(
|
vlan_groups = GenericRelation(
|
||||||
to='ipam.VLANGroup',
|
to='ipam.VLANGroup',
|
||||||
content_type_field='scope_type',
|
content_type_field='scope_type',
|
||||||
object_id_field='scope_id',
|
object_id_field='scope_id',
|
||||||
related_query_name='site_group'
|
related_query_name='site_group'
|
||||||
)
|
)
|
||||||
|
contacts = GenericRelation(
|
||||||
|
to='tenancy.ContactAssignment'
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = (
|
||||||
|
('parent', 'name'),
|
||||||
|
('parent', 'slug'),
|
||||||
|
)
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse('dcim:sitegroup', args=[self.pk])
|
return reverse('dcim:sitegroup', args=[self.pk])
|
||||||
@ -221,12 +239,17 @@ class Site(PrimaryModel):
|
|||||||
comments = models.TextField(
|
comments = models.TextField(
|
||||||
blank=True
|
blank=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Generic relations
|
||||||
vlan_groups = GenericRelation(
|
vlan_groups = GenericRelation(
|
||||||
to='ipam.VLANGroup',
|
to='ipam.VLANGroup',
|
||||||
content_type_field='scope_type',
|
content_type_field='scope_type',
|
||||||
object_id_field='scope_id',
|
object_id_field='scope_id',
|
||||||
related_query_name='site'
|
related_query_name='site'
|
||||||
)
|
)
|
||||||
|
contacts = GenericRelation(
|
||||||
|
to='tenancy.ContactAssignment'
|
||||||
|
)
|
||||||
images = GenericRelation(
|
images = GenericRelation(
|
||||||
to='extras.ImageAttachment'
|
to='extras.ImageAttachment'
|
||||||
)
|
)
|
||||||
@ -255,7 +278,7 @@ class Site(PrimaryModel):
|
|||||||
# Locations
|
# Locations
|
||||||
#
|
#
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||||
class Location(NestedGroupModel):
|
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
|
||||||
@ -291,12 +314,17 @@ class Location(NestedGroupModel):
|
|||||||
max_length=200,
|
max_length=200,
|
||||||
blank=True
|
blank=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Generic relations
|
||||||
vlan_groups = GenericRelation(
|
vlan_groups = GenericRelation(
|
||||||
to='ipam.VLANGroup',
|
to='ipam.VLANGroup',
|
||||||
content_type_field='scope_type',
|
content_type_field='scope_type',
|
||||||
object_id_field='scope_id',
|
object_id_field='scope_id',
|
||||||
related_query_name='location'
|
related_query_name='location'
|
||||||
)
|
)
|
||||||
|
contacts = GenericRelation(
|
||||||
|
to='tenancy.ContactAssignment'
|
||||||
|
)
|
||||||
images = GenericRelation(
|
images = GenericRelation(
|
||||||
to='extras.ImageAttachment'
|
to='extras.ImageAttachment'
|
||||||
)
|
)
|
||||||
@ -305,10 +333,10 @@ class Location(NestedGroupModel):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['site', 'name']
|
ordering = ['site', 'name']
|
||||||
unique_together = [
|
unique_together = ([
|
||||||
['site', 'name'],
|
('site', 'parent', 'name'),
|
||||||
['site', 'slug'],
|
('site', 'parent', 'slug'),
|
||||||
]
|
])
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse('dcim:location', args=[self.pk])
|
return reverse('dcim:location', args=[self.pk])
|
||||||
|
@ -2,6 +2,7 @@ import django_tables2 as tables
|
|||||||
from django_tables2.utils import Accessor
|
from django_tables2.utils import Accessor
|
||||||
|
|
||||||
from dcim.models import Cable
|
from dcim.models import Cable
|
||||||
|
from tenancy.tables import TenantColumn
|
||||||
from utilities.tables import BaseTable, ChoiceFieldColumn, ColorColumn, TagColumn, TemplateColumn, ToggleColumn
|
from utilities.tables import BaseTable, ChoiceFieldColumn, ColorColumn, TagColumn, TemplateColumn, ToggleColumn
|
||||||
from .template_code import CABLE_LENGTH, CABLE_TERMINATION_PARENT
|
from .template_code import CABLE_LENGTH, CABLE_TERMINATION_PARENT
|
||||||
|
|
||||||
@ -45,6 +46,7 @@ class CableTable(BaseTable):
|
|||||||
verbose_name='Termination B'
|
verbose_name='Termination B'
|
||||||
)
|
)
|
||||||
status = ChoiceFieldColumn()
|
status = ChoiceFieldColumn()
|
||||||
|
tenant = TenantColumn()
|
||||||
length = TemplateColumn(
|
length = TemplateColumn(
|
||||||
template_code=CABLE_LENGTH,
|
template_code=CABLE_LENGTH,
|
||||||
order_by='_abs_length'
|
order_by='_abs_length'
|
||||||
@ -58,7 +60,7 @@ class CableTable(BaseTable):
|
|||||||
model = Cable
|
model = Cable
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'label', 'termination_a_parent', 'termination_a', 'termination_b_parent', 'termination_b',
|
'pk', 'id', 'label', 'termination_a_parent', 'termination_a', 'termination_b_parent', 'termination_b',
|
||||||
'status', 'type', 'color', 'length', 'tags',
|
'status', 'type', 'tenant', 'color', 'length', 'tags',
|
||||||
)
|
)
|
||||||
default_columns = (
|
default_columns = (
|
||||||
'pk', 'id', 'label', 'termination_a_parent', 'termination_a', 'termination_b_parent', 'termination_b',
|
'pk', 'id', 'label', 'termination_a_parent', 'termination_a', 'termination_b_parent', 'termination_b',
|
||||||
|
@ -80,11 +80,16 @@ class DeviceRoleTable(BaseTable):
|
|||||||
)
|
)
|
||||||
color = ColorColumn()
|
color = ColorColumn()
|
||||||
vm_role = BooleanColumn()
|
vm_role = BooleanColumn()
|
||||||
|
tags = TagColumn(
|
||||||
|
url_name='dcim:devicerole_list'
|
||||||
|
)
|
||||||
actions = ButtonsColumn(DeviceRole)
|
actions = ButtonsColumn(DeviceRole)
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = DeviceRole
|
model = DeviceRole
|
||||||
fields = ('pk', 'name', 'device_count', 'vm_count', 'color', 'vm_role', 'description', 'slug', 'actions')
|
fields = (
|
||||||
|
'pk', 'name', 'device_count', 'vm_count', 'color', 'vm_role', 'description', 'slug', 'tags', 'actions',
|
||||||
|
)
|
||||||
default_columns = ('pk', 'name', 'device_count', 'vm_count', 'color', 'vm_role', 'description', 'actions')
|
default_columns = ('pk', 'name', 'device_count', 'vm_count', 'color', 'vm_role', 'description', 'actions')
|
||||||
|
|
||||||
|
|
||||||
@ -107,13 +112,16 @@ class PlatformTable(BaseTable):
|
|||||||
url_params={'platform_id': 'pk'},
|
url_params={'platform_id': 'pk'},
|
||||||
verbose_name='VMs'
|
verbose_name='VMs'
|
||||||
)
|
)
|
||||||
|
tags = TagColumn(
|
||||||
|
url_name='dcim:platform_list'
|
||||||
|
)
|
||||||
actions = ButtonsColumn(Platform)
|
actions = ButtonsColumn(Platform)
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = Platform
|
model = Platform
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'name', 'manufacturer', 'device_count', 'vm_count', 'slug', 'napalm_driver', 'napalm_args',
|
'pk', 'name', 'manufacturer', 'device_count', 'vm_count', 'slug', 'napalm_driver', 'napalm_args',
|
||||||
'description', 'actions',
|
'description', 'tags', 'actions',
|
||||||
)
|
)
|
||||||
default_columns = (
|
default_columns = (
|
||||||
'pk', 'name', 'manufacturer', 'device_count', 'vm_count', 'napalm_driver', 'description', 'actions',
|
'pk', 'name', 'manufacturer', 'device_count', 'vm_count', 'napalm_driver', 'description', 'actions',
|
||||||
|
@ -41,12 +41,16 @@ class ManufacturerTable(BaseTable):
|
|||||||
verbose_name='Platforms'
|
verbose_name='Platforms'
|
||||||
)
|
)
|
||||||
slug = tables.Column()
|
slug = tables.Column()
|
||||||
|
tags = TagColumn(
|
||||||
|
url_name='dcim:manufacturer_list'
|
||||||
|
)
|
||||||
actions = ButtonsColumn(Manufacturer)
|
actions = ButtonsColumn(Manufacturer)
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = Manufacturer
|
model = Manufacturer
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'name', 'devicetype_count', 'inventoryitem_count', 'platform_count', 'description', 'slug', 'actions',
|
'pk', 'name', 'devicetype_count', 'inventoryitem_count', 'platform_count', 'description', 'slug', 'tags',
|
||||||
|
'actions',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -24,11 +24,14 @@ class RackRoleTable(BaseTable):
|
|||||||
name = tables.Column(linkify=True)
|
name = tables.Column(linkify=True)
|
||||||
rack_count = tables.Column(verbose_name='Racks')
|
rack_count = tables.Column(verbose_name='Racks')
|
||||||
color = ColorColumn()
|
color = ColorColumn()
|
||||||
|
tags = TagColumn(
|
||||||
|
url_name='dcim:rackrole_list'
|
||||||
|
)
|
||||||
actions = ButtonsColumn(RackRole)
|
actions = ButtonsColumn(RackRole)
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = RackRole
|
model = RackRole
|
||||||
fields = ('pk', 'name', 'rack_count', 'color', 'description', 'slug', 'actions')
|
fields = ('pk', 'name', 'rack_count', 'color', 'description', 'slug', 'tags', 'actions')
|
||||||
default_columns = ('pk', 'name', 'rack_count', 'color', 'description', 'actions')
|
default_columns = ('pk', 'name', 'rack_count', 'color', 'description', 'actions')
|
||||||
|
|
||||||
|
|
||||||
|
@ -29,11 +29,14 @@ class RegionTable(BaseTable):
|
|||||||
url_params={'region_id': 'pk'},
|
url_params={'region_id': 'pk'},
|
||||||
verbose_name='Sites'
|
verbose_name='Sites'
|
||||||
)
|
)
|
||||||
|
tags = TagColumn(
|
||||||
|
url_name='dcim:region_list'
|
||||||
|
)
|
||||||
actions = ButtonsColumn(Region)
|
actions = ButtonsColumn(Region)
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = Region
|
model = Region
|
||||||
fields = ('pk', 'name', 'slug', 'site_count', 'description', 'actions')
|
fields = ('pk', 'name', 'slug', 'site_count', 'description', 'tags', 'actions')
|
||||||
default_columns = ('pk', 'name', 'site_count', 'description', 'actions')
|
default_columns = ('pk', 'name', 'site_count', 'description', 'actions')
|
||||||
|
|
||||||
|
|
||||||
@ -51,11 +54,14 @@ class SiteGroupTable(BaseTable):
|
|||||||
url_params={'group_id': 'pk'},
|
url_params={'group_id': 'pk'},
|
||||||
verbose_name='Sites'
|
verbose_name='Sites'
|
||||||
)
|
)
|
||||||
|
tags = TagColumn(
|
||||||
|
url_name='dcim:sitegroup_list'
|
||||||
|
)
|
||||||
actions = ButtonsColumn(SiteGroup)
|
actions = ButtonsColumn(SiteGroup)
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = SiteGroup
|
model = SiteGroup
|
||||||
fields = ('pk', 'name', 'slug', 'site_count', 'description', 'actions')
|
fields = ('pk', 'name', 'slug', 'site_count', 'description', 'tags', 'actions')
|
||||||
default_columns = ('pk', 'name', 'site_count', 'description', 'actions')
|
default_columns = ('pk', 'name', 'site_count', 'description', 'actions')
|
||||||
|
|
||||||
|
|
||||||
@ -114,6 +120,9 @@ class LocationTable(BaseTable):
|
|||||||
url_params={'location_id': 'pk'},
|
url_params={'location_id': 'pk'},
|
||||||
verbose_name='Devices'
|
verbose_name='Devices'
|
||||||
)
|
)
|
||||||
|
tags = TagColumn(
|
||||||
|
url_name='dcim:location_list'
|
||||||
|
)
|
||||||
actions = ButtonsColumn(
|
actions = ButtonsColumn(
|
||||||
model=Location,
|
model=Location,
|
||||||
prepend_template=LOCATION_ELEVATIONS
|
prepend_template=LOCATION_ELEVATIONS
|
||||||
@ -121,5 +130,7 @@ class LocationTable(BaseTable):
|
|||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = Location
|
model = Location
|
||||||
fields = ('pk', 'name', 'site', 'tenant', 'rack_count', 'device_count', 'description', 'slug', 'actions')
|
fields = (
|
||||||
|
'pk', 'name', 'site', 'tenant', 'rack_count', 'device_count', 'description', 'slug', 'tags', 'actions',
|
||||||
|
)
|
||||||
default_columns = ('pk', 'name', 'site', 'tenant', 'rack_count', 'device_count', 'description', 'actions')
|
default_columns = ('pk', 'name', 'site', 'tenant', 'rack_count', 'device_count', 'description', 'actions')
|
||||||
|
@ -2838,6 +2838,7 @@ class CableTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
tenants = (
|
tenants = (
|
||||||
Tenant(name='Tenant 1', slug='tenant-1'),
|
Tenant(name='Tenant 1', slug='tenant-1'),
|
||||||
Tenant(name='Tenant 2', slug='tenant-2'),
|
Tenant(name='Tenant 2', slug='tenant-2'),
|
||||||
|
Tenant(name='Tenant 3', slug='tenant-3'),
|
||||||
)
|
)
|
||||||
Tenant.objects.bulk_create(tenants)
|
Tenant.objects.bulk_create(tenants)
|
||||||
|
|
||||||
@ -2853,9 +2854,9 @@ class CableTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
device_role = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1')
|
device_role = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1')
|
||||||
|
|
||||||
devices = (
|
devices = (
|
||||||
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], rack=racks[0], position=1, tenant=tenants[0]),
|
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], rack=racks[0], position=1),
|
||||||
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[0], rack=racks[0], position=2, tenant=tenants[0]),
|
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[0], rack=racks[0], position=2),
|
||||||
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[1], rack=racks[1], position=1, tenant=tenants[1]),
|
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[1], rack=racks[1], position=1),
|
||||||
Device(name='Device 4', device_type=device_type, device_role=device_role, site=sites[1], rack=racks[1], position=2),
|
Device(name='Device 4', device_type=device_type, device_role=device_role, site=sites[1], rack=racks[1], position=2),
|
||||||
Device(name='Device 5', device_type=device_type, device_role=device_role, site=sites[2], rack=racks[2], position=1),
|
Device(name='Device 5', device_type=device_type, device_role=device_role, site=sites[2], rack=racks[2], position=1),
|
||||||
Device(name='Device 6', device_type=device_type, device_role=device_role, site=sites[2], rack=racks[2], position=2),
|
Device(name='Device 6', device_type=device_type, device_role=device_role, site=sites[2], rack=racks[2], position=2),
|
||||||
@ -2882,12 +2883,12 @@ class CableTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
console_server_port = ConsoleServerPort.objects.create(device=devices[0], name='Console Server Port 1')
|
console_server_port = ConsoleServerPort.objects.create(device=devices[0], name='Console Server Port 1')
|
||||||
|
|
||||||
# Cables
|
# Cables
|
||||||
Cable(termination_a=interfaces[1], termination_b=interfaces[2], label='Cable 1', type=CableTypeChoices.TYPE_CAT3, status=LinkStatusChoices.STATUS_CONNECTED, color='aa1409', length=10, length_unit=CableLengthUnitChoices.UNIT_FOOT).save()
|
Cable(termination_a=interfaces[1], termination_b=interfaces[2], label='Cable 1', type=CableTypeChoices.TYPE_CAT3, tenant=tenants[0], status=LinkStatusChoices.STATUS_CONNECTED, color='aa1409', length=10, length_unit=CableLengthUnitChoices.UNIT_FOOT).save()
|
||||||
Cable(termination_a=interfaces[3], termination_b=interfaces[4], label='Cable 2', type=CableTypeChoices.TYPE_CAT3, status=LinkStatusChoices.STATUS_CONNECTED, color='aa1409', length=20, length_unit=CableLengthUnitChoices.UNIT_FOOT).save()
|
Cable(termination_a=interfaces[3], termination_b=interfaces[4], label='Cable 2', type=CableTypeChoices.TYPE_CAT3, tenant=tenants[0], status=LinkStatusChoices.STATUS_CONNECTED, color='aa1409', length=20, length_unit=CableLengthUnitChoices.UNIT_FOOT).save()
|
||||||
Cable(termination_a=interfaces[5], termination_b=interfaces[6], label='Cable 3', type=CableTypeChoices.TYPE_CAT5E, status=LinkStatusChoices.STATUS_CONNECTED, color='f44336', length=30, length_unit=CableLengthUnitChoices.UNIT_FOOT).save()
|
Cable(termination_a=interfaces[5], termination_b=interfaces[6], label='Cable 3', type=CableTypeChoices.TYPE_CAT5E, tenant=tenants[1], status=LinkStatusChoices.STATUS_CONNECTED, color='f44336', length=30, length_unit=CableLengthUnitChoices.UNIT_FOOT).save()
|
||||||
Cable(termination_a=interfaces[7], termination_b=interfaces[8], label='Cable 4', type=CableTypeChoices.TYPE_CAT5E, status=LinkStatusChoices.STATUS_PLANNED, color='f44336', length=40, length_unit=CableLengthUnitChoices.UNIT_FOOT).save()
|
Cable(termination_a=interfaces[7], termination_b=interfaces[8], label='Cable 4', type=CableTypeChoices.TYPE_CAT5E, tenant=tenants[1], status=LinkStatusChoices.STATUS_PLANNED, color='f44336', length=40, length_unit=CableLengthUnitChoices.UNIT_FOOT).save()
|
||||||
Cable(termination_a=interfaces[9], termination_b=interfaces[10], label='Cable 5', type=CableTypeChoices.TYPE_CAT6, status=LinkStatusChoices.STATUS_PLANNED, color='e91e63', length=10, length_unit=CableLengthUnitChoices.UNIT_METER).save()
|
Cable(termination_a=interfaces[9], termination_b=interfaces[10], label='Cable 5', type=CableTypeChoices.TYPE_CAT6, tenant=tenants[2], status=LinkStatusChoices.STATUS_PLANNED, color='e91e63', length=10, length_unit=CableLengthUnitChoices.UNIT_METER).save()
|
||||||
Cable(termination_a=interfaces[11], termination_b=interfaces[0], label='Cable 6', type=CableTypeChoices.TYPE_CAT6, status=LinkStatusChoices.STATUS_PLANNED, color='e91e63', length=20, length_unit=CableLengthUnitChoices.UNIT_METER).save()
|
Cable(termination_a=interfaces[11], termination_b=interfaces[0], label='Cable 6', type=CableTypeChoices.TYPE_CAT6, tenant=tenants[2], status=LinkStatusChoices.STATUS_PLANNED, color='e91e63', length=20, length_unit=CableLengthUnitChoices.UNIT_METER).save()
|
||||||
Cable(termination_a=console_port, termination_b=console_server_port, label='Cable 7').save()
|
Cable(termination_a=console_port, termination_b=console_server_port, label='Cable 7').save()
|
||||||
|
|
||||||
def test_label(self):
|
def test_label(self):
|
||||||
@ -2940,9 +2941,9 @@ class CableTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
def test_tenant(self):
|
def test_tenant(self):
|
||||||
tenant = Tenant.objects.all()[:2]
|
tenant = Tenant.objects.all()[:2]
|
||||||
params = {'tenant_id': [tenant[0].pk, tenant[1].pk]}
|
params = {'tenant_id': [tenant[0].pk, tenant[1].pk]}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||||
params = {'tenant': [tenant[0].slug, tenant[1].slug]}
|
params = {'tenant': [tenant[0].slug, tenant[1].slug]}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||||
|
|
||||||
def test_termination_types(self):
|
def test_termination_types(self):
|
||||||
params = {'termination_a_type': 'dcim.consoleport'}
|
params = {'termination_a_type': 'dcim.consoleport'}
|
||||||
|
@ -31,11 +31,14 @@ class RegionTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
|||||||
for region in regions:
|
for region in regions:
|
||||||
region.save()
|
region.save()
|
||||||
|
|
||||||
|
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||||
|
|
||||||
cls.form_data = {
|
cls.form_data = {
|
||||||
'name': 'Region X',
|
'name': 'Region X',
|
||||||
'slug': 'region-x',
|
'slug': 'region-x',
|
||||||
'parent': regions[2].pk,
|
'parent': regions[2].pk,
|
||||||
'description': 'A new region',
|
'description': 'A new region',
|
||||||
|
'tags': [t.pk for t in tags],
|
||||||
}
|
}
|
||||||
|
|
||||||
cls.csv_data = (
|
cls.csv_data = (
|
||||||
@ -65,11 +68,14 @@ class SiteGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
|||||||
for sitegroup in sitegroups:
|
for sitegroup in sitegroups:
|
||||||
sitegroup.save()
|
sitegroup.save()
|
||||||
|
|
||||||
|
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||||
|
|
||||||
cls.form_data = {
|
cls.form_data = {
|
||||||
'name': 'Site Group X',
|
'name': 'Site Group X',
|
||||||
'slug': 'site-group-x',
|
'slug': 'site-group-x',
|
||||||
'parent': sitegroups[2].pk,
|
'parent': sitegroups[2].pk,
|
||||||
'description': 'A new site group',
|
'description': 'A new site group',
|
||||||
|
'tags': [t.pk for t in tags],
|
||||||
}
|
}
|
||||||
|
|
||||||
cls.csv_data = (
|
cls.csv_data = (
|
||||||
@ -169,12 +175,15 @@ class LocationTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
|||||||
for location in locations:
|
for location in locations:
|
||||||
location.save()
|
location.save()
|
||||||
|
|
||||||
|
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||||
|
|
||||||
cls.form_data = {
|
cls.form_data = {
|
||||||
'name': 'Location X',
|
'name': 'Location X',
|
||||||
'slug': 'location-x',
|
'slug': 'location-x',
|
||||||
'site': site.pk,
|
'site': site.pk,
|
||||||
'tenant': tenant.pk,
|
'tenant': tenant.pk,
|
||||||
'description': 'A new location',
|
'description': 'A new location',
|
||||||
|
'tags': [t.pk for t in tags],
|
||||||
}
|
}
|
||||||
|
|
||||||
cls.csv_data = (
|
cls.csv_data = (
|
||||||
@ -201,11 +210,14 @@ class RackRoleTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
|||||||
RackRole(name='Rack Role 3', slug='rack-role-3'),
|
RackRole(name='Rack Role 3', slug='rack-role-3'),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||||
|
|
||||||
cls.form_data = {
|
cls.form_data = {
|
||||||
'name': 'Rack Role X',
|
'name': 'Rack Role X',
|
||||||
'slug': 'rack-role-x',
|
'slug': 'rack-role-x',
|
||||||
'color': 'c0c0c0',
|
'color': 'c0c0c0',
|
||||||
'description': 'New role',
|
'description': 'New role',
|
||||||
|
'tags': [t.pk for t in tags],
|
||||||
}
|
}
|
||||||
|
|
||||||
cls.csv_data = (
|
cls.csv_data = (
|
||||||
@ -368,10 +380,13 @@ class ManufacturerTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
|||||||
Manufacturer(name='Manufacturer 3', slug='manufacturer-3'),
|
Manufacturer(name='Manufacturer 3', slug='manufacturer-3'),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||||
|
|
||||||
cls.form_data = {
|
cls.form_data = {
|
||||||
'name': 'Manufacturer X',
|
'name': 'Manufacturer X',
|
||||||
'slug': 'manufacturer-x',
|
'slug': 'manufacturer-x',
|
||||||
'description': 'A new manufacturer',
|
'description': 'A new manufacturer',
|
||||||
|
'tags': [t.pk for t in tags],
|
||||||
}
|
}
|
||||||
|
|
||||||
cls.csv_data = (
|
cls.csv_data = (
|
||||||
@ -435,6 +450,116 @@ class DeviceTypeTestCase(
|
|||||||
'is_full_depth': False,
|
'is_full_depth': False,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
|
||||||
|
def test_devicetype_consoleports(self):
|
||||||
|
devicetype = DeviceType.objects.first()
|
||||||
|
console_ports = (
|
||||||
|
ConsolePortTemplate(device_type=devicetype, name='Console Port 1'),
|
||||||
|
ConsolePortTemplate(device_type=devicetype, name='Console Port 2'),
|
||||||
|
ConsolePortTemplate(device_type=devicetype, name='Console Port 3'),
|
||||||
|
)
|
||||||
|
ConsolePortTemplate.objects.bulk_create(console_ports)
|
||||||
|
|
||||||
|
url = reverse('dcim:devicetype_consoleports', kwargs={'pk': devicetype.pk})
|
||||||
|
self.assertHttpStatus(self.client.get(url), 200)
|
||||||
|
|
||||||
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
|
||||||
|
def test_devicetype_consoleserverports(self):
|
||||||
|
devicetype = DeviceType.objects.first()
|
||||||
|
console_server_ports = (
|
||||||
|
ConsoleServerPortTemplate(device_type=devicetype, name='Console Server Port 1'),
|
||||||
|
ConsoleServerPortTemplate(device_type=devicetype, name='Console Server Port 2'),
|
||||||
|
ConsoleServerPortTemplate(device_type=devicetype, name='Console Server Port 3'),
|
||||||
|
)
|
||||||
|
ConsoleServerPortTemplate.objects.bulk_create(console_server_ports)
|
||||||
|
|
||||||
|
url = reverse('dcim:devicetype_consoleserverports', kwargs={'pk': devicetype.pk})
|
||||||
|
self.assertHttpStatus(self.client.get(url), 200)
|
||||||
|
|
||||||
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
|
||||||
|
def test_devicetype_powerports(self):
|
||||||
|
devicetype = DeviceType.objects.first()
|
||||||
|
power_ports = (
|
||||||
|
PowerPortTemplate(device_type=devicetype, name='Power Port 1'),
|
||||||
|
PowerPortTemplate(device_type=devicetype, name='Power Port 2'),
|
||||||
|
PowerPortTemplate(device_type=devicetype, name='Power Port 3'),
|
||||||
|
)
|
||||||
|
PowerPortTemplate.objects.bulk_create(power_ports)
|
||||||
|
|
||||||
|
url = reverse('dcim:devicetype_powerports', kwargs={'pk': devicetype.pk})
|
||||||
|
self.assertHttpStatus(self.client.get(url), 200)
|
||||||
|
|
||||||
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
|
||||||
|
def test_devicetype_poweroutlets(self):
|
||||||
|
devicetype = DeviceType.objects.first()
|
||||||
|
power_outlets = (
|
||||||
|
PowerOutletTemplate(device_type=devicetype, name='Power Outlet 1'),
|
||||||
|
PowerOutletTemplate(device_type=devicetype, name='Power Outlet 2'),
|
||||||
|
PowerOutletTemplate(device_type=devicetype, name='Power Outlet 3'),
|
||||||
|
)
|
||||||
|
PowerOutletTemplate.objects.bulk_create(power_outlets)
|
||||||
|
|
||||||
|
url = reverse('dcim:devicetype_poweroutlets', kwargs={'pk': devicetype.pk})
|
||||||
|
self.assertHttpStatus(self.client.get(url), 200)
|
||||||
|
|
||||||
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
|
||||||
|
def test_devicetype_interfaces(self):
|
||||||
|
devicetype = DeviceType.objects.first()
|
||||||
|
interfaces = (
|
||||||
|
InterfaceTemplate(device_type=devicetype, name='Interface 1'),
|
||||||
|
InterfaceTemplate(device_type=devicetype, name='Interface 2'),
|
||||||
|
InterfaceTemplate(device_type=devicetype, name='Interface 3'),
|
||||||
|
)
|
||||||
|
InterfaceTemplate.objects.bulk_create(interfaces)
|
||||||
|
|
||||||
|
url = reverse('dcim:devicetype_interfaces', kwargs={'pk': devicetype.pk})
|
||||||
|
self.assertHttpStatus(self.client.get(url), 200)
|
||||||
|
|
||||||
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
|
||||||
|
def test_devicetype_rearports(self):
|
||||||
|
devicetype = DeviceType.objects.first()
|
||||||
|
rear_ports = (
|
||||||
|
RearPortTemplate(device_type=devicetype, name='Rear Port 1'),
|
||||||
|
RearPortTemplate(device_type=devicetype, name='Rear Port 2'),
|
||||||
|
RearPortTemplate(device_type=devicetype, name='Rear Port 3'),
|
||||||
|
)
|
||||||
|
RearPortTemplate.objects.bulk_create(rear_ports)
|
||||||
|
|
||||||
|
url = reverse('dcim:devicetype_rearports', kwargs={'pk': devicetype.pk})
|
||||||
|
self.assertHttpStatus(self.client.get(url), 200)
|
||||||
|
|
||||||
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
|
||||||
|
def test_devicetype_frontports(self):
|
||||||
|
devicetype = DeviceType.objects.first()
|
||||||
|
rear_ports = (
|
||||||
|
RearPortTemplate(device_type=devicetype, name='Rear Port 1'),
|
||||||
|
RearPortTemplate(device_type=devicetype, name='Rear Port 2'),
|
||||||
|
RearPortTemplate(device_type=devicetype, name='Rear Port 3'),
|
||||||
|
)
|
||||||
|
RearPortTemplate.objects.bulk_create(rear_ports)
|
||||||
|
front_ports = (
|
||||||
|
FrontPortTemplate(device_type=devicetype, name='Front Port 1', rear_port=rear_ports[0], rear_port_position=1),
|
||||||
|
FrontPortTemplate(device_type=devicetype, name='Front Port 2', rear_port=rear_ports[1], rear_port_position=1),
|
||||||
|
FrontPortTemplate(device_type=devicetype, name='Front Port 3', rear_port=rear_ports[2], rear_port_position=1),
|
||||||
|
)
|
||||||
|
FrontPortTemplate.objects.bulk_create(front_ports)
|
||||||
|
|
||||||
|
url = reverse('dcim:devicetype_frontports', kwargs={'pk': devicetype.pk})
|
||||||
|
self.assertHttpStatus(self.client.get(url), 200)
|
||||||
|
|
||||||
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
|
||||||
|
def test_devicetype_devicebays(self):
|
||||||
|
devicetype = DeviceType.objects.first()
|
||||||
|
device_bays = (
|
||||||
|
DeviceBayTemplate(device_type=devicetype, name='Device Bay 1'),
|
||||||
|
DeviceBayTemplate(device_type=devicetype, name='Device Bay 2'),
|
||||||
|
DeviceBayTemplate(device_type=devicetype, name='Device Bay 3'),
|
||||||
|
)
|
||||||
|
DeviceBayTemplate.objects.bulk_create(device_bays)
|
||||||
|
|
||||||
|
url = reverse('dcim:devicetype_devicebays', kwargs={'pk': devicetype.pk})
|
||||||
|
self.assertHttpStatus(self.client.get(url), 200)
|
||||||
|
|
||||||
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
|
||||||
def test_import_objects(self):
|
def test_import_objects(self):
|
||||||
"""
|
"""
|
||||||
@ -924,12 +1049,15 @@ class DeviceRoleTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
|||||||
DeviceRole(name='Device Role 3', slug='device-role-3'),
|
DeviceRole(name='Device Role 3', slug='device-role-3'),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||||
|
|
||||||
cls.form_data = {
|
cls.form_data = {
|
||||||
'name': 'Devie Role X',
|
'name': 'Devie Role X',
|
||||||
'slug': 'device-role-x',
|
'slug': 'device-role-x',
|
||||||
'color': 'c0c0c0',
|
'color': 'c0c0c0',
|
||||||
'vm_role': False,
|
'vm_role': False,
|
||||||
'description': 'New device role',
|
'description': 'New device role',
|
||||||
|
'tags': [t.pk for t in tags],
|
||||||
}
|
}
|
||||||
|
|
||||||
cls.csv_data = (
|
cls.csv_data = (
|
||||||
@ -959,6 +1087,8 @@ class PlatformTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
|||||||
Platform(name='Platform 3', slug='platform-3', manufacturer=manufacturer),
|
Platform(name='Platform 3', slug='platform-3', manufacturer=manufacturer),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||||
|
|
||||||
cls.form_data = {
|
cls.form_data = {
|
||||||
'name': 'Platform X',
|
'name': 'Platform X',
|
||||||
'slug': 'platform-x',
|
'slug': 'platform-x',
|
||||||
@ -966,6 +1096,7 @@ class PlatformTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
|||||||
'napalm_driver': 'junos',
|
'napalm_driver': 'junos',
|
||||||
'napalm_args': None,
|
'napalm_args': None,
|
||||||
'description': 'A new platform',
|
'description': 'A new platform',
|
||||||
|
'tags': [t.pk for t in tags],
|
||||||
}
|
}
|
||||||
|
|
||||||
cls.csv_data = (
|
cls.csv_data = (
|
||||||
|
@ -109,6 +109,14 @@ urlpatterns = [
|
|||||||
path('device-types/edit/', views.DeviceTypeBulkEditView.as_view(), name='devicetype_bulk_edit'),
|
path('device-types/edit/', views.DeviceTypeBulkEditView.as_view(), name='devicetype_bulk_edit'),
|
||||||
path('device-types/delete/', views.DeviceTypeBulkDeleteView.as_view(), name='devicetype_bulk_delete'),
|
path('device-types/delete/', views.DeviceTypeBulkDeleteView.as_view(), name='devicetype_bulk_delete'),
|
||||||
path('device-types/<int:pk>/', views.DeviceTypeView.as_view(), name='devicetype'),
|
path('device-types/<int:pk>/', views.DeviceTypeView.as_view(), name='devicetype'),
|
||||||
|
path('device-types/<int:pk>/console-ports/', views.DeviceTypeConsolePortsView.as_view(), name='devicetype_consoleports'),
|
||||||
|
path('device-types/<int:pk>/console-server-ports/', views.DeviceTypeConsoleServerPortsView.as_view(), name='devicetype_consoleserverports'),
|
||||||
|
path('device-types/<int:pk>/power-ports/', views.DeviceTypePowerPortsView.as_view(), name='devicetype_powerports'),
|
||||||
|
path('device-types/<int:pk>/power-outlets/', views.DeviceTypePowerOutletsView.as_view(), name='devicetype_poweroutlets'),
|
||||||
|
path('device-types/<int:pk>/interfaces/', views.DeviceTypeInterfacesView.as_view(), name='devicetype_interfaces'),
|
||||||
|
path('device-types/<int:pk>/front-ports/', views.DeviceTypeFrontPortsView.as_view(), name='devicetype_frontports'),
|
||||||
|
path('device-types/<int:pk>/rear-ports/', views.DeviceTypeRearPortsView.as_view(), name='devicetype_rearports'),
|
||||||
|
path('device-types/<int:pk>/device-bays/', views.DeviceTypeDeviceBaysView.as_view(), name='devicetype_devicebays'),
|
||||||
path('device-types/<int:pk>/edit/', views.DeviceTypeEditView.as_view(), name='devicetype_edit'),
|
path('device-types/<int:pk>/edit/', views.DeviceTypeEditView.as_view(), name='devicetype_edit'),
|
||||||
path('device-types/<int:pk>/delete/', views.DeviceTypeDeleteView.as_view(), name='devicetype_delete'),
|
path('device-types/<int:pk>/delete/', views.DeviceTypeDeleteView.as_view(), name='devicetype_delete'),
|
||||||
path('device-types/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='devicetype_changelog', kwargs={'model': DeviceType}),
|
path('device-types/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='devicetype_changelog', kwargs={'model': DeviceType}),
|
||||||
|
@ -36,6 +36,37 @@ from .models import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceComponentsView(generic.ObjectView):
|
||||||
|
queryset = Device.objects.all()
|
||||||
|
model = None
|
||||||
|
table = None
|
||||||
|
|
||||||
|
def get_components(self, request, instance):
|
||||||
|
return self.model.objects.restrict(request.user, 'view').filter(device=instance)
|
||||||
|
|
||||||
|
def get_extra_context(self, request, instance):
|
||||||
|
components = self.get_components(request, instance)
|
||||||
|
table = self.table(data=components, user=request.user)
|
||||||
|
change_perm = f'{self.model._meta.app_label}.change_{self.model._meta.model_name}'
|
||||||
|
delete_perm = f'{self.model._meta.app_label}.delete_{self.model._meta.model_name}'
|
||||||
|
if request.user.has_perm(change_perm) or request.user.has_perm(delete_perm):
|
||||||
|
table.columns.show('pk')
|
||||||
|
paginate_table(table, request)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'table': table,
|
||||||
|
'active_tab': f"{self.model._meta.verbose_name_plural.replace(' ', '-')}",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceTypeComponentsView(DeviceComponentsView):
|
||||||
|
queryset = DeviceType.objects.all()
|
||||||
|
template_name = 'dcim/devicetype/component_templates.html'
|
||||||
|
|
||||||
|
def get_components(self, request, instance):
|
||||||
|
return self.model.objects.restrict(request.user, 'view').filter(device_type=instance)
|
||||||
|
|
||||||
|
|
||||||
class BulkDisconnectView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
class BulkDisconnectView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
||||||
"""
|
"""
|
||||||
An extendable view for disconnection console/power/interface components in bulk.
|
An extendable view for disconnection console/power/interface components in bulk.
|
||||||
@ -759,62 +790,52 @@ class DeviceTypeView(generic.ObjectView):
|
|||||||
def get_extra_context(self, request, instance):
|
def get_extra_context(self, request, instance):
|
||||||
instance_count = Device.objects.restrict(request.user).filter(device_type=instance).count()
|
instance_count = Device.objects.restrict(request.user).filter(device_type=instance).count()
|
||||||
|
|
||||||
# Component tables
|
|
||||||
consoleport_table = tables.ConsolePortTemplateTable(
|
|
||||||
ConsolePortTemplate.objects.restrict(request.user, 'view').filter(device_type=instance),
|
|
||||||
orderable=False
|
|
||||||
)
|
|
||||||
consoleserverport_table = tables.ConsoleServerPortTemplateTable(
|
|
||||||
ConsoleServerPortTemplate.objects.restrict(request.user, 'view').filter(device_type=instance),
|
|
||||||
orderable=False
|
|
||||||
)
|
|
||||||
powerport_table = tables.PowerPortTemplateTable(
|
|
||||||
PowerPortTemplate.objects.restrict(request.user, 'view').filter(device_type=instance),
|
|
||||||
orderable=False
|
|
||||||
)
|
|
||||||
poweroutlet_table = tables.PowerOutletTemplateTable(
|
|
||||||
PowerOutletTemplate.objects.restrict(request.user, 'view').filter(device_type=instance),
|
|
||||||
orderable=False
|
|
||||||
)
|
|
||||||
interface_table = tables.InterfaceTemplateTable(
|
|
||||||
list(InterfaceTemplate.objects.restrict(request.user, 'view').filter(device_type=instance)),
|
|
||||||
orderable=False
|
|
||||||
)
|
|
||||||
front_port_table = tables.FrontPortTemplateTable(
|
|
||||||
FrontPortTemplate.objects.restrict(request.user, 'view').filter(device_type=instance),
|
|
||||||
orderable=False
|
|
||||||
)
|
|
||||||
rear_port_table = tables.RearPortTemplateTable(
|
|
||||||
RearPortTemplate.objects.restrict(request.user, 'view').filter(device_type=instance),
|
|
||||||
orderable=False
|
|
||||||
)
|
|
||||||
devicebay_table = tables.DeviceBayTemplateTable(
|
|
||||||
DeviceBayTemplate.objects.restrict(request.user, 'view').filter(device_type=instance),
|
|
||||||
orderable=False
|
|
||||||
)
|
|
||||||
if request.user.has_perm('dcim.change_devicetype'):
|
|
||||||
consoleport_table.columns.show('pk')
|
|
||||||
consoleserverport_table.columns.show('pk')
|
|
||||||
powerport_table.columns.show('pk')
|
|
||||||
poweroutlet_table.columns.show('pk')
|
|
||||||
interface_table.columns.show('pk')
|
|
||||||
front_port_table.columns.show('pk')
|
|
||||||
rear_port_table.columns.show('pk')
|
|
||||||
devicebay_table.columns.show('pk')
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'instance_count': instance_count,
|
'instance_count': instance_count,
|
||||||
'consoleport_table': consoleport_table,
|
'active_tab': 'devicetype',
|
||||||
'consoleserverport_table': consoleserverport_table,
|
|
||||||
'powerport_table': powerport_table,
|
|
||||||
'poweroutlet_table': poweroutlet_table,
|
|
||||||
'interface_table': interface_table,
|
|
||||||
'front_port_table': front_port_table,
|
|
||||||
'rear_port_table': rear_port_table,
|
|
||||||
'devicebay_table': devicebay_table,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceTypeConsolePortsView(DeviceTypeComponentsView):
|
||||||
|
model = ConsolePortTemplate
|
||||||
|
table = tables.ConsolePortTemplateTable
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceTypeConsoleServerPortsView(DeviceTypeComponentsView):
|
||||||
|
model = ConsoleServerPortTemplate
|
||||||
|
table = tables.ConsoleServerPortTemplateTable
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceTypePowerPortsView(DeviceTypeComponentsView):
|
||||||
|
model = PowerPortTemplate
|
||||||
|
table = tables.PowerPortTemplateTable
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceTypePowerOutletsView(DeviceTypeComponentsView):
|
||||||
|
model = PowerOutletTemplate
|
||||||
|
table = tables.PowerOutletTemplateTable
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceTypeInterfacesView(DeviceTypeComponentsView):
|
||||||
|
model = InterfaceTemplate
|
||||||
|
table = tables.InterfaceTemplateTable
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceTypeFrontPortsView(DeviceTypeComponentsView):
|
||||||
|
model = FrontPortTemplate
|
||||||
|
table = tables.FrontPortTemplateTable
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceTypeRearPortsView(DeviceTypeComponentsView):
|
||||||
|
model = RearPortTemplate
|
||||||
|
table = tables.RearPortTemplateTable
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceTypeDeviceBaysView(DeviceTypeComponentsView):
|
||||||
|
model = DeviceBayTemplate
|
||||||
|
table = tables.DeviceBayTemplateTable
|
||||||
|
|
||||||
|
|
||||||
class DeviceTypeEditView(generic.ObjectEditView):
|
class DeviceTypeEditView(generic.ObjectEditView):
|
||||||
queryset = DeviceType.objects.all()
|
queryset = DeviceType.objects.all()
|
||||||
model_form = forms.DeviceTypeForm
|
model_form = forms.DeviceTypeForm
|
||||||
@ -1306,206 +1327,65 @@ class DeviceView(generic.ObjectView):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class DeviceConsolePortsView(generic.ObjectView):
|
class DeviceConsolePortsView(DeviceComponentsView):
|
||||||
queryset = Device.objects.all()
|
model = ConsolePort
|
||||||
|
table = tables.DeviceConsolePortTable
|
||||||
template_name = 'dcim/device/consoleports.html'
|
template_name = 'dcim/device/consoleports.html'
|
||||||
|
|
||||||
def get_extra_context(self, request, instance):
|
|
||||||
consoleports = ConsolePort.objects.restrict(request.user, 'view').filter(device=instance).prefetch_related(
|
|
||||||
'cable', '_path__destination',
|
|
||||||
)
|
|
||||||
consoleport_table = tables.DeviceConsolePortTable(
|
|
||||||
data=consoleports,
|
|
||||||
user=request.user
|
|
||||||
)
|
|
||||||
if request.user.has_perm('dcim.change_consoleport') or request.user.has_perm('dcim.delete_consoleport'):
|
|
||||||
consoleport_table.columns.show('pk')
|
|
||||||
paginate_table(consoleport_table, request)
|
|
||||||
|
|
||||||
return {
|
class DeviceConsoleServerPortsView(DeviceComponentsView):
|
||||||
'consoleport_table': consoleport_table,
|
model = ConsoleServerPort
|
||||||
'active_tab': 'console-ports',
|
table = tables.DeviceConsoleServerPortTable
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class DeviceConsoleServerPortsView(generic.ObjectView):
|
|
||||||
queryset = Device.objects.all()
|
|
||||||
template_name = 'dcim/device/consoleserverports.html'
|
template_name = 'dcim/device/consoleserverports.html'
|
||||||
|
|
||||||
def get_extra_context(self, request, instance):
|
|
||||||
consoleserverports = ConsoleServerPort.objects.restrict(request.user, 'view').filter(
|
|
||||||
device=instance
|
|
||||||
).prefetch_related(
|
|
||||||
'cable', '_path__destination',
|
|
||||||
)
|
|
||||||
consoleserverport_table = tables.DeviceConsoleServerPortTable(
|
|
||||||
data=consoleserverports,
|
|
||||||
user=request.user
|
|
||||||
)
|
|
||||||
if request.user.has_perm('dcim.change_consoleserverport') or \
|
|
||||||
request.user.has_perm('dcim.delete_consoleserverport'):
|
|
||||||
consoleserverport_table.columns.show('pk')
|
|
||||||
paginate_table(consoleserverport_table, request)
|
|
||||||
|
|
||||||
return {
|
class DevicePowerPortsView(DeviceComponentsView):
|
||||||
'consoleserverport_table': consoleserverport_table,
|
model = PowerPort
|
||||||
'active_tab': 'console-server-ports',
|
table = tables.DevicePowerPortTable
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class DevicePowerPortsView(generic.ObjectView):
|
|
||||||
queryset = Device.objects.all()
|
|
||||||
template_name = 'dcim/device/powerports.html'
|
template_name = 'dcim/device/powerports.html'
|
||||||
|
|
||||||
def get_extra_context(self, request, instance):
|
|
||||||
powerports = PowerPort.objects.restrict(request.user, 'view').filter(device=instance).prefetch_related(
|
|
||||||
'cable', '_path__destination',
|
|
||||||
)
|
|
||||||
powerport_table = tables.DevicePowerPortTable(
|
|
||||||
data=powerports,
|
|
||||||
user=request.user
|
|
||||||
)
|
|
||||||
if request.user.has_perm('dcim.change_powerport') or request.user.has_perm('dcim.delete_powerport'):
|
|
||||||
powerport_table.columns.show('pk')
|
|
||||||
paginate_table(powerport_table, request)
|
|
||||||
|
|
||||||
return {
|
class DevicePowerOutletsView(DeviceComponentsView):
|
||||||
'powerport_table': powerport_table,
|
model = PowerOutlet
|
||||||
'active_tab': 'power-ports',
|
table = tables.DevicePowerOutletTable
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class DevicePowerOutletsView(generic.ObjectView):
|
|
||||||
queryset = Device.objects.all()
|
|
||||||
template_name = 'dcim/device/poweroutlets.html'
|
template_name = 'dcim/device/poweroutlets.html'
|
||||||
|
|
||||||
def get_extra_context(self, request, instance):
|
|
||||||
poweroutlets = PowerOutlet.objects.restrict(request.user, 'view').filter(device=instance).prefetch_related(
|
|
||||||
'cable', 'power_port', '_path__destination',
|
|
||||||
)
|
|
||||||
poweroutlet_table = tables.DevicePowerOutletTable(
|
|
||||||
data=poweroutlets,
|
|
||||||
user=request.user
|
|
||||||
)
|
|
||||||
if request.user.has_perm('dcim.change_poweroutlet') or request.user.has_perm('dcim.delete_poweroutlet'):
|
|
||||||
poweroutlet_table.columns.show('pk')
|
|
||||||
paginate_table(poweroutlet_table, request)
|
|
||||||
|
|
||||||
return {
|
class DeviceInterfacesView(DeviceComponentsView):
|
||||||
'poweroutlet_table': poweroutlet_table,
|
model = Interface
|
||||||
'active_tab': 'power-outlets',
|
table = tables.DeviceInterfaceTable
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class DeviceInterfacesView(generic.ObjectView):
|
|
||||||
queryset = Device.objects.all()
|
|
||||||
template_name = 'dcim/device/interfaces.html'
|
template_name = 'dcim/device/interfaces.html'
|
||||||
|
|
||||||
def get_extra_context(self, request, instance):
|
def get_components(self, request, instance):
|
||||||
interfaces = instance.vc_interfaces().restrict(request.user, 'view').prefetch_related(
|
return instance.vc_interfaces().restrict(request.user, 'view').prefetch_related(
|
||||||
Prefetch('ip_addresses', queryset=IPAddress.objects.restrict(request.user)),
|
Prefetch('ip_addresses', queryset=IPAddress.objects.restrict(request.user)),
|
||||||
Prefetch('member_interfaces', queryset=Interface.objects.restrict(request.user)),
|
Prefetch('member_interfaces', queryset=Interface.objects.restrict(request.user))
|
||||||
'lag', 'cable', '_path__destination', 'tags',
|
|
||||||
)
|
)
|
||||||
interface_table = tables.DeviceInterfaceTable(
|
|
||||||
data=interfaces,
|
|
||||||
user=request.user
|
|
||||||
)
|
|
||||||
if request.user.has_perm('dcim.change_interface') or request.user.has_perm('dcim.delete_interface'):
|
|
||||||
interface_table.columns.show('pk')
|
|
||||||
paginate_table(interface_table, request)
|
|
||||||
|
|
||||||
return {
|
|
||||||
'interface_table': interface_table,
|
|
||||||
'active_tab': 'interfaces',
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class DeviceFrontPortsView(generic.ObjectView):
|
class DeviceFrontPortsView(DeviceComponentsView):
|
||||||
queryset = Device.objects.all()
|
model = FrontPort
|
||||||
|
table = tables.DeviceFrontPortTable
|
||||||
template_name = 'dcim/device/frontports.html'
|
template_name = 'dcim/device/frontports.html'
|
||||||
|
|
||||||
def get_extra_context(self, request, instance):
|
|
||||||
frontports = FrontPort.objects.restrict(request.user, 'view').filter(device=instance).prefetch_related(
|
|
||||||
'rear_port', 'cable',
|
|
||||||
)
|
|
||||||
frontport_table = tables.DeviceFrontPortTable(
|
|
||||||
data=frontports,
|
|
||||||
user=request.user
|
|
||||||
)
|
|
||||||
if request.user.has_perm('dcim.change_frontport') or request.user.has_perm('dcim.delete_frontport'):
|
|
||||||
frontport_table.columns.show('pk')
|
|
||||||
paginate_table(frontport_table, request)
|
|
||||||
|
|
||||||
return {
|
class DeviceRearPortsView(DeviceComponentsView):
|
||||||
'frontport_table': frontport_table,
|
model = RearPort
|
||||||
'active_tab': 'front-ports',
|
table = tables.DeviceRearPortTable
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class DeviceRearPortsView(generic.ObjectView):
|
|
||||||
queryset = Device.objects.all()
|
|
||||||
template_name = 'dcim/device/rearports.html'
|
template_name = 'dcim/device/rearports.html'
|
||||||
|
|
||||||
def get_extra_context(self, request, instance):
|
|
||||||
rearports = RearPort.objects.restrict(request.user, 'view').filter(device=instance).prefetch_related('cable')
|
|
||||||
rearport_table = tables.DeviceRearPortTable(
|
|
||||||
data=rearports,
|
|
||||||
user=request.user
|
|
||||||
)
|
|
||||||
if request.user.has_perm('dcim.change_rearport') or request.user.has_perm('dcim.delete_rearport'):
|
|
||||||
rearport_table.columns.show('pk')
|
|
||||||
paginate_table(rearport_table, request)
|
|
||||||
|
|
||||||
return {
|
class DeviceDeviceBaysView(DeviceComponentsView):
|
||||||
'rearport_table': rearport_table,
|
model = DeviceBay
|
||||||
'active_tab': 'rear-ports',
|
table = tables.DeviceDeviceBayTable
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class DeviceDeviceBaysView(generic.ObjectView):
|
|
||||||
queryset = Device.objects.all()
|
|
||||||
template_name = 'dcim/device/devicebays.html'
|
template_name = 'dcim/device/devicebays.html'
|
||||||
|
|
||||||
def get_extra_context(self, request, instance):
|
|
||||||
devicebays = DeviceBay.objects.restrict(request.user, 'view').filter(device=instance).prefetch_related(
|
|
||||||
'installed_device__device_type__manufacturer',
|
|
||||||
)
|
|
||||||
devicebay_table = tables.DeviceDeviceBayTable(
|
|
||||||
data=devicebays,
|
|
||||||
user=request.user
|
|
||||||
)
|
|
||||||
if request.user.has_perm('dcim.change_devicebay') or request.user.has_perm('dcim.delete_devicebay'):
|
|
||||||
devicebay_table.columns.show('pk')
|
|
||||||
paginate_table(devicebay_table, request)
|
|
||||||
|
|
||||||
return {
|
class DeviceInventoryView(DeviceComponentsView):
|
||||||
'devicebay_table': devicebay_table,
|
model = InventoryItem
|
||||||
'active_tab': 'device-bays',
|
table = tables.DeviceInventoryItemTable
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class DeviceInventoryView(generic.ObjectView):
|
|
||||||
queryset = Device.objects.all()
|
|
||||||
template_name = 'dcim/device/inventory.html'
|
template_name = 'dcim/device/inventory.html'
|
||||||
|
|
||||||
def get_extra_context(self, request, instance):
|
|
||||||
inventoryitems = InventoryItem.objects.restrict(request.user, 'view').filter(
|
|
||||||
device=instance
|
|
||||||
).prefetch_related('manufacturer')
|
|
||||||
inventoryitem_table = tables.DeviceInventoryItemTable(
|
|
||||||
data=inventoryitems,
|
|
||||||
user=request.user
|
|
||||||
)
|
|
||||||
if request.user.has_perm('dcim.change_inventoryitem') or request.user.has_perm('dcim.delete_inventoryitem'):
|
|
||||||
inventoryitem_table.columns.show('pk')
|
|
||||||
paginate_table(inventoryitem_table, request)
|
|
||||||
|
|
||||||
return {
|
|
||||||
'inventoryitem_table': inventoryitem_table,
|
|
||||||
'active_tab': 'inventory',
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class DeviceStatusView(generic.ObjectView):
|
class DeviceStatusView(generic.ObjectView):
|
||||||
additional_permissions = ['dcim.napalm_read_device']
|
additional_permissions = ['dcim.napalm_read_device']
|
||||||
|
@ -15,6 +15,7 @@ from .models import *
|
|||||||
__all__ = (
|
__all__ = (
|
||||||
'ConfigContextFilterSet',
|
'ConfigContextFilterSet',
|
||||||
'ContentTypeFilterSet',
|
'ContentTypeFilterSet',
|
||||||
|
'CustomFieldFilterSet',
|
||||||
'CustomLinkFilterSet',
|
'CustomLinkFilterSet',
|
||||||
'ExportTemplateFilterSet',
|
'ExportTemplateFilterSet',
|
||||||
'ImageAttachmentFilterSet',
|
'ImageAttachmentFilterSet',
|
||||||
@ -47,7 +48,7 @@ class WebhookFilterSet(BaseFilterSet):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class CustomFieldFilterSet(django_filters.FilterSet):
|
class CustomFieldFilterSet(BaseFilterSet):
|
||||||
content_types = ContentTypeFilter()
|
content_types = ContentTypeFilter()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -3,14 +3,12 @@ from collections import OrderedDict
|
|||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from drf_yasg.utils import swagger_serializer_method
|
from drf_yasg.utils import swagger_serializer_method
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.validators import UniqueTogetherValidator
|
|
||||||
|
|
||||||
from dcim.api.nested_serializers import NestedDeviceSerializer, NestedSiteSerializer
|
from dcim.api.nested_serializers import NestedDeviceSerializer, NestedSiteSerializer
|
||||||
from ipam.choices import *
|
from ipam.choices import *
|
||||||
from ipam.constants import IPADDRESS_ASSIGNMENT_MODELS, VLANGROUP_SCOPE_TYPES
|
from ipam.constants import IPADDRESS_ASSIGNMENT_MODELS, VLANGROUP_SCOPE_TYPES
|
||||||
from ipam.models import *
|
from ipam.models import *
|
||||||
from netbox.api import ChoiceField, ContentTypeField, SerializedPKRelatedField
|
from netbox.api import ChoiceField, ContentTypeField, SerializedPKRelatedField
|
||||||
from netbox.api.serializers import OrganizationalModelSerializer
|
|
||||||
from netbox.api.serializers import PrimaryModelSerializer
|
from netbox.api.serializers import PrimaryModelSerializer
|
||||||
from tenancy.api.nested_serializers import NestedTenantSerializer
|
from tenancy.api.nested_serializers import NestedTenantSerializer
|
||||||
from utilities.api import get_serializer_for_model
|
from utilities.api import get_serializer_for_model
|
||||||
@ -67,14 +65,14 @@ class RouteTargetSerializer(PrimaryModelSerializer):
|
|||||||
# RIRs/aggregates
|
# RIRs/aggregates
|
||||||
#
|
#
|
||||||
|
|
||||||
class RIRSerializer(OrganizationalModelSerializer):
|
class RIRSerializer(PrimaryModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:rir-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:rir-detail')
|
||||||
aggregate_count = serializers.IntegerField(read_only=True)
|
aggregate_count = serializers.IntegerField(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = RIR
|
model = RIR
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'name', 'slug', 'is_private', 'description', 'custom_fields', 'created',
|
'id', 'url', 'display', 'name', 'slug', 'is_private', 'description', 'tags', 'custom_fields', 'created',
|
||||||
'last_updated', 'aggregate_count',
|
'last_updated', 'aggregate_count',
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -98,7 +96,7 @@ class AggregateSerializer(PrimaryModelSerializer):
|
|||||||
# VLANs
|
# VLANs
|
||||||
#
|
#
|
||||||
|
|
||||||
class RoleSerializer(OrganizationalModelSerializer):
|
class RoleSerializer(PrimaryModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:role-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:role-detail')
|
||||||
prefix_count = serializers.IntegerField(read_only=True)
|
prefix_count = serializers.IntegerField(read_only=True)
|
||||||
vlan_count = serializers.IntegerField(read_only=True)
|
vlan_count = serializers.IntegerField(read_only=True)
|
||||||
@ -106,43 +104,32 @@ class RoleSerializer(OrganizationalModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Role
|
model = Role
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'name', 'slug', 'weight', 'description', 'custom_fields', 'created', 'last_updated',
|
'id', 'url', 'display', 'name', 'slug', 'weight', 'description', 'tags', 'custom_fields', 'created',
|
||||||
'prefix_count', 'vlan_count',
|
'last_updated', 'prefix_count', 'vlan_count',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class VLANGroupSerializer(OrganizationalModelSerializer):
|
class VLANGroupSerializer(PrimaryModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlangroup-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlangroup-detail')
|
||||||
scope_type = ContentTypeField(
|
scope_type = ContentTypeField(
|
||||||
queryset=ContentType.objects.filter(
|
queryset=ContentType.objects.filter(
|
||||||
model__in=VLANGROUP_SCOPE_TYPES
|
model__in=VLANGROUP_SCOPE_TYPES
|
||||||
),
|
),
|
||||||
required=False
|
required=False,
|
||||||
|
default=None
|
||||||
)
|
)
|
||||||
|
scope_id = serializers.IntegerField(allow_null=True, required=False, default=None)
|
||||||
scope = serializers.SerializerMethodField(read_only=True)
|
scope = serializers.SerializerMethodField(read_only=True)
|
||||||
vlan_count = serializers.IntegerField(read_only=True)
|
vlan_count = serializers.IntegerField(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = VLANGroup
|
model = VLANGroup
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'name', 'slug', 'scope_type', 'scope_id', 'scope', 'description', 'custom_fields',
|
'id', 'url', 'display', 'name', 'slug', 'scope_type', 'scope_id', 'scope', 'description', 'tags',
|
||||||
'created', 'last_updated', 'vlan_count',
|
'custom_fields', 'created', 'last_updated', 'vlan_count',
|
||||||
]
|
]
|
||||||
validators = []
|
validators = []
|
||||||
|
|
||||||
def validate(self, data):
|
|
||||||
|
|
||||||
# Validate uniqueness of name and slug if a site has been assigned.
|
|
||||||
if data.get('site', None):
|
|
||||||
for field in ['name', 'slug']:
|
|
||||||
validator = UniqueTogetherValidator(queryset=VLANGroup.objects.all(), fields=('site', field))
|
|
||||||
validator(data, self)
|
|
||||||
|
|
||||||
# Enforce model validation
|
|
||||||
super().validate(data)
|
|
||||||
|
|
||||||
return data
|
|
||||||
|
|
||||||
def get_scope(self, obj):
|
def get_scope(self, obj):
|
||||||
if obj.scope_id is None:
|
if obj.scope_id is None:
|
||||||
return None
|
return None
|
||||||
@ -155,7 +142,7 @@ class VLANGroupSerializer(OrganizationalModelSerializer):
|
|||||||
class VLANSerializer(PrimaryModelSerializer):
|
class VLANSerializer(PrimaryModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlan-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlan-detail')
|
||||||
site = NestedSiteSerializer(required=False, allow_null=True)
|
site = NestedSiteSerializer(required=False, allow_null=True)
|
||||||
group = NestedVLANGroupSerializer(required=False, allow_null=True)
|
group = NestedVLANGroupSerializer(required=False, allow_null=True, default=None)
|
||||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||||
status = ChoiceField(choices=VLANStatusChoices, required=False)
|
status = ChoiceField(choices=VLANStatusChoices, required=False)
|
||||||
role = NestedRoleSerializer(required=False, allow_null=True)
|
role = NestedRoleSerializer(required=False, allow_null=True)
|
||||||
@ -167,20 +154,6 @@ class VLANSerializer(PrimaryModelSerializer):
|
|||||||
'id', 'url', 'display', 'site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description', 'tags',
|
'id', 'url', 'display', 'site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description', 'tags',
|
||||||
'custom_fields', 'created', 'last_updated', 'prefix_count',
|
'custom_fields', 'created', 'last_updated', 'prefix_count',
|
||||||
]
|
]
|
||||||
validators = []
|
|
||||||
|
|
||||||
def validate(self, data):
|
|
||||||
|
|
||||||
# Validate uniqueness of vid and name if a group has been assigned.
|
|
||||||
if data.get('group', None):
|
|
||||||
for field in ['vid', 'name']:
|
|
||||||
validator = UniqueTogetherValidator(queryset=VLAN.objects.all(), fields=('group', field))
|
|
||||||
validator(data, self)
|
|
||||||
|
|
||||||
# Enforce model validation
|
|
||||||
super().validate(data)
|
|
||||||
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
|
@ -48,7 +48,7 @@ class RouteTargetViewSet(CustomFieldModelViewSet):
|
|||||||
class RIRViewSet(CustomFieldModelViewSet):
|
class RIRViewSet(CustomFieldModelViewSet):
|
||||||
queryset = RIR.objects.annotate(
|
queryset = RIR.objects.annotate(
|
||||||
aggregate_count=count_related(Aggregate, 'rir')
|
aggregate_count=count_related(Aggregate, 'rir')
|
||||||
)
|
).prefetch_related('tags')
|
||||||
serializer_class = serializers.RIRSerializer
|
serializer_class = serializers.RIRSerializer
|
||||||
filterset_class = filtersets.RIRFilterSet
|
filterset_class = filtersets.RIRFilterSet
|
||||||
|
|
||||||
@ -71,7 +71,7 @@ class RoleViewSet(CustomFieldModelViewSet):
|
|||||||
queryset = Role.objects.annotate(
|
queryset = Role.objects.annotate(
|
||||||
prefix_count=count_related(Prefix, 'role'),
|
prefix_count=count_related(Prefix, 'role'),
|
||||||
vlan_count=count_related(VLAN, 'role')
|
vlan_count=count_related(VLAN, 'role')
|
||||||
)
|
).prefetch_related('tags')
|
||||||
serializer_class = serializers.RoleSerializer
|
serializer_class = serializers.RoleSerializer
|
||||||
filterset_class = filtersets.RoleFilterSet
|
filterset_class = filtersets.RoleFilterSet
|
||||||
|
|
||||||
@ -126,7 +126,7 @@ class IPAddressViewSet(CustomFieldModelViewSet):
|
|||||||
class VLANGroupViewSet(CustomFieldModelViewSet):
|
class VLANGroupViewSet(CustomFieldModelViewSet):
|
||||||
queryset = VLANGroup.objects.annotate(
|
queryset = VLANGroup.objects.annotate(
|
||||||
vlan_count=count_related(VLAN, 'group')
|
vlan_count=count_related(VLAN, 'group')
|
||||||
)
|
).prefetch_related('tags')
|
||||||
serializer_class = serializers.VLANGroupSerializer
|
serializer_class = serializers.VLANGroupSerializer
|
||||||
filterset_class = filtersets.VLANGroupFilterSet
|
filterset_class = filtersets.VLANGroupFilterSet
|
||||||
|
|
||||||
|
@ -71,7 +71,7 @@ class RouteTargetBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldMode
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class RIRBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm):
|
class RIRBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||||
pk = forms.ModelMultipleChoiceField(
|
pk = forms.ModelMultipleChoiceField(
|
||||||
queryset=RIR.objects.all(),
|
queryset=RIR.objects.all(),
|
||||||
widget=forms.MultipleHiddenInput
|
widget=forms.MultipleHiddenInput
|
||||||
@ -120,7 +120,7 @@ class AggregateBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelB
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class RoleBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm):
|
class RoleBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||||
pk = forms.ModelMultipleChoiceField(
|
pk = forms.ModelMultipleChoiceField(
|
||||||
queryset=Role.objects.all(),
|
queryset=Role.objects.all(),
|
||||||
widget=forms.MultipleHiddenInput
|
widget=forms.MultipleHiddenInput
|
||||||
@ -280,7 +280,7 @@ class IPAddressBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelB
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class VLANGroupBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm):
|
class VLANGroupBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||||
pk = forms.ModelMultipleChoiceField(
|
pk = forms.ModelMultipleChoiceField(
|
||||||
queryset=VLANGroup.objects.all(),
|
queryset=VLANGroup.objects.all(),
|
||||||
widget=forms.MultipleHiddenInput
|
widget=forms.MultipleHiddenInput
|
||||||
|
@ -82,11 +82,15 @@ class RouteTargetForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
|||||||
|
|
||||||
class RIRForm(BootstrapMixin, CustomFieldModelForm):
|
class RIRForm(BootstrapMixin, CustomFieldModelForm):
|
||||||
slug = SlugField()
|
slug = SlugField()
|
||||||
|
tags = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=Tag.objects.all(),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = RIR
|
model = RIR
|
||||||
fields = [
|
fields = [
|
||||||
'name', 'slug', 'is_private', 'description',
|
'name', 'slug', 'is_private', 'description', 'tags',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -120,11 +124,15 @@ class AggregateForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
|||||||
|
|
||||||
class RoleForm(BootstrapMixin, CustomFieldModelForm):
|
class RoleForm(BootstrapMixin, CustomFieldModelForm):
|
||||||
slug = SlugField()
|
slug = SlugField()
|
||||||
|
tags = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=Tag.objects.all(),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Role
|
model = Role
|
||||||
fields = [
|
fields = [
|
||||||
'name', 'slug', 'weight', 'description',
|
'name', 'slug', 'weight', 'description', 'tags',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -530,15 +538,19 @@ class VLANGroupForm(BootstrapMixin, CustomFieldModelForm):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
slug = SlugField()
|
slug = SlugField()
|
||||||
|
tags = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=Tag.objects.all(),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = VLANGroup
|
model = VLANGroup
|
||||||
fields = [
|
fields = [
|
||||||
'name', 'slug', 'description', 'scope_type', 'region', 'sitegroup', 'site', 'location', 'rack',
|
'name', 'slug', 'description', 'scope_type', 'region', 'sitegroup', 'site', 'location', 'rack',
|
||||||
'clustergroup', 'cluster',
|
'clustergroup', 'cluster', 'tags',
|
||||||
]
|
]
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
('VLAN Group', ('name', 'slug', 'description')),
|
('VLAN Group', ('name', 'slug', 'description', 'tags')),
|
||||||
('Scope', ('scope_type', 'region', 'sitegroup', 'site', 'location', 'rack', 'clustergroup', 'cluster')),
|
('Scope', ('scope_type', 'region', 'sitegroup', 'site', 'location', 'rack', 'clustergroup', 'cluster')),
|
||||||
)
|
)
|
||||||
widgets = {
|
widgets = {
|
||||||
|
30
netbox/ipam/migrations/0051_extend_tag_support.py
Normal file
30
netbox/ipam/migrations/0051_extend_tag_support.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
# Generated by Django 3.2.8 on 2021-10-21 14:50
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
import taggit.managers
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('extras', '0062_clear_secrets_changelog'),
|
||||||
|
('ipam', '0050_iprange'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='rir',
|
||||||
|
name='tags',
|
||||||
|
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='role',
|
||||||
|
name='tags',
|
||||||
|
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='vlangroup',
|
||||||
|
name='tags',
|
||||||
|
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
|
||||||
|
),
|
||||||
|
]
|
@ -31,7 +31,7 @@ __all__ = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||||
class RIR(OrganizationalModel):
|
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
|
||||||
@ -168,7 +168,7 @@ class Aggregate(PrimaryModel):
|
|||||||
return min(utilization, 100)
|
return min(utilization, 100)
|
||||||
|
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||||
class Role(OrganizationalModel):
|
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
|
||||||
|
@ -21,7 +21,7 @@ __all__ = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||||
class VLANGroup(OrganizationalModel):
|
class VLANGroup(OrganizationalModel):
|
||||||
"""
|
"""
|
||||||
A VLAN group is an arbitrary collection of VLANs within which VLAN IDs and names must be unique.
|
A VLAN group is an arbitrary collection of VLANs within which VLAN IDs and names must be unique.
|
||||||
|
@ -85,11 +85,14 @@ class RIRTable(BaseTable):
|
|||||||
url_params={'rir_id': 'pk'},
|
url_params={'rir_id': 'pk'},
|
||||||
verbose_name='Aggregates'
|
verbose_name='Aggregates'
|
||||||
)
|
)
|
||||||
|
tags = TagColumn(
|
||||||
|
url_name='ipam:rir_list'
|
||||||
|
)
|
||||||
actions = ButtonsColumn(RIR)
|
actions = ButtonsColumn(RIR)
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = RIR
|
model = RIR
|
||||||
fields = ('pk', 'name', 'slug', 'is_private', 'aggregate_count', 'description', 'actions')
|
fields = ('pk', 'name', 'slug', 'is_private', 'aggregate_count', 'description', 'tags', 'actions')
|
||||||
default_columns = ('pk', 'name', 'is_private', 'aggregate_count', 'description', 'actions')
|
default_columns = ('pk', 'name', 'is_private', 'aggregate_count', 'description', 'actions')
|
||||||
|
|
||||||
|
|
||||||
@ -144,11 +147,14 @@ class RoleTable(BaseTable):
|
|||||||
url_params={'role_id': 'pk'},
|
url_params={'role_id': 'pk'},
|
||||||
verbose_name='VLANs'
|
verbose_name='VLANs'
|
||||||
)
|
)
|
||||||
|
tags = TagColumn(
|
||||||
|
url_name='ipam:role_list'
|
||||||
|
)
|
||||||
actions = ButtonsColumn(Role)
|
actions = ButtonsColumn(Role)
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = Role
|
model = Role
|
||||||
fields = ('pk', 'name', 'slug', 'prefix_count', 'vlan_count', 'description', 'weight', 'actions')
|
fields = ('pk', 'name', 'slug', 'prefix_count', 'vlan_count', 'description', 'weight', 'tags', 'actions')
|
||||||
default_columns = ('pk', 'name', 'prefix_count', 'vlan_count', 'description', 'actions')
|
default_columns = ('pk', 'name', 'prefix_count', 'vlan_count', 'description', 'actions')
|
||||||
|
|
||||||
|
|
||||||
@ -260,11 +266,16 @@ class IPRangeTable(BaseTable):
|
|||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
tenant = TenantColumn()
|
tenant = TenantColumn()
|
||||||
|
utilization = UtilizationColumn(
|
||||||
|
accessor='utilization',
|
||||||
|
orderable=False
|
||||||
|
)
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = IPRange
|
model = IPRange
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'start_address', 'end_address', 'size', 'vrf', 'status', 'role', 'tenant', 'description',
|
'pk', 'start_address', 'end_address', 'size', 'vrf', 'status', 'role', 'tenant', 'description',
|
||||||
|
'utilization',
|
||||||
)
|
)
|
||||||
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',
|
||||||
|
@ -74,6 +74,9 @@ class VLANGroupTable(BaseTable):
|
|||||||
url_params={'group_id': 'pk'},
|
url_params={'group_id': 'pk'},
|
||||||
verbose_name='VLANs'
|
verbose_name='VLANs'
|
||||||
)
|
)
|
||||||
|
tags = TagColumn(
|
||||||
|
url_name='ipam:vlangroup_list'
|
||||||
|
)
|
||||||
actions = ButtonsColumn(
|
actions = ButtonsColumn(
|
||||||
model=VLANGroup,
|
model=VLANGroup,
|
||||||
prepend_template=VLANGROUP_ADD_VLAN
|
prepend_template=VLANGROUP_ADD_VLAN
|
||||||
@ -81,7 +84,7 @@ class VLANGroupTable(BaseTable):
|
|||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = VLANGroup
|
model = VLANGroup
|
||||||
fields = ('pk', 'name', 'scope_type', 'scope', 'vlan_count', 'slug', 'description', 'actions')
|
fields = ('pk', 'name', 'scope_type', 'scope', 'vlan_count', 'slug', 'description', 'tags', 'actions')
|
||||||
default_columns = ('pk', 'name', 'scope_type', 'scope', 'vlan_count', 'description', 'actions')
|
default_columns = ('pk', 'name', 'scope_type', 'scope', 'vlan_count', 'description', 'actions')
|
||||||
|
|
||||||
|
|
||||||
|
@ -104,11 +104,14 @@ class RIRTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
|||||||
RIR(name='RIR 3', slug='rir-3'),
|
RIR(name='RIR 3', slug='rir-3'),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||||
|
|
||||||
cls.form_data = {
|
cls.form_data = {
|
||||||
'name': 'RIR X',
|
'name': 'RIR X',
|
||||||
'slug': 'rir-x',
|
'slug': 'rir-x',
|
||||||
'is_private': True,
|
'is_private': True,
|
||||||
'description': 'A new RIR',
|
'description': 'A new RIR',
|
||||||
|
'tags': [t.pk for t in tags],
|
||||||
}
|
}
|
||||||
|
|
||||||
cls.csv_data = (
|
cls.csv_data = (
|
||||||
@ -177,11 +180,14 @@ class RoleTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
|||||||
Role(name='Role 3', slug='role-3'),
|
Role(name='Role 3', slug='role-3'),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||||
|
|
||||||
cls.form_data = {
|
cls.form_data = {
|
||||||
'name': 'Role X',
|
'name': 'Role X',
|
||||||
'slug': 'role-x',
|
'slug': 'role-x',
|
||||||
'weight': 200,
|
'weight': 200,
|
||||||
'description': 'A new role',
|
'description': 'A new role',
|
||||||
|
'tags': [t.pk for t in tags],
|
||||||
}
|
}
|
||||||
|
|
||||||
cls.csv_data = (
|
cls.csv_data = (
|
||||||
@ -384,10 +390,13 @@ class VLANGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
|||||||
VLANGroup(name='VLAN Group 3', slug='vlan-group-3', scope=sites[0]),
|
VLANGroup(name='VLAN Group 3', slug='vlan-group-3', scope=sites[0]),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||||
|
|
||||||
cls.form_data = {
|
cls.form_data = {
|
||||||
'name': 'VLAN Group X',
|
'name': 'VLAN Group X',
|
||||||
'slug': 'vlan-group-x',
|
'slug': 'vlan-group-x',
|
||||||
'description': 'A new VLAN group',
|
'description': 'A new VLAN group',
|
||||||
|
'tags': [t.pk for t in tags],
|
||||||
}
|
}
|
||||||
|
|
||||||
cls.csv_data = (
|
cls.csv_data = (
|
||||||
|
@ -147,13 +147,6 @@ class NestedTagSerializer(WritableNestedSerializer):
|
|||||||
# Base model serializers
|
# Base model serializers
|
||||||
#
|
#
|
||||||
|
|
||||||
class OrganizationalModelSerializer(CustomFieldModelSerializer):
|
|
||||||
"""
|
|
||||||
Adds support for custom fields.
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class PrimaryModelSerializer(CustomFieldModelSerializer):
|
class PrimaryModelSerializer(CustomFieldModelSerializer):
|
||||||
"""
|
"""
|
||||||
Adds support for custom fields and tags.
|
Adds support for custom fields and tags.
|
||||||
@ -189,9 +182,9 @@ class PrimaryModelSerializer(CustomFieldModelSerializer):
|
|||||||
return instance
|
return instance
|
||||||
|
|
||||||
|
|
||||||
class NestedGroupModelSerializer(CustomFieldModelSerializer):
|
class NestedGroupModelSerializer(PrimaryModelSerializer):
|
||||||
"""
|
"""
|
||||||
Extends OrganizationalModelSerializer to include MPTT support.
|
Extends PrimaryModelSerializer to include MPTT support.
|
||||||
"""
|
"""
|
||||||
_depth = serializers.IntegerField(source='level', read_only=True)
|
_depth = serializers.IntegerField(source='level', read_only=True)
|
||||||
|
|
||||||
|
@ -41,6 +41,7 @@ class ObjectType(
|
|||||||
class OrganizationalObjectType(
|
class OrganizationalObjectType(
|
||||||
ChangelogMixin,
|
ChangelogMixin,
|
||||||
CustomFieldsMixin,
|
CustomFieldsMixin,
|
||||||
|
TagsMixin,
|
||||||
BaseObjectType
|
BaseObjectType
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
|
@ -143,6 +143,18 @@ class CustomValidationMixin(models.Model):
|
|||||||
post_clean.send(sender=self.__class__, instance=self)
|
post_clean.send(sender=self.__class__, instance=self)
|
||||||
|
|
||||||
|
|
||||||
|
class TagsMixin(models.Model):
|
||||||
|
"""
|
||||||
|
Enable the assignment of Tags.
|
||||||
|
"""
|
||||||
|
tags = TaggableManager(
|
||||||
|
through='extras.TaggedItem'
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Base model classes
|
# Base model classes
|
||||||
|
|
||||||
@ -166,7 +178,7 @@ class ChangeLoggedModel(ChangeLoggingMixin, CustomValidationMixin, BigIDModel):
|
|||||||
abstract = True
|
abstract = True
|
||||||
|
|
||||||
|
|
||||||
class PrimaryModel(ChangeLoggingMixin, CustomFieldsMixin, CustomValidationMixin, BigIDModel):
|
class PrimaryModel(ChangeLoggingMixin, CustomFieldsMixin, CustomValidationMixin, TagsMixin, BigIDModel):
|
||||||
"""
|
"""
|
||||||
Primary models represent real objects within the infrastructure being modeled.
|
Primary models represent real objects within the infrastructure being modeled.
|
||||||
"""
|
"""
|
||||||
@ -175,15 +187,12 @@ class PrimaryModel(ChangeLoggingMixin, CustomFieldsMixin, CustomValidationMixin,
|
|||||||
object_id_field='assigned_object_id',
|
object_id_field='assigned_object_id',
|
||||||
content_type_field='assigned_object_type'
|
content_type_field='assigned_object_type'
|
||||||
)
|
)
|
||||||
tags = TaggableManager(
|
|
||||||
through='extras.TaggedItem'
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
abstract = True
|
abstract = True
|
||||||
|
|
||||||
|
|
||||||
class NestedGroupModel(ChangeLoggingMixin, CustomFieldsMixin, CustomValidationMixin, BigIDModel, MPTTModel):
|
class NestedGroupModel(ChangeLoggingMixin, CustomFieldsMixin, CustomValidationMixin, TagsMixin, BigIDModel, MPTTModel):
|
||||||
"""
|
"""
|
||||||
Base model for objects which are used to form a hierarchy (regions, locations, etc.). These models nest
|
Base model for objects which are used to form a hierarchy (regions, locations, etc.). These models nest
|
||||||
recursively using MPTT. Within each parent, each child instance must have a unique name.
|
recursively using MPTT. Within each parent, each child instance must have a unique name.
|
||||||
@ -225,7 +234,7 @@ class NestedGroupModel(ChangeLoggingMixin, CustomFieldsMixin, CustomValidationMi
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
class OrganizationalModel(ChangeLoggingMixin, CustomFieldsMixin, CustomValidationMixin, BigIDModel):
|
class OrganizationalModel(ChangeLoggingMixin, CustomFieldsMixin, CustomValidationMixin, TagsMixin, BigIDModel):
|
||||||
"""
|
"""
|
||||||
Organizational models are those which are used solely to categorize and qualify other objects, and do not convey
|
Organizational models are those which are used solely to categorize and qualify other objects, and do not convey
|
||||||
any real information about the infrastructure being modeled (for example, functional device roles). Organizational
|
any real information about the infrastructure being modeled (for example, functional device roles). Organizational
|
||||||
|
@ -120,6 +120,14 @@ ORGANIZATION_MENU = Menu(
|
|||||||
get_model_item('tenancy', 'tenantgroup', 'Tenant Groups'),
|
get_model_item('tenancy', 'tenantgroup', 'Tenant Groups'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
MenuGroup(
|
||||||
|
label='Contacts',
|
||||||
|
items=(
|
||||||
|
get_model_item('tenancy', 'contact', 'Contacts'),
|
||||||
|
get_model_item('tenancy', 'contactgroup', 'Contact Groups'),
|
||||||
|
get_model_item('tenancy', 'contactrole', 'Contact Roles'),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -16,7 +16,7 @@ from django.core.validators import URLValidator
|
|||||||
# Environment setup
|
# Environment setup
|
||||||
#
|
#
|
||||||
|
|
||||||
VERSION = '3.0.8-dev'
|
VERSION = '3.0.9-dev'
|
||||||
|
|
||||||
# Hostname
|
# Hostname
|
||||||
HOSTNAME = platform.node()
|
HOSTNAME = platform.node()
|
||||||
|
@ -137,7 +137,7 @@ class HomeView(View):
|
|||||||
release_version, release_url = latest_release
|
release_version, release_url = latest_release
|
||||||
if release_version > version.parse(settings.VERSION):
|
if release_version > version.parse(settings.VERSION):
|
||||||
new_release = {
|
new_release = {
|
||||||
'version': str(latest_release),
|
'version': str(release_version),
|
||||||
'url': release_url,
|
'url': release_url,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -282,11 +282,11 @@ class ObjectEditView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
|||||||
messages.success(request, mark_safe(msg))
|
messages.success(request, mark_safe(msg))
|
||||||
|
|
||||||
if '_addanother' in request.POST:
|
if '_addanother' in request.POST:
|
||||||
redirect_url = request.get_full_path()
|
redirect_url = request.path
|
||||||
|
|
||||||
# If the object has clone_fields, pre-populate a new instance of the form
|
# If the object has clone_fields, pre-populate a new instance of the form
|
||||||
if hasattr(obj, 'clone_fields'):
|
if hasattr(obj, 'clone_fields'):
|
||||||
redirect_url += f"{'&' if '?' in redirect_url else '?'}{prepare_cloned_fields(obj)}"
|
redirect_url += f"?{prepare_cloned_fields(obj)}"
|
||||||
|
|
||||||
return redirect(redirect_url)
|
return redirect(redirect_url)
|
||||||
|
|
||||||
|
BIN
netbox/project-static/dist/lldp.js
vendored
BIN
netbox/project-static/dist/lldp.js
vendored
Binary file not shown.
BIN
netbox/project-static/dist/lldp.js.map
vendored
BIN
netbox/project-static/dist/lldp.js.map
vendored
Binary file not shown.
BIN
netbox/project-static/dist/netbox-dark.css
vendored
BIN
netbox/project-static/dist/netbox-dark.css
vendored
Binary file not shown.
BIN
netbox/project-static/dist/netbox-light.css
vendored
BIN
netbox/project-static/dist/netbox-light.css
vendored
Binary file not shown.
BIN
netbox/project-static/dist/netbox-print.css
vendored
BIN
netbox/project-static/dist/netbox-print.css
vendored
Binary file not shown.
BIN
netbox/project-static/dist/netbox.js
vendored
BIN
netbox/project-static/dist/netbox.js
vendored
Binary file not shown.
BIN
netbox/project-static/dist/netbox.js.map
vendored
BIN
netbox/project-static/dist/netbox.js.map
vendored
Binary file not shown.
@ -1,6 +1,17 @@
|
|||||||
import { createToast } from '../bs';
|
import { createToast } from '../bs';
|
||||||
import { getNetboxData, apiGetBase, hasError, isTruthy, toggleLoader } from '../util';
|
import { getNetboxData, apiGetBase, hasError, isTruthy, toggleLoader } from '../util';
|
||||||
|
|
||||||
|
// Match an interface name that begins with a capital letter and is followed by at least one other
|
||||||
|
// alphabetic character, and ends with a forward-slash-separated numeric sequence such as 0/1/2.
|
||||||
|
const CISCO_IOS_PATTERN = new RegExp(/^([A-Z][A-Za-z]+)[^0-9]*([0-9/]+)$/);
|
||||||
|
|
||||||
|
// Mapping of overrides to default Cisco IOS interface alias behavior (default behavior is to use
|
||||||
|
// the first two characters).
|
||||||
|
const CISCO_IOS_OVERRIDES = new Map<string, string>([
|
||||||
|
// Cisco IOS abbreviates 25G (TwentyFiveGigE) interfaces as 'Twe'.
|
||||||
|
['TwentyFiveGigE', 'Twe'],
|
||||||
|
]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get an attribute from a row's cell.
|
* Get an attribute from a row's cell.
|
||||||
*
|
*
|
||||||
@ -12,6 +23,40 @@ function getData(row: HTMLTableRowElement, query: string, attr: string): string
|
|||||||
return row.querySelector(query)?.getAttribute(attr) ?? null;
|
return row.querySelector(query)?.getAttribute(attr) ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get preconfigured alias for given interface. Primarily for matching long-form Cisco IOS
|
||||||
|
* interface names with short-form Cisco IOS interface names. For example, `GigabitEthernet0/1/2`
|
||||||
|
* would become `Gi0/1/2`.
|
||||||
|
*
|
||||||
|
* This should probably be replaced with something in the primary application (Django), such as
|
||||||
|
* a database field attached to given interface types. However, this is a temporary measure to
|
||||||
|
* replace the functionality of this one-liner:
|
||||||
|
*
|
||||||
|
* @see https://github.com/netbox-community/netbox/blob/9cc4992fad2fe04ef0211d998c517414e8871d8c/netbox/templates/dcim/device/lldp_neighbors.html#L69
|
||||||
|
*
|
||||||
|
* @param name Long-form/original interface name.
|
||||||
|
*/
|
||||||
|
function getInterfaceAlias(name: string | null): string | null {
|
||||||
|
if (name === null) {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
if (name.match(CISCO_IOS_PATTERN)) {
|
||||||
|
// Extract the base name and numeric portions of the interface. For example, an input interface
|
||||||
|
// of `GigabitEthernet0/0/1` would result in an array of `['GigabitEthernet', '0/0/1']`.
|
||||||
|
const [base, numeric] = (name.match(CISCO_IOS_PATTERN) ?? []).slice(1, 3);
|
||||||
|
|
||||||
|
if (isTruthy(base) && isTruthy(numeric)) {
|
||||||
|
// Check the override map and use its value if the base name is present in the map.
|
||||||
|
// Otherwise, use the first two characters of the base name. For example,
|
||||||
|
// `GigabitEthernet0/0/1` would become `Gi0/0/1`, but `TwentyFiveGigE0/0/1` would become
|
||||||
|
// `Twe0/0/1`.
|
||||||
|
const aliasBase = CISCO_IOS_OVERRIDES.get(base) || base.slice(0, 2);
|
||||||
|
return `${aliasBase}${numeric}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update row styles based on LLDP neighbor data.
|
* Update row styles based on LLDP neighbor data.
|
||||||
*/
|
*/
|
||||||
@ -23,38 +68,41 @@ function updateRowStyle(data: LLDPNeighborDetail) {
|
|||||||
|
|
||||||
if (row !== null) {
|
if (row !== null) {
|
||||||
for (const neighbor of neighbors) {
|
for (const neighbor of neighbors) {
|
||||||
const cellDevice = row.querySelector<HTMLTableCellElement>('td.device');
|
const deviceCell = row.querySelector<HTMLTableCellElement>('td.device');
|
||||||
const cellInterface = row.querySelector<HTMLTableCellElement>('td.interface');
|
const interfaceCell = row.querySelector<HTMLTableCellElement>('td.interface');
|
||||||
const cDevice = getData(row, 'td.configured_device', 'data');
|
const configuredDevice = getData(row, 'td.configured_device', 'data');
|
||||||
const cChassis = getData(row, 'td.configured_chassis', 'data-chassis');
|
const configuredChassis = getData(row, 'td.configured_chassis', 'data-chassis');
|
||||||
const cInterface = getData(row, 'td.configured_interface', 'data');
|
const configuredIface = getData(row, 'td.configured_interface', 'data');
|
||||||
|
|
||||||
let cInterfaceShort = null;
|
const interfaceAlias = getInterfaceAlias(configuredIface);
|
||||||
if (isTruthy(cInterface)) {
|
|
||||||
cInterfaceShort = cInterface.replace(/^([A-Z][a-z])[^0-9]*([0-9/]+)$/, '$1$2');
|
const remoteName = neighbor.remote_system_name ?? '';
|
||||||
|
const remotePort = neighbor.remote_port ?? '';
|
||||||
|
const [neighborDevice] = remoteName.split('.');
|
||||||
|
const [neighborIface] = remotePort.split('.');
|
||||||
|
|
||||||
|
if (deviceCell !== null) {
|
||||||
|
deviceCell.innerText = neighborDevice;
|
||||||
}
|
}
|
||||||
|
|
||||||
const nHost = neighbor.remote_system_name ?? '';
|
if (interfaceCell !== null) {
|
||||||
const nPort = neighbor.remote_port ?? '';
|
interfaceCell.innerText = neighborIface;
|
||||||
const [nDevice] = nHost.split('.');
|
|
||||||
const [nInterface] = nPort.split('.');
|
|
||||||
|
|
||||||
if (cellDevice !== null) {
|
|
||||||
cellDevice.innerText = nDevice;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cellInterface !== null) {
|
// Interface has an LLDP neighbor, but the neighbor is not configured in NetBox.
|
||||||
cellInterface.innerText = nInterface;
|
const nonConfiguredDevice = !isTruthy(configuredDevice) && isTruthy(neighborDevice);
|
||||||
}
|
|
||||||
|
|
||||||
if (!isTruthy(cDevice) && isTruthy(nDevice)) {
|
// NetBox device or chassis matches LLDP neighbor.
|
||||||
|
const validNode =
|
||||||
|
configuredDevice === neighborDevice || configuredChassis === neighborDevice;
|
||||||
|
|
||||||
|
// NetBox configured interface matches LLDP neighbor interface.
|
||||||
|
const validInterface =
|
||||||
|
configuredIface === neighborIface || interfaceAlias === neighborIface;
|
||||||
|
|
||||||
|
if (nonConfiguredDevice) {
|
||||||
row.classList.add('info');
|
row.classList.add('info');
|
||||||
} else if (
|
} else if (validNode && validInterface) {
|
||||||
(cDevice === nDevice || cChassis === nDevice) &&
|
|
||||||
cInterfaceShort === nInterface
|
|
||||||
) {
|
|
||||||
row.classList.add('success');
|
|
||||||
} else if (cDevice === nDevice || cChassis === nDevice) {
|
|
||||||
row.classList.add('success');
|
row.classList.add('success');
|
||||||
} else {
|
} else {
|
||||||
row.classList.add('danger');
|
row.classList.add('danger');
|
||||||
|
@ -266,10 +266,8 @@ class SideNav {
|
|||||||
for (const link of this.getActiveLinks()) {
|
for (const link of this.getActiveLinks()) {
|
||||||
this.activateLink(link, 'collapse');
|
this.activateLink(link, 'collapse');
|
||||||
}
|
}
|
||||||
setTimeout(() => {
|
this.bodyRemove('hide');
|
||||||
this.bodyRemove('hide');
|
this.bodyAdd('hidden');
|
||||||
this.bodyAdd('hidden');
|
|
||||||
}, 300);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -197,9 +197,15 @@ table {
|
|||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.dropdown {
|
||||||
|
// Presence of 'overflow: scroll' on a table causes dropdowns to be improperly hidden when
|
||||||
|
// opened. See: https://github.com/twbs/bootstrap/issues/24251
|
||||||
|
position: static;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
th {
|
th {
|
||||||
a, a:hover {
|
a,
|
||||||
|
a:hover {
|
||||||
color: $body-color;
|
color: $body-color;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
@ -105,6 +105,11 @@
|
|||||||
// Navbar brand
|
// Navbar brand
|
||||||
.sidenav-brand {
|
.sidenav-brand {
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
|
transition: opacity 0.1s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidenav-brand-icon {
|
||||||
|
transition: opacity 0.1s ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidenav-inner {
|
.sidenav-inner {
|
||||||
@ -141,7 +146,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.sidenav-toggle {
|
.sidenav-toggle {
|
||||||
display: none;
|
// The sidenav toggle's default state is "hidden". Because modifying the `display` property
|
||||||
|
// isn't ideal for smooth transitions, combine opacity 0 (transparent) and position absolute
|
||||||
|
// to yield a similar result.
|
||||||
|
position: absolute;
|
||||||
|
display: inline-block;
|
||||||
|
opacity: 0;
|
||||||
|
// The transition itself is largely irrelevant, but CSS needs *something* to transition in
|
||||||
|
// order to apply a delay.
|
||||||
|
transition: opacity 10ms ease-in-out;
|
||||||
|
// Offset the transition delay so the icon isn't visible during the logo transition.
|
||||||
|
transition-delay: 0.1s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidenav-collapse {
|
.sidenav-collapse {
|
||||||
@ -350,13 +365,21 @@
|
|||||||
.sidenav-brand {
|
.sidenav-brand {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateX(-150%);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidenav-brand-icon {
|
.sidenav-brand-icon {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sidenav-toggle {
|
||||||
|
// Immediately hide the toggle when the sidenav is closed, so it doesn't linger and overlap
|
||||||
|
// with the logo elements.
|
||||||
|
opacity: 0;
|
||||||
|
position: absolute;
|
||||||
|
transition: unset;
|
||||||
|
transition-delay: 0ms;
|
||||||
|
}
|
||||||
|
|
||||||
.navbar-nav > .nav-item {
|
.navbar-nav > .nav-item {
|
||||||
> .nav-link {
|
> .nav-link {
|
||||||
&:after {
|
&:after {
|
||||||
@ -402,7 +425,8 @@
|
|||||||
|
|
||||||
@include media-breakpoint-up(lg) {
|
@include media-breakpoint-up(lg) {
|
||||||
.sidenav-toggle {
|
.sidenav-toggle {
|
||||||
display: inline-block;
|
position: relative;
|
||||||
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -74,6 +74,7 @@ $btn-link-disabled-color: $gray-300;
|
|||||||
|
|
||||||
// Forms
|
// Forms
|
||||||
$component-active-bg: $primary;
|
$component-active-bg: $primary;
|
||||||
|
$component-active-color: $black;
|
||||||
$form-text-color: $text-muted;
|
$form-text-color: $text-muted;
|
||||||
$input-bg: $gray-900;
|
$input-bg: $gray-900;
|
||||||
$input-disabled-bg: $gray-700;
|
$input-disabled-bg: $gray-700;
|
||||||
|
@ -64,17 +64,18 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% include 'inc/custom_fields_panel.html' %}
|
{% include 'inc/panels/custom_fields.html' %}
|
||||||
{% include 'extras/inc/tags_panel.html' with tags=object.tags.all url='circuits:circuit_list' %}
|
{% include 'inc/panels/tags.html' %}
|
||||||
{% include 'inc/comments_panel.html' %}
|
{% include 'inc/panels/comments.html' %}
|
||||||
{% plugin_left_page object %}
|
{% plugin_left_page object %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col col-md-6">
|
<div class="col col-md-6">
|
||||||
{% include 'circuits/inc/circuit_termination.html' with termination=object.termination_a side='A' %}
|
{% include 'circuits/inc/circuit_termination.html' with termination=object.termination_a side='A' %}
|
||||||
{% include 'circuits/inc/circuit_termination.html' with termination=object.termination_z side='Z' %}
|
{% include 'circuits/inc/circuit_termination.html' with termination=object.termination_z side='Z' %}
|
||||||
{% include 'inc/image_attachments_panel.html' %}
|
{% include 'inc/panels/contacts.html' %}
|
||||||
{% plugin_right_page object %}
|
{% include 'inc/panels/image_attachments.html' %}
|
||||||
</div>
|
{% plugin_right_page object %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col col-md-12">
|
<div class="col col-md-12">
|
||||||
|
@ -28,10 +28,11 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% include 'inc/panels/tags.html' %}
|
||||||
{% plugin_left_page object %}
|
{% plugin_left_page object %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col col-md-6">
|
<div class="col col-md-6">
|
||||||
{% include 'inc/custom_fields_panel.html' %}
|
{% include 'inc/panels/custom_fields.html' %}
|
||||||
{% plugin_right_page object %}
|
{% plugin_right_page object %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -47,12 +47,13 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% include 'inc/panels/tags.html' %}
|
||||||
{% plugin_left_page object %}
|
{% plugin_left_page object %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col col-md-6">
|
<div class="col col-md-6">
|
||||||
{% include 'inc/custom_fields_panel.html' %}
|
{% include 'inc/panels/custom_fields.html' %}
|
||||||
{% include 'extras/inc/tags_panel.html' with tags=object.tags.all url='circuits:provider_list' %}
|
{% include 'inc/panels/comments.html' %}
|
||||||
{% include 'inc/comments_panel.html' %}
|
{% include 'inc/panels/contacts.html' %}
|
||||||
{% plugin_right_page object %}
|
{% plugin_right_page object %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col col-md-12">
|
<div class="col col-md-12">
|
||||||
|
@ -37,9 +37,9 @@
|
|||||||
{% plugin_left_page object %}
|
{% plugin_left_page object %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col col-md-6">
|
<div class="col col-md-6">
|
||||||
{% include 'inc/custom_fields_panel.html' %}
|
{% include 'inc/panels/custom_fields.html' %}
|
||||||
{% include 'extras/inc/tags_panel.html' with tags=object.tags.all url='circuits:providernetwork_list' %}
|
{% include 'inc/panels/tags.html' %}
|
||||||
{% include 'inc/comments_panel.html' %}
|
{% include 'inc/panels/comments.html' %}
|
||||||
{% plugin_right_page object %}
|
{% plugin_right_page object %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -23,6 +23,19 @@
|
|||||||
<span class="badge bg-{{ object.get_status_class }}">{{ object.get_status_display }}</span>
|
<span class="badge bg-{{ object.get_status_class }}">{{ object.get_status_display }}</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Tenant</th>
|
||||||
|
<td>
|
||||||
|
{% if object.tenant %}
|
||||||
|
{% if object.tenant.group %}
|
||||||
|
<a href="{{ object.tenant.group.get_absolute_url }}">{{ object.tenant.group }}</a> /
|
||||||
|
{% endif %}
|
||||||
|
<a href="{{ object.tenant.get_absolute_url }}">{{ object.tenant }}</a>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">None</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Label</th>
|
<th scope="row">Label</th>
|
||||||
<td>{{ object.label|placeholder }}</td>
|
<td>{{ object.label|placeholder }}</td>
|
||||||
@ -50,8 +63,8 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% include 'inc/custom_fields_panel.html' %}
|
{% include 'inc/panels/custom_fields.html' %}
|
||||||
{% include 'extras/inc/tags_panel.html' with tags=object.tags.all url='dcim:cable_list' %}
|
{% include 'inc/panels/tags.html' %}
|
||||||
{% plugin_left_page object %}
|
{% plugin_left_page object %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col col-md-6">
|
<div class="col col-md-6">
|
||||||
|
@ -40,8 +40,8 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% include 'inc/custom_fields_panel.html' %}
|
{% include 'inc/panels/custom_fields.html' %}
|
||||||
{% include 'extras/inc/tags_panel.html' with tags=object.tags.all %}
|
{% include 'inc/panels/tags.html' %}
|
||||||
{% plugin_left_page object %}
|
{% plugin_left_page object %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col col-md-6">
|
<div class="col col-md-6">
|
||||||
|
@ -40,8 +40,8 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% include 'inc/custom_fields_panel.html' %}
|
{% include 'inc/panels/custom_fields.html' %}
|
||||||
{% include 'extras/inc/tags_panel.html' with tags=object.tags.all %}
|
{% include 'inc/panels/tags.html' %}
|
||||||
{% plugin_left_page object %}
|
{% plugin_left_page object %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col col-md-6">
|
<div class="col col-md-6">
|
||||||
|
@ -220,9 +220,9 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% include 'inc/custom_fields_panel.html' %}
|
{% include 'inc/panels/custom_fields.html' %}
|
||||||
{% include 'extras/inc/tags_panel.html' with tags=object.tags.all url='dcim:device_list' %}
|
{% include 'inc/panels/tags.html' %}
|
||||||
{% include 'inc/comments_panel.html' %}
|
{% include 'inc/panels/comments.html' %}
|
||||||
{% plugin_left_page object %}
|
{% plugin_left_page object %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col col-md-6">
|
<div class="col col-md-6">
|
||||||
@ -296,7 +296,8 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% include 'inc/image_attachments_panel.html' %}
|
{% include 'inc/panels/contacts.html' %}
|
||||||
|
{% include 'inc/panels/image_attachments.html' %}
|
||||||
<div class="card noprint">
|
<div class="card noprint">
|
||||||
<h5 class="card-header">
|
<h5 class="card-header">
|
||||||
Related Devices
|
Related Devices
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
<form method="post">
|
<form method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% include 'inc/table_controls.html' with table_modal="DeviceConsolePortTable_config" %}
|
{% include 'inc/table_controls.html' with table_modal="DeviceConsolePortTable_config" %}
|
||||||
{% render_table consoleport_table 'inc/table.html' %}
|
{% render_table table 'inc/table.html' %}
|
||||||
<div class="noprint bulk-buttons">
|
<div class="noprint bulk-buttons">
|
||||||
<div class="bulk-button-group">
|
<div class="bulk-button-group">
|
||||||
{% if perms.dcim.change_consoleport %}
|
{% if perms.dcim.change_consoleport %}
|
||||||
@ -36,6 +36,6 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
{% include 'inc/paginator.html' with paginator=consoleport_table.paginator page=consoleport_table.page %}
|
{% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
|
||||||
{% table_config_form consoleport_table %}
|
{% table_config_form table %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
<form method="post">
|
<form method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% include 'inc/table_controls.html' with table_modal="DeviceConsoleServerPortTable_config" %}
|
{% include 'inc/table_controls.html' with table_modal="DeviceConsoleServerPortTable_config" %}
|
||||||
{% render_table consoleserverport_table 'inc/table.html' %}
|
{% render_table table 'inc/table.html' %}
|
||||||
<div class="noprint bulk-buttons">
|
<div class="noprint bulk-buttons">
|
||||||
<div class="bulk-button-group">
|
<div class="bulk-button-group">
|
||||||
{% if perms.dcim.change_consoleserverport %}
|
{% if perms.dcim.change_consoleserverport %}
|
||||||
@ -36,6 +36,6 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
{% include 'inc/paginator.html' with paginator=consoleserverport_table.paginator page=consoleserverport_table.page %}
|
{% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
|
||||||
{% table_config_form consoleserverport_table %}
|
{% table_config_form table %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
<form method="post">
|
<form method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% include 'inc/table_controls.html' with table_modal="DeviceDeviceBayTable_config" %}
|
{% include 'inc/table_controls.html' with table_modal="DeviceDeviceBayTable_config" %}
|
||||||
{% render_table devicebay_table 'inc/table.html' %}
|
{% render_table table 'inc/table.html' %}
|
||||||
<div class="noprint bulk-buttons">
|
<div class="noprint bulk-buttons">
|
||||||
<div class="bulk-button-group">
|
<div class="bulk-button-group">
|
||||||
{% if perms.dcim.change_devicebay %}
|
{% if perms.dcim.change_devicebay %}
|
||||||
@ -33,6 +33,6 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
{% include 'inc/paginator.html' with paginator=devicebay_table.paginator page=devicebay_table.page %}
|
{% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
|
||||||
{% table_config_form devicebay_table %}
|
{% table_config_form table %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
<form method="post">
|
<form method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% include 'inc/table_controls.html' with table_modal="DeviceFrontPortTable_config" %}
|
{% include 'inc/table_controls.html' with table_modal="DeviceFrontPortTable_config" %}
|
||||||
{% render_table frontport_table 'inc/table.html' %}
|
{% render_table table 'inc/table.html' %}
|
||||||
<div class="noprint bulk-buttons">
|
<div class="noprint bulk-buttons">
|
||||||
<div class="bulk-button-group">
|
<div class="bulk-button-group">
|
||||||
{% if perms.dcim.change_frontport %}
|
{% if perms.dcim.change_frontport %}
|
||||||
@ -36,6 +36,6 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
{% include 'inc/paginator.html' with paginator=frontport_table.paginator page=frontport_table.page %}
|
{% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
|
||||||
{% table_config_form frontport_table %}
|
{% table_config_form table %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -34,7 +34,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% render_table interface_table 'inc/table.html' %}
|
{% render_table table 'inc/table.html' %}
|
||||||
<div class="noprint bulk-buttons">
|
<div class="noprint bulk-buttons">
|
||||||
<div class="bulk-button-group">
|
<div class="bulk-button-group">
|
||||||
{% if perms.dcim.change_interface %}
|
{% if perms.dcim.change_interface %}
|
||||||
@ -63,6 +63,6 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
{% include 'inc/paginator.html' with paginator=interface_table.paginator page=interface_table.page %}
|
{% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
|
||||||
{% table_config_form interface_table %}
|
{% table_config_form table %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
<form method="post">
|
<form method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% include 'inc/table_controls.html' with table_modal="DeviceInventoryItemTable_config" %}
|
{% include 'inc/table_controls.html' with table_modal="DeviceInventoryItemTable_config" %}
|
||||||
{% render_table inventoryitem_table 'inc/table.html' %}
|
{% render_table table 'inc/table.html' %}
|
||||||
<div class="noprint bulk-buttons">
|
<div class="noprint bulk-buttons">
|
||||||
<div class="bulk-button-group">
|
<div class="bulk-button-group">
|
||||||
{% if perms.dcim.change_inventoryitem %}
|
{% if perms.dcim.change_inventoryitem %}
|
||||||
@ -33,6 +33,6 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
{% include 'inc/paginator.html' with paginator=inventoryitem_table.paginator page=inventoryitem_table.page %}
|
{% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
|
||||||
{% table_config_form inventoryitem_table %}
|
{% table_config_form table %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
<form method="post">
|
<form method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% include 'inc/table_controls.html' with table_modal="DevicePowerOutletTable_config" %}
|
{% include 'inc/table_controls.html' with table_modal="DevicePowerOutletTable_config" %}
|
||||||
{% render_table poweroutlet_table 'inc/table.html' %}
|
{% render_table table 'inc/table.html' %}
|
||||||
<div class="noprint bulk-buttons">
|
<div class="noprint bulk-buttons">
|
||||||
<div class="bulk-button-group">
|
<div class="bulk-button-group">
|
||||||
{% if perms.dcim.change_powerport %}
|
{% if perms.dcim.change_powerport %}
|
||||||
@ -36,6 +36,6 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
{% include 'inc/paginator.html' with paginator=poweroutlet_table.paginator page=poweroutlet_table.page %}
|
{% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
|
||||||
{% table_config_form poweroutlet_table %}
|
{% table_config_form table %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user