mirror of
https://github.com/netbox-community/netbox.git
synced 2026-02-06 08:59:32 +01:00
Compare commits
83 Commits
v3.3-beta1
...
v3.3-beta2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f11a6f0135 | ||
|
|
44850feaf8 | ||
|
|
367bf25618 | ||
|
|
b9678c7c0e | ||
|
|
37c4f1a7d3 | ||
|
|
d3a567a2f5 | ||
|
|
5b3ef04550 | ||
|
|
e96620260a | ||
|
|
29a611c729 | ||
|
|
562769fb89 | ||
|
|
4591237bfd | ||
|
|
262a0cf397 | ||
|
|
ff3fcb8134 | ||
|
|
d4d73674fc | ||
|
|
984d15d7fb | ||
|
|
efa449faff | ||
|
|
3af989763e | ||
|
|
9646f88384 | ||
|
|
1bbf5d214b | ||
|
|
8a075bcff9 | ||
|
|
9fe5f09742 | ||
|
|
84f2225f42 | ||
|
|
728ad51624 | ||
|
|
5ab03b7e92 | ||
|
|
6904666e2a | ||
|
|
890efa5400 | ||
|
|
04fb0bd51c | ||
|
|
2c43c8d077 | ||
|
|
c5fb7b72f0 | ||
|
|
07620db027 | ||
|
|
f8a3ffae4e | ||
|
|
62d1510c55 | ||
|
|
498b655cb7 | ||
|
|
fa94d9c82c | ||
|
|
6cee12b153 | ||
|
|
a6be8dccf5 | ||
|
|
466931d2fb | ||
|
|
d442f8fd60 | ||
|
|
7631722f97 | ||
|
|
6d30c07dd0 | ||
|
|
6f7289f932 | ||
|
|
2583abc39d | ||
|
|
12476036cd | ||
|
|
91070f823a | ||
|
|
8df4966a2b | ||
|
|
451a0067c7 | ||
|
|
e2580ea469 | ||
|
|
abf11fbcb8 | ||
|
|
383918d83b | ||
|
|
f8cbd322ba | ||
|
|
9835d6b2df | ||
|
|
17e00ac040 | ||
|
|
e92b7f8bb9 | ||
|
|
1c9db2d9f8 | ||
|
|
44586743ea | ||
|
|
802d9d2b6e | ||
|
|
a7a20ad2ea | ||
|
|
124ff23e3d | ||
|
|
abfa6a325a | ||
|
|
0e18292e41 | ||
|
|
6d53788ea2 | ||
|
|
fae9874dde | ||
|
|
b8da66bb55 | ||
|
|
8a2276e791 | ||
|
|
4bdef80554 | ||
|
|
1a028f77d4 | ||
|
|
7603468abc | ||
|
|
b854cefb57 | ||
|
|
58b191b439 | ||
|
|
3d475e5afa | ||
|
|
f385a5fd5e | ||
|
|
250265c3d9 | ||
|
|
e07dd3ddcb | ||
|
|
68f53aaa87 | ||
|
|
5fda5cc08c | ||
|
|
68b87dd668 | ||
|
|
6da171a699 | ||
|
|
024e7d8651 | ||
|
|
e8dd952aa5 | ||
|
|
fe2fae5b86 | ||
|
|
5b5160ca6f | ||
|
|
b9dd654e7a | ||
|
|
b0df24e6d1 |
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@@ -14,7 +14,7 @@ body:
|
||||
attributes:
|
||||
label: NetBox version
|
||||
description: What version of NetBox are you currently running?
|
||||
placeholder: v3.3-beta1
|
||||
placeholder: v3.3-beta2
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
2
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
@@ -14,7 +14,7 @@ body:
|
||||
attributes:
|
||||
label: NetBox version
|
||||
description: What version of NetBox are you currently running?
|
||||
placeholder: v3.3-beta1
|
||||
placeholder: v3.3-beta2
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
||||
@@ -34,4 +34,4 @@ REMOTE_AUTH_BACKEND = 'social_core.backends.google.GoogleOAuth2'
|
||||
|
||||
NetBox supports single sign-on authentication via the [python-social-auth](https://github.com/python-social-auth) library. To enable SSO, specify the path to the desired authentication backend within the `social_core` Python package. Please see the complete list of [supported authentication backends](https://github.com/python-social-auth/social-core/tree/master/social_core/backends) for the available options.
|
||||
|
||||
Most remote authentication backends require some additional configuration through settings prefixed with `SOCIAL_AUTH_`. These will be automatically imported from NetBox's `configuration.py` file. Additionally, the [authentication pipeline](https://python-social-auth.readthedocs.io/en/latest/pipeline.html) can be customized via the `SOCIAL_AUTH_PIPELINE` parameter.
|
||||
Most remote authentication backends require some additional configuration through settings prefixed with `SOCIAL_AUTH_`. These will be automatically imported from NetBox's `configuration.py` file. Additionally, the [authentication pipeline](https://python-social-auth.readthedocs.io/en/latest/pipeline.html) can be customized via the `SOCIAL_AUTH_PIPELINE` parameter. (NetBox's default pipeline is defined in `netbox/settings.py` for your reference.)
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
## Interface Templates
|
||||
|
||||
A template for a network interface that will be created on all instantiations of the parent device type. Each interface may be assigned a physical or virtual type, and may be designated as "management-only."
|
||||
A template for a network interface that will be created on all instantiations of the parent device type. Each interface may be assigned a physical or virtual type, and may be designated as "management-only." Power over Ethernet (PoE) mode and type may also be assigned to interface templates.
|
||||
|
||||
@@ -43,7 +43,7 @@ The following data is available as context for Jinja2 templates:
|
||||
* `username` - The name of the user account associated with the change.
|
||||
* `request_id` - The unique request ID. This may be used to correlate multiple changes associated with a single request.
|
||||
* `data` - A detailed representation of the object in its current state. This is typically equivalent to the model's representation in NetBox's REST API.
|
||||
* `snapshots` - Minimal "snapshots" of the object state both before and after the change was made; provided ass a dictionary with keys named `prechange` and `postchange`. These are not as extensive as the fully serialized representation, but contain enough information to convey what has changed.
|
||||
* `snapshots` - Minimal "snapshots" of the object state both before and after the change was made; provided as a dictionary with keys named `prechange` and `postchange`. These are not as extensive as the fully serialized representation, but contain enough information to convey what has changed.
|
||||
|
||||
### Default Request Body
|
||||
|
||||
|
||||
@@ -10,6 +10,17 @@ Minor releases are published in April, August, and December of each calendar yea
|
||||
|
||||
This page contains a history of all major and minor releases since NetBox v2.0. For more detail on a specific patch release, please see the release notes page for that specific minor release.
|
||||
|
||||
#### [Version 3.3](./version-3.3.md) (August 2022)
|
||||
|
||||
* Multi-object Cable Terminations ([#9102](https://github.com/netbox-community/netbox/issues/9102))
|
||||
* L2VPN Modeling ([#8157](https://github.com/netbox-community/netbox/issues/8157))
|
||||
* PoE Interface Attributes ([#1099](https://github.com/netbox-community/netbox/issues/1099))
|
||||
* Half-Height Rack Units ([#51](https://github.com/netbox-community/netbox/issues/51))
|
||||
* Restrict API Tokens by Client IP ([#8233](https://github.com/netbox-community/netbox/issues/8233))
|
||||
* Reference User in Permission Constraints ([#9074](https://github.com/netbox-community/netbox/issues/9074))
|
||||
* Custom Field Grouping ([#8495](https://github.com/netbox-community/netbox/issues/8495))
|
||||
* Toggle Custom Field Visibility ([#9166](https://github.com/netbox-community/netbox/issues/9166))
|
||||
|
||||
#### [Version 3.2](./version-3.2.md) (April 2022)
|
||||
|
||||
* Plugins Framework Extensions ([#8333](https://github.com/netbox-community/netbox/issues/8333))
|
||||
|
||||
@@ -1,6 +1,44 @@
|
||||
# NetBox v3.2
|
||||
|
||||
## v3.2.7 (FUTURE)
|
||||
## v3.2.8 (FUTURE)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#9062](https://github.com/netbox-community/netbox/issues/9062) - Add/edit {module} substitution to help text for component template name
|
||||
* [#9637](https://github.com/netbox-community/netbox/issues/9637) - Add site group field to rack reservation form
|
||||
* [#9762](https://github.com/netbox-community/netbox/issues/9762) - Add `nat_outside` column to the IPAddress table
|
||||
* [#9825](https://github.com/netbox-community/netbox/issues/9825) - Add contacts column to virtual machines table
|
||||
* [#9881](https://github.com/netbox-community/netbox/issues/9881) - Increase granularity in utilization graph values
|
||||
* [#9882](https://github.com/netbox-community/netbox/issues/9882) - Add manufacturer column to modules table
|
||||
* [#9883](https://github.com/netbox-community/netbox/issues/9883) - Linkify location column in power panels table
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#9871](https://github.com/netbox-community/netbox/issues/9871) - Fix utilization graph value alignments
|
||||
* [#9884](https://github.com/netbox-community/netbox/issues/9884) - Prevent querying assigned VRF on prefix object init
|
||||
* [#9885](https://github.com/netbox-community/netbox/issues/9885) - Fix child prefix counts when editing/deleting aggregates in bulk
|
||||
* [#9891](https://github.com/netbox-community/netbox/issues/9891) - Ensure consistent ordering for tags during object serialization
|
||||
|
||||
---
|
||||
|
||||
## v3.2.7 (2022-07-20)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#9705](https://github.com/netbox-community/netbox/issues/9705) - Support filter expressions for the `serial` field on racks, devices, and inventory items
|
||||
* [#9741](https://github.com/netbox-community/netbox/issues/9741) - Check for UserConfig instance during user login
|
||||
* [#9745](https://github.com/netbox-community/netbox/issues/9745) - Add wireless LANs and links to global search
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#9437](https://github.com/netbox-community/netbox/issues/9437) - Standardize form submission buttons and behavior when using enter key
|
||||
* [#9499](https://github.com/netbox-community/netbox/issues/9499) - Fix filtered bulk deletion of VM Interfaces
|
||||
* [#9634](https://github.com/netbox-community/netbox/issues/9634) - Fix image URLs in rack elevations when using external storage
|
||||
* [#9715](https://github.com/netbox-community/netbox/issues/9715) - Fix `SOCIAL_AUTH_PIPELINE` config parameter not taking effect
|
||||
* [#9754](https://github.com/netbox-community/netbox/issues/9754) - Fix regression introduced by #9632
|
||||
* [#9746](https://github.com/netbox-community/netbox/issues/9746) - Permit filtering interfaces by arbitrary speed value in UI
|
||||
* [#9749](https://github.com/netbox-community/netbox/issues/9749) - Retain original slug values when modifying object names
|
||||
* [#9775](https://github.com/netbox-community/netbox/issues/9775) - Fix exception when viewing a report with no description
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# NetBox v3.3
|
||||
|
||||
## v3.3-beta1 (2022-07-14)
|
||||
## v3.3-beta2 (2022-08-03)
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
@@ -93,9 +93,27 @@ Custom field UI visibility has no impact on API operation.
|
||||
* [#8995](https://github.com/netbox-community/netbox/issues/8995) - Enable arbitrary ordering of REST API results
|
||||
* [#9070](https://github.com/netbox-community/netbox/issues/9070) - Hide navigation menu items based on user permissions
|
||||
* [#9177](https://github.com/netbox-community/netbox/issues/9177) - Add tenant assignment for wireless LANs & links
|
||||
* [#9391](https://github.com/netbox-community/netbox/issues/9391) - Remove 500-character limit for custom link text & URL fields
|
||||
* [#9536](https://github.com/netbox-community/netbox/issues/9536) - Track API token usage times
|
||||
* [#9582](https://github.com/netbox-community/netbox/issues/9582) - Enable assigning config contexts based on device location
|
||||
|
||||
### Bug Fixes (from Beta1)
|
||||
|
||||
* [#9728](https://github.com/netbox-community/netbox/issues/9728) - Fix validation when assigning a virtual machine to a device
|
||||
* [#9729](https://github.com/netbox-community/netbox/issues/9729) - Fix ordering of content type creation to ensure compatability with demo data
|
||||
* [#9730](https://github.com/netbox-community/netbox/issues/9730) - Fix validation error when creating a new cable via UI form
|
||||
* [#9733](https://github.com/netbox-community/netbox/issues/9733) - Handle split paths during trace when fanning out to front ports with differing cables
|
||||
* [#9765](https://github.com/netbox-community/netbox/issues/9765) - Report correct segment count under cable trace UI view
|
||||
* [#9778](https://github.com/netbox-community/netbox/issues/9778) - Fix exception during cable deletion after deleting a connected termination
|
||||
* [#9788](https://github.com/netbox-community/netbox/issues/9788) - Ensure denormalized fields on CableTermination are kept in sync with related objects
|
||||
* [#9789](https://github.com/netbox-community/netbox/issues/9789) - Fix rendering of cable traces ending at provider networks
|
||||
* [#9794](https://github.com/netbox-community/netbox/issues/9794) - Fix link to connect a rear port to a circuit termination
|
||||
* [#9818](https://github.com/netbox-community/netbox/issues/9818) - Fix circuit side selection when connecting a cable to a circuit termination
|
||||
* [#9829](https://github.com/netbox-community/netbox/issues/9829) - Arrange custom fields by group when editing objects
|
||||
* [#9843](https://github.com/netbox-community/netbox/issues/9843) - Fix rendering of custom field values (regression from #9647)
|
||||
* [#9844](https://github.com/netbox-community/netbox/issues/9844) - Fix interface api request when creating/editing L2VPN termination
|
||||
* [#9847](https://github.com/netbox-community/netbox/issues/9847) - Respect `desc_units` when ordering rack units
|
||||
|
||||
### Plugins API
|
||||
|
||||
* [#9075](https://github.com/netbox-community/netbox/issues/9075) - Introduce `AbortRequest` exception for cleanly interrupting object mutations
|
||||
@@ -108,6 +126,7 @@ Custom field UI visibility has no impact on API operation.
|
||||
|
||||
* [#9261](https://github.com/netbox-community/netbox/issues/9261) - `NetBoxTable` no longer automatically clears pre-existing calls to `prefetch_related()` on its queryset
|
||||
* [#9434](https://github.com/netbox-community/netbox/issues/9434) - Enabled `django-rich` test runner for more user-friendly output
|
||||
* [#9903](https://github.com/netbox-community/netbox/issues/9903) - Implement a mechanism for automatically updating denormalized fields
|
||||
|
||||
### REST API Changes
|
||||
|
||||
@@ -162,6 +181,8 @@ Custom field UI visibility has no impact on API operation.
|
||||
* `connected_endpoint_reachable` has been renamed to `connected_endpoints_reachable`
|
||||
* Added the optional `poe_mode` and `poe_type` fields
|
||||
* Added the `l2vpn_termination` read-only field
|
||||
* dcim.InterfaceTemplate
|
||||
* Added the optional `poe_mode` and `poe_type` fields
|
||||
* dcim.Location
|
||||
* Added required `status` field (default value: `active`)
|
||||
* dcim.PowerOutlet
|
||||
|
||||
@@ -30,7 +30,8 @@ class ProviderView(generic.ObjectView):
|
||||
circuits = Circuit.objects.restrict(request.user, 'view').filter(
|
||||
provider=instance
|
||||
).prefetch_related(
|
||||
'type', 'tenant', 'tenant__group', 'terminations__site'
|
||||
'tenant__group', 'termination_a__site', 'termination_z__site',
|
||||
'termination_a__provider_network', 'termination_z__provider_network',
|
||||
)
|
||||
circuits_table = tables.CircuitTable(circuits, user=request.user, exclude=('provider',))
|
||||
circuits_table.configure(request)
|
||||
@@ -91,7 +92,8 @@ class ProviderNetworkView(generic.ObjectView):
|
||||
Q(termination_a__provider_network=instance.pk) |
|
||||
Q(termination_z__provider_network=instance.pk)
|
||||
).prefetch_related(
|
||||
'type', 'tenant', 'tenant__group', 'terminations__site'
|
||||
'tenant__group', 'termination_a__site', 'termination_z__site',
|
||||
'termination_a__provider_network', 'termination_z__provider_network',
|
||||
)
|
||||
circuits_table = tables.CircuitTable(circuits, user=request.user)
|
||||
circuits_table.configure(request)
|
||||
@@ -192,7 +194,8 @@ class CircuitTypeBulkDeleteView(generic.BulkDeleteView):
|
||||
|
||||
class CircuitListView(generic.ObjectListView):
|
||||
queryset = Circuit.objects.prefetch_related(
|
||||
'provider', 'type', 'tenant', 'tenant__group', 'termination_a', 'termination_z'
|
||||
'tenant__group', 'termination_a__site', 'termination_z__site',
|
||||
'termination_a__provider_network', 'termination_z__provider_network',
|
||||
)
|
||||
filterset = filtersets.CircuitFilterSet
|
||||
filterset_form = forms.CircuitFilterForm
|
||||
@@ -220,7 +223,8 @@ class CircuitBulkImportView(generic.BulkImportView):
|
||||
|
||||
class CircuitBulkEditView(generic.BulkEditView):
|
||||
queryset = Circuit.objects.prefetch_related(
|
||||
'provider', 'type', 'tenant', 'terminations'
|
||||
'termination_a__site', 'termination_z__site',
|
||||
'termination_a__provider_network', 'termination_z__provider_network',
|
||||
)
|
||||
filterset = filtersets.CircuitFilterSet
|
||||
table = tables.CircuitTable
|
||||
@@ -229,7 +233,8 @@ class CircuitBulkEditView(generic.BulkEditView):
|
||||
|
||||
class CircuitBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = Circuit.objects.prefetch_related(
|
||||
'provider', 'type', 'tenant', 'terminations'
|
||||
'termination_a__site', 'termination_z__site',
|
||||
'termination_a__provider_network', 'termination_z__provider_network',
|
||||
)
|
||||
filterset = filtersets.CircuitFilterSet
|
||||
table = tables.CircuitTable
|
||||
|
||||
@@ -19,6 +19,7 @@ from netbox.api.serializers import (
|
||||
WritableNestedSerializer,
|
||||
)
|
||||
from netbox.config import ConfigItem
|
||||
from netbox.constants import NESTED_SERIALIZER_PREFIX
|
||||
from tenancy.api.nested_serializers import NestedTenantSerializer
|
||||
from users.api.nested_serializers import NestedUserSerializer
|
||||
from utilities.api import get_serializer_for_model
|
||||
@@ -57,7 +58,7 @@ class CabledObjectSerializer(serializers.ModelSerializer):
|
||||
return []
|
||||
|
||||
# Return serialized peer termination objects
|
||||
serializer = get_serializer_for_model(obj.link_peers[0], prefix='Nested')
|
||||
serializer = get_serializer_for_model(obj.link_peers[0], prefix=NESTED_SERIALIZER_PREFIX)
|
||||
context = {'request': self.context['request']}
|
||||
return serializer(obj.link_peers, context=context, many=True).data
|
||||
|
||||
@@ -84,7 +85,7 @@ class ConnectedEndpointsSerializer(serializers.ModelSerializer):
|
||||
Return the appropriate serializer for the type of connected object.
|
||||
"""
|
||||
if endpoints := obj.connected_endpoints:
|
||||
serializer = get_serializer_for_model(endpoints[0], prefix='Nested')
|
||||
serializer = get_serializer_for_model(endpoints[0], prefix=NESTED_SERIALIZER_PREFIX)
|
||||
context = {'request': self.context['request']}
|
||||
return serializer(endpoints, many=True, context=context).data
|
||||
|
||||
@@ -468,12 +469,22 @@ class InterfaceTemplateSerializer(ValidatedModelSerializer):
|
||||
default=None
|
||||
)
|
||||
type = ChoiceField(choices=InterfaceTypeChoices)
|
||||
poe_mode = ChoiceField(
|
||||
choices=InterfacePoEModeChoices,
|
||||
required=False,
|
||||
allow_blank=True
|
||||
)
|
||||
poe_type = ChoiceField(
|
||||
choices=InterfacePoETypeChoices,
|
||||
required=False,
|
||||
allow_blank=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = InterfaceTemplate
|
||||
fields = [
|
||||
'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'mgmt_only', 'description',
|
||||
'created', 'last_updated',
|
||||
'poe_mode', 'poe_type', 'created', 'last_updated',
|
||||
]
|
||||
|
||||
|
||||
@@ -572,7 +583,7 @@ class InventoryItemTemplateSerializer(ValidatedModelSerializer):
|
||||
def get_component(self, obj):
|
||||
if obj.component is None:
|
||||
return None
|
||||
serializer = get_serializer_for_model(obj.component, prefix='Nested')
|
||||
serializer = get_serializer_for_model(obj.component, prefix=NESTED_SERIALIZER_PREFIX)
|
||||
context = {'request': self.context['request']}
|
||||
return serializer(obj.component, context=context).data
|
||||
|
||||
@@ -968,7 +979,7 @@ class InventoryItemSerializer(NetBoxModelSerializer):
|
||||
def get_component(self, obj):
|
||||
if obj.component is None:
|
||||
return None
|
||||
serializer = get_serializer_for_model(obj.component, prefix='Nested')
|
||||
serializer = get_serializer_for_model(obj.component, prefix=NESTED_SERIALIZER_PREFIX)
|
||||
context = {'request': self.context['request']}
|
||||
return serializer(obj.component, context=context).data
|
||||
|
||||
@@ -1037,7 +1048,7 @@ class CableTerminationSerializer(NetBoxModelSerializer):
|
||||
|
||||
@swagger_serializer_method(serializer_or_field=serializers.DictField)
|
||||
def get_termination(self, obj):
|
||||
serializer = get_serializer_for_model(obj.termination, prefix='Nested')
|
||||
serializer = get_serializer_for_model(obj.termination, prefix=NESTED_SERIALIZER_PREFIX)
|
||||
context = {'request': self.context['request']}
|
||||
return serializer(obj.termination, context=context).data
|
||||
|
||||
@@ -1053,7 +1064,7 @@ class CablePathSerializer(serializers.ModelSerializer):
|
||||
def get_path(self, obj):
|
||||
ret = []
|
||||
for nodes in obj.path_objects:
|
||||
serializer = get_serializer_for_model(nodes[0], prefix='Nested')
|
||||
serializer = get_serializer_for_model(nodes[0], prefix=NESTED_SERIALIZER_PREFIX)
|
||||
context = {'request': self.context['request']}
|
||||
ret.append(serializer(nodes, context=context, many=True).data)
|
||||
return ret
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import socket
|
||||
from collections import OrderedDict
|
||||
|
||||
from django.http import Http404, HttpResponse, HttpResponseForbidden
|
||||
from django.shortcuts import get_object_or_404
|
||||
@@ -24,6 +23,7 @@ from netbox.api.metadata import ContentTypeMetadata
|
||||
from netbox.api.pagination import StripCountAnnotationsPaginator
|
||||
from netbox.api.viewsets import NetBoxModelViewSet
|
||||
from netbox.config import get_config
|
||||
from netbox.constants import NESTED_SERIALIZER_PREFIX
|
||||
from utilities.api import get_serializer_for_model
|
||||
from utilities.utils import count_related
|
||||
from virtualization.models import VirtualMachine
|
||||
@@ -63,20 +63,20 @@ class PathEndpointMixin(object):
|
||||
return HttpResponse(drawing.render().tostring(), content_type='image/svg+xml')
|
||||
|
||||
# Serialize path objects, iterating over each three-tuple in the path
|
||||
for near_end, cable, far_end in obj.trace():
|
||||
if near_end is not None:
|
||||
serializer_a = get_serializer_for_model(near_end[0], prefix='Nested')
|
||||
near_end = serializer_a(near_end, many=True, context={'request': request}).data
|
||||
for near_ends, cable, far_ends in obj.trace():
|
||||
if near_ends:
|
||||
serializer_a = get_serializer_for_model(near_ends[0], prefix=NESTED_SERIALIZER_PREFIX)
|
||||
near_ends = serializer_a(near_ends, many=True, context={'request': request}).data
|
||||
else:
|
||||
# Path is split; stop here
|
||||
break
|
||||
if cable is not None:
|
||||
if cable:
|
||||
cable = serializers.TracedCableSerializer(cable[0], context={'request': request}).data
|
||||
if far_end is not None:
|
||||
serializer_b = get_serializer_for_model(far_end[0], prefix='Nested')
|
||||
far_end = serializer_b(far_end, many=True, context={'request': request}).data
|
||||
if far_ends:
|
||||
serializer_b = get_serializer_for_model(far_ends[0], prefix=NESTED_SERIALIZER_PREFIX)
|
||||
far_ends = serializer_b(far_ends, many=True, context={'request': request}).data
|
||||
|
||||
path.append((near_end, cable, far_end))
|
||||
path.append((near_ends, cable, far_ends))
|
||||
|
||||
return Response(path)
|
||||
|
||||
@@ -483,7 +483,7 @@ class DeviceViewSet(ConfigContextQuerySetMixin, NetBoxModelViewSet):
|
||||
return HttpResponseForbidden()
|
||||
|
||||
napalm_methods = request.GET.getlist('method')
|
||||
response = OrderedDict([(m, None) for m in napalm_methods])
|
||||
response = {m: None for m in napalm_methods}
|
||||
|
||||
config = get_config()
|
||||
username = config.NAPALM_USERNAME
|
||||
|
||||
@@ -1,10 +1,26 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
from netbox import denormalized
|
||||
|
||||
|
||||
class DCIMConfig(AppConfig):
|
||||
name = "dcim"
|
||||
verbose_name = "DCIM"
|
||||
|
||||
def ready(self):
|
||||
|
||||
import dcim.signals
|
||||
from .models import CableTermination
|
||||
|
||||
# Register denormalized fields
|
||||
denormalized.register(CableTermination, '_device', {
|
||||
'_rack': 'rack',
|
||||
'_location': 'location',
|
||||
'_site': 'site',
|
||||
})
|
||||
denormalized.register(CableTermination, '_rack', {
|
||||
'_location': 'location',
|
||||
'_site': 'site',
|
||||
})
|
||||
denormalized.register(CableTermination, '_location', {
|
||||
'_site': 'site',
|
||||
})
|
||||
|
||||
@@ -312,7 +312,7 @@ class RackFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSe
|
||||
to_field_name='slug',
|
||||
label='Role (slug)',
|
||||
)
|
||||
serial = django_filters.CharFilter(
|
||||
serial = MultiValueCharFilter(
|
||||
lookup_expr='iexact'
|
||||
)
|
||||
|
||||
@@ -652,6 +652,12 @@ class InterfaceTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeCo
|
||||
choices=InterfaceTypeChoices,
|
||||
null_value=None
|
||||
)
|
||||
poe_mode = django_filters.MultipleChoiceFilter(
|
||||
choices=InterfacePoEModeChoices
|
||||
)
|
||||
poe_type = django_filters.MultipleChoiceFilter(
|
||||
choices=InterfacePoETypeChoices
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = InterfaceTemplate
|
||||
@@ -1007,10 +1013,13 @@ class ModuleFilterSet(NetBoxModelFilterSet):
|
||||
queryset=Device.objects.all(),
|
||||
label='Device (ID)',
|
||||
)
|
||||
serial = MultiValueCharFilter(
|
||||
lookup_expr='iexact'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Module
|
||||
fields = ['id', 'serial', 'asset_tag']
|
||||
fields = ['id', 'asset_tag']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
@@ -1411,7 +1420,7 @@ class InventoryItemFilterSet(DeviceComponentFilterSet, NetBoxModelFilterSet):
|
||||
)
|
||||
component_type = ContentTypeFilter()
|
||||
component_id = MultiValueNumberFilter()
|
||||
serial = django_filters.CharFilter(
|
||||
serial = MultiValueCharFilter(
|
||||
lookup_expr='iexact'
|
||||
)
|
||||
|
||||
|
||||
@@ -818,8 +818,22 @@ class InterfaceTemplateBulkEditForm(BulkEditForm):
|
||||
description = forms.CharField(
|
||||
required=False
|
||||
)
|
||||
poe_mode = forms.ChoiceField(
|
||||
choices=add_blank_choice(InterfacePoEModeChoices),
|
||||
required=False,
|
||||
initial='',
|
||||
widget=StaticSelect(),
|
||||
label='PoE mode'
|
||||
)
|
||||
poe_type = forms.ChoiceField(
|
||||
choices=add_blank_choice(InterfacePoETypeChoices),
|
||||
required=False,
|
||||
initial='',
|
||||
widget=StaticSelect(),
|
||||
label='PoE type'
|
||||
)
|
||||
|
||||
nullable_fields = ('label', 'description')
|
||||
nullable_fields = ('label', 'description', 'poe_mode', 'poe_type')
|
||||
|
||||
|
||||
class FrontPortTemplateBulkEditForm(BulkEditForm):
|
||||
|
||||
@@ -138,7 +138,7 @@ def get_cable_form(a_type, b_type):
|
||||
label='Side',
|
||||
disabled_indicator='_occupied',
|
||||
query_params={
|
||||
'circuit_id': f'termination_{cable_end}_circuit',
|
||||
'circuit_id': f'$termination_{cable_end}_circuit',
|
||||
}
|
||||
)
|
||||
|
||||
@@ -160,12 +160,11 @@ def get_cable_form(a_type, b_type):
|
||||
self.initial['a_terminations'] = self.instance.a_terminations
|
||||
self.initial['b_terminations'] = self.instance.b_terminations
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Set the A/B terminations on the Cable instance
|
||||
self.instance.a_terminations = self.cleaned_data['a_terminations']
|
||||
self.instance.b_terminations = self.cleaned_data['b_terminations']
|
||||
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
return _CableForm
|
||||
|
||||
@@ -291,7 +291,7 @@ class RackReservationFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
||||
fieldsets = (
|
||||
(None, ('q', 'tag')),
|
||||
('User', ('user_id',)),
|
||||
('Rack', ('region_id', 'site_group_id', 'site_id', 'location_id')),
|
||||
('Rack', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
|
||||
('Tenant', ('tenant_group_id', 'tenant_id')),
|
||||
)
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
@@ -299,25 +299,38 @@ class RackReservationFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
||||
required=False,
|
||||
label=_('Region')
|
||||
)
|
||||
site_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'region_id': '$region_id'
|
||||
},
|
||||
label=_('Site')
|
||||
)
|
||||
site_group_id = DynamicModelMultipleChoiceField(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
required=False,
|
||||
label=_('Site group')
|
||||
)
|
||||
location_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Location.objects.prefetch_related('site'),
|
||||
site_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'region_id': '$region_id',
|
||||
'group_id': '$site_group_id',
|
||||
},
|
||||
label=_('Site')
|
||||
)
|
||||
location_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Location.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'site_id': '$site_id',
|
||||
},
|
||||
label=_('Location'),
|
||||
null_option='None'
|
||||
)
|
||||
rack_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Rack.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'site_id': '$site_id',
|
||||
'location_id': '$location_id',
|
||||
},
|
||||
label=_('Rack')
|
||||
)
|
||||
user_id = DynamicModelMultipleChoiceField(
|
||||
queryset=User.objects.all(),
|
||||
required=False,
|
||||
@@ -998,8 +1011,8 @@ class InterfaceFilterForm(DeviceComponentFilterForm):
|
||||
)
|
||||
speed = forms.IntegerField(
|
||||
required=False,
|
||||
label='Select Speed',
|
||||
widget=SelectSpeedWidget(attrs={'readonly': None})
|
||||
label='Speed',
|
||||
widget=SelectSpeedWidget()
|
||||
)
|
||||
duplex = MultipleChoiceField(
|
||||
choices=InterfaceDuplexChoices,
|
||||
@@ -1027,11 +1040,13 @@ class InterfaceFilterForm(DeviceComponentFilterForm):
|
||||
)
|
||||
poe_mode = MultipleChoiceField(
|
||||
choices=InterfacePoEModeChoices,
|
||||
required=False
|
||||
required=False,
|
||||
label='PoE mode'
|
||||
)
|
||||
poe_type = MultipleChoiceField(
|
||||
choices=InterfacePoEModeChoices,
|
||||
required=False
|
||||
required=False,
|
||||
label='PoE type'
|
||||
)
|
||||
rf_role = MultipleChoiceField(
|
||||
choices=WirelessRoleChoices,
|
||||
|
||||
@@ -325,7 +325,7 @@ class RackReservationForm(TenancyForm, NetBoxModelForm):
|
||||
)
|
||||
|
||||
fieldsets = (
|
||||
('Reservation', ('region', 'site', 'location', 'rack', 'units', 'user', 'description', 'tags')),
|
||||
('Reservation', ('region', 'site_group', 'site', 'location', 'rack', 'units', 'user', 'description', 'tags')),
|
||||
('Tenancy', ('tenant_group', 'tenant')),
|
||||
)
|
||||
|
||||
@@ -1052,12 +1052,14 @@ class InterfaceTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||
class Meta:
|
||||
model = InterfaceTemplate
|
||||
fields = [
|
||||
'device_type', 'module_type', 'name', 'label', 'type', 'mgmt_only', 'description',
|
||||
'device_type', 'module_type', 'name', 'label', 'type', 'mgmt_only', 'description', 'poe_mode', 'poe_type',
|
||||
]
|
||||
widgets = {
|
||||
'device_type': forms.HiddenInput(),
|
||||
'module_type': forms.HiddenInput(),
|
||||
'type': StaticSelect(),
|
||||
'poe_mode': StaticSelect(),
|
||||
'poe_type': StaticSelect(),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -64,6 +64,14 @@ class ModularComponentTemplateCreateForm(ComponentCreateForm):
|
||||
"""
|
||||
Creation form for component templates that can be assigned to either a DeviceType *or* a ModuleType.
|
||||
"""
|
||||
name_pattern = ExpandableNameField(
|
||||
label='Name',
|
||||
help_text="""
|
||||
Alphanumeric ranges are supported for bulk creation. Mixed cases and types within a single range
|
||||
are not supported. Example: <code>[ge,xe]-0/0/[0-9]</code>. {module} is accepted as a substitution for
|
||||
the module bay position.
|
||||
"""
|
||||
)
|
||||
device_type = DynamicModelChoiceField(
|
||||
queryset=DeviceType.objects.all(),
|
||||
required=False
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from django import forms
|
||||
|
||||
from dcim.choices import InterfaceTypeChoices, PortTypeChoices
|
||||
from dcim.choices import InterfacePoEModeChoices, InterfacePoETypeChoices, InterfaceTypeChoices, PortTypeChoices
|
||||
from dcim.models import *
|
||||
from utilities.forms import BootstrapMixin
|
||||
|
||||
@@ -112,11 +112,21 @@ class InterfaceTemplateImportForm(ComponentTemplateImportForm):
|
||||
type = forms.ChoiceField(
|
||||
choices=InterfaceTypeChoices.CHOICES
|
||||
)
|
||||
poe_mode = forms.ChoiceField(
|
||||
choices=InterfacePoEModeChoices,
|
||||
required=False,
|
||||
label='PoE mode'
|
||||
)
|
||||
poe_type = forms.ChoiceField(
|
||||
choices=InterfacePoETypeChoices,
|
||||
required=False,
|
||||
label='PoE type'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = InterfaceTemplate
|
||||
fields = [
|
||||
'device_type', 'module_type', 'name', 'label', 'type', 'mgmt_only', 'description',
|
||||
'device_type', 'module_type', 'name', 'label', 'type', 'mgmt_only', 'description', 'poe_mode', 'poe_type',
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -258,6 +258,12 @@ class InterfaceTemplateType(ComponentTemplateObjectType):
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.InterfaceTemplateFilterSet
|
||||
|
||||
def resolve_poe_mode(self, info):
|
||||
return self.poe_mode or None
|
||||
|
||||
def resolve_poe_type(self, info):
|
||||
return self.poe_type or None
|
||||
|
||||
|
||||
class InventoryItemType(ComponentObjectType):
|
||||
|
||||
|
||||
@@ -20,4 +20,14 @@ class Migration(migrations.Migration):
|
||||
name='poe_type',
|
||||
field=models.CharField(blank=True, max_length=50),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='interfacetemplate',
|
||||
name='poe_mode',
|
||||
field=models.CharField(blank=True, max_length=50),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='interfacetemplate',
|
||||
name='poe_type',
|
||||
field=models.CharField(blank=True, max_length=50),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -64,6 +64,8 @@ def populate_cable_terminations(apps, schema_editor):
|
||||
# Output progress occasionally
|
||||
if 'test' not in sys.argv and not i % 100:
|
||||
progress = float(i) * 100 / cable_count
|
||||
if i == 100:
|
||||
print('')
|
||||
sys.stdout.write(f"\r Updated {i}/{cable_count} cables ({progress:.2f}%)")
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ from django.db import migrations
|
||||
|
||||
def populate_cable_terminations(apps, schema_editor):
|
||||
Cable = apps.get_model('dcim', 'Cable')
|
||||
ContentType = apps.get_model('contenttypes', 'ContentType')
|
||||
|
||||
cable_termination_models = (
|
||||
apps.get_model('dcim', 'ConsolePort'),
|
||||
@@ -18,12 +17,17 @@ def populate_cable_terminations(apps, schema_editor):
|
||||
)
|
||||
|
||||
for model in cable_termination_models:
|
||||
ct = ContentType.objects.get_for_model(model)
|
||||
model.objects.filter(
|
||||
id__in=Cable.objects.filter(termination_a_type=ct).values_list('termination_a_id', flat=True)
|
||||
id__in=Cable.objects.filter(
|
||||
termination_a_type__app_label=model._meta.app_label,
|
||||
termination_a_type__model=model._meta.model_name
|
||||
).values_list('termination_a_id', flat=True)
|
||||
).update(cable_end='A')
|
||||
model.objects.filter(
|
||||
id__in=Cable.objects.filter(termination_b_type=ct).values_list('termination_b_id', flat=True)
|
||||
id__in=Cable.objects.filter(
|
||||
termination_b_type__app_label=model._meta.app_label,
|
||||
termination_b_type__model=model._meta.model_name
|
||||
).values_list('termination_b_id', flat=True)
|
||||
).update(cable_end='B')
|
||||
|
||||
|
||||
|
||||
@@ -431,11 +431,7 @@ class CablePath(models.Model):
|
||||
"""
|
||||
Return the list of originating objects.
|
||||
"""
|
||||
if hasattr(self, '_path_objects'):
|
||||
return self.path_objects[0]
|
||||
return [
|
||||
path_node_to_object(node) for node in self.path[0]
|
||||
]
|
||||
return self.path_objects[0]
|
||||
|
||||
@property
|
||||
def destinations(self):
|
||||
@@ -444,11 +440,7 @@ class CablePath(models.Model):
|
||||
"""
|
||||
if not self.is_complete:
|
||||
return []
|
||||
if hasattr(self, '_path_objects'):
|
||||
return self.path_objects[-1]
|
||||
return [
|
||||
path_node_to_object(node) for node in self.path[-1]
|
||||
]
|
||||
return self.path_objects[-1]
|
||||
|
||||
@property
|
||||
def segment_count(self):
|
||||
@@ -463,6 +455,13 @@ class CablePath(models.Model):
|
||||
"""
|
||||
from circuits.models import CircuitTermination
|
||||
|
||||
if not terminations:
|
||||
return None
|
||||
|
||||
# Ensure all originating terminations are attached to the same link
|
||||
if len(terminations) > 1:
|
||||
assert all(t.link == terminations[0].link for t in terminations[1:])
|
||||
|
||||
path = []
|
||||
position_stack = []
|
||||
is_complete = False
|
||||
@@ -474,6 +473,12 @@ class CablePath(models.Model):
|
||||
# Terminations must all be of the same type
|
||||
assert all(isinstance(t, type(terminations[0])) for t in terminations[1:])
|
||||
|
||||
# Check for a split path (e.g. rear port fanning out to multiple front ports with
|
||||
# different cables attached)
|
||||
if len(set(t.link for t in terminations)) > 1:
|
||||
is_split = True
|
||||
break
|
||||
|
||||
# Step 1: Record the near-end termination object(s)
|
||||
path.append([
|
||||
object_to_path_node(t) for t in terminations
|
||||
@@ -481,7 +486,6 @@ class CablePath(models.Model):
|
||||
|
||||
# Step 2: Determine the attached link (Cable or WirelessLink), if any
|
||||
link = terminations[0].link
|
||||
assert all(t.link == link for t in terminations[1:])
|
||||
if link is None and len(path) == 1:
|
||||
# If this is the start of the path and no link exists, return None
|
||||
return None
|
||||
@@ -520,6 +524,9 @@ class CablePath(models.Model):
|
||||
])
|
||||
|
||||
# Step 6: Determine the "next hop" terminations, if applicable
|
||||
if not remote_terminations:
|
||||
break
|
||||
|
||||
if isinstance(remote_terminations[0], FrontPort):
|
||||
# Follow FrontPorts to their corresponding RearPorts
|
||||
rear_ports = RearPort.objects.filter(
|
||||
@@ -631,7 +638,11 @@ class CablePath(models.Model):
|
||||
nodes = []
|
||||
for node in step:
|
||||
ct_id, object_id = decompile_path_node(node)
|
||||
nodes.append(prefetched[ct_id][object_id])
|
||||
try:
|
||||
nodes.append(prefetched[ct_id][object_id])
|
||||
except KeyError:
|
||||
# Ignore stale (deleted) object IDs
|
||||
pass
|
||||
path.append(nodes)
|
||||
|
||||
return path
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
from django.db import models
|
||||
from mptt.models import MPTTModel, TreeForeignKey
|
||||
@@ -39,7 +39,10 @@ class ComponentTemplateModel(WebhooksMixin, ChangeLoggedModel):
|
||||
related_name='%(class)ss'
|
||||
)
|
||||
name = models.CharField(
|
||||
max_length=64
|
||||
max_length=64,
|
||||
help_text="""
|
||||
{module} is accepted as a substitution for the module bay position when attached to a module type.
|
||||
"""
|
||||
)
|
||||
_name = NaturalOrderingField(
|
||||
target_field='name',
|
||||
@@ -157,6 +160,14 @@ class ConsolePortTemplate(ModularComponentTemplateModel):
|
||||
**kwargs
|
||||
)
|
||||
|
||||
def to_yaml(self):
|
||||
return {
|
||||
'name': self.name,
|
||||
'type': self.type,
|
||||
'label': self.label,
|
||||
'description': self.description,
|
||||
}
|
||||
|
||||
|
||||
class ConsoleServerPortTemplate(ModularComponentTemplateModel):
|
||||
"""
|
||||
@@ -185,6 +196,14 @@ class ConsoleServerPortTemplate(ModularComponentTemplateModel):
|
||||
**kwargs
|
||||
)
|
||||
|
||||
def to_yaml(self):
|
||||
return {
|
||||
'name': self.name,
|
||||
'type': self.type,
|
||||
'label': self.label,
|
||||
'description': self.description,
|
||||
}
|
||||
|
||||
|
||||
class PowerPortTemplate(ModularComponentTemplateModel):
|
||||
"""
|
||||
@@ -236,6 +255,16 @@ class PowerPortTemplate(ModularComponentTemplateModel):
|
||||
'allocated_draw': f"Allocated draw cannot exceed the maximum draw ({self.maximum_draw}W)."
|
||||
})
|
||||
|
||||
def to_yaml(self):
|
||||
return {
|
||||
'name': self.name,
|
||||
'type': self.type,
|
||||
'maximum_draw': self.maximum_draw,
|
||||
'allocated_draw': self.allocated_draw,
|
||||
'label': self.label,
|
||||
'description': self.description,
|
||||
}
|
||||
|
||||
|
||||
class PowerOutletTemplate(ModularComponentTemplateModel):
|
||||
"""
|
||||
@@ -298,6 +327,16 @@ class PowerOutletTemplate(ModularComponentTemplateModel):
|
||||
**kwargs
|
||||
)
|
||||
|
||||
def to_yaml(self):
|
||||
return {
|
||||
'name': self.name,
|
||||
'type': self.type,
|
||||
'power_port': self.power_port.name if self.power_port else None,
|
||||
'feed_leg': self.feed_leg,
|
||||
'label': self.label,
|
||||
'description': self.description,
|
||||
}
|
||||
|
||||
|
||||
class InterfaceTemplate(ModularComponentTemplateModel):
|
||||
"""
|
||||
@@ -318,6 +357,18 @@ class InterfaceTemplate(ModularComponentTemplateModel):
|
||||
default=False,
|
||||
verbose_name='Management only'
|
||||
)
|
||||
poe_mode = models.CharField(
|
||||
max_length=50,
|
||||
choices=InterfacePoEModeChoices,
|
||||
blank=True,
|
||||
verbose_name='PoE mode'
|
||||
)
|
||||
poe_type = models.CharField(
|
||||
max_length=50,
|
||||
choices=InterfacePoETypeChoices,
|
||||
blank=True,
|
||||
verbose_name='PoE type'
|
||||
)
|
||||
|
||||
component_model = Interface
|
||||
|
||||
@@ -334,9 +385,22 @@ class InterfaceTemplate(ModularComponentTemplateModel):
|
||||
label=self.resolve_label(kwargs.get('module')),
|
||||
type=self.type,
|
||||
mgmt_only=self.mgmt_only,
|
||||
poe_mode=self.poe_mode,
|
||||
poe_type=self.poe_type,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
def to_yaml(self):
|
||||
return {
|
||||
'name': self.name,
|
||||
'type': self.type,
|
||||
'mgmt_only': self.mgmt_only,
|
||||
'label': self.label,
|
||||
'description': self.description,
|
||||
'poe_mode': self.poe_mode,
|
||||
'poe_type': self.poe_type,
|
||||
}
|
||||
|
||||
|
||||
class FrontPortTemplate(ModularComponentTemplateModel):
|
||||
"""
|
||||
@@ -410,6 +474,16 @@ class FrontPortTemplate(ModularComponentTemplateModel):
|
||||
**kwargs
|
||||
)
|
||||
|
||||
def to_yaml(self):
|
||||
return {
|
||||
'name': self.name,
|
||||
'type': self.type,
|
||||
'rear_port': self.rear_port.name,
|
||||
'rear_port_position': self.rear_port_position,
|
||||
'label': self.label,
|
||||
'description': self.description,
|
||||
}
|
||||
|
||||
|
||||
class RearPortTemplate(ModularComponentTemplateModel):
|
||||
"""
|
||||
@@ -449,6 +523,15 @@ class RearPortTemplate(ModularComponentTemplateModel):
|
||||
**kwargs
|
||||
)
|
||||
|
||||
def to_yaml(self):
|
||||
return {
|
||||
'name': self.name,
|
||||
'type': self.type,
|
||||
'positions': self.positions,
|
||||
'label': self.label,
|
||||
'description': self.description,
|
||||
}
|
||||
|
||||
|
||||
class ModuleBayTemplate(ComponentTemplateModel):
|
||||
"""
|
||||
@@ -474,6 +557,14 @@ class ModuleBayTemplate(ComponentTemplateModel):
|
||||
position=self.position
|
||||
)
|
||||
|
||||
def to_yaml(self):
|
||||
return {
|
||||
'name': self.name,
|
||||
'label': self.label,
|
||||
'position': self.position,
|
||||
'description': self.description,
|
||||
}
|
||||
|
||||
|
||||
class DeviceBayTemplate(ComponentTemplateModel):
|
||||
"""
|
||||
@@ -498,6 +589,13 @@ class DeviceBayTemplate(ComponentTemplateModel):
|
||||
f"Subdevice role of device type ({self.device_type}) must be set to \"parent\" to allow device bays."
|
||||
)
|
||||
|
||||
def to_yaml(self):
|
||||
return {
|
||||
'name': self.name,
|
||||
'label': self.label,
|
||||
'description': self.description,
|
||||
}
|
||||
|
||||
|
||||
class InventoryItemTemplate(MPTTModel, ComponentTemplateModel):
|
||||
"""
|
||||
|
||||
@@ -212,10 +212,13 @@ class PathEndpoint(models.Model):
|
||||
break
|
||||
|
||||
path.extend(origin._path.path_objects)
|
||||
while (len(path)) % 3:
|
||||
# Pad to ensure we have complete three-tuples (e.g. for paths that end at a non-connected FrontPort)
|
||||
# by inserting empty entries immediately prior to the path's destination node(s)
|
||||
path.append([])
|
||||
|
||||
# If the path ends at a non-connected pass-through port, pad out the link and far-end terminations
|
||||
if len(path) % 3 == 1:
|
||||
path.extend(([], []))
|
||||
# If the path ends at a site or provider network, inject a null "link" to render an attachment
|
||||
elif len(path) % 3 == 2:
|
||||
path.insert(-1, [])
|
||||
|
||||
# Check for a bridged relationship to continue the trace
|
||||
destinations = origin._path.destinations
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import decimal
|
||||
from collections import OrderedDict
|
||||
|
||||
import yaml
|
||||
from django.contrib.contenttypes.fields import GenericRelation
|
||||
@@ -164,115 +163,54 @@ class DeviceType(NetBoxModel):
|
||||
return reverse('dcim:devicetype', args=[self.pk])
|
||||
|
||||
def to_yaml(self):
|
||||
data = OrderedDict((
|
||||
('manufacturer', self.manufacturer.name),
|
||||
('model', self.model),
|
||||
('slug', self.slug),
|
||||
('part_number', self.part_number),
|
||||
('u_height', float(self.u_height)),
|
||||
('is_full_depth', self.is_full_depth),
|
||||
('subdevice_role', self.subdevice_role),
|
||||
('airflow', self.airflow),
|
||||
('comments', self.comments),
|
||||
))
|
||||
data = {
|
||||
'manufacturer': self.manufacturer.name,
|
||||
'model': self.model,
|
||||
'slug': self.slug,
|
||||
'part_number': self.part_number,
|
||||
'u_height': float(self.u_height),
|
||||
'is_full_depth': self.is_full_depth,
|
||||
'subdevice_role': self.subdevice_role,
|
||||
'airflow': self.airflow,
|
||||
'comments': self.comments,
|
||||
}
|
||||
|
||||
# Component templates
|
||||
if self.consoleporttemplates.exists():
|
||||
data['console-ports'] = [
|
||||
{
|
||||
'name': c.name,
|
||||
'type': c.type,
|
||||
'label': c.label,
|
||||
'description': c.description,
|
||||
}
|
||||
for c in self.consoleporttemplates.all()
|
||||
c.to_yaml() for c in self.consoleporttemplates.all()
|
||||
]
|
||||
if self.consoleserverporttemplates.exists():
|
||||
data['console-server-ports'] = [
|
||||
{
|
||||
'name': c.name,
|
||||
'type': c.type,
|
||||
'label': c.label,
|
||||
'description': c.description,
|
||||
}
|
||||
for c in self.consoleserverporttemplates.all()
|
||||
c.to_yaml() for c in self.consoleserverporttemplates.all()
|
||||
]
|
||||
if self.powerporttemplates.exists():
|
||||
data['power-ports'] = [
|
||||
{
|
||||
'name': c.name,
|
||||
'type': c.type,
|
||||
'maximum_draw': c.maximum_draw,
|
||||
'allocated_draw': c.allocated_draw,
|
||||
'label': c.label,
|
||||
'description': c.description,
|
||||
}
|
||||
for c in self.powerporttemplates.all()
|
||||
c.to_yaml() for c in self.powerporttemplates.all()
|
||||
]
|
||||
if self.poweroutlettemplates.exists():
|
||||
data['power-outlets'] = [
|
||||
{
|
||||
'name': c.name,
|
||||
'type': c.type,
|
||||
'power_port': c.power_port.name if c.power_port else None,
|
||||
'feed_leg': c.feed_leg,
|
||||
'label': c.label,
|
||||
'description': c.description,
|
||||
}
|
||||
for c in self.poweroutlettemplates.all()
|
||||
c.to_yaml() for c in self.poweroutlettemplates.all()
|
||||
]
|
||||
if self.interfacetemplates.exists():
|
||||
data['interfaces'] = [
|
||||
{
|
||||
'name': c.name,
|
||||
'type': c.type,
|
||||
'mgmt_only': c.mgmt_only,
|
||||
'label': c.label,
|
||||
'description': c.description,
|
||||
}
|
||||
for c in self.interfacetemplates.all()
|
||||
c.to_yaml() for c in self.interfacetemplates.all()
|
||||
]
|
||||
if self.frontporttemplates.exists():
|
||||
data['front-ports'] = [
|
||||
{
|
||||
'name': c.name,
|
||||
'type': c.type,
|
||||
'rear_port': c.rear_port.name,
|
||||
'rear_port_position': c.rear_port_position,
|
||||
'label': c.label,
|
||||
'description': c.description,
|
||||
}
|
||||
for c in self.frontporttemplates.all()
|
||||
c.to_yaml() for c in self.frontporttemplates.all()
|
||||
]
|
||||
if self.rearporttemplates.exists():
|
||||
data['rear-ports'] = [
|
||||
{
|
||||
'name': c.name,
|
||||
'type': c.type,
|
||||
'positions': c.positions,
|
||||
'label': c.label,
|
||||
'description': c.description,
|
||||
}
|
||||
for c in self.rearporttemplates.all()
|
||||
c.to_yaml() for c in self.rearporttemplates.all()
|
||||
]
|
||||
if self.modulebaytemplates.exists():
|
||||
data['module-bays'] = [
|
||||
{
|
||||
'name': c.name,
|
||||
'label': c.label,
|
||||
'position': c.position,
|
||||
'description': c.description,
|
||||
}
|
||||
for c in self.modulebaytemplates.all()
|
||||
c.to_yaml() for c in self.modulebaytemplates.all()
|
||||
]
|
||||
if self.devicebaytemplates.exists():
|
||||
data['device-bays'] = [
|
||||
{
|
||||
'name': c.name,
|
||||
'label': c.label,
|
||||
'description': c.description,
|
||||
}
|
||||
for c in self.devicebaytemplates.all()
|
||||
c.to_yaml() for c in self.devicebaytemplates.all()
|
||||
]
|
||||
|
||||
return yaml.dump(dict(data), sort_keys=False)
|
||||
@@ -404,91 +342,41 @@ class ModuleType(NetBoxModel):
|
||||
return reverse('dcim:moduletype', args=[self.pk])
|
||||
|
||||
def to_yaml(self):
|
||||
data = OrderedDict((
|
||||
('manufacturer', self.manufacturer.name),
|
||||
('model', self.model),
|
||||
('part_number', self.part_number),
|
||||
('comments', self.comments),
|
||||
))
|
||||
data = {
|
||||
'manufacturer': self.manufacturer.name,
|
||||
'model': self.model,
|
||||
'part_number': self.part_number,
|
||||
'comments': self.comments,
|
||||
}
|
||||
|
||||
# Component templates
|
||||
if self.consoleporttemplates.exists():
|
||||
data['console-ports'] = [
|
||||
{
|
||||
'name': c.name,
|
||||
'type': c.type,
|
||||
'label': c.label,
|
||||
'description': c.description,
|
||||
}
|
||||
for c in self.consoleporttemplates.all()
|
||||
c.to_yaml() for c in self.consoleporttemplates.all()
|
||||
]
|
||||
if self.consoleserverporttemplates.exists():
|
||||
data['console-server-ports'] = [
|
||||
{
|
||||
'name': c.name,
|
||||
'type': c.type,
|
||||
'label': c.label,
|
||||
'description': c.description,
|
||||
}
|
||||
for c in self.consoleserverporttemplates.all()
|
||||
c.to_yaml() for c in self.consoleserverporttemplates.all()
|
||||
]
|
||||
if self.powerporttemplates.exists():
|
||||
data['power-ports'] = [
|
||||
{
|
||||
'name': c.name,
|
||||
'type': c.type,
|
||||
'maximum_draw': c.maximum_draw,
|
||||
'allocated_draw': c.allocated_draw,
|
||||
'label': c.label,
|
||||
'description': c.description,
|
||||
}
|
||||
for c in self.powerporttemplates.all()
|
||||
c.to_yaml() for c in self.powerporttemplates.all()
|
||||
]
|
||||
if self.poweroutlettemplates.exists():
|
||||
data['power-outlets'] = [
|
||||
{
|
||||
'name': c.name,
|
||||
'type': c.type,
|
||||
'power_port': c.power_port.name if c.power_port else None,
|
||||
'feed_leg': c.feed_leg,
|
||||
'label': c.label,
|
||||
'description': c.description,
|
||||
}
|
||||
for c in self.poweroutlettemplates.all()
|
||||
c.to_yaml() for c in self.poweroutlettemplates.all()
|
||||
]
|
||||
if self.interfacetemplates.exists():
|
||||
data['interfaces'] = [
|
||||
{
|
||||
'name': c.name,
|
||||
'type': c.type,
|
||||
'mgmt_only': c.mgmt_only,
|
||||
'label': c.label,
|
||||
'description': c.description,
|
||||
}
|
||||
for c in self.interfacetemplates.all()
|
||||
c.to_yaml() for c in self.interfacetemplates.all()
|
||||
]
|
||||
if self.frontporttemplates.exists():
|
||||
data['front-ports'] = [
|
||||
{
|
||||
'name': c.name,
|
||||
'type': c.type,
|
||||
'rear_port': c.rear_port.name,
|
||||
'rear_port_position': c.rear_port_position,
|
||||
'label': c.label,
|
||||
'description': c.description,
|
||||
}
|
||||
for c in self.frontporttemplates.all()
|
||||
c.to_yaml() for c in self.frontporttemplates.all()
|
||||
]
|
||||
if self.rearporttemplates.exists():
|
||||
data['rear-ports'] = [
|
||||
{
|
||||
'name': c.name,
|
||||
'type': c.type,
|
||||
'positions': c.positions,
|
||||
'label': c.label,
|
||||
'description': c.description,
|
||||
}
|
||||
for c in self.rearporttemplates.all()
|
||||
c.to_yaml() for c in self.rearporttemplates.all()
|
||||
]
|
||||
|
||||
return yaml.dump(dict(data), sort_keys=False)
|
||||
|
||||
@@ -244,10 +244,9 @@ class Rack(NetBoxModel):
|
||||
"""
|
||||
Return a list of unit numbers, top to bottom.
|
||||
"""
|
||||
max_position = self.u_height + decimal.Decimal(0.5)
|
||||
if self.desc_units:
|
||||
drange(0.5, max_position, 0.5)
|
||||
return drange(max_position, 0.5, -0.5)
|
||||
return drange(decimal.Decimal(1.0), self.u_height + 1, 0.5)
|
||||
return drange(self.u_height + decimal.Decimal(0.5), 0.5, -0.5)
|
||||
|
||||
def get_status_color(self):
|
||||
return RackStatusChoices.colors.get(self.status)
|
||||
|
||||
@@ -116,7 +116,10 @@ def retrace_cable_paths(instance, **kwargs):
|
||||
@receiver(post_delete, sender=CableTermination)
|
||||
def nullify_connected_endpoints(instance, **kwargs):
|
||||
"""
|
||||
Disassociate the Cable from the termination object.
|
||||
Disassociate the Cable from the termination object, and retrace any affected CablePaths.
|
||||
"""
|
||||
model = instance.termination_type.model_class()
|
||||
model.objects.filter(pk=instance.termination_id).update(cable=None, cable_end='')
|
||||
|
||||
for cablepath in CablePath.objects.filter(_nodes__contains=instance.cable):
|
||||
cablepath.retrace()
|
||||
|
||||
@@ -362,21 +362,26 @@ class CableTraceSVG:
|
||||
terminations = self.draw_terminations(far_ends)
|
||||
for term in terminations:
|
||||
self.draw_fanout(term, cable)
|
||||
else:
|
||||
elif far_ends:
|
||||
self.draw_terminations(far_ends)
|
||||
else:
|
||||
# Link is not connected to anything
|
||||
break
|
||||
|
||||
# Far end parent
|
||||
parent_objects = set(end.parent_object for end in far_ends)
|
||||
self.draw_parent_objects(parent_objects)
|
||||
|
||||
# Render a far-end object not connected via a link (e.g. a ProviderNetwork or Site associated with
|
||||
# a CircuitTermination)
|
||||
elif far_ends:
|
||||
|
||||
# Attachment
|
||||
attachment = self.draw_attachment()
|
||||
self.connectors.append(attachment)
|
||||
|
||||
# ProviderNetwork
|
||||
self.draw_parent_objects(set(end.parent_object for end in far_ends))
|
||||
# Object
|
||||
self.draw_parent_objects(far_ends)
|
||||
|
||||
# Determine drawing size
|
||||
self.drawing = svgwrite.Drawing(
|
||||
|
||||
@@ -163,8 +163,9 @@ class RackElevationSVG:
|
||||
|
||||
# Embed device type image if provided
|
||||
if self.include_images and image:
|
||||
url = f'{self.base_url}{image.url}' if image.url.startswith('/') else image.url
|
||||
image = Image(
|
||||
href=f'{self.base_url}{image.url}',
|
||||
href=url,
|
||||
insert=coords,
|
||||
size=size,
|
||||
class_=f'device-image{css_extra}'
|
||||
|
||||
@@ -172,7 +172,7 @@ class InterfaceTemplateTable(ComponentTemplateTable):
|
||||
|
||||
class Meta(ComponentTemplateTable.Meta):
|
||||
model = InterfaceTemplate
|
||||
fields = ('pk', 'name', 'label', 'mgmt_only', 'type', 'description', 'actions')
|
||||
fields = ('pk', 'name', 'label', 'mgmt_only', 'type', 'description', 'poe_mode', 'poe_type', 'actions')
|
||||
empty_text = "None"
|
||||
|
||||
|
||||
|
||||
@@ -14,6 +14,9 @@ class ModuleTypeTable(NetBoxTable):
|
||||
linkify=True,
|
||||
verbose_name='Module Type'
|
||||
)
|
||||
manufacturer = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
instance_count = columns.LinkedCountColumn(
|
||||
viewname='dcim:module_list',
|
||||
url_params={'module_type_id': 'pk'},
|
||||
@@ -41,6 +44,10 @@ class ModuleTable(NetBoxTable):
|
||||
module_bay = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
manufacturer = tables.Column(
|
||||
accessor=tables.A('module_type__manufacturer'),
|
||||
linkify=True
|
||||
)
|
||||
module_type = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
@@ -52,8 +59,9 @@ class ModuleTable(NetBoxTable):
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = Module
|
||||
fields = (
|
||||
'pk', 'id', 'device', 'module_bay', 'module_type', 'serial', 'asset_tag', 'comments', 'tags',
|
||||
'pk', 'id', 'device', 'module_bay', 'manufacturer', 'module_type', 'serial', 'asset_tag', 'comments',
|
||||
'tags',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'id', 'device', 'module_bay', 'module_type', 'serial', 'asset_tag',
|
||||
'pk', 'id', 'device', 'module_bay', 'manufacturer', 'module_type', 'serial', 'asset_tag',
|
||||
)
|
||||
|
||||
@@ -21,6 +21,9 @@ class PowerPanelTable(NetBoxTable):
|
||||
site = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
location = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
powerfeed_count = columns.LinkedCountColumn(
|
||||
viewname='dcim:powerfeed_list',
|
||||
url_params={'power_panel_id': 'pk'},
|
||||
@@ -35,7 +38,9 @@ class PowerPanelTable(NetBoxTable):
|
||||
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = PowerPanel
|
||||
fields = ('pk', 'id', 'name', 'site', 'location', 'powerfeed_count', 'contacts', 'tags', 'created', 'last_updated',)
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'site', 'location', 'powerfeed_count', 'contacts', 'tags', 'created', 'last_updated',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'site', 'location', 'powerfeed_count')
|
||||
|
||||
|
||||
|
||||
@@ -109,6 +109,10 @@ class RackReservationTable(TenancyColumnsMixin, NetBoxTable):
|
||||
accessor=Accessor('rack__site'),
|
||||
linkify=True
|
||||
)
|
||||
location = tables.Column(
|
||||
accessor=Accessor('rack__location'),
|
||||
linkify=True
|
||||
)
|
||||
rack = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
@@ -123,7 +127,7 @@ class RackReservationTable(TenancyColumnsMixin, NetBoxTable):
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = RackReservation
|
||||
fields = (
|
||||
'pk', 'id', 'reservation', 'site', 'rack', 'unit_list', 'user', 'created', 'tenant', 'tenant_group', 'description', 'tags',
|
||||
'pk', 'id', 'reservation', 'site', 'location', 'rack', 'unit_list', 'user', 'created', 'tenant', 'tenant_group', 'description', 'tags',
|
||||
'actions', 'created', 'last_updated',
|
||||
)
|
||||
default_columns = ('pk', 'reservation', 'site', 'rack', 'unit_list', 'user', 'description')
|
||||
|
||||
@@ -343,7 +343,7 @@ REARPORT_BUTTONS = """
|
||||
<li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.rearport&a_terminations={{ record.pk }}&b_terminations_type=dcim.consoleport&return_url={% url 'dcim:device_rearports' pk=object.pk %}">Console Port</a></li>
|
||||
<li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.rearport&a_terminations={{ record.pk }}&b_terminations_type=dcim.frontport&return_url={% url 'dcim:device_rearports' pk=object.pk %}">Front Port</a></li>
|
||||
<li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.rearport&a_terminations={{ record.pk }}&b_terminations_type=dcim.rearport&return_url={% url 'dcim:device_rearports' pk=object.pk %}">Rear Port</a></li>
|
||||
<li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.rearport&a_terminations={{ record.pk }}&b_terminations_type=circuits.circuitterminations&return_url={% url 'dcim:device_rearports' pk=object.pk %}">Circuit Termination</a></li>
|
||||
<li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.rearport&a_terminations={{ record.pk }}&b_terminations_type=circuits.circuittermination&return_url={% url 'dcim:device_rearports' pk=object.pk %}">Circuit Termination</a></li>
|
||||
</ul>
|
||||
</span>
|
||||
{% else %}
|
||||
|
||||
@@ -498,10 +498,10 @@ class RackTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_serial(self):
|
||||
params = {'serial': 'ABC'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
params = {'serial': 'abc'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
params = {'serial': ['ABC', 'DEF']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'serial': ['abc', 'def']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_tenant(self):
|
||||
tenants = Tenant.objects.all()[:2]
|
||||
@@ -1089,8 +1089,8 @@ class InterfaceTemplateTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
DeviceType.objects.bulk_create(device_types)
|
||||
|
||||
InterfaceTemplate.objects.bulk_create((
|
||||
InterfaceTemplate(device_type=device_types[0], name='Interface 1', type=InterfaceTypeChoices.TYPE_1GE_FIXED, mgmt_only=True),
|
||||
InterfaceTemplate(device_type=device_types[1], name='Interface 2', type=InterfaceTypeChoices.TYPE_1GE_GBIC, mgmt_only=False),
|
||||
InterfaceTemplate(device_type=device_types[0], name='Interface 1', type=InterfaceTypeChoices.TYPE_1GE_FIXED, mgmt_only=True, poe_mode=InterfacePoEModeChoices.MODE_PD, poe_type=InterfacePoETypeChoices.TYPE_1_8023AF),
|
||||
InterfaceTemplate(device_type=device_types[1], name='Interface 2', type=InterfaceTypeChoices.TYPE_1GE_GBIC, mgmt_only=False, poe_mode=InterfacePoEModeChoices.MODE_PSE, poe_type=InterfacePoETypeChoices.TYPE_2_8023AT),
|
||||
InterfaceTemplate(device_type=device_types[2], name='Interface 3', type=InterfaceTypeChoices.TYPE_1GE_SFP, mgmt_only=False),
|
||||
))
|
||||
|
||||
@@ -1113,6 +1113,14 @@ class InterfaceTemplateTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
params = {'mgmt_only': 'false'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_poe_mode(self):
|
||||
params = {'poe_mode': [InterfacePoEModeChoices.MODE_PD, InterfacePoEModeChoices.MODE_PSE]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_poe_type(self):
|
||||
params = {'poe_type': [InterfacePoETypeChoices.TYPE_1_8023AF, InterfacePoETypeChoices.TYPE_2_8023AT]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
|
||||
class FrontPortTemplateTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = FrontPortTemplate.objects.all()
|
||||
@@ -1864,7 +1872,9 @@ class ModuleTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
|
||||
|
||||
def test_serial(self):
|
||||
params = {'asset_tag': ['A', 'B']}
|
||||
params = {'serial': ['A', 'B']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'serial': ['a', 'b']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_asset_tag(self):
|
||||
@@ -3520,10 +3530,10 @@ class InventoryItemTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_serial(self):
|
||||
params = {'serial': 'ABC'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
params = {'serial': 'abc'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
params = {'serial': ['ABC', 'DEF']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'serial': ['abc', 'def']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_component_type(self):
|
||||
params = {'component_type': 'dcim.interface'}
|
||||
|
||||
@@ -163,8 +163,8 @@ class RackTestCase(TestCase):
|
||||
}
|
||||
self.assertEqual(rack1_inventory_front[10.0]['device'], device1)
|
||||
self.assertEqual(rack1_inventory_front[10.5]['device'], device1)
|
||||
del(rack1_inventory_front[10.0])
|
||||
del(rack1_inventory_front[10.5])
|
||||
del rack1_inventory_front[10.0]
|
||||
del rack1_inventory_front[10.5]
|
||||
for u in rack1_inventory_front.values():
|
||||
self.assertIsNone(u['device'])
|
||||
|
||||
@@ -174,8 +174,8 @@ class RackTestCase(TestCase):
|
||||
}
|
||||
self.assertEqual(rack1_inventory_rear[10.0]['device'], device1)
|
||||
self.assertEqual(rack1_inventory_rear[10.5]['device'], device1)
|
||||
del(rack1_inventory_rear[10.0])
|
||||
del(rack1_inventory_rear[10.5])
|
||||
del rack1_inventory_rear[10.0]
|
||||
del rack1_inventory_rear[10.5]
|
||||
for u in rack1_inventory_rear.values():
|
||||
self.assertIsNone(u['device'])
|
||||
|
||||
|
||||
@@ -24,11 +24,12 @@ def object_to_path_node(obj):
|
||||
|
||||
def path_node_to_object(repr):
|
||||
"""
|
||||
Given the string representation of a path node, return the corresponding instance.
|
||||
Given the string representation of a path node, return the corresponding instance. If the object no longer
|
||||
exists, return None.
|
||||
"""
|
||||
ct_id, object_id = decompile_path_node(repr)
|
||||
ct = ContentType.objects.get_for_id(ct_id)
|
||||
return ct.model_class().objects.get(pk=object_id)
|
||||
return ct.model_class().objects.filter(pk=object_id).first()
|
||||
|
||||
|
||||
def create_cablepath(terminations):
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from collections import OrderedDict
|
||||
|
||||
from django.contrib import messages
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.paginator import EmptyPage, PageNotAnInteger
|
||||
@@ -324,7 +322,7 @@ class SiteListView(generic.ObjectListView):
|
||||
|
||||
|
||||
class SiteView(generic.ObjectView):
|
||||
queryset = Site.objects.prefetch_related('region', 'tenant__group')
|
||||
queryset = Site.objects.prefetch_related('tenant__group')
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
stats = {
|
||||
@@ -359,7 +357,7 @@ class SiteView(generic.ObjectView):
|
||||
site=instance,
|
||||
position__isnull=True,
|
||||
parent_bay__isnull=True
|
||||
).prefetch_related('device_type__manufacturer')
|
||||
).prefetch_related('device_type__manufacturer', 'parent_bay', 'device_role')
|
||||
|
||||
asns = ASN.objects.restrict(request.user, 'view').filter(sites=instance)
|
||||
asn_count = asns.count()
|
||||
@@ -391,14 +389,14 @@ class SiteBulkImportView(generic.BulkImportView):
|
||||
|
||||
|
||||
class SiteBulkEditView(generic.BulkEditView):
|
||||
queryset = Site.objects.prefetch_related('region', 'tenant')
|
||||
queryset = Site.objects.all()
|
||||
filterset = filtersets.SiteFilterSet
|
||||
table = tables.SiteTable
|
||||
form = forms.SiteBulkEditForm
|
||||
|
||||
|
||||
class SiteBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = Site.objects.prefetch_related('region', 'tenant')
|
||||
queryset = Site.objects.all()
|
||||
filterset = filtersets.SiteFilterSet
|
||||
table = tables.SiteTable
|
||||
|
||||
@@ -454,7 +452,7 @@ class LocationView(generic.ObjectView):
|
||||
location=instance,
|
||||
position__isnull=True,
|
||||
parent_bay__isnull=True
|
||||
).prefetch_related('device_type__manufacturer')
|
||||
).prefetch_related('device_type__manufacturer', 'parent_bay', 'device_role')
|
||||
|
||||
return {
|
||||
'rack_count': rack_count,
|
||||
@@ -572,7 +570,7 @@ class RackRoleBulkDeleteView(generic.BulkDeleteView):
|
||||
#
|
||||
|
||||
class RackListView(generic.ObjectListView):
|
||||
queryset = Rack.objects.prefetch_related('devices__device_type').annotate(
|
||||
queryset = Rack.objects.annotate(
|
||||
device_count=count_related(Device, 'rack')
|
||||
)
|
||||
filterset = filtersets.RackFilterSet
|
||||
@@ -631,7 +629,7 @@ class RackView(generic.ObjectView):
|
||||
rack=instance,
|
||||
position__isnull=True,
|
||||
parent_bay__isnull=True
|
||||
).prefetch_related('device_type__manufacturer')
|
||||
).prefetch_related('device_type__manufacturer', 'parent_bay', 'device_role')
|
||||
|
||||
peer_racks = Rack.objects.restrict(request.user, 'view').filter(site=instance.site)
|
||||
|
||||
@@ -682,14 +680,14 @@ class RackBulkImportView(generic.BulkImportView):
|
||||
|
||||
|
||||
class RackBulkEditView(generic.BulkEditView):
|
||||
queryset = Rack.objects.prefetch_related('site', 'location', 'tenant', 'role')
|
||||
queryset = Rack.objects.all()
|
||||
filterset = filtersets.RackFilterSet
|
||||
table = tables.RackTable
|
||||
form = forms.RackBulkEditForm
|
||||
|
||||
|
||||
class RackBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = Rack.objects.prefetch_related('site', 'location', 'tenant', 'role')
|
||||
queryset = Rack.objects.all()
|
||||
filterset = filtersets.RackFilterSet
|
||||
table = tables.RackTable
|
||||
|
||||
@@ -706,7 +704,7 @@ class RackReservationListView(generic.ObjectListView):
|
||||
|
||||
|
||||
class RackReservationView(generic.ObjectView):
|
||||
queryset = RackReservation.objects.prefetch_related('rack')
|
||||
queryset = RackReservation.objects.all()
|
||||
|
||||
|
||||
class RackReservationEditView(generic.ObjectEditView):
|
||||
@@ -742,14 +740,14 @@ class RackReservationImportView(generic.BulkImportView):
|
||||
|
||||
|
||||
class RackReservationBulkEditView(generic.BulkEditView):
|
||||
queryset = RackReservation.objects.prefetch_related('rack', 'user')
|
||||
queryset = RackReservation.objects.all()
|
||||
filterset = filtersets.RackReservationFilterSet
|
||||
table = tables.RackReservationTable
|
||||
form = forms.RackReservationBulkEditForm
|
||||
|
||||
|
||||
class RackReservationBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = RackReservation.objects.prefetch_related('rack', 'user')
|
||||
queryset = RackReservation.objects.all()
|
||||
filterset = filtersets.RackReservationFilterSet
|
||||
table = tables.RackReservationTable
|
||||
|
||||
@@ -831,7 +829,7 @@ class ManufacturerBulkDeleteView(generic.BulkDeleteView):
|
||||
#
|
||||
|
||||
class DeviceTypeListView(generic.ObjectListView):
|
||||
queryset = DeviceType.objects.prefetch_related('manufacturer').annotate(
|
||||
queryset = DeviceType.objects.annotate(
|
||||
instance_count=count_related(Device, 'device_type')
|
||||
)
|
||||
filterset = filtersets.DeviceTypeFilterSet
|
||||
@@ -840,7 +838,7 @@ class DeviceTypeListView(generic.ObjectListView):
|
||||
|
||||
|
||||
class DeviceTypeView(generic.ObjectView):
|
||||
queryset = DeviceType.objects.prefetch_related('manufacturer')
|
||||
queryset = DeviceType.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
instance_count = Device.objects.restrict(request.user).filter(device_type=instance).count()
|
||||
@@ -945,18 +943,18 @@ class DeviceTypeImportView(generic.ObjectImportView):
|
||||
]
|
||||
queryset = DeviceType.objects.all()
|
||||
model_form = forms.DeviceTypeImportForm
|
||||
related_object_forms = OrderedDict((
|
||||
('console-ports', forms.ConsolePortTemplateImportForm),
|
||||
('console-server-ports', forms.ConsoleServerPortTemplateImportForm),
|
||||
('power-ports', forms.PowerPortTemplateImportForm),
|
||||
('power-outlets', forms.PowerOutletTemplateImportForm),
|
||||
('interfaces', forms.InterfaceTemplateImportForm),
|
||||
('rear-ports', forms.RearPortTemplateImportForm),
|
||||
('front-ports', forms.FrontPortTemplateImportForm),
|
||||
('module-bays', forms.ModuleBayTemplateImportForm),
|
||||
('device-bays', forms.DeviceBayTemplateImportForm),
|
||||
('inventory-items', forms.InventoryItemTemplateImportForm),
|
||||
))
|
||||
related_object_forms = {
|
||||
'console-ports': forms.ConsolePortTemplateImportForm,
|
||||
'console-server-ports': forms.ConsoleServerPortTemplateImportForm,
|
||||
'power-ports': forms.PowerPortTemplateImportForm,
|
||||
'power-outlets': forms.PowerOutletTemplateImportForm,
|
||||
'interfaces': forms.InterfaceTemplateImportForm,
|
||||
'rear-ports': forms.RearPortTemplateImportForm,
|
||||
'front-ports': forms.FrontPortTemplateImportForm,
|
||||
'module-bays': forms.ModuleBayTemplateImportForm,
|
||||
'device-bays': forms.DeviceBayTemplateImportForm,
|
||||
'inventory-items': forms.InventoryItemTemplateImportForm,
|
||||
}
|
||||
|
||||
def prep_related_object_data(self, parent, data):
|
||||
data.update({'device_type': parent})
|
||||
@@ -964,7 +962,7 @@ class DeviceTypeImportView(generic.ObjectImportView):
|
||||
|
||||
|
||||
class DeviceTypeBulkEditView(generic.BulkEditView):
|
||||
queryset = DeviceType.objects.prefetch_related('manufacturer').annotate(
|
||||
queryset = DeviceType.objects.annotate(
|
||||
instance_count=count_related(Device, 'device_type')
|
||||
)
|
||||
filterset = filtersets.DeviceTypeFilterSet
|
||||
@@ -973,7 +971,7 @@ class DeviceTypeBulkEditView(generic.BulkEditView):
|
||||
|
||||
|
||||
class DeviceTypeBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = DeviceType.objects.prefetch_related('manufacturer').annotate(
|
||||
queryset = DeviceType.objects.annotate(
|
||||
instance_count=count_related(Device, 'device_type')
|
||||
)
|
||||
filterset = filtersets.DeviceTypeFilterSet
|
||||
@@ -985,7 +983,7 @@ class DeviceTypeBulkDeleteView(generic.BulkDeleteView):
|
||||
#
|
||||
|
||||
class ModuleTypeListView(generic.ObjectListView):
|
||||
queryset = ModuleType.objects.prefetch_related('manufacturer').annotate(
|
||||
queryset = ModuleType.objects.annotate(
|
||||
instance_count=count_related(Module, 'module_type')
|
||||
)
|
||||
filterset = filtersets.ModuleTypeFilterSet
|
||||
@@ -994,7 +992,7 @@ class ModuleTypeListView(generic.ObjectListView):
|
||||
|
||||
|
||||
class ModuleTypeView(generic.ObjectView):
|
||||
queryset = ModuleType.objects.prefetch_related('manufacturer')
|
||||
queryset = ModuleType.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
instance_count = Module.objects.restrict(request.user).filter(module_type=instance).count()
|
||||
@@ -1075,15 +1073,15 @@ class ModuleTypeImportView(generic.ObjectImportView):
|
||||
]
|
||||
queryset = ModuleType.objects.all()
|
||||
model_form = forms.ModuleTypeImportForm
|
||||
related_object_forms = OrderedDict((
|
||||
('console-ports', forms.ConsolePortTemplateImportForm),
|
||||
('console-server-ports', forms.ConsoleServerPortTemplateImportForm),
|
||||
('power-ports', forms.PowerPortTemplateImportForm),
|
||||
('power-outlets', forms.PowerOutletTemplateImportForm),
|
||||
('interfaces', forms.InterfaceTemplateImportForm),
|
||||
('rear-ports', forms.RearPortTemplateImportForm),
|
||||
('front-ports', forms.FrontPortTemplateImportForm),
|
||||
))
|
||||
related_object_forms = {
|
||||
'console-ports': forms.ConsolePortTemplateImportForm,
|
||||
'console-server-ports': forms.ConsoleServerPortTemplateImportForm,
|
||||
'power-ports': forms.PowerPortTemplateImportForm,
|
||||
'power-outlets': forms.PowerOutletTemplateImportForm,
|
||||
'interfaces': forms.InterfaceTemplateImportForm,
|
||||
'rear-ports': forms.RearPortTemplateImportForm,
|
||||
'front-ports': forms.FrontPortTemplateImportForm,
|
||||
}
|
||||
|
||||
def prep_related_object_data(self, parent, data):
|
||||
data.update({'module_type': parent})
|
||||
@@ -1091,7 +1089,7 @@ class ModuleTypeImportView(generic.ObjectImportView):
|
||||
|
||||
|
||||
class ModuleTypeBulkEditView(generic.BulkEditView):
|
||||
queryset = ModuleType.objects.prefetch_related('manufacturer').annotate(
|
||||
queryset = ModuleType.objects.annotate(
|
||||
instance_count=count_related(Module, 'module_type')
|
||||
)
|
||||
filterset = filtersets.ModuleTypeFilterSet
|
||||
@@ -1100,7 +1098,7 @@ class ModuleTypeBulkEditView(generic.BulkEditView):
|
||||
|
||||
|
||||
class ModuleTypeBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = ModuleType.objects.prefetch_related('manufacturer').annotate(
|
||||
queryset = ModuleType.objects.annotate(
|
||||
instance_count=count_related(Module, 'module_type')
|
||||
)
|
||||
filterset = filtersets.ModuleTypeFilterSet
|
||||
@@ -1611,9 +1609,7 @@ class DeviceListView(generic.ObjectListView):
|
||||
|
||||
|
||||
class DeviceView(generic.ObjectView):
|
||||
queryset = Device.objects.prefetch_related(
|
||||
'site__region', 'location', 'rack', 'tenant__group', 'device_role', 'platform', 'primary_ip4', 'primary_ip6'
|
||||
)
|
||||
queryset = Device.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
# VirtualChassis members
|
||||
@@ -1790,14 +1786,14 @@ class ChildDeviceBulkImportView(generic.BulkImportView):
|
||||
|
||||
|
||||
class DeviceBulkEditView(generic.BulkEditView):
|
||||
queryset = Device.objects.prefetch_related('tenant', 'site', 'rack', 'device_role', 'device_type__manufacturer')
|
||||
queryset = Device.objects.prefetch_related('device_type__manufacturer')
|
||||
filterset = filtersets.DeviceFilterSet
|
||||
table = tables.DeviceTable
|
||||
form = forms.DeviceBulkEditForm
|
||||
|
||||
|
||||
class DeviceBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = Device.objects.prefetch_related('tenant', 'site', 'rack', 'device_role', 'device_type__manufacturer')
|
||||
queryset = Device.objects.prefetch_related('device_type__manufacturer')
|
||||
filterset = filtersets.DeviceFilterSet
|
||||
table = tables.DeviceTable
|
||||
|
||||
@@ -1807,7 +1803,7 @@ class DeviceBulkDeleteView(generic.BulkDeleteView):
|
||||
#
|
||||
|
||||
class ModuleListView(generic.ObjectListView):
|
||||
queryset = Module.objects.prefetch_related('device', 'module_type__manufacturer')
|
||||
queryset = Module.objects.prefetch_related('module_type__manufacturer')
|
||||
filterset = filtersets.ModuleFilterSet
|
||||
filterset_form = forms.ModuleFilterForm
|
||||
table = tables.ModuleTable
|
||||
@@ -1833,14 +1829,14 @@ class ModuleBulkImportView(generic.BulkImportView):
|
||||
|
||||
|
||||
class ModuleBulkEditView(generic.BulkEditView):
|
||||
queryset = Module.objects.prefetch_related('device', 'module_type__manufacturer')
|
||||
queryset = Module.objects.prefetch_related('module_type__manufacturer')
|
||||
filterset = filtersets.ModuleFilterSet
|
||||
table = tables.ModuleTable
|
||||
form = forms.ModuleBulkEditForm
|
||||
|
||||
|
||||
class ModuleBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = Module.objects.prefetch_related('device', 'module_type__manufacturer')
|
||||
queryset = Module.objects.prefetch_related('module_type__manufacturer')
|
||||
filterset = filtersets.ModuleFilterSet
|
||||
table = tables.ModuleTable
|
||||
|
||||
@@ -2566,7 +2562,7 @@ class InventoryItemBulkImportView(generic.BulkImportView):
|
||||
|
||||
|
||||
class InventoryItemBulkEditView(generic.BulkEditView):
|
||||
queryset = InventoryItem.objects.prefetch_related('device', 'manufacturer', 'role')
|
||||
queryset = InventoryItem.objects.all()
|
||||
filterset = filtersets.InventoryItemFilterSet
|
||||
table = tables.InventoryItemTable
|
||||
form = forms.InventoryItemBulkEditForm
|
||||
@@ -2577,7 +2573,7 @@ class InventoryItemBulkRenameView(generic.BulkRenameView):
|
||||
|
||||
|
||||
class InventoryItemBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = InventoryItem.objects.prefetch_related('device', 'manufacturer', 'role')
|
||||
queryset = InventoryItem.objects.all()
|
||||
table = tables.InventoryItemTable
|
||||
template_name = 'dcim/inventoryitem_bulk_delete.html'
|
||||
|
||||
@@ -2850,7 +2846,7 @@ class CableEditView(generic.ObjectEditView):
|
||||
termination_a = obj.terminations.filter(cable_end='A').first()
|
||||
a_type = termination_a.termination._meta.model if termination_a else None
|
||||
termination_b = obj.terminations.filter(cable_end='B').first()
|
||||
b_type = termination_b.termination._meta.model if termination_a else None
|
||||
b_type = termination_b.termination._meta.model if termination_b else None
|
||||
self.form = forms.get_cable_form(a_type, b_type)
|
||||
|
||||
return obj
|
||||
@@ -2867,14 +2863,20 @@ class CableBulkImportView(generic.BulkImportView):
|
||||
|
||||
|
||||
class CableBulkEditView(generic.BulkEditView):
|
||||
queryset = Cable.objects.prefetch_related('terminations')
|
||||
queryset = Cable.objects.prefetch_related(
|
||||
'terminations__termination', 'terminations___device', 'terminations___rack', 'terminations___location',
|
||||
'terminations___site',
|
||||
)
|
||||
filterset = filtersets.CableFilterSet
|
||||
table = tables.CableTable
|
||||
form = forms.CableBulkEditForm
|
||||
|
||||
|
||||
class CableBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = Cable.objects.prefetch_related('terminations')
|
||||
queryset = Cable.objects.prefetch_related(
|
||||
'terminations__termination', 'terminations___device', 'terminations___rack', 'terminations___location',
|
||||
'terminations___site',
|
||||
)
|
||||
filterset = filtersets.CableFilterSet
|
||||
table = tables.CableTable
|
||||
|
||||
@@ -2930,7 +2932,7 @@ class InterfaceConnectionsListView(generic.ObjectListView):
|
||||
#
|
||||
|
||||
class VirtualChassisListView(generic.ObjectListView):
|
||||
queryset = VirtualChassis.objects.prefetch_related('master').annotate(
|
||||
queryset = VirtualChassis.objects.annotate(
|
||||
member_count=count_related(Device, 'virtual_chassis')
|
||||
)
|
||||
table = tables.VirtualChassisTable
|
||||
@@ -3158,9 +3160,7 @@ class VirtualChassisBulkDeleteView(generic.BulkDeleteView):
|
||||
#
|
||||
|
||||
class PowerPanelListView(generic.ObjectListView):
|
||||
queryset = PowerPanel.objects.prefetch_related(
|
||||
'site', 'location'
|
||||
).annotate(
|
||||
queryset = PowerPanel.objects.annotate(
|
||||
powerfeed_count=count_related(PowerFeed, 'power_panel')
|
||||
)
|
||||
filterset = filtersets.PowerPanelFilterSet
|
||||
@@ -3169,10 +3169,10 @@ class PowerPanelListView(generic.ObjectListView):
|
||||
|
||||
|
||||
class PowerPanelView(generic.ObjectView):
|
||||
queryset = PowerPanel.objects.prefetch_related('site', 'location')
|
||||
queryset = PowerPanel.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
power_feeds = PowerFeed.objects.restrict(request.user).filter(power_panel=instance).prefetch_related('rack')
|
||||
power_feeds = PowerFeed.objects.restrict(request.user).filter(power_panel=instance)
|
||||
powerfeed_table = tables.PowerFeedTable(
|
||||
data=power_feeds,
|
||||
orderable=False
|
||||
@@ -3202,16 +3202,14 @@ class PowerPanelBulkImportView(generic.BulkImportView):
|
||||
|
||||
|
||||
class PowerPanelBulkEditView(generic.BulkEditView):
|
||||
queryset = PowerPanel.objects.prefetch_related('site', 'location')
|
||||
queryset = PowerPanel.objects.all()
|
||||
filterset = filtersets.PowerPanelFilterSet
|
||||
table = tables.PowerPanelTable
|
||||
form = forms.PowerPanelBulkEditForm
|
||||
|
||||
|
||||
class PowerPanelBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = PowerPanel.objects.prefetch_related(
|
||||
'site', 'location'
|
||||
).annotate(
|
||||
queryset = PowerPanel.objects.annotate(
|
||||
powerfeed_count=count_related(PowerFeed, 'power_panel')
|
||||
)
|
||||
filterset = filtersets.PowerPanelFilterSet
|
||||
@@ -3230,7 +3228,7 @@ class PowerFeedListView(generic.ObjectListView):
|
||||
|
||||
|
||||
class PowerFeedView(generic.ObjectView):
|
||||
queryset = PowerFeed.objects.prefetch_related('power_panel', 'rack')
|
||||
queryset = PowerFeed.objects.all()
|
||||
|
||||
|
||||
class PowerFeedEditView(generic.ObjectEditView):
|
||||
@@ -3249,7 +3247,7 @@ class PowerFeedBulkImportView(generic.BulkImportView):
|
||||
|
||||
|
||||
class PowerFeedBulkEditView(generic.BulkEditView):
|
||||
queryset = PowerFeed.objects.prefetch_related('power_panel', 'rack')
|
||||
queryset = PowerFeed.objects.all()
|
||||
filterset = filtersets.PowerFeedFilterSet
|
||||
table = tables.PowerFeedTable
|
||||
form = forms.PowerFeedBulkEditForm
|
||||
@@ -3260,6 +3258,6 @@ class PowerFeedBulkDisconnectView(BulkDisconnectView):
|
||||
|
||||
|
||||
class PowerFeedBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = PowerFeed.objects.prefetch_related('power_panel', 'rack')
|
||||
queryset = PowerFeed.objects.all()
|
||||
filterset = filtersets.PowerFeedFilterSet
|
||||
table = tables.PowerFeedTable
|
||||
|
||||
@@ -3,6 +3,7 @@ from rest_framework.fields import Field
|
||||
|
||||
from extras.choices import CustomFieldTypeChoices
|
||||
from extras.models import CustomField
|
||||
from netbox.constants import NESTED_SERIALIZER_PREFIX
|
||||
|
||||
|
||||
#
|
||||
@@ -51,10 +52,10 @@ class CustomFieldsDataField(Field):
|
||||
for cf in self._get_custom_fields():
|
||||
value = cf.deserialize(obj.get(cf.name))
|
||||
if value is not None and cf.type == CustomFieldTypeChoices.TYPE_OBJECT:
|
||||
serializer = get_serializer_for_model(cf.object_type.model_class(), prefix='Nested')
|
||||
serializer = get_serializer_for_model(cf.object_type.model_class(), prefix=NESTED_SERIALIZER_PREFIX)
|
||||
value = serializer(value, context=self.parent.context).data
|
||||
elif value is not None and cf.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT:
|
||||
serializer = get_serializer_for_model(cf.object_type.model_class(), prefix='Nested')
|
||||
serializer = get_serializer_for_model(cf.object_type.model_class(), prefix=NESTED_SERIALIZER_PREFIX)
|
||||
value = serializer(value, many=True, context=self.parent.context).data
|
||||
data[cf.name] = value
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ from extras.utils import FeatureQuery
|
||||
from netbox.api.exceptions import SerializerNotFound
|
||||
from netbox.api.fields import ChoiceField, ContentTypeField, SerializedPKRelatedField
|
||||
from netbox.api.serializers import BaseModelSerializer, NetBoxModelSerializer, ValidatedModelSerializer
|
||||
from netbox.constants import NESTED_SERIALIZER_PREFIX
|
||||
from tenancy.api.nested_serializers import NestedTenantSerializer, NestedTenantGroupSerializer
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from users.api.nested_serializers import NestedUserSerializer
|
||||
@@ -193,7 +194,7 @@ class ImageAttachmentSerializer(ValidatedModelSerializer):
|
||||
|
||||
@swagger_serializer_method(serializer_or_field=serializers.DictField)
|
||||
def get_parent(self, obj):
|
||||
serializer = get_serializer_for_model(obj.parent, prefix='Nested')
|
||||
serializer = get_serializer_for_model(obj.parent, prefix=NESTED_SERIALIZER_PREFIX)
|
||||
return serializer(obj.parent, context={'request': self.context['request']}).data
|
||||
|
||||
|
||||
@@ -243,7 +244,7 @@ class JournalEntrySerializer(NetBoxModelSerializer):
|
||||
|
||||
@swagger_serializer_method(serializer_or_field=serializers.DictField)
|
||||
def get_assigned_object(self, instance):
|
||||
serializer = get_serializer_for_model(instance.assigned_object_type.model_class(), prefix='Nested')
|
||||
serializer = get_serializer_for_model(instance.assigned_object_type.model_class(), prefix=NESTED_SERIALIZER_PREFIX)
|
||||
context = {'request': self.context['request']}
|
||||
return serializer(instance.assigned_object, context=context).data
|
||||
|
||||
@@ -469,7 +470,7 @@ class ObjectChangeSerializer(BaseModelSerializer):
|
||||
return None
|
||||
|
||||
try:
|
||||
serializer = get_serializer_for_model(obj.changed_object, prefix='Nested')
|
||||
serializer = get_serializer_for_model(obj.changed_object, prefix=NESTED_SERIALIZER_PREFIX)
|
||||
except SerializerNotFound:
|
||||
return obj.object_repr
|
||||
context = {
|
||||
|
||||
@@ -19,6 +19,7 @@ class CustomFieldsMixin:
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.custom_fields = {}
|
||||
self.custom_field_groups = {}
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@@ -58,3 +59,6 @@ class CustomFieldsMixin:
|
||||
|
||||
# Annotate the field in the list of CustomField form fields
|
||||
self.custom_fields[field_name] = customfield
|
||||
if customfield.group_name not in self.custom_field_groups:
|
||||
self.custom_field_groups[customfield.group_name] = []
|
||||
self.custom_field_groups[customfield.group_name].append(field_name)
|
||||
|
||||
@@ -136,6 +136,7 @@ class WebhookForm(BootstrapMixin, forms.ModelForm):
|
||||
'http_method': StaticSelect(),
|
||||
'additional_headers': forms.Textarea(attrs={'class': 'font-monospace'}),
|
||||
'body_template': forms.Textarea(attrs={'class': 'font-monospace'}),
|
||||
'conditions': forms.Textarea(attrs={'class': 'font-monospace'}),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('extras', '0076_tag_slug_unicode'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='customlink',
|
||||
name='link_text',
|
||||
field=models.TextField(),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='customlink',
|
||||
name='link_url',
|
||||
field=models.TextField(),
|
||||
),
|
||||
]
|
||||
@@ -181,7 +181,7 @@ class CustomField(ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel):
|
||||
model = ct.model_class()
|
||||
instances = model.objects.filter(**{f'custom_field_data__{self.name}__isnull': False})
|
||||
for instance in instances:
|
||||
del(instance.custom_field_data[self.name])
|
||||
del instance.custom_field_data[self.name]
|
||||
model.objects.bulk_update(instances, ['custom_field_data'], batch_size=100)
|
||||
|
||||
def rename_object_data(self, old_name, new_name):
|
||||
|
||||
@@ -204,12 +204,10 @@ class CustomLink(ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel):
|
||||
enabled = models.BooleanField(
|
||||
default=True
|
||||
)
|
||||
link_text = models.CharField(
|
||||
max_length=500,
|
||||
link_text = models.TextField(
|
||||
help_text="Jinja2 template code for link text"
|
||||
)
|
||||
link_url = models.CharField(
|
||||
max_length=500,
|
||||
link_url = models.TextField(
|
||||
verbose_name='Link URL',
|
||||
help_text="Jinja2 template code for link URL"
|
||||
)
|
||||
|
||||
@@ -28,3 +28,4 @@ registry = Registry()
|
||||
registry['model_features'] = {
|
||||
feature: collections.defaultdict(set) for feature in EXTRAS_FEATURES
|
||||
}
|
||||
registry['denormalized_fields'] = collections.defaultdict(list)
|
||||
|
||||
@@ -3,7 +3,6 @@ import inspect
|
||||
import logging
|
||||
import pkgutil
|
||||
import traceback
|
||||
from collections import OrderedDict
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
@@ -114,7 +113,7 @@ class Report(object):
|
||||
|
||||
def __init__(self):
|
||||
|
||||
self._results = OrderedDict()
|
||||
self._results = {}
|
||||
self.active_test = None
|
||||
self.failed = False
|
||||
|
||||
@@ -125,13 +124,13 @@ class Report(object):
|
||||
for method in dir(self):
|
||||
if method.startswith('test_') and callable(getattr(self, method)):
|
||||
test_methods.append(method)
|
||||
self._results[method] = OrderedDict([
|
||||
('success', 0),
|
||||
('info', 0),
|
||||
('warning', 0),
|
||||
('failure', 0),
|
||||
('log', []),
|
||||
])
|
||||
self._results[method] = {
|
||||
'success': 0,
|
||||
'info': 0,
|
||||
'warning': 0,
|
||||
'failure': 0,
|
||||
'log': [],
|
||||
}
|
||||
if not test_methods:
|
||||
raise Exception("A report must contain at least one test method.")
|
||||
self.test_methods = test_methods
|
||||
|
||||
@@ -6,7 +6,6 @@ import pkgutil
|
||||
import sys
|
||||
import traceback
|
||||
import threading
|
||||
from collections import OrderedDict
|
||||
|
||||
import yaml
|
||||
from django import forms
|
||||
@@ -496,7 +495,7 @@ def get_scripts(use_names=False):
|
||||
Return a dict of dicts mapping all scripts to their modules. Set use_names to True to use each module's human-
|
||||
defined name in place of the actual module name.
|
||||
"""
|
||||
scripts = OrderedDict()
|
||||
scripts = {}
|
||||
# Iterate through all modules within the scripts path. These are the user-created files in which reports are
|
||||
# defined.
|
||||
for importer, module_name, _ in pkgutil.iter_modules([settings.SCRIPTS_ROOT]):
|
||||
@@ -510,7 +509,7 @@ def get_scripts(use_names=False):
|
||||
|
||||
if use_names and hasattr(module, 'name'):
|
||||
module_name = module.name
|
||||
module_scripts = OrderedDict()
|
||||
module_scripts = {}
|
||||
script_order = getattr(module, "script_order", ())
|
||||
ordered_scripts = [cls for cls in script_order if is_script(cls)]
|
||||
unordered_scripts = [cls for _, cls in inspect.getmembers(module, is_script) if cls not in script_order]
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from collections import OrderedDict
|
||||
|
||||
from django import template
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.utils.safestring import mark_safe
|
||||
@@ -50,7 +48,7 @@ def custom_links(context, obj):
|
||||
'perms': context['perms'], # django.contrib.auth.context_processors.auth
|
||||
}
|
||||
template_code = ''
|
||||
group_names = OrderedDict()
|
||||
group_names = {}
|
||||
|
||||
for cl in custom_links:
|
||||
|
||||
|
||||
@@ -992,7 +992,7 @@ class CustomFieldModelTest(TestCase):
|
||||
with self.assertRaises(ValidationError):
|
||||
site.clean()
|
||||
|
||||
del(site.cf['bar'])
|
||||
del site.cf['bar']
|
||||
site.clean()
|
||||
|
||||
def test_missing_required_field(self):
|
||||
|
||||
@@ -30,4 +30,4 @@ class RegistryTest(TestCase):
|
||||
reg['foo'] = 123
|
||||
|
||||
with self.assertRaises(TypeError):
|
||||
del(reg['foo'])
|
||||
del reg['foo']
|
||||
|
||||
@@ -492,14 +492,14 @@ class JournalEntryDeleteView(generic.ObjectDeleteView):
|
||||
|
||||
|
||||
class JournalEntryBulkEditView(generic.BulkEditView):
|
||||
queryset = JournalEntry.objects.prefetch_related('created_by')
|
||||
queryset = JournalEntry.objects.all()
|
||||
filterset = filtersets.JournalEntryFilterSet
|
||||
table = tables.JournalEntryTable
|
||||
form = forms.JournalEntryBulkEditForm
|
||||
|
||||
|
||||
class JournalEntryBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = JournalEntry.objects.prefetch_related('created_by')
|
||||
queryset = JournalEntry.objects.all()
|
||||
filterset = filtersets.JournalEntryFilterSet
|
||||
table = tables.JournalEntryTable
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from collections import OrderedDict
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from drf_yasg.utils import swagger_serializer_method
|
||||
from rest_framework import serializers
|
||||
@@ -10,6 +8,7 @@ from ipam.constants import IPADDRESS_ASSIGNMENT_MODELS, VLANGROUP_SCOPE_TYPES
|
||||
from ipam.models import *
|
||||
from netbox.api.fields import ChoiceField, ContentTypeField, SerializedPKRelatedField
|
||||
from netbox.api.serializers import NetBoxModelSerializer
|
||||
from netbox.constants import NESTED_SERIALIZER_PREFIX
|
||||
from tenancy.api.nested_serializers import NestedTenantSerializer
|
||||
from utilities.api import get_serializer_for_model
|
||||
from virtualization.api.nested_serializers import NestedVirtualMachineSerializer
|
||||
@@ -148,7 +147,7 @@ class FHRPGroupAssignmentSerializer(NetBoxModelSerializer):
|
||||
def get_interface(self, obj):
|
||||
if obj.interface is None:
|
||||
return None
|
||||
serializer = get_serializer_for_model(obj.interface, prefix='Nested')
|
||||
serializer = get_serializer_for_model(obj.interface, prefix=NESTED_SERIALIZER_PREFIX)
|
||||
context = {'request': self.context['request']}
|
||||
return serializer(obj.interface, context=context).data
|
||||
|
||||
@@ -194,7 +193,7 @@ class VLANGroupSerializer(NetBoxModelSerializer):
|
||||
def get_scope(self, obj):
|
||||
if obj.scope_id is None:
|
||||
return None
|
||||
serializer = get_serializer_for_model(obj.scope, prefix='Nested')
|
||||
serializer = get_serializer_for_model(obj.scope, prefix=NESTED_SERIALIZER_PREFIX)
|
||||
context = {'request': self.context['request']}
|
||||
|
||||
return serializer(obj.scope, context=context).data
|
||||
@@ -226,13 +225,13 @@ class AvailableVLANSerializer(serializers.Serializer):
|
||||
group = NestedVLANGroupSerializer(read_only=True)
|
||||
|
||||
def to_representation(self, instance):
|
||||
return OrderedDict([
|
||||
('vid', instance),
|
||||
('group', NestedVLANGroupSerializer(
|
||||
return {
|
||||
'vid': instance,
|
||||
'group': NestedVLANGroupSerializer(
|
||||
self.context['group'],
|
||||
context={'request': self.context['request']}
|
||||
).data),
|
||||
])
|
||||
).data,
|
||||
}
|
||||
|
||||
|
||||
class CreateAvailableVLANSerializer(NetBoxModelSerializer):
|
||||
@@ -317,11 +316,11 @@ class AvailablePrefixSerializer(serializers.Serializer):
|
||||
vrf = NestedVRFSerializer(self.context['vrf'], context={'request': self.context['request']}).data
|
||||
else:
|
||||
vrf = None
|
||||
return OrderedDict([
|
||||
('family', instance.version),
|
||||
('prefix', str(instance)),
|
||||
('vrf', vrf),
|
||||
])
|
||||
return {
|
||||
'family': instance.version,
|
||||
'prefix': str(instance),
|
||||
'vrf': vrf,
|
||||
}
|
||||
|
||||
|
||||
#
|
||||
@@ -378,7 +377,7 @@ class IPAddressSerializer(NetBoxModelSerializer):
|
||||
def get_assigned_object(self, obj):
|
||||
if obj.assigned_object is None:
|
||||
return None
|
||||
serializer = get_serializer_for_model(obj.assigned_object, prefix='Nested')
|
||||
serializer = get_serializer_for_model(obj.assigned_object, prefix=NESTED_SERIALIZER_PREFIX)
|
||||
context = {'request': self.context['request']}
|
||||
return serializer(obj.assigned_object, context=context).data
|
||||
|
||||
@@ -396,11 +395,11 @@ class AvailableIPSerializer(serializers.Serializer):
|
||||
vrf = NestedVRFSerializer(self.context['vrf'], context={'request': self.context['request']}).data
|
||||
else:
|
||||
vrf = None
|
||||
return OrderedDict([
|
||||
('family', self.context['parent'].family),
|
||||
('address', f"{instance}/{self.context['parent'].mask_length}"),
|
||||
('vrf', vrf),
|
||||
])
|
||||
return {
|
||||
'family': self.context['parent'].family,
|
||||
'address': f"{instance}/{self.context['parent'].mask_length}",
|
||||
'vrf': vrf,
|
||||
}
|
||||
|
||||
|
||||
#
|
||||
@@ -485,6 +484,6 @@ class L2VPNTerminationSerializer(NetBoxModelSerializer):
|
||||
|
||||
@swagger_serializer_method(serializer_or_field=serializers.DictField)
|
||||
def get_assigned_object(self, instance):
|
||||
serializer = get_serializer_for_model(instance.assigned_object, prefix='Nested')
|
||||
serializer = get_serializer_for_model(instance.assigned_object, prefix=NESTED_SERIALIZER_PREFIX)
|
||||
context = {'request': self.context['request']}
|
||||
return serializer(instance.assigned_object, context=context).data
|
||||
|
||||
@@ -980,21 +980,65 @@ class L2VPNTerminationFilterSet(NetBoxModelFilterSet):
|
||||
to_field_name='slug',
|
||||
label='L2VPN (slug)',
|
||||
)
|
||||
device = MultiValueCharFilter(
|
||||
method='filter_device',
|
||||
field_name='name',
|
||||
region = MultiValueCharFilter(
|
||||
method='filter_region',
|
||||
field_name='slug',
|
||||
label='Region (slug)',
|
||||
)
|
||||
region_id = MultiValueNumberFilter(
|
||||
method='filter_region',
|
||||
field_name='pk',
|
||||
label='Region (ID)',
|
||||
)
|
||||
site = MultiValueCharFilter(
|
||||
method='filter_site',
|
||||
field_name='slug',
|
||||
label='Site (slug)',
|
||||
)
|
||||
site_id = MultiValueNumberFilter(
|
||||
method='filter_site',
|
||||
field_name='pk',
|
||||
label='Site (ID)',
|
||||
)
|
||||
device = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='interface__device__name',
|
||||
queryset=Device.objects.all(),
|
||||
to_field_name='name',
|
||||
label='Device (name)',
|
||||
)
|
||||
device_id = MultiValueNumberFilter(
|
||||
method='filter_device',
|
||||
field_name='pk',
|
||||
device_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='interface__device',
|
||||
queryset=Device.objects.all(),
|
||||
label='Device (ID)',
|
||||
)
|
||||
virtual_machine = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='vminterface__virtual_machine__name',
|
||||
queryset=VirtualMachine.objects.all(),
|
||||
to_field_name='name',
|
||||
label='Virtual machine (name)',
|
||||
)
|
||||
virtual_machine_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='vminterface__virtual_machine',
|
||||
queryset=VirtualMachine.objects.all(),
|
||||
label='Virtual machine (ID)',
|
||||
)
|
||||
interface = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='interface__name',
|
||||
queryset=Interface.objects.all(),
|
||||
to_field_name='name',
|
||||
label='Interface (name)',
|
||||
)
|
||||
interface_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='interface',
|
||||
queryset=Interface.objects.all(),
|
||||
label='Interface (ID)',
|
||||
)
|
||||
vminterface = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='vminterface__name',
|
||||
queryset=VMInterface.objects.all(),
|
||||
to_field_name='name',
|
||||
label='VM interface (name)',
|
||||
)
|
||||
vminterface_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='vminterface',
|
||||
queryset=VMInterface.objects.all(),
|
||||
@@ -1027,13 +1071,22 @@ class L2VPNTerminationFilterSet(NetBoxModelFilterSet):
|
||||
qs_filter = Q(l2vpn__name__icontains=value)
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
def filter_device(self, queryset, name, value):
|
||||
devices = Device.objects.filter(**{'{}__in'.format(name): value})
|
||||
if not devices.exists():
|
||||
return queryset.none()
|
||||
interface_ids = []
|
||||
for device in devices:
|
||||
interface_ids.extend(device.vc_interfaces().values_list('id', flat=True))
|
||||
return queryset.filter(
|
||||
interface__in=interface_ids
|
||||
def filter_site(self, queryset, name, value):
|
||||
qs = queryset.filter(
|
||||
Q(
|
||||
Q(**{'vlan__site__{}__in'.format(name): value}) |
|
||||
Q(**{'interface__device__site__{}__in'.format(name): value}) |
|
||||
Q(**{'vminterface__virtual_machine__site__{}__in'.format(name): value})
|
||||
)
|
||||
)
|
||||
return qs
|
||||
|
||||
def filter_region(self, queryset, name, value):
|
||||
qs = queryset.filter(
|
||||
Q(
|
||||
Q(**{'vlan__site__region__{}__in'.format(name): value}) |
|
||||
Q(**{'interface__device__site__region__{}__in'.format(name): value}) |
|
||||
Q(**{'vminterface__virtual_machine__site__region__{}__in'.format(name): value})
|
||||
)
|
||||
)
|
||||
return qs
|
||||
|
||||
@@ -11,7 +11,7 @@ from netbox.forms import NetBoxModelFilterSetForm
|
||||
from tenancy.forms import TenancyFilterForm
|
||||
from utilities.forms import (
|
||||
add_blank_choice, ContentTypeMultipleChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField,
|
||||
MultipleChoiceField, StaticSelect, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES,
|
||||
MultipleChoiceField, StaticSelect, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES, APISelectMultiple,
|
||||
)
|
||||
from virtualization.models import VirtualMachine
|
||||
|
||||
@@ -508,7 +508,8 @@ class L2VPNFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
||||
class L2VPNTerminationFilterForm(NetBoxModelFilterSetForm):
|
||||
model = L2VPNTermination
|
||||
fieldsets = (
|
||||
(None, ('l2vpn_id', 'assigned_object_type_id')),
|
||||
(None, ('l2vpn_id', )),
|
||||
('Assigned Object', ('assigned_object_type_id', 'region_id', 'site_id', 'device_id', 'virtual_machine_id', 'vlan_id')),
|
||||
)
|
||||
l2vpn_id = DynamicModelChoiceField(
|
||||
queryset=L2VPN.objects.all(),
|
||||
@@ -516,7 +517,49 @@ class L2VPNTerminationFilterForm(NetBoxModelFilterSetForm):
|
||||
label='L2VPN'
|
||||
)
|
||||
assigned_object_type_id = ContentTypeMultipleChoiceField(
|
||||
queryset=ContentType.objects.all(),
|
||||
queryset=ContentType.objects.filter(L2VPN_ASSIGNMENT_MODELS),
|
||||
required=False,
|
||||
label='Object type'
|
||||
label=_('Assigned Object Type'),
|
||||
limit_choices_to=L2VPN_ASSIGNMENT_MODELS
|
||||
)
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
label=_('Region')
|
||||
)
|
||||
site_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
required=False,
|
||||
null_option='None',
|
||||
query_params={
|
||||
'region_id': '$region_id'
|
||||
},
|
||||
label=_('Site')
|
||||
)
|
||||
device_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
required=False,
|
||||
null_option='None',
|
||||
query_params={
|
||||
'site_id': '$site_id'
|
||||
},
|
||||
label=_('Device')
|
||||
)
|
||||
vlan_id = DynamicModelMultipleChoiceField(
|
||||
queryset=VLAN.objects.all(),
|
||||
required=False,
|
||||
null_option='None',
|
||||
query_params={
|
||||
'site_id': '$site_id'
|
||||
},
|
||||
label=_('VLAN')
|
||||
)
|
||||
virtual_machine_id = DynamicModelMultipleChoiceField(
|
||||
queryset=VirtualMachine.objects.all(),
|
||||
required=False,
|
||||
null_option='None',
|
||||
query_params={
|
||||
'site_id': '$site_id'
|
||||
},
|
||||
label=_('Virtual Machine')
|
||||
)
|
||||
|
||||
@@ -851,7 +851,7 @@ class ServiceCreateForm(ServiceForm):
|
||||
# Fields which may be populated from a ServiceTemplate are not required
|
||||
for field in ('name', 'protocol', 'ports'):
|
||||
self.fields[field].required = False
|
||||
del(self.fields[field].widget.attrs['required'])
|
||||
del self.fields[field].widget.attrs['required']
|
||||
|
||||
def clean(self):
|
||||
if self.cleaned_data['service_template']:
|
||||
@@ -906,8 +906,9 @@ class L2VPNTerminationForm(NetBoxModelForm):
|
||||
label='L2VPN',
|
||||
fetch_trigger='open'
|
||||
)
|
||||
device = DynamicModelChoiceField(
|
||||
device_vlan = DynamicModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
label="Available on Device",
|
||||
required=False,
|
||||
query_params={}
|
||||
)
|
||||
@@ -915,10 +916,15 @@ class L2VPNTerminationForm(NetBoxModelForm):
|
||||
queryset=VLAN.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'available_on_device': '$device'
|
||||
'available_on_device': '$device_vlan'
|
||||
},
|
||||
label='VLAN'
|
||||
)
|
||||
device = DynamicModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
required=False,
|
||||
query_params={}
|
||||
)
|
||||
interface = DynamicModelChoiceField(
|
||||
queryset=Interface.objects.all(),
|
||||
required=False,
|
||||
|
||||
@@ -373,7 +373,7 @@ class Prefix(GetAvailablePrefixesMixin, NetBoxModel):
|
||||
|
||||
# Cache the original prefix and VRF so we can check if they have changed on post_save
|
||||
self._prefix = self.prefix
|
||||
self._vrf = self.vrf
|
||||
self._vrf_id = self.vrf_id
|
||||
|
||||
def __str__(self):
|
||||
return str(self.prefix)
|
||||
|
||||
@@ -113,3 +113,18 @@ class L2VPNTermination(NetBoxModel):
|
||||
f'{l2vpn_type} L2VPNs cannot have more than two terminations; found {terminations_count} already '
|
||||
f'defined.'
|
||||
)
|
||||
|
||||
@property
|
||||
def assigned_object_parent(self):
|
||||
obj_type = ContentType.objects.get_for_model(self.assigned_object)
|
||||
if obj_type.model == 'vminterface':
|
||||
return self.assigned_object.virtual_machine
|
||||
elif obj_type.model == 'interface':
|
||||
return self.assigned_object.device
|
||||
elif obj_type.model == 'vminterface':
|
||||
return self.assigned_object.virtual_machine
|
||||
return None
|
||||
|
||||
@property
|
||||
def assigned_object_site(self):
|
||||
return self.assigned_object_parent.site
|
||||
|
||||
@@ -30,14 +30,14 @@ def update_children_depth(prefix):
|
||||
def handle_prefix_saved(instance, created, **kwargs):
|
||||
|
||||
# Prefix has changed (or new instance has been created)
|
||||
if created or instance.vrf != instance._vrf or instance.prefix != instance._prefix:
|
||||
if created or instance.vrf_id != instance._vrf_id or instance.prefix != instance._prefix:
|
||||
|
||||
update_parents_children(instance)
|
||||
update_children_depth(instance)
|
||||
|
||||
# If this is not a new prefix, clean up parent/children of previous prefix
|
||||
if not created:
|
||||
old_prefix = Prefix(vrf=instance._vrf, prefix=instance._prefix)
|
||||
old_prefix = Prefix(vrf_id=instance._vrf_id, prefix=instance._prefix)
|
||||
update_parents_children(old_prefix)
|
||||
update_children_depth(old_prefix)
|
||||
|
||||
|
||||
@@ -369,6 +369,11 @@ class IPAddressTable(TenancyColumnsMixin, NetBoxTable):
|
||||
orderable=False,
|
||||
verbose_name='NAT (Inside)'
|
||||
)
|
||||
nat_outside = tables.Column(
|
||||
linkify=True,
|
||||
orderable=False,
|
||||
verbose_name='NAT (Outside)'
|
||||
)
|
||||
assigned = columns.BooleanColumn(
|
||||
accessor='assigned_object_id',
|
||||
linkify=True,
|
||||
@@ -381,7 +386,7 @@ class IPAddressTable(TenancyColumnsMixin, NetBoxTable):
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = IPAddress
|
||||
fields = (
|
||||
'pk', 'id', 'address', 'vrf', 'status', 'role', 'tenant', 'tenant_group', 'nat_inside', 'assigned', 'dns_name', 'description',
|
||||
'pk', 'id', 'address', 'vrf', 'status', 'role', 'tenant', 'tenant_group', 'nat_inside', 'nat_outside', 'assigned', 'dns_name', 'description',
|
||||
'tags', 'created', 'last_updated',
|
||||
)
|
||||
default_columns = (
|
||||
|
||||
@@ -53,8 +53,17 @@ class L2VPNTerminationTable(NetBoxTable):
|
||||
linkify=True,
|
||||
orderable=False
|
||||
)
|
||||
assigned_object_parent = tables.Column(
|
||||
linkify=True,
|
||||
orderable=False
|
||||
)
|
||||
assigned_object_site = tables.Column(
|
||||
linkify=True,
|
||||
orderable=False
|
||||
)
|
||||
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = L2VPNTermination
|
||||
fields = ('pk', 'l2vpn', 'assigned_object_type', 'assigned_object', 'actions')
|
||||
fields = ('pk', 'l2vpn', 'assigned_object_type', 'assigned_object', 'assigned_object_parent',
|
||||
'assigned_object_site', 'actions')
|
||||
default_columns = ('pk', 'l2vpn', 'assigned_object_type', 'assigned_object', 'actions')
|
||||
|
||||
@@ -1600,3 +1600,24 @@ class L2VPNTerminationTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'vlan': ['VLAN 1', 'VLAN 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_site(self):
|
||||
site = Site.objects.all().first()
|
||||
params = {'site_id': [site.pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
params = {'site': ['site-1']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
|
||||
def test_device(self):
|
||||
device = Device.objects.all().first()
|
||||
params = {'device_id': [device.pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
params = {'device': ['Device 1']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
|
||||
def test_virtual_machine(self):
|
||||
virtual_machine = VirtualMachine.objects.all().first()
|
||||
params = {'virtual_machine_id': [virtual_machine.pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
params = {'virtual_machine': ['Virtual Machine 1']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
|
||||
@@ -40,11 +40,11 @@ class VRFView(generic.ObjectView):
|
||||
ipaddress_count = IPAddress.objects.restrict(request.user, 'view').filter(vrf=instance).count()
|
||||
|
||||
import_targets_table = tables.RouteTargetTable(
|
||||
instance.import_targets.prefetch_related('tenant'),
|
||||
instance.import_targets.all(),
|
||||
orderable=False
|
||||
)
|
||||
export_targets_table = tables.RouteTargetTable(
|
||||
instance.export_targets.prefetch_related('tenant'),
|
||||
instance.export_targets.all(),
|
||||
orderable=False
|
||||
)
|
||||
|
||||
@@ -72,14 +72,14 @@ class VRFBulkImportView(generic.BulkImportView):
|
||||
|
||||
|
||||
class VRFBulkEditView(generic.BulkEditView):
|
||||
queryset = VRF.objects.prefetch_related('tenant')
|
||||
queryset = VRF.objects.all()
|
||||
filterset = filtersets.VRFFilterSet
|
||||
table = tables.VRFTable
|
||||
form = forms.VRFBulkEditForm
|
||||
|
||||
|
||||
class VRFBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = VRF.objects.prefetch_related('tenant')
|
||||
queryset = VRF.objects.all()
|
||||
filterset = filtersets.VRFFilterSet
|
||||
table = tables.VRFTable
|
||||
|
||||
@@ -100,11 +100,11 @@ class RouteTargetView(generic.ObjectView):
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
importing_vrfs_table = tables.VRFTable(
|
||||
instance.importing_vrfs.prefetch_related('tenant'),
|
||||
instance.importing_vrfs.all(),
|
||||
orderable=False
|
||||
)
|
||||
exporting_vrfs_table = tables.VRFTable(
|
||||
instance.exporting_vrfs.prefetch_related('tenant'),
|
||||
instance.exporting_vrfs.all(),
|
||||
orderable=False
|
||||
)
|
||||
|
||||
@@ -130,14 +130,14 @@ class RouteTargetBulkImportView(generic.BulkImportView):
|
||||
|
||||
|
||||
class RouteTargetBulkEditView(generic.BulkEditView):
|
||||
queryset = RouteTarget.objects.prefetch_related('tenant')
|
||||
queryset = RouteTarget.objects.all()
|
||||
filterset = filtersets.RouteTargetFilterSet
|
||||
table = tables.RouteTargetTable
|
||||
form = forms.RouteTargetBulkEditForm
|
||||
|
||||
|
||||
class RouteTargetBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = RouteTarget.objects.prefetch_related('tenant')
|
||||
queryset = RouteTarget.objects.all()
|
||||
filterset = filtersets.RouteTargetFilterSet
|
||||
table = tables.RouteTargetTable
|
||||
|
||||
@@ -334,14 +334,18 @@ class AggregateBulkImportView(generic.BulkImportView):
|
||||
|
||||
|
||||
class AggregateBulkEditView(generic.BulkEditView):
|
||||
queryset = Aggregate.objects.prefetch_related('rir')
|
||||
queryset = Aggregate.objects.annotate(
|
||||
child_count=RawSQL('SELECT COUNT(*) FROM ipam_prefix WHERE ipam_prefix.prefix <<= ipam_aggregate.prefix', ())
|
||||
)
|
||||
filterset = filtersets.AggregateFilterSet
|
||||
table = tables.AggregateTable
|
||||
form = forms.AggregateBulkEditForm
|
||||
|
||||
|
||||
class AggregateBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = Aggregate.objects.prefetch_related('rir')
|
||||
queryset = Aggregate.objects.annotate(
|
||||
child_count=RawSQL('SELECT COUNT(*) FROM ipam_prefix WHERE ipam_prefix.prefix <<= ipam_aggregate.prefix', ())
|
||||
)
|
||||
filterset = filtersets.AggregateFilterSet
|
||||
table = tables.AggregateTable
|
||||
|
||||
@@ -417,7 +421,7 @@ class PrefixListView(generic.ObjectListView):
|
||||
|
||||
|
||||
class PrefixView(generic.ObjectView):
|
||||
queryset = Prefix.objects.prefetch_related('vrf', 'site__region', 'tenant__group', 'vlan__group', 'role')
|
||||
queryset = Prefix.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
try:
|
||||
@@ -433,7 +437,7 @@ class PrefixView(generic.ObjectView):
|
||||
).filter(
|
||||
prefix__net_contains=str(instance.prefix)
|
||||
).prefetch_related(
|
||||
'site', 'role', 'tenant'
|
||||
'site', 'role', 'tenant', 'vlan',
|
||||
)
|
||||
parent_prefix_table = tables.PrefixTable(
|
||||
list(parent_prefixes),
|
||||
@@ -447,7 +451,7 @@ class PrefixView(generic.ObjectView):
|
||||
).exclude(
|
||||
pk=instance.pk
|
||||
).prefetch_related(
|
||||
'site', 'role'
|
||||
'site', 'role', 'tenant', 'vlan',
|
||||
)
|
||||
duplicate_prefix_table = tables.PrefixTable(
|
||||
list(duplicate_prefixes),
|
||||
@@ -500,7 +504,7 @@ class PrefixIPRangesView(generic.ObjectChildrenView):
|
||||
|
||||
def get_children(self, request, parent):
|
||||
return parent.get_child_ranges().restrict(request.user, 'view').prefetch_related(
|
||||
'vrf', 'role', 'tenant', 'tenant__group',
|
||||
'tenant__group',
|
||||
)
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
@@ -519,7 +523,7 @@ class PrefixIPAddressesView(generic.ObjectChildrenView):
|
||||
template_name = 'ipam/prefix/ip_addresses.html'
|
||||
|
||||
def get_children(self, request, parent):
|
||||
return parent.get_child_ips().restrict(request.user, 'view').prefetch_related('vrf', 'tenant')
|
||||
return parent.get_child_ips().restrict(request.user, 'view').prefetch_related('vrf', 'tenant', 'tenant__group')
|
||||
|
||||
def prep_table_data(self, request, queryset, parent):
|
||||
show_available = bool(request.GET.get('show_available', 'true') == 'true')
|
||||
@@ -552,14 +556,14 @@ class PrefixBulkImportView(generic.BulkImportView):
|
||||
|
||||
|
||||
class PrefixBulkEditView(generic.BulkEditView):
|
||||
queryset = Prefix.objects.prefetch_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role')
|
||||
queryset = Prefix.objects.prefetch_related('vrf__tenant')
|
||||
filterset = filtersets.PrefixFilterSet
|
||||
table = tables.PrefixTable
|
||||
form = forms.PrefixBulkEditForm
|
||||
|
||||
|
||||
class PrefixBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = Prefix.objects.prefetch_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role')
|
||||
queryset = Prefix.objects.prefetch_related('vrf__tenant')
|
||||
filterset = filtersets.PrefixFilterSet
|
||||
table = tables.PrefixTable
|
||||
|
||||
@@ -611,14 +615,14 @@ class IPRangeBulkImportView(generic.BulkImportView):
|
||||
|
||||
|
||||
class IPRangeBulkEditView(generic.BulkEditView):
|
||||
queryset = IPRange.objects.prefetch_related('vrf', 'tenant')
|
||||
queryset = IPRange.objects.all()
|
||||
filterset = filtersets.IPRangeFilterSet
|
||||
table = tables.IPRangeTable
|
||||
form = forms.IPRangeBulkEditForm
|
||||
|
||||
|
||||
class IPRangeBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = IPRange.objects.prefetch_related('vrf', 'tenant')
|
||||
queryset = IPRange.objects.all()
|
||||
filterset = filtersets.IPRangeFilterSet
|
||||
table = tables.IPRangeTable
|
||||
|
||||
@@ -789,14 +793,14 @@ class IPAddressBulkImportView(generic.BulkImportView):
|
||||
|
||||
|
||||
class IPAddressBulkEditView(generic.BulkEditView):
|
||||
queryset = IPAddress.objects.prefetch_related('vrf__tenant', 'tenant')
|
||||
queryset = IPAddress.objects.prefetch_related('vrf__tenant')
|
||||
filterset = filtersets.IPAddressFilterSet
|
||||
table = tables.IPAddressTable
|
||||
form = forms.IPAddressBulkEditForm
|
||||
|
||||
|
||||
class IPAddressBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = IPAddress.objects.prefetch_related('vrf__tenant', 'tenant')
|
||||
queryset = IPAddress.objects.prefetch_related('vrf__tenant')
|
||||
filterset = filtersets.IPAddressFilterSet
|
||||
table = tables.IPAddressTable
|
||||
|
||||
@@ -819,7 +823,8 @@ class VLANGroupView(generic.ObjectView):
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
vlans = VLAN.objects.restrict(request.user, 'view').filter(group=instance).prefetch_related(
|
||||
Prefetch('prefixes', queryset=Prefix.objects.restrict(request.user))
|
||||
Prefetch('prefixes', queryset=Prefix.objects.restrict(request.user)),
|
||||
'tenant', 'site', 'role',
|
||||
).order_by('vid')
|
||||
vlans_count = vlans.count()
|
||||
vlans = add_available_vlans(vlans, vlan_group=instance)
|
||||
@@ -894,7 +899,7 @@ class FHRPGroupView(generic.ObjectView):
|
||||
def get_extra_context(self, request, instance):
|
||||
# Get assigned IP addresses
|
||||
ipaddress_table = tables.AssignedIPAddressesTable(
|
||||
data=instance.ip_addresses.restrict(request.user, 'view').prefetch_related('vrf', 'tenant'),
|
||||
data=instance.ip_addresses.restrict(request.user, 'view'),
|
||||
orderable=False
|
||||
)
|
||||
|
||||
@@ -984,11 +989,11 @@ class VLANListView(generic.ObjectListView):
|
||||
|
||||
|
||||
class VLANView(generic.ObjectView):
|
||||
queryset = VLAN.objects.prefetch_related('site__region', 'tenant__group', 'role')
|
||||
queryset = VLAN.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
prefixes = Prefix.objects.restrict(request.user, 'view').filter(vlan=instance).prefetch_related(
|
||||
'vrf', 'site', 'role'
|
||||
'vrf', 'site', 'role', 'tenant'
|
||||
)
|
||||
prefix_table = tables.PrefixTable(list(prefixes), exclude=('vlan', 'utilization'), orderable=False)
|
||||
|
||||
@@ -1046,14 +1051,14 @@ class VLANBulkImportView(generic.BulkImportView):
|
||||
|
||||
|
||||
class VLANBulkEditView(generic.BulkEditView):
|
||||
queryset = VLAN.objects.prefetch_related('site', 'group', 'tenant', 'role')
|
||||
queryset = VLAN.objects.all()
|
||||
filterset = filtersets.VLANFilterSet
|
||||
table = tables.VLANTable
|
||||
form = forms.VLANBulkEditForm
|
||||
|
||||
|
||||
class VLANBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = VLAN.objects.prefetch_related('site', 'group', 'tenant', 'role')
|
||||
queryset = VLAN.objects.all()
|
||||
filterset = filtersets.VLANFilterSet
|
||||
table = tables.VLANTable
|
||||
|
||||
@@ -1106,14 +1111,14 @@ class ServiceTemplateBulkDeleteView(generic.BulkDeleteView):
|
||||
#
|
||||
|
||||
class ServiceListView(generic.ObjectListView):
|
||||
queryset = Service.objects.all()
|
||||
queryset = Service.objects.prefetch_related('device', 'virtual_machine')
|
||||
filterset = filtersets.ServiceFilterSet
|
||||
filterset_form = forms.ServiceFilterForm
|
||||
table = tables.ServiceTable
|
||||
|
||||
|
||||
class ServiceView(generic.ObjectView):
|
||||
queryset = Service.objects.prefetch_related('ipaddresses')
|
||||
queryset = Service.objects.all()
|
||||
|
||||
|
||||
class ServiceCreateView(generic.ObjectEditView):
|
||||
@@ -1123,7 +1128,7 @@ class ServiceCreateView(generic.ObjectEditView):
|
||||
|
||||
|
||||
class ServiceEditView(generic.ObjectEditView):
|
||||
queryset = Service.objects.prefetch_related('ipaddresses')
|
||||
queryset = Service.objects.all()
|
||||
form = forms.ServiceForm
|
||||
template_name = 'ipam/service_edit.html'
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from collections import OrderedDict
|
||||
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from netaddr import IPNetwork
|
||||
from rest_framework import serializers
|
||||
@@ -48,10 +46,10 @@ class ChoiceField(serializers.Field):
|
||||
def to_representation(self, obj):
|
||||
if obj == '':
|
||||
return None
|
||||
return OrderedDict([
|
||||
('value', obj),
|
||||
('label', self._choices[obj])
|
||||
])
|
||||
return {
|
||||
'value': obj,
|
||||
'label': self._choices[obj],
|
||||
}
|
||||
|
||||
def to_internal_value(self, data):
|
||||
if data == '':
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from drf_yasg.utils import swagger_serializer_method
|
||||
from rest_framework import serializers
|
||||
|
||||
from netbox.api.fields import ContentTypeField
|
||||
from netbox.constants import NESTED_SERIALIZER_PREFIX
|
||||
from utilities.api import get_serializer_for_model
|
||||
from utilities.utils import content_type_identifier
|
||||
|
||||
__all__ = (
|
||||
@@ -17,6 +20,7 @@ class GenericObjectSerializer(serializers.Serializer):
|
||||
queryset=ContentType.objects.all()
|
||||
)
|
||||
object_id = serializers.IntegerField()
|
||||
object = serializers.SerializerMethodField(read_only=True)
|
||||
|
||||
def to_internal_value(self, data):
|
||||
data = super().to_internal_value(data)
|
||||
@@ -25,7 +29,17 @@ class GenericObjectSerializer(serializers.Serializer):
|
||||
|
||||
def to_representation(self, instance):
|
||||
ct = ContentType.objects.get_for_model(instance)
|
||||
return {
|
||||
data = {
|
||||
'object_type': content_type_identifier(ct),
|
||||
'object_id': instance.pk,
|
||||
}
|
||||
if 'request' in self.context:
|
||||
data['object'] = self.get_object(instance)
|
||||
|
||||
return data
|
||||
|
||||
@swagger_serializer_method(serializer_or_field=serializers.DictField)
|
||||
def get_object(self, obj):
|
||||
serializer = get_serializer_for_model(obj, prefix=NESTED_SERIALIZER_PREFIX)
|
||||
# context = {'request': self.context['request']}
|
||||
return serializer(obj, context=self.context).data
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import platform
|
||||
from collections import OrderedDict
|
||||
|
||||
from django import __version__ as DJANGO_VERSION
|
||||
from django.apps import apps
|
||||
@@ -26,18 +25,18 @@ class APIRootView(APIView):
|
||||
|
||||
def get(self, request, format=None):
|
||||
|
||||
return Response(OrderedDict((
|
||||
('circuits', reverse('circuits-api:api-root', request=request, format=format)),
|
||||
('dcim', reverse('dcim-api:api-root', request=request, format=format)),
|
||||
('extras', reverse('extras-api:api-root', request=request, format=format)),
|
||||
('ipam', reverse('ipam-api:api-root', request=request, format=format)),
|
||||
('plugins', reverse('plugins-api:api-root', request=request, format=format)),
|
||||
('status', reverse('api-status', request=request, format=format)),
|
||||
('tenancy', reverse('tenancy-api:api-root', request=request, format=format)),
|
||||
('users', reverse('users-api:api-root', request=request, format=format)),
|
||||
('virtualization', reverse('virtualization-api:api-root', request=request, format=format)),
|
||||
('wireless', reverse('wireless-api:api-root', request=request, format=format)),
|
||||
)))
|
||||
return Response({
|
||||
'circuits': reverse('circuits-api:api-root', request=request, format=format),
|
||||
'dcim': reverse('dcim-api:api-root', request=request, format=format),
|
||||
'extras': reverse('extras-api:api-root', request=request, format=format),
|
||||
'ipam': reverse('ipam-api:api-root', request=request, format=format),
|
||||
'plugins': reverse('plugins-api:api-root', request=request, format=format),
|
||||
'status': reverse('api-status', request=request, format=format),
|
||||
'tenancy': reverse('tenancy-api:api-root', request=request, format=format),
|
||||
'users': reverse('users-api:api-root', request=request, format=format),
|
||||
'virtualization': reverse('virtualization-api:api-root', request=request, format=format),
|
||||
'wireless': reverse('wireless-api:api-root', request=request, format=format),
|
||||
})
|
||||
|
||||
|
||||
class StatusView(APIView):
|
||||
|
||||
@@ -10,6 +10,7 @@ from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from extras.models import ExportTemplate
|
||||
from netbox.api.exceptions import SerializerNotFound
|
||||
from netbox.constants import NESTED_SERIALIZER_PREFIX
|
||||
from utilities.api import get_serializer_for_model
|
||||
from utilities.exceptions import AbortRequest
|
||||
from .mixins import *
|
||||
@@ -61,7 +62,7 @@ class NetBoxModelViewSet(BulkUpdateModelMixin, BulkDestroyModelMixin, ObjectVali
|
||||
if self.brief:
|
||||
logger.debug("Request is for 'brief' format; initializing nested serializer")
|
||||
try:
|
||||
serializer = get_serializer_for_model(self.queryset.model, prefix='Nested')
|
||||
serializer = get_serializer_for_model(self.queryset.model, prefix=NESTED_SERIALIZER_PREFIX)
|
||||
logger.debug(f"Using serializer {serializer}")
|
||||
return serializer
|
||||
except SerializerNotFound:
|
||||
|
||||
@@ -1,256 +1,5 @@
|
||||
from collections import OrderedDict
|
||||
from typing import Dict
|
||||
|
||||
import circuits.filtersets
|
||||
import circuits.tables
|
||||
import dcim.filtersets
|
||||
import dcim.tables
|
||||
import ipam.filtersets
|
||||
import ipam.tables
|
||||
import tenancy.filtersets
|
||||
import tenancy.tables
|
||||
import virtualization.filtersets
|
||||
import virtualization.tables
|
||||
from circuits.models import Circuit, ProviderNetwork, Provider
|
||||
from dcim.models import (
|
||||
Cable, Device, DeviceType, Location, Module, ModuleType, PowerFeed, Rack, RackReservation, Site, VirtualChassis,
|
||||
)
|
||||
from ipam.models import Aggregate, ASN, IPAddress, Prefix, Service, VLAN, VRF
|
||||
from tenancy.models import Contact, Tenant, ContactAssignment
|
||||
from utilities.utils import count_related
|
||||
from virtualization.models import Cluster, VirtualMachine
|
||||
# Prefix for nested serializers
|
||||
NESTED_SERIALIZER_PREFIX = 'Nested'
|
||||
|
||||
# Max results per object type
|
||||
SEARCH_MAX_RESULTS = 15
|
||||
|
||||
CIRCUIT_TYPES = OrderedDict(
|
||||
(
|
||||
('provider', {
|
||||
'queryset': Provider.objects.annotate(
|
||||
count_circuits=count_related(Circuit, 'provider')
|
||||
),
|
||||
'filterset': circuits.filtersets.ProviderFilterSet,
|
||||
'table': circuits.tables.ProviderTable,
|
||||
'url': 'circuits:provider_list',
|
||||
}),
|
||||
('circuit', {
|
||||
'queryset': Circuit.objects.prefetch_related(
|
||||
'type', 'provider', 'tenant', 'tenant__group', 'terminations__site'
|
||||
),
|
||||
'filterset': circuits.filtersets.CircuitFilterSet,
|
||||
'table': circuits.tables.CircuitTable,
|
||||
'url': 'circuits:circuit_list',
|
||||
}),
|
||||
('providernetwork', {
|
||||
'queryset': ProviderNetwork.objects.prefetch_related('provider'),
|
||||
'filterset': circuits.filtersets.ProviderNetworkFilterSet,
|
||||
'table': circuits.tables.ProviderNetworkTable,
|
||||
'url': 'circuits:providernetwork_list',
|
||||
}),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
DCIM_TYPES = OrderedDict(
|
||||
(
|
||||
('site', {
|
||||
'queryset': Site.objects.prefetch_related('region', 'tenant', 'tenant__group'),
|
||||
'filterset': dcim.filtersets.SiteFilterSet,
|
||||
'table': dcim.tables.SiteTable,
|
||||
'url': 'dcim:site_list',
|
||||
}),
|
||||
('rack', {
|
||||
'queryset': Rack.objects.prefetch_related('site', 'location', 'tenant', 'tenant__group', 'role').annotate(
|
||||
device_count=count_related(Device, 'rack')
|
||||
),
|
||||
'filterset': dcim.filtersets.RackFilterSet,
|
||||
'table': dcim.tables.RackTable,
|
||||
'url': 'dcim:rack_list',
|
||||
}),
|
||||
('rackreservation', {
|
||||
'queryset': RackReservation.objects.prefetch_related('site', 'rack', 'user'),
|
||||
'filterset': dcim.filtersets.RackReservationFilterSet,
|
||||
'table': dcim.tables.RackReservationTable,
|
||||
'url': 'dcim:rackreservation_list',
|
||||
}),
|
||||
('location', {
|
||||
'queryset': Location.objects.add_related_count(
|
||||
Location.objects.add_related_count(
|
||||
Location.objects.all(),
|
||||
Device,
|
||||
'location',
|
||||
'device_count',
|
||||
cumulative=True
|
||||
),
|
||||
Rack,
|
||||
'location',
|
||||
'rack_count',
|
||||
cumulative=True
|
||||
).prefetch_related('site'),
|
||||
'filterset': dcim.filtersets.LocationFilterSet,
|
||||
'table': dcim.tables.LocationTable,
|
||||
'url': 'dcim:location_list',
|
||||
}),
|
||||
('devicetype', {
|
||||
'queryset': DeviceType.objects.prefetch_related('manufacturer').annotate(
|
||||
instance_count=count_related(Device, 'device_type')
|
||||
),
|
||||
'filterset': dcim.filtersets.DeviceTypeFilterSet,
|
||||
'table': dcim.tables.DeviceTypeTable,
|
||||
'url': 'dcim:devicetype_list',
|
||||
}),
|
||||
('device', {
|
||||
'queryset': Device.objects.prefetch_related(
|
||||
'device_type__manufacturer', 'device_role', 'tenant', 'tenant__group', 'site', 'rack', 'primary_ip4', 'primary_ip6',
|
||||
),
|
||||
'filterset': dcim.filtersets.DeviceFilterSet,
|
||||
'table': dcim.tables.DeviceTable,
|
||||
'url': 'dcim:device_list',
|
||||
}),
|
||||
('moduletype', {
|
||||
'queryset': ModuleType.objects.prefetch_related('manufacturer').annotate(
|
||||
instance_count=count_related(Module, 'module_type')
|
||||
),
|
||||
'filterset': dcim.filtersets.ModuleTypeFilterSet,
|
||||
'table': dcim.tables.ModuleTypeTable,
|
||||
'url': 'dcim:moduletype_list',
|
||||
}),
|
||||
('module', {
|
||||
'queryset': Module.objects.prefetch_related(
|
||||
'module_type__manufacturer', 'device', 'module_bay',
|
||||
),
|
||||
'filterset': dcim.filtersets.ModuleFilterSet,
|
||||
'table': dcim.tables.ModuleTable,
|
||||
'url': 'dcim:module_list',
|
||||
}),
|
||||
('virtualchassis', {
|
||||
'queryset': VirtualChassis.objects.prefetch_related('master').annotate(
|
||||
member_count=count_related(Device, 'virtual_chassis')
|
||||
),
|
||||
'filterset': dcim.filtersets.VirtualChassisFilterSet,
|
||||
'table': dcim.tables.VirtualChassisTable,
|
||||
'url': 'dcim:virtualchassis_list',
|
||||
}),
|
||||
('cable', {
|
||||
'queryset': Cable.objects.all(),
|
||||
'filterset': dcim.filtersets.CableFilterSet,
|
||||
'table': dcim.tables.CableTable,
|
||||
'url': 'dcim:cable_list',
|
||||
}),
|
||||
('powerfeed', {
|
||||
'queryset': PowerFeed.objects.all(),
|
||||
'filterset': dcim.filtersets.PowerFeedFilterSet,
|
||||
'table': dcim.tables.PowerFeedTable,
|
||||
'url': 'dcim:powerfeed_list',
|
||||
}),
|
||||
)
|
||||
)
|
||||
|
||||
IPAM_TYPES = OrderedDict(
|
||||
(
|
||||
('vrf', {
|
||||
'queryset': VRF.objects.prefetch_related('tenant', 'tenant__group'),
|
||||
'filterset': ipam.filtersets.VRFFilterSet,
|
||||
'table': ipam.tables.VRFTable,
|
||||
'url': 'ipam:vrf_list',
|
||||
}),
|
||||
('aggregate', {
|
||||
'queryset': Aggregate.objects.prefetch_related('rir'),
|
||||
'filterset': ipam.filtersets.AggregateFilterSet,
|
||||
'table': ipam.tables.AggregateTable,
|
||||
'url': 'ipam:aggregate_list',
|
||||
}),
|
||||
('prefix', {
|
||||
'queryset': Prefix.objects.prefetch_related('site', 'vrf__tenant', 'tenant', 'tenant__group', 'vlan', 'role'),
|
||||
'filterset': ipam.filtersets.PrefixFilterSet,
|
||||
'table': ipam.tables.PrefixTable,
|
||||
'url': 'ipam:prefix_list',
|
||||
}),
|
||||
('ipaddress', {
|
||||
'queryset': IPAddress.objects.prefetch_related('vrf__tenant', 'tenant', 'tenant__group'),
|
||||
'filterset': ipam.filtersets.IPAddressFilterSet,
|
||||
'table': ipam.tables.IPAddressTable,
|
||||
'url': 'ipam:ipaddress_list',
|
||||
}),
|
||||
('vlan', {
|
||||
'queryset': VLAN.objects.prefetch_related('site', 'group', 'tenant', 'tenant__group', 'role'),
|
||||
'filterset': ipam.filtersets.VLANFilterSet,
|
||||
'table': ipam.tables.VLANTable,
|
||||
'url': 'ipam:vlan_list',
|
||||
}),
|
||||
('asn', {
|
||||
'queryset': ASN.objects.prefetch_related('rir', 'tenant', 'tenant__group'),
|
||||
'filterset': ipam.filtersets.ASNFilterSet,
|
||||
'table': ipam.tables.ASNTable,
|
||||
'url': 'ipam:asn_list',
|
||||
}),
|
||||
('service', {
|
||||
'queryset': Service.objects.prefetch_related('device', 'virtual_machine'),
|
||||
'filterset': ipam.filtersets.ServiceFilterSet,
|
||||
'table': ipam.tables.ServiceTable,
|
||||
'url': 'ipam:service_list',
|
||||
}),
|
||||
)
|
||||
)
|
||||
|
||||
TENANCY_TYPES = OrderedDict(
|
||||
(
|
||||
('tenant', {
|
||||
'queryset': Tenant.objects.prefetch_related('group'),
|
||||
'filterset': tenancy.filtersets.TenantFilterSet,
|
||||
'table': tenancy.tables.TenantTable,
|
||||
'url': 'tenancy:tenant_list',
|
||||
}),
|
||||
('contact', {
|
||||
'queryset': Contact.objects.prefetch_related('group', 'assignments').annotate(
|
||||
assignment_count=count_related(ContactAssignment, 'contact')),
|
||||
'filterset': tenancy.filtersets.ContactFilterSet,
|
||||
'table': tenancy.tables.ContactTable,
|
||||
'url': 'tenancy:contact_list',
|
||||
}),
|
||||
)
|
||||
)
|
||||
|
||||
VIRTUALIZATION_TYPES = OrderedDict(
|
||||
(
|
||||
('cluster', {
|
||||
'queryset': Cluster.objects.prefetch_related('type', 'group').annotate(
|
||||
device_count=count_related(Device, 'cluster'),
|
||||
vm_count=count_related(VirtualMachine, 'cluster')
|
||||
),
|
||||
'filterset': virtualization.filtersets.ClusterFilterSet,
|
||||
'table': virtualization.tables.ClusterTable,
|
||||
'url': 'virtualization:cluster_list',
|
||||
}),
|
||||
('virtualmachine', {
|
||||
'queryset': VirtualMachine.objects.prefetch_related(
|
||||
'cluster', 'tenant', 'tenant__group', 'platform', 'primary_ip4', 'primary_ip6',
|
||||
),
|
||||
'filterset': virtualization.filtersets.VirtualMachineFilterSet,
|
||||
'table': virtualization.tables.VirtualMachineTable,
|
||||
'url': 'virtualization:virtualmachine_list',
|
||||
}),
|
||||
)
|
||||
)
|
||||
|
||||
SEARCH_TYPE_HIERARCHY = OrderedDict(
|
||||
(
|
||||
("Circuits", CIRCUIT_TYPES),
|
||||
("DCIM", DCIM_TYPES),
|
||||
("IPAM", IPAM_TYPES),
|
||||
("Tenancy", TENANCY_TYPES),
|
||||
("Virtualization", VIRTUALIZATION_TYPES),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def build_search_types() -> Dict[str, Dict]:
|
||||
result = dict()
|
||||
|
||||
for app_types in SEARCH_TYPE_HIERARCHY.values():
|
||||
for name, items in app_types.items():
|
||||
result[name] = items
|
||||
|
||||
return result
|
||||
|
||||
|
||||
SEARCH_TYPES = build_search_types()
|
||||
|
||||
54
netbox/netbox/denormalized.py
Normal file
54
netbox/netbox/denormalized.py
Normal file
@@ -0,0 +1,54 @@
|
||||
import logging
|
||||
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
|
||||
from extras.registry import registry
|
||||
|
||||
|
||||
logger = logging.getLogger('netbox.denormalized')
|
||||
|
||||
|
||||
def register(model, field_name, mappings):
|
||||
"""
|
||||
Register a denormalized model field to ensure that it is kept up-to-date with the related object.
|
||||
|
||||
Args:
|
||||
model: The class being updated
|
||||
field_name: The name of the field related to the triggering instance
|
||||
mappings: Dictionary mapping of local to remote fields
|
||||
"""
|
||||
logger.debug(f'Registering denormalized field {model}.{field_name}')
|
||||
|
||||
field = model._meta.get_field(field_name)
|
||||
rel_model = field.related_model
|
||||
|
||||
registry['denormalized_fields'][rel_model].append(
|
||||
(model, field_name, mappings)
|
||||
)
|
||||
|
||||
|
||||
@receiver(post_save)
|
||||
def update_denormalized_fields(sender, instance, created, raw, **kwargs):
|
||||
"""
|
||||
Check if the sender has denormalized fields registered, and update them as necessary.
|
||||
"""
|
||||
# Skip for new objects or those being populated from raw data
|
||||
if created or raw:
|
||||
return
|
||||
|
||||
# Look up any denormalized fields referencing this model from the application registry
|
||||
for model, field_name, mappings in registry['denormalized_fields'].get(sender, []):
|
||||
logger.debug(f'Updating denormalized values for {model}.{field_name}')
|
||||
filter_params = {
|
||||
field_name: instance.pk,
|
||||
}
|
||||
update_params = {
|
||||
# Map the denormalized field names to the instance's values
|
||||
denorm: getattr(instance, origin) for denorm, origin in mappings.items()
|
||||
}
|
||||
|
||||
# TODO: Improve efficiency here by placing conditions on the query?
|
||||
# Update all the denormalized fields with the triggering object's new values
|
||||
count = model.objects.filter(**filter_params).update(**update_params)
|
||||
logger.debug(f'Updated {count} rows')
|
||||
@@ -125,7 +125,7 @@ class BaseFilterSet(django_filters.FilterSet):
|
||||
return {}
|
||||
|
||||
# Skip nonstandard lookup expressions
|
||||
if existing_filter.method is not None or existing_filter.lookup_expr not in ['exact', 'in']:
|
||||
if existing_filter.method is not None or existing_filter.lookup_expr not in ['exact', 'iexact', 'in']:
|
||||
return {}
|
||||
|
||||
# Choose the lookup expression map based on the filter type
|
||||
@@ -197,24 +197,11 @@ class BaseFilterSet(django_filters.FilterSet):
|
||||
|
||||
|
||||
class ChangeLoggedModelFilterSet(BaseFilterSet):
|
||||
created = django_filters.DateTimeFilter()
|
||||
created__gte = django_filters.DateTimeFilter(
|
||||
field_name='created',
|
||||
lookup_expr='gte'
|
||||
)
|
||||
created__lte = django_filters.DateTimeFilter(
|
||||
field_name='created',
|
||||
lookup_expr='lte'
|
||||
)
|
||||
last_updated = django_filters.DateTimeFilter()
|
||||
last_updated__gte = django_filters.DateTimeFilter(
|
||||
field_name='last_updated',
|
||||
lookup_expr='gte'
|
||||
)
|
||||
last_updated__lte = django_filters.DateTimeFilter(
|
||||
field_name='last_updated',
|
||||
lookup_expr='lte'
|
||||
)
|
||||
"""
|
||||
Base FilterSet for ChangeLoggedModel classes.
|
||||
"""
|
||||
created = filters.MultiValueDateTimeFilter()
|
||||
last_updated = filters.MultiValueDateTimeFilter()
|
||||
|
||||
|
||||
class NetBoxModelFilterSet(ChangeLoggedModelFilterSet):
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from django import forms
|
||||
|
||||
from netbox.constants import SEARCH_TYPE_HIERARCHY
|
||||
from netbox.search import SEARCH_TYPE_HIERARCHY
|
||||
from utilities.forms import BootstrapMixin
|
||||
from .base import *
|
||||
|
||||
|
||||
@@ -94,30 +94,19 @@ class NetBoxModelBulkEditForm(BootstrapMixin, CustomFieldsMixin, forms.Form):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.fields['pk'].queryset = self.model.objects.all()
|
||||
|
||||
self._extend_nullable_fields()
|
||||
|
||||
def _get_form_field(self, customfield):
|
||||
return customfield.to_form_field(set_initial=False, enforce_required=False)
|
||||
|
||||
def _append_customfield_fields(self):
|
||||
"""
|
||||
Append form fields for all CustomFields assigned to this object type.
|
||||
"""
|
||||
nullable_custom_fields = []
|
||||
for customfield in self._get_custom_fields(self._get_content_type()):
|
||||
field_name = f'cf_{customfield.name}'
|
||||
self.fields[field_name] = self._get_form_field(customfield)
|
||||
|
||||
# Record non-required custom fields as nullable
|
||||
if not customfield.required:
|
||||
nullable_custom_fields.append(field_name)
|
||||
|
||||
# Annotate the field in the list of CustomField form fields
|
||||
self.custom_fields[field_name] = customfield
|
||||
|
||||
# Annotate nullable custom fields (if any) on the form instance
|
||||
if nullable_custom_fields:
|
||||
self.nullable_fields = (*self.nullable_fields, *nullable_custom_fields)
|
||||
def _extend_nullable_fields(self):
|
||||
nullable_custom_fields = [
|
||||
name for name, customfield in self.custom_fields.items() if not customfield.required
|
||||
]
|
||||
self.nullable_fields = (*self.nullable_fields, *nullable_custom_fields)
|
||||
|
||||
|
||||
class NetBoxModelFilterSetForm(BootstrapMixin, CustomFieldsMixin, forms.Form):
|
||||
|
||||
261
netbox/netbox/search.py
Normal file
261
netbox/netbox/search.py
Normal file
@@ -0,0 +1,261 @@
|
||||
import circuits.filtersets
|
||||
import circuits.tables
|
||||
import dcim.filtersets
|
||||
import dcim.tables
|
||||
import ipam.filtersets
|
||||
import ipam.tables
|
||||
import tenancy.filtersets
|
||||
import tenancy.tables
|
||||
import virtualization.filtersets
|
||||
import wireless.tables
|
||||
import wireless.filtersets
|
||||
import virtualization.tables
|
||||
from circuits.models import Circuit, ProviderNetwork, Provider
|
||||
from dcim.models import (
|
||||
Cable, Device, DeviceType, Interface, Location, Module, ModuleType, PowerFeed, Rack, RackReservation, Site,
|
||||
VirtualChassis,
|
||||
)
|
||||
from ipam.models import Aggregate, ASN, IPAddress, Prefix, Service, VLAN, VRF
|
||||
from tenancy.models import Contact, Tenant, ContactAssignment
|
||||
from utilities.utils import count_related
|
||||
from wireless.models import WirelessLAN, WirelessLink
|
||||
from virtualization.models import Cluster, VirtualMachine
|
||||
|
||||
CIRCUIT_TYPES = {
|
||||
'provider': {
|
||||
'queryset': Provider.objects.annotate(
|
||||
count_circuits=count_related(Circuit, 'provider')
|
||||
),
|
||||
'filterset': circuits.filtersets.ProviderFilterSet,
|
||||
'table': circuits.tables.ProviderTable,
|
||||
'url': 'circuits:provider_list',
|
||||
},
|
||||
'circuit': {
|
||||
'queryset': Circuit.objects.prefetch_related(
|
||||
'type', 'provider', 'tenant', 'tenant__group', 'terminations__site'
|
||||
),
|
||||
'filterset': circuits.filtersets.CircuitFilterSet,
|
||||
'table': circuits.tables.CircuitTable,
|
||||
'url': 'circuits:circuit_list',
|
||||
},
|
||||
'providernetwork': {
|
||||
'queryset': ProviderNetwork.objects.prefetch_related('provider'),
|
||||
'filterset': circuits.filtersets.ProviderNetworkFilterSet,
|
||||
'table': circuits.tables.ProviderNetworkTable,
|
||||
'url': 'circuits:providernetwork_list',
|
||||
},
|
||||
}
|
||||
|
||||
DCIM_TYPES = {
|
||||
'site': {
|
||||
'queryset': Site.objects.prefetch_related('region', 'tenant', 'tenant__group'),
|
||||
'filterset': dcim.filtersets.SiteFilterSet,
|
||||
'table': dcim.tables.SiteTable,
|
||||
'url': 'dcim:site_list',
|
||||
},
|
||||
'rack': {
|
||||
'queryset': Rack.objects.prefetch_related('site', 'location', 'tenant', 'tenant__group', 'role').annotate(
|
||||
device_count=count_related(Device, 'rack')
|
||||
),
|
||||
'filterset': dcim.filtersets.RackFilterSet,
|
||||
'table': dcim.tables.RackTable,
|
||||
'url': 'dcim:rack_list',
|
||||
},
|
||||
'rackreservation': {
|
||||
'queryset': RackReservation.objects.prefetch_related('site', 'rack', 'user'),
|
||||
'filterset': dcim.filtersets.RackReservationFilterSet,
|
||||
'table': dcim.tables.RackReservationTable,
|
||||
'url': 'dcim:rackreservation_list',
|
||||
},
|
||||
'location': {
|
||||
'queryset': Location.objects.add_related_count(
|
||||
Location.objects.add_related_count(
|
||||
Location.objects.all(),
|
||||
Device,
|
||||
'location',
|
||||
'device_count',
|
||||
cumulative=True
|
||||
),
|
||||
Rack,
|
||||
'location',
|
||||
'rack_count',
|
||||
cumulative=True
|
||||
).prefetch_related('site'),
|
||||
'filterset': dcim.filtersets.LocationFilterSet,
|
||||
'table': dcim.tables.LocationTable,
|
||||
'url': 'dcim:location_list',
|
||||
},
|
||||
'devicetype': {
|
||||
'queryset': DeviceType.objects.prefetch_related('manufacturer').annotate(
|
||||
instance_count=count_related(Device, 'device_type')
|
||||
),
|
||||
'filterset': dcim.filtersets.DeviceTypeFilterSet,
|
||||
'table': dcim.tables.DeviceTypeTable,
|
||||
'url': 'dcim:devicetype_list',
|
||||
},
|
||||
'device': {
|
||||
'queryset': Device.objects.prefetch_related(
|
||||
'device_type__manufacturer', 'device_role', 'tenant', 'tenant__group', 'site', 'rack', 'primary_ip4',
|
||||
'primary_ip6',
|
||||
),
|
||||
'filterset': dcim.filtersets.DeviceFilterSet,
|
||||
'table': dcim.tables.DeviceTable,
|
||||
'url': 'dcim:device_list',
|
||||
},
|
||||
'moduletype': {
|
||||
'queryset': ModuleType.objects.prefetch_related('manufacturer').annotate(
|
||||
instance_count=count_related(Module, 'module_type')
|
||||
),
|
||||
'filterset': dcim.filtersets.ModuleTypeFilterSet,
|
||||
'table': dcim.tables.ModuleTypeTable,
|
||||
'url': 'dcim:moduletype_list',
|
||||
},
|
||||
'module': {
|
||||
'queryset': Module.objects.prefetch_related(
|
||||
'module_type__manufacturer', 'device', 'module_bay',
|
||||
),
|
||||
'filterset': dcim.filtersets.ModuleFilterSet,
|
||||
'table': dcim.tables.ModuleTable,
|
||||
'url': 'dcim:module_list',
|
||||
},
|
||||
'virtualchassis': {
|
||||
'queryset': VirtualChassis.objects.prefetch_related('master').annotate(
|
||||
member_count=count_related(Device, 'virtual_chassis')
|
||||
),
|
||||
'filterset': dcim.filtersets.VirtualChassisFilterSet,
|
||||
'table': dcim.tables.VirtualChassisTable,
|
||||
'url': 'dcim:virtualchassis_list',
|
||||
},
|
||||
'cable': {
|
||||
'queryset': Cable.objects.all(),
|
||||
'filterset': dcim.filtersets.CableFilterSet,
|
||||
'table': dcim.tables.CableTable,
|
||||
'url': 'dcim:cable_list',
|
||||
},
|
||||
'powerfeed': {
|
||||
'queryset': PowerFeed.objects.all(),
|
||||
'filterset': dcim.filtersets.PowerFeedFilterSet,
|
||||
'table': dcim.tables.PowerFeedTable,
|
||||
'url': 'dcim:powerfeed_list',
|
||||
},
|
||||
}
|
||||
|
||||
IPAM_TYPES = {
|
||||
'vrf': {
|
||||
'queryset': VRF.objects.prefetch_related('tenant', 'tenant__group'),
|
||||
'filterset': ipam.filtersets.VRFFilterSet,
|
||||
'table': ipam.tables.VRFTable,
|
||||
'url': 'ipam:vrf_list',
|
||||
},
|
||||
'aggregate': {
|
||||
'queryset': Aggregate.objects.prefetch_related('rir'),
|
||||
'filterset': ipam.filtersets.AggregateFilterSet,
|
||||
'table': ipam.tables.AggregateTable,
|
||||
'url': 'ipam:aggregate_list',
|
||||
},
|
||||
'prefix': {
|
||||
'queryset': Prefix.objects.prefetch_related('site', 'vrf__tenant', 'tenant', 'tenant__group', 'vlan', 'role'),
|
||||
'filterset': ipam.filtersets.PrefixFilterSet,
|
||||
'table': ipam.tables.PrefixTable,
|
||||
'url': 'ipam:prefix_list',
|
||||
},
|
||||
'ipaddress': {
|
||||
'queryset': IPAddress.objects.prefetch_related('vrf__tenant', 'tenant', 'tenant__group'),
|
||||
'filterset': ipam.filtersets.IPAddressFilterSet,
|
||||
'table': ipam.tables.IPAddressTable,
|
||||
'url': 'ipam:ipaddress_list',
|
||||
},
|
||||
'vlan': {
|
||||
'queryset': VLAN.objects.prefetch_related('site', 'group', 'tenant', 'tenant__group', 'role'),
|
||||
'filterset': ipam.filtersets.VLANFilterSet,
|
||||
'table': ipam.tables.VLANTable,
|
||||
'url': 'ipam:vlan_list',
|
||||
},
|
||||
'asn': {
|
||||
'queryset': ASN.objects.prefetch_related('rir', 'tenant', 'tenant__group'),
|
||||
'filterset': ipam.filtersets.ASNFilterSet,
|
||||
'table': ipam.tables.ASNTable,
|
||||
'url': 'ipam:asn_list',
|
||||
},
|
||||
'service': {
|
||||
'queryset': Service.objects.prefetch_related('device', 'virtual_machine'),
|
||||
'filterset': ipam.filtersets.ServiceFilterSet,
|
||||
'table': ipam.tables.ServiceTable,
|
||||
'url': 'ipam:service_list',
|
||||
},
|
||||
}
|
||||
|
||||
TENANCY_TYPES = {
|
||||
'tenant': {
|
||||
'queryset': Tenant.objects.prefetch_related('group'),
|
||||
'filterset': tenancy.filtersets.TenantFilterSet,
|
||||
'table': tenancy.tables.TenantTable,
|
||||
'url': 'tenancy:tenant_list',
|
||||
},
|
||||
'contact': {
|
||||
'queryset': Contact.objects.prefetch_related('group', 'assignments').annotate(
|
||||
assignment_count=count_related(ContactAssignment, 'contact')),
|
||||
'filterset': tenancy.filtersets.ContactFilterSet,
|
||||
'table': tenancy.tables.ContactTable,
|
||||
'url': 'tenancy:contact_list',
|
||||
},
|
||||
}
|
||||
|
||||
VIRTUALIZATION_TYPES = {
|
||||
'cluster': {
|
||||
'queryset': Cluster.objects.prefetch_related('type', 'group').annotate(
|
||||
device_count=count_related(Device, 'cluster'),
|
||||
vm_count=count_related(VirtualMachine, 'cluster')
|
||||
),
|
||||
'filterset': virtualization.filtersets.ClusterFilterSet,
|
||||
'table': virtualization.tables.ClusterTable,
|
||||
'url': 'virtualization:cluster_list',
|
||||
},
|
||||
'virtualmachine': {
|
||||
'queryset': VirtualMachine.objects.prefetch_related(
|
||||
'cluster', 'tenant', 'tenant__group', 'platform', 'primary_ip4', 'primary_ip6',
|
||||
),
|
||||
'filterset': virtualization.filtersets.VirtualMachineFilterSet,
|
||||
'table': virtualization.tables.VirtualMachineTable,
|
||||
'url': 'virtualization:virtualmachine_list',
|
||||
},
|
||||
}
|
||||
|
||||
WIRELESS_TYPES = {
|
||||
'wirelesslan': {
|
||||
'queryset': WirelessLAN.objects.prefetch_related('group', 'vlan').annotate(
|
||||
interface_count=count_related(Interface, 'wireless_lans')
|
||||
),
|
||||
'filterset': wireless.filtersets.WirelessLANFilterSet,
|
||||
'table': wireless.tables.WirelessLANTable,
|
||||
'url': 'wireless:wirelesslan_list',
|
||||
},
|
||||
'wirelesslink': {
|
||||
'queryset': WirelessLink.objects.prefetch_related('interface_a__device', 'interface_b__device'),
|
||||
'filterset': wireless.filtersets.WirelessLinkFilterSet,
|
||||
'table': wireless.tables.WirelessLinkTable,
|
||||
'url': 'wireless:wirelesslink_list',
|
||||
},
|
||||
}
|
||||
|
||||
SEARCH_TYPE_HIERARCHY = {
|
||||
'Circuits': CIRCUIT_TYPES,
|
||||
'DCIM': DCIM_TYPES,
|
||||
'IPAM': IPAM_TYPES,
|
||||
'Tenancy': TENANCY_TYPES,
|
||||
'Virtualization': VIRTUALIZATION_TYPES,
|
||||
'Wireless': WIRELESS_TYPES,
|
||||
}
|
||||
|
||||
|
||||
def build_search_types():
|
||||
result = dict()
|
||||
|
||||
for app_types in SEARCH_TYPE_HIERARCHY.values():
|
||||
for name, items in app_types.items():
|
||||
result[name] = items
|
||||
|
||||
return result
|
||||
|
||||
|
||||
SEARCH_TYPES = build_search_types()
|
||||
@@ -29,7 +29,7 @@ django.utils.encoding.force_text = force_str
|
||||
# Environment setup
|
||||
#
|
||||
|
||||
VERSION = '3.3-beta1'
|
||||
VERSION = '3.3-beta2'
|
||||
|
||||
# Hostname
|
||||
HOSTNAME = platform.node()
|
||||
@@ -478,13 +478,6 @@ if SENTRY_ENABLED:
|
||||
# Django social auth
|
||||
#
|
||||
|
||||
# Load all SOCIAL_AUTH_* settings from the user configuration
|
||||
for param in dir(configuration):
|
||||
if param.startswith('SOCIAL_AUTH_'):
|
||||
globals()[param] = getattr(configuration, param)
|
||||
|
||||
SOCIAL_AUTH_JSONFIELD_ENABLED = True
|
||||
|
||||
SOCIAL_AUTH_PIPELINE = (
|
||||
'social_core.pipeline.social_auth.social_details',
|
||||
'social_core.pipeline.social_auth.social_uid',
|
||||
@@ -498,6 +491,14 @@ SOCIAL_AUTH_PIPELINE = (
|
||||
'social_core.pipeline.user.user_details',
|
||||
)
|
||||
|
||||
# Load all SOCIAL_AUTH_* settings from the user configuration
|
||||
for param in dir(configuration):
|
||||
if param.startswith('SOCIAL_AUTH_'):
|
||||
globals()[param] = getattr(configuration, param)
|
||||
|
||||
# Force usage of PostgreSQL's JSONB field for extra data
|
||||
SOCIAL_AUTH_JSONFIELD_ENABLED = True
|
||||
|
||||
|
||||
#
|
||||
# Django Prometheus
|
||||
|
||||
@@ -21,8 +21,9 @@ from dcim.models import (
|
||||
from extras.models import ObjectChange
|
||||
from extras.tables import ObjectChangeTable
|
||||
from ipam.models import Aggregate, IPAddress, IPRange, Prefix, VLAN, VRF
|
||||
from netbox.constants import SEARCH_MAX_RESULTS, SEARCH_TYPES
|
||||
from netbox.constants import SEARCH_MAX_RESULTS
|
||||
from netbox.forms import SearchForm
|
||||
from netbox.search import SEARCH_TYPES
|
||||
from tenancy.models import Tenant
|
||||
from virtualization.models import Cluster, VirtualMachine
|
||||
from wireless.models import WirelessLAN, WirelessLink
|
||||
|
||||
14
netbox/project-static/dist/netbox.js
vendored
14
netbox/project-static/dist/netbox.js
vendored
File diff suppressed because one or more lines are too long
2
netbox/project-static/dist/netbox.js.map
vendored
2
netbox/project-static/dist/netbox.js.map
vendored
File diff suppressed because one or more lines are too long
@@ -38,7 +38,9 @@ export function initReslug(): void {
|
||||
slugLength = Number(slugLengthAttr);
|
||||
}
|
||||
sourceField.addEventListener('blur', () => {
|
||||
slugField.value = slugify(sourceField.value, slugLength);
|
||||
if (!slugField.value) {
|
||||
slugField.value = slugify(sourceField.value, slugLength);
|
||||
}
|
||||
});
|
||||
slugButton.addEventListener('click', () => {
|
||||
slugField.value = slugify(sourceField.value, slugLength);
|
||||
|
||||
@@ -1,32 +1,4 @@
|
||||
import { getElements, scrollTo, isTruthy } from '../util';
|
||||
|
||||
/**
|
||||
* When editing an object, it is sometimes desirable to customize the form action *without*
|
||||
* overriding the form's `submit` event. For example, the 'Save & Continue' button. We don't want
|
||||
* to use the `formaction` attribute on that element because it will be included on the form even
|
||||
* if the button isn't clicked.
|
||||
*
|
||||
* @example
|
||||
* ```html
|
||||
* <button type="button" return-url="/special-url/">
|
||||
* Save & Continue
|
||||
* </button>
|
||||
* ```
|
||||
*
|
||||
* @param event Click event.
|
||||
*/
|
||||
function handleSubmitWithReturnUrl(event: MouseEvent): void {
|
||||
const element = event.target as HTMLElement;
|
||||
if (element.tagName === 'BUTTON') {
|
||||
const button = element as HTMLButtonElement;
|
||||
const action = button.getAttribute('return-url');
|
||||
const form = button.form;
|
||||
if (form !== null && isTruthy(action)) {
|
||||
form.action = action;
|
||||
form.submit();
|
||||
}
|
||||
}
|
||||
}
|
||||
import { getElements, scrollTo } from '../util';
|
||||
|
||||
function handleFormSubmit(event: Event, form: HTMLFormElement): void {
|
||||
// Track the names of each invalid field.
|
||||
@@ -57,15 +29,6 @@ function handleFormSubmit(event: Event, form: HTMLFormElement): void {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach event listeners to form buttons with the `return-url` attribute present.
|
||||
*/
|
||||
function initReturnUrlSubmitButtons(): void {
|
||||
for (const button of getElements<HTMLButtonElement>('button[return-url]')) {
|
||||
button.addEventListener('click', handleSubmitWithReturnUrl);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach an event listener to each form's submitter (button[type=submit]). When called, the
|
||||
* callback checks the validity of each form field and adds the appropriate Bootstrap CSS class
|
||||
@@ -82,5 +45,4 @@ export function initFormElements(): void {
|
||||
submitter.addEventListener('click', (event: Event) => handleFormSubmit(event, form));
|
||||
}
|
||||
}
|
||||
initReturnUrlSubmitButtons();
|
||||
}
|
||||
|
||||
@@ -411,7 +411,6 @@ export class APISelect {
|
||||
} finally {
|
||||
this.setOptionStyles();
|
||||
this.enable();
|
||||
this.slim.slim.search.input.focus();
|
||||
this.base.dispatchEvent(this.loadEvent);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,17 +56,3 @@
|
||||
{% render_custom_fields form %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{# Override buttons block, 'Create & Add Another'/'_addanother' is not needed on a circuit. #}
|
||||
{% block buttons %}
|
||||
<a class="btn btn-outline-danger" href="{{ return_url }}">Cancel</a>
|
||||
{% if object.pk %}
|
||||
<button type="submit" name="_update" class="btn btn-primary">
|
||||
Save
|
||||
</button>
|
||||
{% else %}
|
||||
<button type="submit" name="_create" class="btn btn-primary">
|
||||
Create
|
||||
</button>
|
||||
{% endif %}
|
||||
{% endblock buttons %}
|
||||
|
||||
@@ -18,43 +18,41 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="trace-end">
|
||||
{% with traced_path=path.origin.trace %}
|
||||
{% if path.is_split %}
|
||||
<h3 class="text-danger">Path split!</h3>
|
||||
<p>Select a node below to continue:</p>
|
||||
<ul class="text-start">
|
||||
{% for next_node in path.get_split_nodes %}
|
||||
{% if next_node.cable %}
|
||||
<li>
|
||||
<a href="{% url 'dcim:frontport_trace' pk=next_node.pk %}">{{ next_node }}</a>
|
||||
(Cable {{ next_node.cable|linkify }})
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="text-muted">{{ next_node }}</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<h3 class="text-center text-success">Trace Completed</h3>
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th scope="row">Total segments</th>
|
||||
<td>{{ traced_path|length }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Total length</th>
|
||||
<td>
|
||||
{% if total_length %}
|
||||
{{ total_length|floatformat:"-2" }}{% if not is_definitive %}+{% endif %} Meters /
|
||||
{{ total_length|meters_to_feet|floatformat:"-2" }} Feet
|
||||
{% else %}
|
||||
<span class="text-muted">N/A</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% if path.is_split %}
|
||||
<h3 class="text-danger">Path split!</h3>
|
||||
<p>Select a node below to continue:</p>
|
||||
<ul class="text-start">
|
||||
{% for next_node in path.get_split_nodes %}
|
||||
{% if next_node.cable %}
|
||||
<li>
|
||||
<a href="{% url 'dcim:frontport_trace' pk=next_node.pk %}">{{ next_node }}</a>
|
||||
(Cable {{ next_node.cable|linkify }})
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="text-muted">{{ next_node }}</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<h3 class="text-center text-success">Trace Completed</h3>
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th scope="row">Total segments</th>
|
||||
<td>{{ path.segment_count }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Total length</th>
|
||||
<td>
|
||||
{% if total_length %}
|
||||
{{ total_length|floatformat:"-2" }}{% if not is_definitive %}+{% endif %} Meters /
|
||||
{{ total_length|meters_to_feet|floatformat:"-2" }} Feet
|
||||
{% else %}
|
||||
<span class="text-muted">N/A</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<h3 class="text-center text-muted my-3">
|
||||
|
||||
@@ -1,58 +1,62 @@
|
||||
{% load helpers %}
|
||||
<table class="table table-hover panel-body attr-table">
|
||||
{% if terminations.0.device %}
|
||||
{# Device component #}
|
||||
<tr>
|
||||
<td>Site</td>
|
||||
<td>{{ terminations.0.device.site|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Rack</td>
|
||||
<td>{{ terminations.0.device.rack|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Device</td>
|
||||
<td>{{ terminations.0.device|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ terminations.0|meta:"verbose_name"|capfirst }}</td>
|
||||
<td>
|
||||
{% for term in terminations %}
|
||||
{{ term|linkify }}{% if not forloop.last %},{% endif %}
|
||||
{% endfor %}
|
||||
</td>
|
||||
</tr>
|
||||
{% elif terminations.0.power_panel %}
|
||||
{# Power feed #}
|
||||
<tr>
|
||||
<td>Site</td>
|
||||
<td>{{ terminations.0.power_panel.site|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Power Panel</td>
|
||||
<td>{{ terminations.0.power_panel|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ terminations.0|meta:"verbose_name"|capfirst }}</td>
|
||||
<td>
|
||||
{% for term in terminations %}
|
||||
{{ term|linkify }}{% if not forloop.last %},{% endif %}
|
||||
{% endfor %}
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
{# Circuit termination #}
|
||||
<tr>
|
||||
<td>Provider</td>
|
||||
<td>{{ terminations.0.circuit.provider|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Circuit</td>
|
||||
<td>
|
||||
{% for term in terminations %}
|
||||
{{ term.circuit|linkify }} ({{ term }}){% if not forloop.last %},{% endif %}
|
||||
{% endfor %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
{% if terminations.0 %}
|
||||
<table class="table table-hover panel-body attr-table">
|
||||
{% if terminations.0.device %}
|
||||
{# Device component #}
|
||||
<tr>
|
||||
<td>Site</td>
|
||||
<td>{{ terminations.0.device.site|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Rack</td>
|
||||
<td>{{ terminations.0.device.rack|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Device</td>
|
||||
<td>{{ terminations.0.device|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ terminations.0|meta:"verbose_name"|capfirst }}</td>
|
||||
<td>
|
||||
{% for term in terminations %}
|
||||
{{ term|linkify }}{% if not forloop.last %},{% endif %}
|
||||
{% endfor %}
|
||||
</td>
|
||||
</tr>
|
||||
{% elif terminations.0.power_panel %}
|
||||
{# Power feed #}
|
||||
<tr>
|
||||
<td>Site</td>
|
||||
<td>{{ terminations.0.power_panel.site|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Power Panel</td>
|
||||
<td>{{ terminations.0.power_panel|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ terminations.0|meta:"verbose_name"|capfirst }}</td>
|
||||
<td>
|
||||
{% for term in terminations %}
|
||||
{{ term|linkify }}{% if not forloop.last %},{% endif %}
|
||||
{% endfor %}
|
||||
</td>
|
||||
</tr>
|
||||
{% elif terminations.0.circuit %}
|
||||
{# Circuit termination #}
|
||||
<tr>
|
||||
<td>Provider</td>
|
||||
<td>{{ terminations.0.circuit.provider|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Circuit</td>
|
||||
<td>
|
||||
{% for term in terminations %}
|
||||
{{ term.circuit|linkify }} ({{ term }}){% if not forloop.last %},{% endif %}
|
||||
{% endfor %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
{% else %}
|
||||
<span class="text-muted">No termination</span>
|
||||
{% endif %}
|
||||
|
||||
@@ -219,7 +219,7 @@
|
||||
<tr>
|
||||
<th scope="row">Path Status</th>
|
||||
<td>
|
||||
{% if object.path.is_active %}
|
||||
{% if object.path.is_complete and object.path.is_active %}
|
||||
<span class="badge bg-success">Reachable</span>
|
||||
{% else %}
|
||||
<span class="badge bg-danger">Not Reachable</span>
|
||||
|
||||
@@ -99,14 +99,3 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block buttons %}
|
||||
<a href="{{ return_url }}" class="btn btn-outline-danger">Cancel</a>
|
||||
{% if object.pk %}
|
||||
<button type="button" return-url="?return_url={% url 'dcim:interface_edit' pk=object.pk %}" class="btn btn-outline-primary">Save & Continue Editing</button>
|
||||
<button type="submit" name="_update" class="btn btn-primary">Save</button>
|
||||
{% else %}
|
||||
<button type="submit" name="_addanother" class="btn btn-outline-primary">Create & Add Another</button>
|
||||
<button type="submit" name="_create" class="btn btn-primary">Create</button>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -36,8 +36,8 @@ Context:
|
||||
{{ field }}
|
||||
{% endfor %}
|
||||
<div class="text-end">
|
||||
<a href="{{ return_url }}" class="btn btn-outline-dark">Cancel</a>
|
||||
<button type="submit" name="_confirm" class="btn btn-danger">Delete {{ table.rows|length }} {{ model|meta:"verbose_name_plural" }}</button>
|
||||
<a href="{{ return_url }}" class="btn btn-outline-dark">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -118,8 +118,8 @@ Context:
|
||||
</div>
|
||||
|
||||
<div class="text-end">
|
||||
<a href="{{ return_url }}" class="btn btn-sm btn-outline-danger">Cancel</a>
|
||||
<button type="submit" name="_apply" class="btn btn-sm btn-primary">Apply</button>
|
||||
<a href="{{ return_url }}" class="btn btn-sm btn-outline-danger">Cancel</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -44,12 +44,12 @@ Context:
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col col-md-12 text-end">
|
||||
{% if return_url %}
|
||||
<a href="{{ return_url }}" class="btn btn-outline-danger">Cancel</a>
|
||||
{% endif %}
|
||||
<button type="submit" class="btn btn-primary">Submit</button>
|
||||
</div>
|
||||
<div class="col col-md-12 text-end">
|
||||
<button type="submit" class="btn btn-primary">Submit</button>
|
||||
{% if return_url %}
|
||||
<a href="{{ return_url }}" class="btn btn-outline-danger">Cancel</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% if fields %}
|
||||
|
||||
@@ -23,8 +23,8 @@
|
||||
{{ field }}
|
||||
{% endfor %}
|
||||
<div class="text-center">
|
||||
<a href="{{ return_url }}" class="btn btn-outline-dark">Cancel</a>
|
||||
<button type="submit" name="_confirm" class="btn btn-danger">Delete these {{ table.rows|length }} {{ obj_type_plural }}</button>
|
||||
<a href="{{ return_url }}" class="btn btn-outline-dark">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -34,11 +34,11 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="col col-md-12 my-3 text-end">
|
||||
<a href="{{ return_url }}" class="btn btn-outline-danger">Cancel</a>
|
||||
<button type="submit" name="_preview" class="btn btn-primary">Preview</button>
|
||||
{% if '_preview' in request.POST and not form.errors %}
|
||||
<button type="submit" name="_apply" class="btn btn-primary">Apply</button>
|
||||
{% endif %}
|
||||
<a href="{{ return_url }}" class="btn btn-outline-danger">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -2,33 +2,24 @@
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mt-5">
|
||||
|
||||
<div class="col col-md-6 offset-md-3">
|
||||
|
||||
<form action="" method="post" class="form">
|
||||
{% csrf_token %}
|
||||
{% for field in form.hidden_fields %}
|
||||
{{ field }}
|
||||
{% endfor %}
|
||||
|
||||
<div class="card border-danger">
|
||||
<h5 class="card-header">{% block confirmation_title %}{% endblock %}</h5>
|
||||
|
||||
<div class="card-body">
|
||||
{% block message %}<p>Are you sure?</p>{% endblock %}
|
||||
</div>
|
||||
|
||||
<div class="card-footer text-end">
|
||||
<a href="{{ return_url }}" class="btn btn-outline-dark">Cancel</a>
|
||||
<button type="submit" name="_confirm" class="btn btn-{{ button_class|default:"danger" }}">Confirm</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
<div class="row mt-5">
|
||||
<div class="col col-md-6 offset-md-3">
|
||||
<form action="" method="post" class="form">
|
||||
{% csrf_token %}
|
||||
{% for field in form.hidden_fields %}
|
||||
{{ field }}
|
||||
{% endfor %}
|
||||
<div class="card border-danger">
|
||||
<h5 class="card-header">{% block confirmation_title %}{% endblock %}</h5>
|
||||
<div class="card-body">
|
||||
{% block message %}<p>Are you sure?</p>{% endblock %}
|
||||
</div>
|
||||
<div class="card-footer text-end">
|
||||
<button type="submit" name="_confirm" class="btn btn-{{ button_class|default:"danger" }}">Confirm</button>
|
||||
<a href="{{ return_url }}" class="btn btn-outline-dark">Cancel</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -94,19 +94,19 @@ Context:
|
||||
|
||||
<div class="text-end my-3">
|
||||
{% block buttons %}
|
||||
<a class="btn btn-outline-danger" href="{{ return_url }}">Cancel</a>
|
||||
{% if object.pk %}
|
||||
<button type="submit" name="_update" class="btn btn-primary">
|
||||
Save
|
||||
</button>
|
||||
{% else %}
|
||||
<button type="submit" name="_addanother" class="btn btn-outline-primary">
|
||||
Create & Add Another
|
||||
</button>
|
||||
<button type="submit" name="_create" class="btn btn-primary">
|
||||
Create
|
||||
</button>
|
||||
<button type="submit" name="_addanother" class="btn btn-outline-primary">
|
||||
Create & Add Another
|
||||
</button>
|
||||
{% endif %}
|
||||
<a class="btn btn-outline-danger" href="{{ return_url }}">Cancel</a>
|
||||
{% endblock buttons %}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -5,19 +5,19 @@
|
||||
{% block title %}{{ obj_type|bettertitle }} Import{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-12 col-xl-8 offset-xl-2">
|
||||
<form action="" method="post" class="form form-horizontal">
|
||||
{% csrf_token %}
|
||||
{% render_form form %}
|
||||
<div class="col col-md-12 text-end">
|
||||
{% if return_url %}
|
||||
<a href="{{ return_url }}" class="btn btn-outline-danger">Cancel</a>
|
||||
{% endif %}
|
||||
<button type="submit" name="_addanother" class="btn btn-outline-primary">Submit & Import Another</button>
|
||||
<button type="submit" name="_create" class="btn btn-primary">Submit</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-12 col-xl-8 offset-xl-2">
|
||||
<form action="" method="post" class="form form-horizontal">
|
||||
{% csrf_token %}
|
||||
{% render_form form %}
|
||||
<div class="col col-md-12 text-end">
|
||||
<button type="submit" name="_create" class="btn btn-primary">Submit</button>
|
||||
<button type="submit" name="_addanother" class="btn btn-outline-primary">Submit & Import Another</button>
|
||||
{% if return_url %}
|
||||
<a href="{{ return_url }}" class="btn btn-outline-danger">Cancel</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<div class="card-body">
|
||||
{% for group_name, fields in custom_fields.items %}
|
||||
{% if group_name %}
|
||||
<h6><strong>{{ group_name }}</strong></h6>
|
||||
<h6>{{ group_name }}</h6>
|
||||
{% endif %}
|
||||
<table class="table table-hover attr-table">
|
||||
{% for field, value in fields.items %}
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
<div class="row mb-3">
|
||||
<div class="tab-content p-0 border-0">
|
||||
<div class="tab-pane {% if not form.initial.interface or form.initial.vminterface %}active{% endif %}" id="vlan" role="tabpanel" aria-labeled-by="vlan_tab">
|
||||
{% render_field form.device %}
|
||||
{% render_field form.device_vlan %}
|
||||
{% render_field form.vlan %}
|
||||
</div>
|
||||
<div class="tab-pane {% if form.initial.interface %}active{% endif %}" id="interface" role="tabpanel" aria-labeled-by="interface_tab">
|
||||
|
||||
@@ -55,14 +55,3 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block buttons %}
|
||||
<a href="{{ return_url }}" class="btn btn-outline-danger">Cancel</a>
|
||||
{% if object.pk %}
|
||||
<button type="button" return-url="?return_url={% url 'virtualization:vminterface_edit' pk=object.pk %}" class="btn btn-outline-primary">Save & Continue Editing</button>
|
||||
<button type="submit" name="_update" class="btn btn-primary">Save</button>
|
||||
{% else %}
|
||||
<button type="submit" name="_addanother" class="btn btn-outline-primary">Create & Add Another</button>
|
||||
<button type="submit" name="_create" class="btn btn-primary">Create</button>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -4,6 +4,7 @@ from rest_framework import serializers
|
||||
|
||||
from netbox.api.fields import ChoiceField, ContentTypeField
|
||||
from netbox.api.serializers import NestedGroupModelSerializer, NetBoxModelSerializer
|
||||
from netbox.constants import NESTED_SERIALIZER_PREFIX
|
||||
from tenancy.choices import ContactPriorityChoices
|
||||
from tenancy.models import *
|
||||
from utilities.api import get_serializer_for_model
|
||||
@@ -108,6 +109,6 @@ class ContactAssignmentSerializer(NetBoxModelSerializer):
|
||||
|
||||
@swagger_serializer_method(serializer_or_field=serializers.DictField)
|
||||
def get_object(self, instance):
|
||||
serializer = get_serializer_for_model(instance.content_type.model_class(), prefix='Nested')
|
||||
serializer = get_serializer_for_model(instance.content_type.model_class(), prefix=NESTED_SERIALIZER_PREFIX)
|
||||
context = {'request': self.context['request']}
|
||||
return serializer(instance.object, context=context).data
|
||||
|
||||
@@ -95,7 +95,7 @@ class TenantListView(generic.ObjectListView):
|
||||
|
||||
|
||||
class TenantView(generic.ObjectView):
|
||||
queryset = Tenant.objects.prefetch_related('group')
|
||||
queryset = Tenant.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
stats = {
|
||||
@@ -140,14 +140,14 @@ class TenantBulkImportView(generic.BulkImportView):
|
||||
|
||||
|
||||
class TenantBulkEditView(generic.BulkEditView):
|
||||
queryset = Tenant.objects.prefetch_related('group')
|
||||
queryset = Tenant.objects.all()
|
||||
filterset = filtersets.TenantFilterSet
|
||||
table = tables.TenantTable
|
||||
form = forms.TenantBulkEditForm
|
||||
|
||||
|
||||
class TenantBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = Tenant.objects.prefetch_related('group')
|
||||
queryset = Tenant.objects.all()
|
||||
filterset = filtersets.TenantFilterSet
|
||||
table = tables.TenantTable
|
||||
|
||||
@@ -337,14 +337,14 @@ class ContactBulkImportView(generic.BulkImportView):
|
||||
|
||||
|
||||
class ContactBulkEditView(generic.BulkEditView):
|
||||
queryset = Contact.objects.prefetch_related('group')
|
||||
queryset = Contact.objects.all()
|
||||
filterset = filtersets.ContactFilterSet
|
||||
table = tables.ContactTable
|
||||
form = forms.ContactBulkEditForm
|
||||
|
||||
|
||||
class ContactBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = Contact.objects.prefetch_related('group')
|
||||
queryset = Contact.objects.all()
|
||||
filterset = filtersets.ContactFilterSet
|
||||
table = tables.ContactTable
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user