mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-14 09:51:22 -06:00
Merge branch 'feature' into docs-refresh
This commit is contained in:
commit
a7bf7bf7a5
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@ -14,7 +14,7 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: NetBox version
|
label: NetBox version
|
||||||
description: What version of NetBox are you currently running?
|
description: What version of NetBox are you currently running?
|
||||||
placeholder: v3.3-beta1
|
placeholder: v3.3-beta2
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
|
2
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
2
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
@ -14,7 +14,7 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: NetBox version
|
label: NetBox version
|
||||||
description: What version of NetBox are you currently running?
|
description: What version of NetBox are you currently running?
|
||||||
placeholder: v3.3-beta1
|
placeholder: v3.3-beta2
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
|
@ -2,6 +2,23 @@
|
|||||||
|
|
||||||
## v3.2.8 (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)
|
## v3.2.7 (2022-07-20)
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# NetBox v3.3
|
# NetBox v3.3
|
||||||
|
|
||||||
## v3.3.0 (FUTURE)
|
## v3.3-beta2 (2022-08-03)
|
||||||
|
|
||||||
### Breaking Changes
|
### Breaking Changes
|
||||||
|
|
||||||
@ -104,6 +104,9 @@ Custom field UI visibility has no impact on API operation.
|
|||||||
* [#9730](https://github.com/netbox-community/netbox/issues/9730) - Fix validation error when creating a new cable via UI form
|
* [#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
|
* [#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
|
* [#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
|
* [#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
|
* [#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
|
* [#9829](https://github.com/netbox-community/netbox/issues/9829) - Arrange custom fields by group when editing objects
|
||||||
@ -123,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
|
* [#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
|
* [#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
|
### REST API Changes
|
||||||
|
|
||||||
|
@ -30,7 +30,8 @@ class ProviderView(generic.ObjectView):
|
|||||||
circuits = Circuit.objects.restrict(request.user, 'view').filter(
|
circuits = Circuit.objects.restrict(request.user, 'view').filter(
|
||||||
provider=instance
|
provider=instance
|
||||||
).prefetch_related(
|
).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 = tables.CircuitTable(circuits, user=request.user, exclude=('provider',))
|
||||||
circuits_table.configure(request)
|
circuits_table.configure(request)
|
||||||
@ -91,7 +92,8 @@ class ProviderNetworkView(generic.ObjectView):
|
|||||||
Q(termination_a__provider_network=instance.pk) |
|
Q(termination_a__provider_network=instance.pk) |
|
||||||
Q(termination_z__provider_network=instance.pk)
|
Q(termination_z__provider_network=instance.pk)
|
||||||
).prefetch_related(
|
).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 = tables.CircuitTable(circuits, user=request.user)
|
||||||
circuits_table.configure(request)
|
circuits_table.configure(request)
|
||||||
@ -192,7 +194,8 @@ class CircuitTypeBulkDeleteView(generic.BulkDeleteView):
|
|||||||
|
|
||||||
class CircuitListView(generic.ObjectListView):
|
class CircuitListView(generic.ObjectListView):
|
||||||
queryset = Circuit.objects.prefetch_related(
|
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 = filtersets.CircuitFilterSet
|
||||||
filterset_form = forms.CircuitFilterForm
|
filterset_form = forms.CircuitFilterForm
|
||||||
@ -220,7 +223,8 @@ class CircuitBulkImportView(generic.BulkImportView):
|
|||||||
|
|
||||||
class CircuitBulkEditView(generic.BulkEditView):
|
class CircuitBulkEditView(generic.BulkEditView):
|
||||||
queryset = Circuit.objects.prefetch_related(
|
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
|
filterset = filtersets.CircuitFilterSet
|
||||||
table = tables.CircuitTable
|
table = tables.CircuitTable
|
||||||
@ -229,7 +233,8 @@ class CircuitBulkEditView(generic.BulkEditView):
|
|||||||
|
|
||||||
class CircuitBulkDeleteView(generic.BulkDeleteView):
|
class CircuitBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = Circuit.objects.prefetch_related(
|
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
|
filterset = filtersets.CircuitFilterSet
|
||||||
table = tables.CircuitTable
|
table = tables.CircuitTable
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import socket
|
import socket
|
||||||
from collections import OrderedDict
|
|
||||||
|
|
||||||
from django.http import Http404, HttpResponse, HttpResponseForbidden
|
from django.http import Http404, HttpResponse, HttpResponseForbidden
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
@ -64,20 +63,20 @@ class PathEndpointMixin(object):
|
|||||||
return HttpResponse(drawing.render().tostring(), content_type='image/svg+xml')
|
return HttpResponse(drawing.render().tostring(), content_type='image/svg+xml')
|
||||||
|
|
||||||
# Serialize path objects, iterating over each three-tuple in the path
|
# Serialize path objects, iterating over each three-tuple in the path
|
||||||
for near_end, cable, far_end in obj.trace():
|
for near_ends, cable, far_ends in obj.trace():
|
||||||
if near_end is not None:
|
if near_ends:
|
||||||
serializer_a = get_serializer_for_model(near_end[0], prefix=NESTED_SERIALIZER_PREFIX)
|
serializer_a = get_serializer_for_model(near_ends[0], prefix=NESTED_SERIALIZER_PREFIX)
|
||||||
near_end = serializer_a(near_end, many=True, context={'request': request}).data
|
near_ends = serializer_a(near_ends, many=True, context={'request': request}).data
|
||||||
else:
|
else:
|
||||||
# Path is split; stop here
|
# Path is split; stop here
|
||||||
break
|
break
|
||||||
if cable is not None:
|
if cable:
|
||||||
cable = serializers.TracedCableSerializer(cable[0], context={'request': request}).data
|
cable = serializers.TracedCableSerializer(cable[0], context={'request': request}).data
|
||||||
if far_end is not None:
|
if far_ends:
|
||||||
serializer_b = get_serializer_for_model(far_end[0], prefix=NESTED_SERIALIZER_PREFIX)
|
serializer_b = get_serializer_for_model(far_ends[0], prefix=NESTED_SERIALIZER_PREFIX)
|
||||||
far_end = serializer_b(far_end, many=True, context={'request': request}).data
|
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)
|
return Response(path)
|
||||||
|
|
||||||
@ -484,7 +483,7 @@ class DeviceViewSet(ConfigContextQuerySetMixin, NetBoxModelViewSet):
|
|||||||
return HttpResponseForbidden()
|
return HttpResponseForbidden()
|
||||||
|
|
||||||
napalm_methods = request.GET.getlist('method')
|
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()
|
config = get_config()
|
||||||
username = config.NAPALM_USERNAME
|
username = config.NAPALM_USERNAME
|
||||||
|
@ -1,10 +1,26 @@
|
|||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
from netbox import denormalized
|
||||||
|
|
||||||
|
|
||||||
class DCIMConfig(AppConfig):
|
class DCIMConfig(AppConfig):
|
||||||
name = "dcim"
|
name = "dcim"
|
||||||
verbose_name = "DCIM"
|
verbose_name = "DCIM"
|
||||||
|
|
||||||
def ready(self):
|
def ready(self):
|
||||||
|
|
||||||
import dcim.signals
|
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',
|
||||||
|
})
|
||||||
|
@ -291,7 +291,7 @@ class RackReservationFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
|||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'tag')),
|
(None, ('q', 'tag')),
|
||||||
('User', ('user_id',)),
|
('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')),
|
('Tenant', ('tenant_group_id', 'tenant_id')),
|
||||||
)
|
)
|
||||||
region_id = DynamicModelMultipleChoiceField(
|
region_id = DynamicModelMultipleChoiceField(
|
||||||
@ -299,25 +299,38 @@ class RackReservationFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
|||||||
required=False,
|
required=False,
|
||||||
label=_('Region')
|
label=_('Region')
|
||||||
)
|
)
|
||||||
site_id = DynamicModelMultipleChoiceField(
|
|
||||||
queryset=Site.objects.all(),
|
|
||||||
required=False,
|
|
||||||
query_params={
|
|
||||||
'region_id': '$region_id'
|
|
||||||
},
|
|
||||||
label=_('Site')
|
|
||||||
)
|
|
||||||
site_group_id = DynamicModelMultipleChoiceField(
|
site_group_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=SiteGroup.objects.all(),
|
queryset=SiteGroup.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
label=_('Site group')
|
label=_('Site group')
|
||||||
)
|
)
|
||||||
location_id = DynamicModelMultipleChoiceField(
|
site_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=Location.objects.prefetch_related('site'),
|
queryset=Site.objects.all(),
|
||||||
required=False,
|
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'),
|
label=_('Location'),
|
||||||
null_option='None'
|
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(
|
user_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=User.objects.all(),
|
queryset=User.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
|
@ -325,7 +325,7 @@ class RackReservationForm(TenancyForm, NetBoxModelForm):
|
|||||||
)
|
)
|
||||||
|
|
||||||
fieldsets = (
|
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')),
|
('Tenancy', ('tenant_group', 'tenant')),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -64,6 +64,14 @@ class ModularComponentTemplateCreateForm(ComponentCreateForm):
|
|||||||
"""
|
"""
|
||||||
Creation form for component templates that can be assigned to either a DeviceType *or* a ModuleType.
|
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(
|
device_type = DynamicModelChoiceField(
|
||||||
queryset=DeviceType.objects.all(),
|
queryset=DeviceType.objects.all(),
|
||||||
required=False
|
required=False
|
||||||
|
@ -431,11 +431,7 @@ class CablePath(models.Model):
|
|||||||
"""
|
"""
|
||||||
Return the list of originating objects.
|
Return the list of originating objects.
|
||||||
"""
|
"""
|
||||||
if hasattr(self, '_path_objects'):
|
|
||||||
return self.path_objects[0]
|
return self.path_objects[0]
|
||||||
return [
|
|
||||||
path_node_to_object(node) for node in self.path[0]
|
|
||||||
]
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def destinations(self):
|
def destinations(self):
|
||||||
@ -444,11 +440,7 @@ class CablePath(models.Model):
|
|||||||
"""
|
"""
|
||||||
if not self.is_complete:
|
if not self.is_complete:
|
||||||
return []
|
return []
|
||||||
if hasattr(self, '_path_objects'):
|
|
||||||
return self.path_objects[-1]
|
return self.path_objects[-1]
|
||||||
return [
|
|
||||||
path_node_to_object(node) for node in self.path[-1]
|
|
||||||
]
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def segment_count(self):
|
def segment_count(self):
|
||||||
@ -463,6 +455,9 @@ class CablePath(models.Model):
|
|||||||
"""
|
"""
|
||||||
from circuits.models import CircuitTermination
|
from circuits.models import CircuitTermination
|
||||||
|
|
||||||
|
if not terminations:
|
||||||
|
return None
|
||||||
|
|
||||||
# Ensure all originating terminations are attached to the same link
|
# Ensure all originating terminations are attached to the same link
|
||||||
if len(terminations) > 1:
|
if len(terminations) > 1:
|
||||||
assert all(t.link == terminations[0].link for t in terminations[1:])
|
assert all(t.link == terminations[0].link for t in terminations[1:])
|
||||||
@ -529,6 +524,9 @@ class CablePath(models.Model):
|
|||||||
])
|
])
|
||||||
|
|
||||||
# Step 6: Determine the "next hop" terminations, if applicable
|
# Step 6: Determine the "next hop" terminations, if applicable
|
||||||
|
if not remote_terminations:
|
||||||
|
break
|
||||||
|
|
||||||
if isinstance(remote_terminations[0], FrontPort):
|
if isinstance(remote_terminations[0], FrontPort):
|
||||||
# Follow FrontPorts to their corresponding RearPorts
|
# Follow FrontPorts to their corresponding RearPorts
|
||||||
rear_ports = RearPort.objects.filter(
|
rear_ports = RearPort.objects.filter(
|
||||||
@ -640,7 +638,11 @@ class CablePath(models.Model):
|
|||||||
nodes = []
|
nodes = []
|
||||||
for node in step:
|
for node in step:
|
||||||
ct_id, object_id = decompile_path_node(node)
|
ct_id, object_id = decompile_path_node(node)
|
||||||
|
try:
|
||||||
nodes.append(prefetched[ct_id][object_id])
|
nodes.append(prefetched[ct_id][object_id])
|
||||||
|
except KeyError:
|
||||||
|
# Ignore stale (deleted) object IDs
|
||||||
|
pass
|
||||||
path.append(nodes)
|
path.append(nodes)
|
||||||
|
|
||||||
return path
|
return path
|
||||||
|
@ -39,7 +39,10 @@ class ComponentTemplateModel(WebhooksMixin, ChangeLoggedModel):
|
|||||||
related_name='%(class)ss'
|
related_name='%(class)ss'
|
||||||
)
|
)
|
||||||
name = models.CharField(
|
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(
|
_name = NaturalOrderingField(
|
||||||
target_field='name',
|
target_field='name',
|
||||||
@ -157,6 +160,14 @@ class ConsolePortTemplate(ModularComponentTemplateModel):
|
|||||||
**kwargs
|
**kwargs
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def to_yaml(self):
|
||||||
|
return {
|
||||||
|
'name': self.name,
|
||||||
|
'type': self.type,
|
||||||
|
'label': self.label,
|
||||||
|
'description': self.description,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class ConsoleServerPortTemplate(ModularComponentTemplateModel):
|
class ConsoleServerPortTemplate(ModularComponentTemplateModel):
|
||||||
"""
|
"""
|
||||||
@ -185,6 +196,14 @@ class ConsoleServerPortTemplate(ModularComponentTemplateModel):
|
|||||||
**kwargs
|
**kwargs
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def to_yaml(self):
|
||||||
|
return {
|
||||||
|
'name': self.name,
|
||||||
|
'type': self.type,
|
||||||
|
'label': self.label,
|
||||||
|
'description': self.description,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class PowerPortTemplate(ModularComponentTemplateModel):
|
class PowerPortTemplate(ModularComponentTemplateModel):
|
||||||
"""
|
"""
|
||||||
@ -236,6 +255,16 @@ class PowerPortTemplate(ModularComponentTemplateModel):
|
|||||||
'allocated_draw': f"Allocated draw cannot exceed the maximum draw ({self.maximum_draw}W)."
|
'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):
|
class PowerOutletTemplate(ModularComponentTemplateModel):
|
||||||
"""
|
"""
|
||||||
@ -298,6 +327,16 @@ class PowerOutletTemplate(ModularComponentTemplateModel):
|
|||||||
**kwargs
|
**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):
|
class InterfaceTemplate(ModularComponentTemplateModel):
|
||||||
"""
|
"""
|
||||||
@ -351,6 +390,17 @@ class InterfaceTemplate(ModularComponentTemplateModel):
|
|||||||
**kwargs
|
**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):
|
class FrontPortTemplate(ModularComponentTemplateModel):
|
||||||
"""
|
"""
|
||||||
@ -424,6 +474,16 @@ class FrontPortTemplate(ModularComponentTemplateModel):
|
|||||||
**kwargs
|
**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):
|
class RearPortTemplate(ModularComponentTemplateModel):
|
||||||
"""
|
"""
|
||||||
@ -463,6 +523,15 @@ class RearPortTemplate(ModularComponentTemplateModel):
|
|||||||
**kwargs
|
**kwargs
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def to_yaml(self):
|
||||||
|
return {
|
||||||
|
'name': self.name,
|
||||||
|
'type': self.type,
|
||||||
|
'positions': self.positions,
|
||||||
|
'label': self.label,
|
||||||
|
'description': self.description,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class ModuleBayTemplate(ComponentTemplateModel):
|
class ModuleBayTemplate(ComponentTemplateModel):
|
||||||
"""
|
"""
|
||||||
@ -488,6 +557,14 @@ class ModuleBayTemplate(ComponentTemplateModel):
|
|||||||
position=self.position
|
position=self.position
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def to_yaml(self):
|
||||||
|
return {
|
||||||
|
'name': self.name,
|
||||||
|
'label': self.label,
|
||||||
|
'position': self.position,
|
||||||
|
'description': self.description,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class DeviceBayTemplate(ComponentTemplateModel):
|
class DeviceBayTemplate(ComponentTemplateModel):
|
||||||
"""
|
"""
|
||||||
@ -512,6 +589,13 @@ class DeviceBayTemplate(ComponentTemplateModel):
|
|||||||
f"Subdevice role of device type ({self.device_type}) must be set to \"parent\" to allow device bays."
|
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):
|
class InventoryItemTemplate(MPTTModel, ComponentTemplateModel):
|
||||||
"""
|
"""
|
||||||
|
@ -212,10 +212,13 @@ class PathEndpoint(models.Model):
|
|||||||
break
|
break
|
||||||
|
|
||||||
path.extend(origin._path.path_objects)
|
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)
|
# If the path ends at a non-connected pass-through port, pad out the link and far-end terminations
|
||||||
# by inserting empty entries immediately prior to the path's destination node(s)
|
if len(path) % 3 == 1:
|
||||||
path.append([])
|
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
|
# Check for a bridged relationship to continue the trace
|
||||||
destinations = origin._path.destinations
|
destinations = origin._path.destinations
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import decimal
|
import decimal
|
||||||
from collections import OrderedDict
|
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
from django.contrib.contenttypes.fields import GenericRelation
|
from django.contrib.contenttypes.fields import GenericRelation
|
||||||
@ -164,117 +163,54 @@ class DeviceType(NetBoxModel):
|
|||||||
return reverse('dcim:devicetype', args=[self.pk])
|
return reverse('dcim:devicetype', args=[self.pk])
|
||||||
|
|
||||||
def to_yaml(self):
|
def to_yaml(self):
|
||||||
data = OrderedDict((
|
data = {
|
||||||
('manufacturer', self.manufacturer.name),
|
'manufacturer': self.manufacturer.name,
|
||||||
('model', self.model),
|
'model': self.model,
|
||||||
('slug', self.slug),
|
'slug': self.slug,
|
||||||
('part_number', self.part_number),
|
'part_number': self.part_number,
|
||||||
('u_height', float(self.u_height)),
|
'u_height': float(self.u_height),
|
||||||
('is_full_depth', self.is_full_depth),
|
'is_full_depth': self.is_full_depth,
|
||||||
('subdevice_role', self.subdevice_role),
|
'subdevice_role': self.subdevice_role,
|
||||||
('airflow', self.airflow),
|
'airflow': self.airflow,
|
||||||
('comments', self.comments),
|
'comments': self.comments,
|
||||||
))
|
}
|
||||||
|
|
||||||
# Component templates
|
# Component templates
|
||||||
if self.consoleporttemplates.exists():
|
if self.consoleporttemplates.exists():
|
||||||
data['console-ports'] = [
|
data['console-ports'] = [
|
||||||
{
|
c.to_yaml() for c in self.consoleporttemplates.all()
|
||||||
'name': c.name,
|
|
||||||
'type': c.type,
|
|
||||||
'label': c.label,
|
|
||||||
'description': c.description,
|
|
||||||
}
|
|
||||||
for c in self.consoleporttemplates.all()
|
|
||||||
]
|
]
|
||||||
if self.consoleserverporttemplates.exists():
|
if self.consoleserverporttemplates.exists():
|
||||||
data['console-server-ports'] = [
|
data['console-server-ports'] = [
|
||||||
{
|
c.to_yaml() for c in self.consoleserverporttemplates.all()
|
||||||
'name': c.name,
|
|
||||||
'type': c.type,
|
|
||||||
'label': c.label,
|
|
||||||
'description': c.description,
|
|
||||||
}
|
|
||||||
for c in self.consoleserverporttemplates.all()
|
|
||||||
]
|
]
|
||||||
if self.powerporttemplates.exists():
|
if self.powerporttemplates.exists():
|
||||||
data['power-ports'] = [
|
data['power-ports'] = [
|
||||||
{
|
c.to_yaml() for c in self.powerporttemplates.all()
|
||||||
'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()
|
|
||||||
]
|
]
|
||||||
if self.poweroutlettemplates.exists():
|
if self.poweroutlettemplates.exists():
|
||||||
data['power-outlets'] = [
|
data['power-outlets'] = [
|
||||||
{
|
c.to_yaml() for c in self.poweroutlettemplates.all()
|
||||||
'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()
|
|
||||||
]
|
]
|
||||||
if self.interfacetemplates.exists():
|
if self.interfacetemplates.exists():
|
||||||
data['interfaces'] = [
|
data['interfaces'] = [
|
||||||
{
|
c.to_yaml() for c in self.interfacetemplates.all()
|
||||||
'name': c.name,
|
|
||||||
'type': c.type,
|
|
||||||
'mgmt_only': c.mgmt_only,
|
|
||||||
'label': c.label,
|
|
||||||
'description': c.description,
|
|
||||||
'poe_mode': c.poe_mode,
|
|
||||||
'poe_type': c.poe_type,
|
|
||||||
}
|
|
||||||
for c in self.interfacetemplates.all()
|
|
||||||
]
|
]
|
||||||
if self.frontporttemplates.exists():
|
if self.frontporttemplates.exists():
|
||||||
data['front-ports'] = [
|
data['front-ports'] = [
|
||||||
{
|
c.to_yaml() for c in self.frontporttemplates.all()
|
||||||
'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()
|
|
||||||
]
|
]
|
||||||
if self.rearporttemplates.exists():
|
if self.rearporttemplates.exists():
|
||||||
data['rear-ports'] = [
|
data['rear-ports'] = [
|
||||||
{
|
c.to_yaml() for c in self.rearporttemplates.all()
|
||||||
'name': c.name,
|
|
||||||
'type': c.type,
|
|
||||||
'positions': c.positions,
|
|
||||||
'label': c.label,
|
|
||||||
'description': c.description,
|
|
||||||
}
|
|
||||||
for c in self.rearporttemplates.all()
|
|
||||||
]
|
]
|
||||||
if self.modulebaytemplates.exists():
|
if self.modulebaytemplates.exists():
|
||||||
data['module-bays'] = [
|
data['module-bays'] = [
|
||||||
{
|
c.to_yaml() for c in self.modulebaytemplates.all()
|
||||||
'name': c.name,
|
|
||||||
'label': c.label,
|
|
||||||
'position': c.position,
|
|
||||||
'description': c.description,
|
|
||||||
}
|
|
||||||
for c in self.modulebaytemplates.all()
|
|
||||||
]
|
]
|
||||||
if self.devicebaytemplates.exists():
|
if self.devicebaytemplates.exists():
|
||||||
data['device-bays'] = [
|
data['device-bays'] = [
|
||||||
{
|
c.to_yaml() for c in self.devicebaytemplates.all()
|
||||||
'name': c.name,
|
|
||||||
'label': c.label,
|
|
||||||
'description': c.description,
|
|
||||||
}
|
|
||||||
for c in self.devicebaytemplates.all()
|
|
||||||
]
|
]
|
||||||
|
|
||||||
return yaml.dump(dict(data), sort_keys=False)
|
return yaml.dump(dict(data), sort_keys=False)
|
||||||
@ -406,91 +342,41 @@ class ModuleType(NetBoxModel):
|
|||||||
return reverse('dcim:moduletype', args=[self.pk])
|
return reverse('dcim:moduletype', args=[self.pk])
|
||||||
|
|
||||||
def to_yaml(self):
|
def to_yaml(self):
|
||||||
data = OrderedDict((
|
data = {
|
||||||
('manufacturer', self.manufacturer.name),
|
'manufacturer': self.manufacturer.name,
|
||||||
('model', self.model),
|
'model': self.model,
|
||||||
('part_number', self.part_number),
|
'part_number': self.part_number,
|
||||||
('comments', self.comments),
|
'comments': self.comments,
|
||||||
))
|
}
|
||||||
|
|
||||||
# Component templates
|
# Component templates
|
||||||
if self.consoleporttemplates.exists():
|
if self.consoleporttemplates.exists():
|
||||||
data['console-ports'] = [
|
data['console-ports'] = [
|
||||||
{
|
c.to_yaml() for c in self.consoleporttemplates.all()
|
||||||
'name': c.name,
|
|
||||||
'type': c.type,
|
|
||||||
'label': c.label,
|
|
||||||
'description': c.description,
|
|
||||||
}
|
|
||||||
for c in self.consoleporttemplates.all()
|
|
||||||
]
|
]
|
||||||
if self.consoleserverporttemplates.exists():
|
if self.consoleserverporttemplates.exists():
|
||||||
data['console-server-ports'] = [
|
data['console-server-ports'] = [
|
||||||
{
|
c.to_yaml() for c in self.consoleserverporttemplates.all()
|
||||||
'name': c.name,
|
|
||||||
'type': c.type,
|
|
||||||
'label': c.label,
|
|
||||||
'description': c.description,
|
|
||||||
}
|
|
||||||
for c in self.consoleserverporttemplates.all()
|
|
||||||
]
|
]
|
||||||
if self.powerporttemplates.exists():
|
if self.powerporttemplates.exists():
|
||||||
data['power-ports'] = [
|
data['power-ports'] = [
|
||||||
{
|
c.to_yaml() for c in self.powerporttemplates.all()
|
||||||
'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()
|
|
||||||
]
|
]
|
||||||
if self.poweroutlettemplates.exists():
|
if self.poweroutlettemplates.exists():
|
||||||
data['power-outlets'] = [
|
data['power-outlets'] = [
|
||||||
{
|
c.to_yaml() for c in self.poweroutlettemplates.all()
|
||||||
'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()
|
|
||||||
]
|
]
|
||||||
if self.interfacetemplates.exists():
|
if self.interfacetemplates.exists():
|
||||||
data['interfaces'] = [
|
data['interfaces'] = [
|
||||||
{
|
c.to_yaml() for c in self.interfacetemplates.all()
|
||||||
'name': c.name,
|
|
||||||
'type': c.type,
|
|
||||||
'mgmt_only': c.mgmt_only,
|
|
||||||
'label': c.label,
|
|
||||||
'description': c.description,
|
|
||||||
}
|
|
||||||
for c in self.interfacetemplates.all()
|
|
||||||
]
|
]
|
||||||
if self.frontporttemplates.exists():
|
if self.frontporttemplates.exists():
|
||||||
data['front-ports'] = [
|
data['front-ports'] = [
|
||||||
{
|
c.to_yaml() for c in self.frontporttemplates.all()
|
||||||
'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()
|
|
||||||
]
|
]
|
||||||
if self.rearporttemplates.exists():
|
if self.rearporttemplates.exists():
|
||||||
data['rear-ports'] = [
|
data['rear-ports'] = [
|
||||||
{
|
c.to_yaml() for c in self.rearporttemplates.all()
|
||||||
'name': c.name,
|
|
||||||
'type': c.type,
|
|
||||||
'positions': c.positions,
|
|
||||||
'label': c.label,
|
|
||||||
'description': c.description,
|
|
||||||
}
|
|
||||||
for c in self.rearporttemplates.all()
|
|
||||||
]
|
]
|
||||||
|
|
||||||
return yaml.dump(dict(data), sort_keys=False)
|
return yaml.dump(dict(data), sort_keys=False)
|
||||||
|
@ -116,7 +116,10 @@ def retrace_cable_paths(instance, **kwargs):
|
|||||||
@receiver(post_delete, sender=CableTermination)
|
@receiver(post_delete, sender=CableTermination)
|
||||||
def nullify_connected_endpoints(instance, **kwargs):
|
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 = instance.termination_type.model_class()
|
||||||
model.objects.filter(pk=instance.termination_id).update(cable=None, cable_end='')
|
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)
|
terminations = self.draw_terminations(far_ends)
|
||||||
for term in terminations:
|
for term in terminations:
|
||||||
self.draw_fanout(term, cable)
|
self.draw_fanout(term, cable)
|
||||||
else:
|
elif far_ends:
|
||||||
self.draw_terminations(far_ends)
|
self.draw_terminations(far_ends)
|
||||||
|
else:
|
||||||
|
# Link is not connected to anything
|
||||||
|
break
|
||||||
|
|
||||||
# Far end parent
|
# Far end parent
|
||||||
parent_objects = set(end.parent_object for end in far_ends)
|
parent_objects = set(end.parent_object for end in far_ends)
|
||||||
self.draw_parent_objects(parent_objects)
|
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:
|
elif far_ends:
|
||||||
|
|
||||||
# Attachment
|
# Attachment
|
||||||
attachment = self.draw_attachment()
|
attachment = self.draw_attachment()
|
||||||
self.connectors.append(attachment)
|
self.connectors.append(attachment)
|
||||||
|
|
||||||
# ProviderNetwork
|
# Object
|
||||||
self.draw_parent_objects(set(end.parent_object for end in far_ends))
|
self.draw_parent_objects(far_ends)
|
||||||
|
|
||||||
# Determine drawing size
|
# Determine drawing size
|
||||||
self.drawing = svgwrite.Drawing(
|
self.drawing = svgwrite.Drawing(
|
||||||
|
@ -14,6 +14,9 @@ class ModuleTypeTable(NetBoxTable):
|
|||||||
linkify=True,
|
linkify=True,
|
||||||
verbose_name='Module Type'
|
verbose_name='Module Type'
|
||||||
)
|
)
|
||||||
|
manufacturer = tables.Column(
|
||||||
|
linkify=True
|
||||||
|
)
|
||||||
instance_count = columns.LinkedCountColumn(
|
instance_count = columns.LinkedCountColumn(
|
||||||
viewname='dcim:module_list',
|
viewname='dcim:module_list',
|
||||||
url_params={'module_type_id': 'pk'},
|
url_params={'module_type_id': 'pk'},
|
||||||
@ -41,6 +44,10 @@ class ModuleTable(NetBoxTable):
|
|||||||
module_bay = tables.Column(
|
module_bay = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
|
manufacturer = tables.Column(
|
||||||
|
accessor=tables.A('module_type__manufacturer'),
|
||||||
|
linkify=True
|
||||||
|
)
|
||||||
module_type = tables.Column(
|
module_type = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
@ -52,8 +59,9 @@ class ModuleTable(NetBoxTable):
|
|||||||
class Meta(NetBoxTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = Module
|
model = Module
|
||||||
fields = (
|
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 = (
|
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(
|
site = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
|
location = tables.Column(
|
||||||
|
linkify=True
|
||||||
|
)
|
||||||
powerfeed_count = columns.LinkedCountColumn(
|
powerfeed_count = columns.LinkedCountColumn(
|
||||||
viewname='dcim:powerfeed_list',
|
viewname='dcim:powerfeed_list',
|
||||||
url_params={'power_panel_id': 'pk'},
|
url_params={'power_panel_id': 'pk'},
|
||||||
@ -35,7 +38,9 @@ class PowerPanelTable(NetBoxTable):
|
|||||||
|
|
||||||
class Meta(NetBoxTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = PowerPanel
|
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')
|
default_columns = ('pk', 'name', 'site', 'location', 'powerfeed_count')
|
||||||
|
|
||||||
|
|
||||||
|
@ -109,6 +109,10 @@ class RackReservationTable(TenancyColumnsMixin, NetBoxTable):
|
|||||||
accessor=Accessor('rack__site'),
|
accessor=Accessor('rack__site'),
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
|
location = tables.Column(
|
||||||
|
accessor=Accessor('rack__location'),
|
||||||
|
linkify=True
|
||||||
|
)
|
||||||
rack = tables.Column(
|
rack = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
@ -123,7 +127,7 @@ class RackReservationTable(TenancyColumnsMixin, NetBoxTable):
|
|||||||
class Meta(NetBoxTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = RackReservation
|
model = RackReservation
|
||||||
fields = (
|
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',
|
'actions', 'created', 'last_updated',
|
||||||
)
|
)
|
||||||
default_columns = ('pk', 'reservation', 'site', 'rack', 'unit_list', 'user', 'description')
|
default_columns = ('pk', 'reservation', 'site', 'rack', 'unit_list', 'user', 'description')
|
||||||
|
@ -163,8 +163,8 @@ class RackTestCase(TestCase):
|
|||||||
}
|
}
|
||||||
self.assertEqual(rack1_inventory_front[10.0]['device'], device1)
|
self.assertEqual(rack1_inventory_front[10.0]['device'], device1)
|
||||||
self.assertEqual(rack1_inventory_front[10.5]['device'], device1)
|
self.assertEqual(rack1_inventory_front[10.5]['device'], device1)
|
||||||
del(rack1_inventory_front[10.0])
|
del rack1_inventory_front[10.0]
|
||||||
del(rack1_inventory_front[10.5])
|
del rack1_inventory_front[10.5]
|
||||||
for u in rack1_inventory_front.values():
|
for u in rack1_inventory_front.values():
|
||||||
self.assertIsNone(u['device'])
|
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.0]['device'], device1)
|
||||||
self.assertEqual(rack1_inventory_rear[10.5]['device'], device1)
|
self.assertEqual(rack1_inventory_rear[10.5]['device'], device1)
|
||||||
del(rack1_inventory_rear[10.0])
|
del rack1_inventory_rear[10.0]
|
||||||
del(rack1_inventory_rear[10.5])
|
del rack1_inventory_rear[10.5]
|
||||||
for u in rack1_inventory_rear.values():
|
for u in rack1_inventory_rear.values():
|
||||||
self.assertIsNone(u['device'])
|
self.assertIsNone(u['device'])
|
||||||
|
|
||||||
|
@ -24,11 +24,12 @@ def object_to_path_node(obj):
|
|||||||
|
|
||||||
def path_node_to_object(repr):
|
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_id, object_id = decompile_path_node(repr)
|
||||||
ct = ContentType.objects.get_for_id(ct_id)
|
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):
|
def create_cablepath(terminations):
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
from collections import OrderedDict
|
|
||||||
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.core.paginator import EmptyPage, PageNotAnInteger
|
from django.core.paginator import EmptyPage, PageNotAnInteger
|
||||||
@ -324,7 +322,7 @@ class SiteListView(generic.ObjectListView):
|
|||||||
|
|
||||||
|
|
||||||
class SiteView(generic.ObjectView):
|
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):
|
def get_extra_context(self, request, instance):
|
||||||
stats = {
|
stats = {
|
||||||
@ -359,7 +357,7 @@ class SiteView(generic.ObjectView):
|
|||||||
site=instance,
|
site=instance,
|
||||||
position__isnull=True,
|
position__isnull=True,
|
||||||
parent_bay__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)
|
asns = ASN.objects.restrict(request.user, 'view').filter(sites=instance)
|
||||||
asn_count = asns.count()
|
asn_count = asns.count()
|
||||||
@ -391,14 +389,14 @@ class SiteBulkImportView(generic.BulkImportView):
|
|||||||
|
|
||||||
|
|
||||||
class SiteBulkEditView(generic.BulkEditView):
|
class SiteBulkEditView(generic.BulkEditView):
|
||||||
queryset = Site.objects.prefetch_related('region', 'tenant')
|
queryset = Site.objects.all()
|
||||||
filterset = filtersets.SiteFilterSet
|
filterset = filtersets.SiteFilterSet
|
||||||
table = tables.SiteTable
|
table = tables.SiteTable
|
||||||
form = forms.SiteBulkEditForm
|
form = forms.SiteBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
class SiteBulkDeleteView(generic.BulkDeleteView):
|
class SiteBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = Site.objects.prefetch_related('region', 'tenant')
|
queryset = Site.objects.all()
|
||||||
filterset = filtersets.SiteFilterSet
|
filterset = filtersets.SiteFilterSet
|
||||||
table = tables.SiteTable
|
table = tables.SiteTable
|
||||||
|
|
||||||
@ -454,7 +452,7 @@ class LocationView(generic.ObjectView):
|
|||||||
location=instance,
|
location=instance,
|
||||||
position__isnull=True,
|
position__isnull=True,
|
||||||
parent_bay__isnull=True
|
parent_bay__isnull=True
|
||||||
).prefetch_related('device_type__manufacturer')
|
).prefetch_related('device_type__manufacturer', 'parent_bay', 'device_role')
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'rack_count': rack_count,
|
'rack_count': rack_count,
|
||||||
@ -572,7 +570,7 @@ class RackRoleBulkDeleteView(generic.BulkDeleteView):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class RackListView(generic.ObjectListView):
|
class RackListView(generic.ObjectListView):
|
||||||
queryset = Rack.objects.prefetch_related('devices__device_type').annotate(
|
queryset = Rack.objects.annotate(
|
||||||
device_count=count_related(Device, 'rack')
|
device_count=count_related(Device, 'rack')
|
||||||
)
|
)
|
||||||
filterset = filtersets.RackFilterSet
|
filterset = filtersets.RackFilterSet
|
||||||
@ -631,7 +629,7 @@ class RackView(generic.ObjectView):
|
|||||||
rack=instance,
|
rack=instance,
|
||||||
position__isnull=True,
|
position__isnull=True,
|
||||||
parent_bay__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)
|
peer_racks = Rack.objects.restrict(request.user, 'view').filter(site=instance.site)
|
||||||
|
|
||||||
@ -682,14 +680,14 @@ class RackBulkImportView(generic.BulkImportView):
|
|||||||
|
|
||||||
|
|
||||||
class RackBulkEditView(generic.BulkEditView):
|
class RackBulkEditView(generic.BulkEditView):
|
||||||
queryset = Rack.objects.prefetch_related('site', 'location', 'tenant', 'role')
|
queryset = Rack.objects.all()
|
||||||
filterset = filtersets.RackFilterSet
|
filterset = filtersets.RackFilterSet
|
||||||
table = tables.RackTable
|
table = tables.RackTable
|
||||||
form = forms.RackBulkEditForm
|
form = forms.RackBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
class RackBulkDeleteView(generic.BulkDeleteView):
|
class RackBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = Rack.objects.prefetch_related('site', 'location', 'tenant', 'role')
|
queryset = Rack.objects.all()
|
||||||
filterset = filtersets.RackFilterSet
|
filterset = filtersets.RackFilterSet
|
||||||
table = tables.RackTable
|
table = tables.RackTable
|
||||||
|
|
||||||
@ -706,7 +704,7 @@ class RackReservationListView(generic.ObjectListView):
|
|||||||
|
|
||||||
|
|
||||||
class RackReservationView(generic.ObjectView):
|
class RackReservationView(generic.ObjectView):
|
||||||
queryset = RackReservation.objects.prefetch_related('rack')
|
queryset = RackReservation.objects.all()
|
||||||
|
|
||||||
|
|
||||||
class RackReservationEditView(generic.ObjectEditView):
|
class RackReservationEditView(generic.ObjectEditView):
|
||||||
@ -742,14 +740,14 @@ class RackReservationImportView(generic.BulkImportView):
|
|||||||
|
|
||||||
|
|
||||||
class RackReservationBulkEditView(generic.BulkEditView):
|
class RackReservationBulkEditView(generic.BulkEditView):
|
||||||
queryset = RackReservation.objects.prefetch_related('rack', 'user')
|
queryset = RackReservation.objects.all()
|
||||||
filterset = filtersets.RackReservationFilterSet
|
filterset = filtersets.RackReservationFilterSet
|
||||||
table = tables.RackReservationTable
|
table = tables.RackReservationTable
|
||||||
form = forms.RackReservationBulkEditForm
|
form = forms.RackReservationBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
class RackReservationBulkDeleteView(generic.BulkDeleteView):
|
class RackReservationBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = RackReservation.objects.prefetch_related('rack', 'user')
|
queryset = RackReservation.objects.all()
|
||||||
filterset = filtersets.RackReservationFilterSet
|
filterset = filtersets.RackReservationFilterSet
|
||||||
table = tables.RackReservationTable
|
table = tables.RackReservationTable
|
||||||
|
|
||||||
@ -831,7 +829,7 @@ class ManufacturerBulkDeleteView(generic.BulkDeleteView):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class DeviceTypeListView(generic.ObjectListView):
|
class DeviceTypeListView(generic.ObjectListView):
|
||||||
queryset = DeviceType.objects.prefetch_related('manufacturer').annotate(
|
queryset = DeviceType.objects.annotate(
|
||||||
instance_count=count_related(Device, 'device_type')
|
instance_count=count_related(Device, 'device_type')
|
||||||
)
|
)
|
||||||
filterset = filtersets.DeviceTypeFilterSet
|
filterset = filtersets.DeviceTypeFilterSet
|
||||||
@ -840,7 +838,7 @@ class DeviceTypeListView(generic.ObjectListView):
|
|||||||
|
|
||||||
|
|
||||||
class DeviceTypeView(generic.ObjectView):
|
class DeviceTypeView(generic.ObjectView):
|
||||||
queryset = DeviceType.objects.prefetch_related('manufacturer')
|
queryset = DeviceType.objects.all()
|
||||||
|
|
||||||
def get_extra_context(self, request, instance):
|
def get_extra_context(self, request, instance):
|
||||||
instance_count = Device.objects.restrict(request.user).filter(device_type=instance).count()
|
instance_count = Device.objects.restrict(request.user).filter(device_type=instance).count()
|
||||||
@ -945,18 +943,18 @@ class DeviceTypeImportView(generic.ObjectImportView):
|
|||||||
]
|
]
|
||||||
queryset = DeviceType.objects.all()
|
queryset = DeviceType.objects.all()
|
||||||
model_form = forms.DeviceTypeImportForm
|
model_form = forms.DeviceTypeImportForm
|
||||||
related_object_forms = OrderedDict((
|
related_object_forms = {
|
||||||
('console-ports', forms.ConsolePortTemplateImportForm),
|
'console-ports': forms.ConsolePortTemplateImportForm,
|
||||||
('console-server-ports', forms.ConsoleServerPortTemplateImportForm),
|
'console-server-ports': forms.ConsoleServerPortTemplateImportForm,
|
||||||
('power-ports', forms.PowerPortTemplateImportForm),
|
'power-ports': forms.PowerPortTemplateImportForm,
|
||||||
('power-outlets', forms.PowerOutletTemplateImportForm),
|
'power-outlets': forms.PowerOutletTemplateImportForm,
|
||||||
('interfaces', forms.InterfaceTemplateImportForm),
|
'interfaces': forms.InterfaceTemplateImportForm,
|
||||||
('rear-ports', forms.RearPortTemplateImportForm),
|
'rear-ports': forms.RearPortTemplateImportForm,
|
||||||
('front-ports', forms.FrontPortTemplateImportForm),
|
'front-ports': forms.FrontPortTemplateImportForm,
|
||||||
('module-bays', forms.ModuleBayTemplateImportForm),
|
'module-bays': forms.ModuleBayTemplateImportForm,
|
||||||
('device-bays', forms.DeviceBayTemplateImportForm),
|
'device-bays': forms.DeviceBayTemplateImportForm,
|
||||||
('inventory-items', forms.InventoryItemTemplateImportForm),
|
'inventory-items': forms.InventoryItemTemplateImportForm,
|
||||||
))
|
}
|
||||||
|
|
||||||
def prep_related_object_data(self, parent, data):
|
def prep_related_object_data(self, parent, data):
|
||||||
data.update({'device_type': parent})
|
data.update({'device_type': parent})
|
||||||
@ -964,7 +962,7 @@ class DeviceTypeImportView(generic.ObjectImportView):
|
|||||||
|
|
||||||
|
|
||||||
class DeviceTypeBulkEditView(generic.BulkEditView):
|
class DeviceTypeBulkEditView(generic.BulkEditView):
|
||||||
queryset = DeviceType.objects.prefetch_related('manufacturer').annotate(
|
queryset = DeviceType.objects.annotate(
|
||||||
instance_count=count_related(Device, 'device_type')
|
instance_count=count_related(Device, 'device_type')
|
||||||
)
|
)
|
||||||
filterset = filtersets.DeviceTypeFilterSet
|
filterset = filtersets.DeviceTypeFilterSet
|
||||||
@ -973,7 +971,7 @@ class DeviceTypeBulkEditView(generic.BulkEditView):
|
|||||||
|
|
||||||
|
|
||||||
class DeviceTypeBulkDeleteView(generic.BulkDeleteView):
|
class DeviceTypeBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = DeviceType.objects.prefetch_related('manufacturer').annotate(
|
queryset = DeviceType.objects.annotate(
|
||||||
instance_count=count_related(Device, 'device_type')
|
instance_count=count_related(Device, 'device_type')
|
||||||
)
|
)
|
||||||
filterset = filtersets.DeviceTypeFilterSet
|
filterset = filtersets.DeviceTypeFilterSet
|
||||||
@ -985,7 +983,7 @@ class DeviceTypeBulkDeleteView(generic.BulkDeleteView):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class ModuleTypeListView(generic.ObjectListView):
|
class ModuleTypeListView(generic.ObjectListView):
|
||||||
queryset = ModuleType.objects.prefetch_related('manufacturer').annotate(
|
queryset = ModuleType.objects.annotate(
|
||||||
instance_count=count_related(Module, 'module_type')
|
instance_count=count_related(Module, 'module_type')
|
||||||
)
|
)
|
||||||
filterset = filtersets.ModuleTypeFilterSet
|
filterset = filtersets.ModuleTypeFilterSet
|
||||||
@ -994,7 +992,7 @@ class ModuleTypeListView(generic.ObjectListView):
|
|||||||
|
|
||||||
|
|
||||||
class ModuleTypeView(generic.ObjectView):
|
class ModuleTypeView(generic.ObjectView):
|
||||||
queryset = ModuleType.objects.prefetch_related('manufacturer')
|
queryset = ModuleType.objects.all()
|
||||||
|
|
||||||
def get_extra_context(self, request, instance):
|
def get_extra_context(self, request, instance):
|
||||||
instance_count = Module.objects.restrict(request.user).filter(module_type=instance).count()
|
instance_count = Module.objects.restrict(request.user).filter(module_type=instance).count()
|
||||||
@ -1075,15 +1073,15 @@ class ModuleTypeImportView(generic.ObjectImportView):
|
|||||||
]
|
]
|
||||||
queryset = ModuleType.objects.all()
|
queryset = ModuleType.objects.all()
|
||||||
model_form = forms.ModuleTypeImportForm
|
model_form = forms.ModuleTypeImportForm
|
||||||
related_object_forms = OrderedDict((
|
related_object_forms = {
|
||||||
('console-ports', forms.ConsolePortTemplateImportForm),
|
'console-ports': forms.ConsolePortTemplateImportForm,
|
||||||
('console-server-ports', forms.ConsoleServerPortTemplateImportForm),
|
'console-server-ports': forms.ConsoleServerPortTemplateImportForm,
|
||||||
('power-ports', forms.PowerPortTemplateImportForm),
|
'power-ports': forms.PowerPortTemplateImportForm,
|
||||||
('power-outlets', forms.PowerOutletTemplateImportForm),
|
'power-outlets': forms.PowerOutletTemplateImportForm,
|
||||||
('interfaces', forms.InterfaceTemplateImportForm),
|
'interfaces': forms.InterfaceTemplateImportForm,
|
||||||
('rear-ports', forms.RearPortTemplateImportForm),
|
'rear-ports': forms.RearPortTemplateImportForm,
|
||||||
('front-ports', forms.FrontPortTemplateImportForm),
|
'front-ports': forms.FrontPortTemplateImportForm,
|
||||||
))
|
}
|
||||||
|
|
||||||
def prep_related_object_data(self, parent, data):
|
def prep_related_object_data(self, parent, data):
|
||||||
data.update({'module_type': parent})
|
data.update({'module_type': parent})
|
||||||
@ -1091,7 +1089,7 @@ class ModuleTypeImportView(generic.ObjectImportView):
|
|||||||
|
|
||||||
|
|
||||||
class ModuleTypeBulkEditView(generic.BulkEditView):
|
class ModuleTypeBulkEditView(generic.BulkEditView):
|
||||||
queryset = ModuleType.objects.prefetch_related('manufacturer').annotate(
|
queryset = ModuleType.objects.annotate(
|
||||||
instance_count=count_related(Module, 'module_type')
|
instance_count=count_related(Module, 'module_type')
|
||||||
)
|
)
|
||||||
filterset = filtersets.ModuleTypeFilterSet
|
filterset = filtersets.ModuleTypeFilterSet
|
||||||
@ -1100,7 +1098,7 @@ class ModuleTypeBulkEditView(generic.BulkEditView):
|
|||||||
|
|
||||||
|
|
||||||
class ModuleTypeBulkDeleteView(generic.BulkDeleteView):
|
class ModuleTypeBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = ModuleType.objects.prefetch_related('manufacturer').annotate(
|
queryset = ModuleType.objects.annotate(
|
||||||
instance_count=count_related(Module, 'module_type')
|
instance_count=count_related(Module, 'module_type')
|
||||||
)
|
)
|
||||||
filterset = filtersets.ModuleTypeFilterSet
|
filterset = filtersets.ModuleTypeFilterSet
|
||||||
@ -1611,9 +1609,7 @@ class DeviceListView(generic.ObjectListView):
|
|||||||
|
|
||||||
|
|
||||||
class DeviceView(generic.ObjectView):
|
class DeviceView(generic.ObjectView):
|
||||||
queryset = Device.objects.prefetch_related(
|
queryset = Device.objects.all()
|
||||||
'site__region', 'location', 'rack', 'tenant__group', 'device_role', 'platform', 'primary_ip4', 'primary_ip6'
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_extra_context(self, request, instance):
|
def get_extra_context(self, request, instance):
|
||||||
# VirtualChassis members
|
# VirtualChassis members
|
||||||
@ -1790,14 +1786,14 @@ class ChildDeviceBulkImportView(generic.BulkImportView):
|
|||||||
|
|
||||||
|
|
||||||
class DeviceBulkEditView(generic.BulkEditView):
|
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
|
filterset = filtersets.DeviceFilterSet
|
||||||
table = tables.DeviceTable
|
table = tables.DeviceTable
|
||||||
form = forms.DeviceBulkEditForm
|
form = forms.DeviceBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
class DeviceBulkDeleteView(generic.BulkDeleteView):
|
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
|
filterset = filtersets.DeviceFilterSet
|
||||||
table = tables.DeviceTable
|
table = tables.DeviceTable
|
||||||
|
|
||||||
@ -1807,7 +1803,7 @@ class DeviceBulkDeleteView(generic.BulkDeleteView):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class ModuleListView(generic.ObjectListView):
|
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 = filtersets.ModuleFilterSet
|
||||||
filterset_form = forms.ModuleFilterForm
|
filterset_form = forms.ModuleFilterForm
|
||||||
table = tables.ModuleTable
|
table = tables.ModuleTable
|
||||||
@ -1833,14 +1829,14 @@ class ModuleBulkImportView(generic.BulkImportView):
|
|||||||
|
|
||||||
|
|
||||||
class ModuleBulkEditView(generic.BulkEditView):
|
class ModuleBulkEditView(generic.BulkEditView):
|
||||||
queryset = Module.objects.prefetch_related('device', 'module_type__manufacturer')
|
queryset = Module.objects.prefetch_related('module_type__manufacturer')
|
||||||
filterset = filtersets.ModuleFilterSet
|
filterset = filtersets.ModuleFilterSet
|
||||||
table = tables.ModuleTable
|
table = tables.ModuleTable
|
||||||
form = forms.ModuleBulkEditForm
|
form = forms.ModuleBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
class ModuleBulkDeleteView(generic.BulkDeleteView):
|
class ModuleBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = Module.objects.prefetch_related('device', 'module_type__manufacturer')
|
queryset = Module.objects.prefetch_related('module_type__manufacturer')
|
||||||
filterset = filtersets.ModuleFilterSet
|
filterset = filtersets.ModuleFilterSet
|
||||||
table = tables.ModuleTable
|
table = tables.ModuleTable
|
||||||
|
|
||||||
@ -2566,7 +2562,7 @@ class InventoryItemBulkImportView(generic.BulkImportView):
|
|||||||
|
|
||||||
|
|
||||||
class InventoryItemBulkEditView(generic.BulkEditView):
|
class InventoryItemBulkEditView(generic.BulkEditView):
|
||||||
queryset = InventoryItem.objects.prefetch_related('device', 'manufacturer', 'role')
|
queryset = InventoryItem.objects.all()
|
||||||
filterset = filtersets.InventoryItemFilterSet
|
filterset = filtersets.InventoryItemFilterSet
|
||||||
table = tables.InventoryItemTable
|
table = tables.InventoryItemTable
|
||||||
form = forms.InventoryItemBulkEditForm
|
form = forms.InventoryItemBulkEditForm
|
||||||
@ -2577,7 +2573,7 @@ class InventoryItemBulkRenameView(generic.BulkRenameView):
|
|||||||
|
|
||||||
|
|
||||||
class InventoryItemBulkDeleteView(generic.BulkDeleteView):
|
class InventoryItemBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = InventoryItem.objects.prefetch_related('device', 'manufacturer', 'role')
|
queryset = InventoryItem.objects.all()
|
||||||
table = tables.InventoryItemTable
|
table = tables.InventoryItemTable
|
||||||
template_name = 'dcim/inventoryitem_bulk_delete.html'
|
template_name = 'dcim/inventoryitem_bulk_delete.html'
|
||||||
|
|
||||||
@ -2867,14 +2863,20 @@ class CableBulkImportView(generic.BulkImportView):
|
|||||||
|
|
||||||
|
|
||||||
class CableBulkEditView(generic.BulkEditView):
|
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
|
filterset = filtersets.CableFilterSet
|
||||||
table = tables.CableTable
|
table = tables.CableTable
|
||||||
form = forms.CableBulkEditForm
|
form = forms.CableBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
class CableBulkDeleteView(generic.BulkDeleteView):
|
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
|
filterset = filtersets.CableFilterSet
|
||||||
table = tables.CableTable
|
table = tables.CableTable
|
||||||
|
|
||||||
@ -2930,7 +2932,7 @@ class InterfaceConnectionsListView(generic.ObjectListView):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class VirtualChassisListView(generic.ObjectListView):
|
class VirtualChassisListView(generic.ObjectListView):
|
||||||
queryset = VirtualChassis.objects.prefetch_related('master').annotate(
|
queryset = VirtualChassis.objects.annotate(
|
||||||
member_count=count_related(Device, 'virtual_chassis')
|
member_count=count_related(Device, 'virtual_chassis')
|
||||||
)
|
)
|
||||||
table = tables.VirtualChassisTable
|
table = tables.VirtualChassisTable
|
||||||
@ -3158,9 +3160,7 @@ class VirtualChassisBulkDeleteView(generic.BulkDeleteView):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class PowerPanelListView(generic.ObjectListView):
|
class PowerPanelListView(generic.ObjectListView):
|
||||||
queryset = PowerPanel.objects.prefetch_related(
|
queryset = PowerPanel.objects.annotate(
|
||||||
'site', 'location'
|
|
||||||
).annotate(
|
|
||||||
powerfeed_count=count_related(PowerFeed, 'power_panel')
|
powerfeed_count=count_related(PowerFeed, 'power_panel')
|
||||||
)
|
)
|
||||||
filterset = filtersets.PowerPanelFilterSet
|
filterset = filtersets.PowerPanelFilterSet
|
||||||
@ -3169,10 +3169,10 @@ class PowerPanelListView(generic.ObjectListView):
|
|||||||
|
|
||||||
|
|
||||||
class PowerPanelView(generic.ObjectView):
|
class PowerPanelView(generic.ObjectView):
|
||||||
queryset = PowerPanel.objects.prefetch_related('site', 'location')
|
queryset = PowerPanel.objects.all()
|
||||||
|
|
||||||
def get_extra_context(self, request, instance):
|
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(
|
powerfeed_table = tables.PowerFeedTable(
|
||||||
data=power_feeds,
|
data=power_feeds,
|
||||||
orderable=False
|
orderable=False
|
||||||
@ -3202,16 +3202,14 @@ class PowerPanelBulkImportView(generic.BulkImportView):
|
|||||||
|
|
||||||
|
|
||||||
class PowerPanelBulkEditView(generic.BulkEditView):
|
class PowerPanelBulkEditView(generic.BulkEditView):
|
||||||
queryset = PowerPanel.objects.prefetch_related('site', 'location')
|
queryset = PowerPanel.objects.all()
|
||||||
filterset = filtersets.PowerPanelFilterSet
|
filterset = filtersets.PowerPanelFilterSet
|
||||||
table = tables.PowerPanelTable
|
table = tables.PowerPanelTable
|
||||||
form = forms.PowerPanelBulkEditForm
|
form = forms.PowerPanelBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
class PowerPanelBulkDeleteView(generic.BulkDeleteView):
|
class PowerPanelBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = PowerPanel.objects.prefetch_related(
|
queryset = PowerPanel.objects.annotate(
|
||||||
'site', 'location'
|
|
||||||
).annotate(
|
|
||||||
powerfeed_count=count_related(PowerFeed, 'power_panel')
|
powerfeed_count=count_related(PowerFeed, 'power_panel')
|
||||||
)
|
)
|
||||||
filterset = filtersets.PowerPanelFilterSet
|
filterset = filtersets.PowerPanelFilterSet
|
||||||
@ -3230,7 +3228,7 @@ class PowerFeedListView(generic.ObjectListView):
|
|||||||
|
|
||||||
|
|
||||||
class PowerFeedView(generic.ObjectView):
|
class PowerFeedView(generic.ObjectView):
|
||||||
queryset = PowerFeed.objects.prefetch_related('power_panel', 'rack')
|
queryset = PowerFeed.objects.all()
|
||||||
|
|
||||||
|
|
||||||
class PowerFeedEditView(generic.ObjectEditView):
|
class PowerFeedEditView(generic.ObjectEditView):
|
||||||
@ -3249,7 +3247,7 @@ class PowerFeedBulkImportView(generic.BulkImportView):
|
|||||||
|
|
||||||
|
|
||||||
class PowerFeedBulkEditView(generic.BulkEditView):
|
class PowerFeedBulkEditView(generic.BulkEditView):
|
||||||
queryset = PowerFeed.objects.prefetch_related('power_panel', 'rack')
|
queryset = PowerFeed.objects.all()
|
||||||
filterset = filtersets.PowerFeedFilterSet
|
filterset = filtersets.PowerFeedFilterSet
|
||||||
table = tables.PowerFeedTable
|
table = tables.PowerFeedTable
|
||||||
form = forms.PowerFeedBulkEditForm
|
form = forms.PowerFeedBulkEditForm
|
||||||
@ -3260,6 +3258,6 @@ class PowerFeedBulkDisconnectView(BulkDisconnectView):
|
|||||||
|
|
||||||
|
|
||||||
class PowerFeedBulkDeleteView(generic.BulkDeleteView):
|
class PowerFeedBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = PowerFeed.objects.prefetch_related('power_panel', 'rack')
|
queryset = PowerFeed.objects.all()
|
||||||
filterset = filtersets.PowerFeedFilterSet
|
filterset = filtersets.PowerFeedFilterSet
|
||||||
table = tables.PowerFeedTable
|
table = tables.PowerFeedTable
|
||||||
|
@ -136,6 +136,7 @@ class WebhookForm(BootstrapMixin, forms.ModelForm):
|
|||||||
'http_method': StaticSelect(),
|
'http_method': StaticSelect(),
|
||||||
'additional_headers': forms.Textarea(attrs={'class': 'font-monospace'}),
|
'additional_headers': forms.Textarea(attrs={'class': 'font-monospace'}),
|
||||||
'body_template': forms.Textarea(attrs={'class': 'font-monospace'}),
|
'body_template': forms.Textarea(attrs={'class': 'font-monospace'}),
|
||||||
|
'conditions': forms.Textarea(attrs={'class': 'font-monospace'}),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -181,7 +181,7 @@ class CustomField(ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel):
|
|||||||
model = ct.model_class()
|
model = ct.model_class()
|
||||||
instances = model.objects.filter(**{f'custom_field_data__{self.name}__isnull': False})
|
instances = model.objects.filter(**{f'custom_field_data__{self.name}__isnull': False})
|
||||||
for instance in instances:
|
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)
|
model.objects.bulk_update(instances, ['custom_field_data'], batch_size=100)
|
||||||
|
|
||||||
def rename_object_data(self, old_name, new_name):
|
def rename_object_data(self, old_name, new_name):
|
||||||
|
@ -28,3 +28,4 @@ registry = Registry()
|
|||||||
registry['model_features'] = {
|
registry['model_features'] = {
|
||||||
feature: collections.defaultdict(set) for feature in EXTRAS_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 logging
|
||||||
import pkgutil
|
import pkgutil
|
||||||
import traceback
|
import traceback
|
||||||
from collections import OrderedDict
|
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
@ -114,7 +113,7 @@ class Report(object):
|
|||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
|
||||||
self._results = OrderedDict()
|
self._results = {}
|
||||||
self.active_test = None
|
self.active_test = None
|
||||||
self.failed = False
|
self.failed = False
|
||||||
|
|
||||||
@ -125,13 +124,13 @@ class Report(object):
|
|||||||
for method in dir(self):
|
for method in dir(self):
|
||||||
if method.startswith('test_') and callable(getattr(self, method)):
|
if method.startswith('test_') and callable(getattr(self, method)):
|
||||||
test_methods.append(method)
|
test_methods.append(method)
|
||||||
self._results[method] = OrderedDict([
|
self._results[method] = {
|
||||||
('success', 0),
|
'success': 0,
|
||||||
('info', 0),
|
'info': 0,
|
||||||
('warning', 0),
|
'warning': 0,
|
||||||
('failure', 0),
|
'failure': 0,
|
||||||
('log', []),
|
'log': [],
|
||||||
])
|
}
|
||||||
if not test_methods:
|
if not test_methods:
|
||||||
raise Exception("A report must contain at least one test method.")
|
raise Exception("A report must contain at least one test method.")
|
||||||
self.test_methods = test_methods
|
self.test_methods = test_methods
|
||||||
|
@ -6,7 +6,6 @@ import pkgutil
|
|||||||
import sys
|
import sys
|
||||||
import traceback
|
import traceback
|
||||||
import threading
|
import threading
|
||||||
from collections import OrderedDict
|
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
from django import forms
|
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-
|
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.
|
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
|
# Iterate through all modules within the scripts path. These are the user-created files in which reports are
|
||||||
# defined.
|
# defined.
|
||||||
for importer, module_name, _ in pkgutil.iter_modules([settings.SCRIPTS_ROOT]):
|
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'):
|
if use_names and hasattr(module, 'name'):
|
||||||
module_name = module.name
|
module_name = module.name
|
||||||
module_scripts = OrderedDict()
|
module_scripts = {}
|
||||||
script_order = getattr(module, "script_order", ())
|
script_order = getattr(module, "script_order", ())
|
||||||
ordered_scripts = [cls for cls in script_order if is_script(cls)]
|
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]
|
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 import template
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.utils.safestring import mark_safe
|
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
|
'perms': context['perms'], # django.contrib.auth.context_processors.auth
|
||||||
}
|
}
|
||||||
template_code = ''
|
template_code = ''
|
||||||
group_names = OrderedDict()
|
group_names = {}
|
||||||
|
|
||||||
for cl in custom_links:
|
for cl in custom_links:
|
||||||
|
|
||||||
|
@ -992,7 +992,7 @@ class CustomFieldModelTest(TestCase):
|
|||||||
with self.assertRaises(ValidationError):
|
with self.assertRaises(ValidationError):
|
||||||
site.clean()
|
site.clean()
|
||||||
|
|
||||||
del(site.cf['bar'])
|
del site.cf['bar']
|
||||||
site.clean()
|
site.clean()
|
||||||
|
|
||||||
def test_missing_required_field(self):
|
def test_missing_required_field(self):
|
||||||
|
@ -30,4 +30,4 @@ class RegistryTest(TestCase):
|
|||||||
reg['foo'] = 123
|
reg['foo'] = 123
|
||||||
|
|
||||||
with self.assertRaises(TypeError):
|
with self.assertRaises(TypeError):
|
||||||
del(reg['foo'])
|
del reg['foo']
|
||||||
|
@ -492,14 +492,14 @@ class JournalEntryDeleteView(generic.ObjectDeleteView):
|
|||||||
|
|
||||||
|
|
||||||
class JournalEntryBulkEditView(generic.BulkEditView):
|
class JournalEntryBulkEditView(generic.BulkEditView):
|
||||||
queryset = JournalEntry.objects.prefetch_related('created_by')
|
queryset = JournalEntry.objects.all()
|
||||||
filterset = filtersets.JournalEntryFilterSet
|
filterset = filtersets.JournalEntryFilterSet
|
||||||
table = tables.JournalEntryTable
|
table = tables.JournalEntryTable
|
||||||
form = forms.JournalEntryBulkEditForm
|
form = forms.JournalEntryBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
class JournalEntryBulkDeleteView(generic.BulkDeleteView):
|
class JournalEntryBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = JournalEntry.objects.prefetch_related('created_by')
|
queryset = JournalEntry.objects.all()
|
||||||
filterset = filtersets.JournalEntryFilterSet
|
filterset = filtersets.JournalEntryFilterSet
|
||||||
table = tables.JournalEntryTable
|
table = tables.JournalEntryTable
|
||||||
|
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
from collections import OrderedDict
|
|
||||||
|
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from drf_yasg.utils import swagger_serializer_method
|
from drf_yasg.utils import swagger_serializer_method
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
@ -227,13 +225,13 @@ class AvailableVLANSerializer(serializers.Serializer):
|
|||||||
group = NestedVLANGroupSerializer(read_only=True)
|
group = NestedVLANGroupSerializer(read_only=True)
|
||||||
|
|
||||||
def to_representation(self, instance):
|
def to_representation(self, instance):
|
||||||
return OrderedDict([
|
return {
|
||||||
('vid', instance),
|
'vid': instance,
|
||||||
('group', NestedVLANGroupSerializer(
|
'group': NestedVLANGroupSerializer(
|
||||||
self.context['group'],
|
self.context['group'],
|
||||||
context={'request': self.context['request']}
|
context={'request': self.context['request']}
|
||||||
).data),
|
).data,
|
||||||
])
|
}
|
||||||
|
|
||||||
|
|
||||||
class CreateAvailableVLANSerializer(NetBoxModelSerializer):
|
class CreateAvailableVLANSerializer(NetBoxModelSerializer):
|
||||||
@ -318,11 +316,11 @@ class AvailablePrefixSerializer(serializers.Serializer):
|
|||||||
vrf = NestedVRFSerializer(self.context['vrf'], context={'request': self.context['request']}).data
|
vrf = NestedVRFSerializer(self.context['vrf'], context={'request': self.context['request']}).data
|
||||||
else:
|
else:
|
||||||
vrf = None
|
vrf = None
|
||||||
return OrderedDict([
|
return {
|
||||||
('family', instance.version),
|
'family': instance.version,
|
||||||
('prefix', str(instance)),
|
'prefix': str(instance),
|
||||||
('vrf', vrf),
|
'vrf': vrf,
|
||||||
])
|
}
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -397,11 +395,11 @@ class AvailableIPSerializer(serializers.Serializer):
|
|||||||
vrf = NestedVRFSerializer(self.context['vrf'], context={'request': self.context['request']}).data
|
vrf = NestedVRFSerializer(self.context['vrf'], context={'request': self.context['request']}).data
|
||||||
else:
|
else:
|
||||||
vrf = None
|
vrf = None
|
||||||
return OrderedDict([
|
return {
|
||||||
('family', self.context['parent'].family),
|
'family': self.context['parent'].family,
|
||||||
('address', f"{instance}/{self.context['parent'].mask_length}"),
|
'address': f"{instance}/{self.context['parent'].mask_length}",
|
||||||
('vrf', vrf),
|
'vrf': vrf,
|
||||||
])
|
}
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
|
@ -980,21 +980,65 @@ class L2VPNTerminationFilterSet(NetBoxModelFilterSet):
|
|||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='L2VPN (slug)',
|
label='L2VPN (slug)',
|
||||||
)
|
)
|
||||||
device = MultiValueCharFilter(
|
region = MultiValueCharFilter(
|
||||||
method='filter_device',
|
method='filter_region',
|
||||||
field_name='name',
|
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)',
|
label='Device (name)',
|
||||||
)
|
)
|
||||||
device_id = MultiValueNumberFilter(
|
device_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
method='filter_device',
|
field_name='interface__device',
|
||||||
field_name='pk',
|
queryset=Device.objects.all(),
|
||||||
label='Device (ID)',
|
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(
|
interface_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
field_name='interface',
|
field_name='interface',
|
||||||
queryset=Interface.objects.all(),
|
queryset=Interface.objects.all(),
|
||||||
label='Interface (ID)',
|
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(
|
vminterface_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
field_name='vminterface',
|
field_name='vminterface',
|
||||||
queryset=VMInterface.objects.all(),
|
queryset=VMInterface.objects.all(),
|
||||||
@ -1027,13 +1071,22 @@ class L2VPNTerminationFilterSet(NetBoxModelFilterSet):
|
|||||||
qs_filter = Q(l2vpn__name__icontains=value)
|
qs_filter = Q(l2vpn__name__icontains=value)
|
||||||
return queryset.filter(qs_filter)
|
return queryset.filter(qs_filter)
|
||||||
|
|
||||||
def filter_device(self, queryset, name, value):
|
def filter_site(self, queryset, name, value):
|
||||||
devices = Device.objects.filter(**{'{}__in'.format(name): value})
|
qs = queryset.filter(
|
||||||
if not devices.exists():
|
Q(
|
||||||
return queryset.none()
|
Q(**{'vlan__site__{}__in'.format(name): value}) |
|
||||||
interface_ids = []
|
Q(**{'interface__device__site__{}__in'.format(name): value}) |
|
||||||
for device in devices:
|
Q(**{'vminterface__virtual_machine__site__{}__in'.format(name): value})
|
||||||
interface_ids.extend(device.vc_interfaces().values_list('id', flat=True))
|
|
||||||
return queryset.filter(
|
|
||||||
interface__in=interface_ids
|
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
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 tenancy.forms import TenancyFilterForm
|
||||||
from utilities.forms import (
|
from utilities.forms import (
|
||||||
add_blank_choice, ContentTypeMultipleChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField,
|
add_blank_choice, ContentTypeMultipleChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField,
|
||||||
MultipleChoiceField, StaticSelect, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES,
|
MultipleChoiceField, StaticSelect, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES, APISelectMultiple,
|
||||||
)
|
)
|
||||||
from virtualization.models import VirtualMachine
|
from virtualization.models import VirtualMachine
|
||||||
|
|
||||||
@ -508,7 +508,8 @@ class L2VPNFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
|||||||
class L2VPNTerminationFilterForm(NetBoxModelFilterSetForm):
|
class L2VPNTerminationFilterForm(NetBoxModelFilterSetForm):
|
||||||
model = L2VPNTermination
|
model = L2VPNTermination
|
||||||
fieldsets = (
|
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(
|
l2vpn_id = DynamicModelChoiceField(
|
||||||
queryset=L2VPN.objects.all(),
|
queryset=L2VPN.objects.all(),
|
||||||
@ -516,7 +517,49 @@ class L2VPNTerminationFilterForm(NetBoxModelFilterSetForm):
|
|||||||
label='L2VPN'
|
label='L2VPN'
|
||||||
)
|
)
|
||||||
assigned_object_type_id = ContentTypeMultipleChoiceField(
|
assigned_object_type_id = ContentTypeMultipleChoiceField(
|
||||||
queryset=ContentType.objects.all(),
|
queryset=ContentType.objects.filter(L2VPN_ASSIGNMENT_MODELS),
|
||||||
required=False,
|
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
|
# Fields which may be populated from a ServiceTemplate are not required
|
||||||
for field in ('name', 'protocol', 'ports'):
|
for field in ('name', 'protocol', 'ports'):
|
||||||
self.fields[field].required = False
|
self.fields[field].required = False
|
||||||
del(self.fields[field].widget.attrs['required'])
|
del self.fields[field].widget.attrs['required']
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
if self.cleaned_data['service_template']:
|
if self.cleaned_data['service_template']:
|
||||||
|
@ -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
|
# Cache the original prefix and VRF so we can check if they have changed on post_save
|
||||||
self._prefix = self.prefix
|
self._prefix = self.prefix
|
||||||
self._vrf = self.vrf
|
self._vrf_id = self.vrf_id
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return str(self.prefix)
|
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'{l2vpn_type} L2VPNs cannot have more than two terminations; found {terminations_count} already '
|
||||||
f'defined.'
|
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):
|
def handle_prefix_saved(instance, created, **kwargs):
|
||||||
|
|
||||||
# Prefix has changed (or new instance has been created)
|
# 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_parents_children(instance)
|
||||||
update_children_depth(instance)
|
update_children_depth(instance)
|
||||||
|
|
||||||
# If this is not a new prefix, clean up parent/children of previous prefix
|
# If this is not a new prefix, clean up parent/children of previous prefix
|
||||||
if not created:
|
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_parents_children(old_prefix)
|
||||||
update_children_depth(old_prefix)
|
update_children_depth(old_prefix)
|
||||||
|
|
||||||
|
@ -369,6 +369,11 @@ class IPAddressTable(TenancyColumnsMixin, NetBoxTable):
|
|||||||
orderable=False,
|
orderable=False,
|
||||||
verbose_name='NAT (Inside)'
|
verbose_name='NAT (Inside)'
|
||||||
)
|
)
|
||||||
|
nat_outside = tables.Column(
|
||||||
|
linkify=True,
|
||||||
|
orderable=False,
|
||||||
|
verbose_name='NAT (Outside)'
|
||||||
|
)
|
||||||
assigned = columns.BooleanColumn(
|
assigned = columns.BooleanColumn(
|
||||||
accessor='assigned_object_id',
|
accessor='assigned_object_id',
|
||||||
linkify=True,
|
linkify=True,
|
||||||
@ -381,7 +386,7 @@ class IPAddressTable(TenancyColumnsMixin, NetBoxTable):
|
|||||||
class Meta(NetBoxTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = IPAddress
|
model = IPAddress
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'address', 'vrf', 'status', 'role', 'tenant', 'tenant_group', 'nat_inside', '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',
|
'tags', 'created', 'last_updated',
|
||||||
)
|
)
|
||||||
default_columns = (
|
default_columns = (
|
||||||
|
@ -53,8 +53,17 @@ class L2VPNTerminationTable(NetBoxTable):
|
|||||||
linkify=True,
|
linkify=True,
|
||||||
orderable=False
|
orderable=False
|
||||||
)
|
)
|
||||||
|
assigned_object_parent = tables.Column(
|
||||||
|
linkify=True,
|
||||||
|
orderable=False
|
||||||
|
)
|
||||||
|
assigned_object_site = tables.Column(
|
||||||
|
linkify=True,
|
||||||
|
orderable=False
|
||||||
|
)
|
||||||
|
|
||||||
class Meta(NetBoxTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = L2VPNTermination
|
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')
|
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)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
params = {'vlan': ['VLAN 1', 'VLAN 2']}
|
params = {'vlan': ['VLAN 1', 'VLAN 2']}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 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()
|
ipaddress_count = IPAddress.objects.restrict(request.user, 'view').filter(vrf=instance).count()
|
||||||
|
|
||||||
import_targets_table = tables.RouteTargetTable(
|
import_targets_table = tables.RouteTargetTable(
|
||||||
instance.import_targets.prefetch_related('tenant'),
|
instance.import_targets.all(),
|
||||||
orderable=False
|
orderable=False
|
||||||
)
|
)
|
||||||
export_targets_table = tables.RouteTargetTable(
|
export_targets_table = tables.RouteTargetTable(
|
||||||
instance.export_targets.prefetch_related('tenant'),
|
instance.export_targets.all(),
|
||||||
orderable=False
|
orderable=False
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -72,14 +72,14 @@ class VRFBulkImportView(generic.BulkImportView):
|
|||||||
|
|
||||||
|
|
||||||
class VRFBulkEditView(generic.BulkEditView):
|
class VRFBulkEditView(generic.BulkEditView):
|
||||||
queryset = VRF.objects.prefetch_related('tenant')
|
queryset = VRF.objects.all()
|
||||||
filterset = filtersets.VRFFilterSet
|
filterset = filtersets.VRFFilterSet
|
||||||
table = tables.VRFTable
|
table = tables.VRFTable
|
||||||
form = forms.VRFBulkEditForm
|
form = forms.VRFBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
class VRFBulkDeleteView(generic.BulkDeleteView):
|
class VRFBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = VRF.objects.prefetch_related('tenant')
|
queryset = VRF.objects.all()
|
||||||
filterset = filtersets.VRFFilterSet
|
filterset = filtersets.VRFFilterSet
|
||||||
table = tables.VRFTable
|
table = tables.VRFTable
|
||||||
|
|
||||||
@ -100,11 +100,11 @@ class RouteTargetView(generic.ObjectView):
|
|||||||
|
|
||||||
def get_extra_context(self, request, instance):
|
def get_extra_context(self, request, instance):
|
||||||
importing_vrfs_table = tables.VRFTable(
|
importing_vrfs_table = tables.VRFTable(
|
||||||
instance.importing_vrfs.prefetch_related('tenant'),
|
instance.importing_vrfs.all(),
|
||||||
orderable=False
|
orderable=False
|
||||||
)
|
)
|
||||||
exporting_vrfs_table = tables.VRFTable(
|
exporting_vrfs_table = tables.VRFTable(
|
||||||
instance.exporting_vrfs.prefetch_related('tenant'),
|
instance.exporting_vrfs.all(),
|
||||||
orderable=False
|
orderable=False
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -130,14 +130,14 @@ class RouteTargetBulkImportView(generic.BulkImportView):
|
|||||||
|
|
||||||
|
|
||||||
class RouteTargetBulkEditView(generic.BulkEditView):
|
class RouteTargetBulkEditView(generic.BulkEditView):
|
||||||
queryset = RouteTarget.objects.prefetch_related('tenant')
|
queryset = RouteTarget.objects.all()
|
||||||
filterset = filtersets.RouteTargetFilterSet
|
filterset = filtersets.RouteTargetFilterSet
|
||||||
table = tables.RouteTargetTable
|
table = tables.RouteTargetTable
|
||||||
form = forms.RouteTargetBulkEditForm
|
form = forms.RouteTargetBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
class RouteTargetBulkDeleteView(generic.BulkDeleteView):
|
class RouteTargetBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = RouteTarget.objects.prefetch_related('tenant')
|
queryset = RouteTarget.objects.all()
|
||||||
filterset = filtersets.RouteTargetFilterSet
|
filterset = filtersets.RouteTargetFilterSet
|
||||||
table = tables.RouteTargetTable
|
table = tables.RouteTargetTable
|
||||||
|
|
||||||
@ -334,14 +334,18 @@ class AggregateBulkImportView(generic.BulkImportView):
|
|||||||
|
|
||||||
|
|
||||||
class AggregateBulkEditView(generic.BulkEditView):
|
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
|
filterset = filtersets.AggregateFilterSet
|
||||||
table = tables.AggregateTable
|
table = tables.AggregateTable
|
||||||
form = forms.AggregateBulkEditForm
|
form = forms.AggregateBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
class AggregateBulkDeleteView(generic.BulkDeleteView):
|
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
|
filterset = filtersets.AggregateFilterSet
|
||||||
table = tables.AggregateTable
|
table = tables.AggregateTable
|
||||||
|
|
||||||
@ -417,7 +421,7 @@ class PrefixListView(generic.ObjectListView):
|
|||||||
|
|
||||||
|
|
||||||
class PrefixView(generic.ObjectView):
|
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):
|
def get_extra_context(self, request, instance):
|
||||||
try:
|
try:
|
||||||
@ -433,7 +437,7 @@ class PrefixView(generic.ObjectView):
|
|||||||
).filter(
|
).filter(
|
||||||
prefix__net_contains=str(instance.prefix)
|
prefix__net_contains=str(instance.prefix)
|
||||||
).prefetch_related(
|
).prefetch_related(
|
||||||
'site', 'role', 'tenant'
|
'site', 'role', 'tenant', 'vlan',
|
||||||
)
|
)
|
||||||
parent_prefix_table = tables.PrefixTable(
|
parent_prefix_table = tables.PrefixTable(
|
||||||
list(parent_prefixes),
|
list(parent_prefixes),
|
||||||
@ -447,7 +451,7 @@ class PrefixView(generic.ObjectView):
|
|||||||
).exclude(
|
).exclude(
|
||||||
pk=instance.pk
|
pk=instance.pk
|
||||||
).prefetch_related(
|
).prefetch_related(
|
||||||
'site', 'role'
|
'site', 'role', 'tenant', 'vlan',
|
||||||
)
|
)
|
||||||
duplicate_prefix_table = tables.PrefixTable(
|
duplicate_prefix_table = tables.PrefixTable(
|
||||||
list(duplicate_prefixes),
|
list(duplicate_prefixes),
|
||||||
@ -500,7 +504,7 @@ class PrefixIPRangesView(generic.ObjectChildrenView):
|
|||||||
|
|
||||||
def get_children(self, request, parent):
|
def get_children(self, request, parent):
|
||||||
return parent.get_child_ranges().restrict(request.user, 'view').prefetch_related(
|
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):
|
def get_extra_context(self, request, instance):
|
||||||
@ -519,7 +523,7 @@ class PrefixIPAddressesView(generic.ObjectChildrenView):
|
|||||||
template_name = 'ipam/prefix/ip_addresses.html'
|
template_name = 'ipam/prefix/ip_addresses.html'
|
||||||
|
|
||||||
def get_children(self, request, parent):
|
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):
|
def prep_table_data(self, request, queryset, parent):
|
||||||
show_available = bool(request.GET.get('show_available', 'true') == 'true')
|
show_available = bool(request.GET.get('show_available', 'true') == 'true')
|
||||||
@ -552,14 +556,14 @@ class PrefixBulkImportView(generic.BulkImportView):
|
|||||||
|
|
||||||
|
|
||||||
class PrefixBulkEditView(generic.BulkEditView):
|
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
|
filterset = filtersets.PrefixFilterSet
|
||||||
table = tables.PrefixTable
|
table = tables.PrefixTable
|
||||||
form = forms.PrefixBulkEditForm
|
form = forms.PrefixBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
class PrefixBulkDeleteView(generic.BulkDeleteView):
|
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
|
filterset = filtersets.PrefixFilterSet
|
||||||
table = tables.PrefixTable
|
table = tables.PrefixTable
|
||||||
|
|
||||||
@ -611,14 +615,14 @@ class IPRangeBulkImportView(generic.BulkImportView):
|
|||||||
|
|
||||||
|
|
||||||
class IPRangeBulkEditView(generic.BulkEditView):
|
class IPRangeBulkEditView(generic.BulkEditView):
|
||||||
queryset = IPRange.objects.prefetch_related('vrf', 'tenant')
|
queryset = IPRange.objects.all()
|
||||||
filterset = filtersets.IPRangeFilterSet
|
filterset = filtersets.IPRangeFilterSet
|
||||||
table = tables.IPRangeTable
|
table = tables.IPRangeTable
|
||||||
form = forms.IPRangeBulkEditForm
|
form = forms.IPRangeBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
class IPRangeBulkDeleteView(generic.BulkDeleteView):
|
class IPRangeBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = IPRange.objects.prefetch_related('vrf', 'tenant')
|
queryset = IPRange.objects.all()
|
||||||
filterset = filtersets.IPRangeFilterSet
|
filterset = filtersets.IPRangeFilterSet
|
||||||
table = tables.IPRangeTable
|
table = tables.IPRangeTable
|
||||||
|
|
||||||
@ -789,14 +793,14 @@ class IPAddressBulkImportView(generic.BulkImportView):
|
|||||||
|
|
||||||
|
|
||||||
class IPAddressBulkEditView(generic.BulkEditView):
|
class IPAddressBulkEditView(generic.BulkEditView):
|
||||||
queryset = IPAddress.objects.prefetch_related('vrf__tenant', 'tenant')
|
queryset = IPAddress.objects.prefetch_related('vrf__tenant')
|
||||||
filterset = filtersets.IPAddressFilterSet
|
filterset = filtersets.IPAddressFilterSet
|
||||||
table = tables.IPAddressTable
|
table = tables.IPAddressTable
|
||||||
form = forms.IPAddressBulkEditForm
|
form = forms.IPAddressBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
class IPAddressBulkDeleteView(generic.BulkDeleteView):
|
class IPAddressBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = IPAddress.objects.prefetch_related('vrf__tenant', 'tenant')
|
queryset = IPAddress.objects.prefetch_related('vrf__tenant')
|
||||||
filterset = filtersets.IPAddressFilterSet
|
filterset = filtersets.IPAddressFilterSet
|
||||||
table = tables.IPAddressTable
|
table = tables.IPAddressTable
|
||||||
|
|
||||||
@ -819,7 +823,8 @@ class VLANGroupView(generic.ObjectView):
|
|||||||
|
|
||||||
def get_extra_context(self, request, instance):
|
def get_extra_context(self, request, instance):
|
||||||
vlans = VLAN.objects.restrict(request.user, 'view').filter(group=instance).prefetch_related(
|
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')
|
).order_by('vid')
|
||||||
vlans_count = vlans.count()
|
vlans_count = vlans.count()
|
||||||
vlans = add_available_vlans(vlans, vlan_group=instance)
|
vlans = add_available_vlans(vlans, vlan_group=instance)
|
||||||
@ -894,7 +899,7 @@ class FHRPGroupView(generic.ObjectView):
|
|||||||
def get_extra_context(self, request, instance):
|
def get_extra_context(self, request, instance):
|
||||||
# Get assigned IP addresses
|
# Get assigned IP addresses
|
||||||
ipaddress_table = tables.AssignedIPAddressesTable(
|
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
|
orderable=False
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -984,11 +989,11 @@ class VLANListView(generic.ObjectListView):
|
|||||||
|
|
||||||
|
|
||||||
class VLANView(generic.ObjectView):
|
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):
|
def get_extra_context(self, request, instance):
|
||||||
prefixes = Prefix.objects.restrict(request.user, 'view').filter(vlan=instance).prefetch_related(
|
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)
|
prefix_table = tables.PrefixTable(list(prefixes), exclude=('vlan', 'utilization'), orderable=False)
|
||||||
|
|
||||||
@ -1046,14 +1051,14 @@ class VLANBulkImportView(generic.BulkImportView):
|
|||||||
|
|
||||||
|
|
||||||
class VLANBulkEditView(generic.BulkEditView):
|
class VLANBulkEditView(generic.BulkEditView):
|
||||||
queryset = VLAN.objects.prefetch_related('site', 'group', 'tenant', 'role')
|
queryset = VLAN.objects.all()
|
||||||
filterset = filtersets.VLANFilterSet
|
filterset = filtersets.VLANFilterSet
|
||||||
table = tables.VLANTable
|
table = tables.VLANTable
|
||||||
form = forms.VLANBulkEditForm
|
form = forms.VLANBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
class VLANBulkDeleteView(generic.BulkDeleteView):
|
class VLANBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = VLAN.objects.prefetch_related('site', 'group', 'tenant', 'role')
|
queryset = VLAN.objects.all()
|
||||||
filterset = filtersets.VLANFilterSet
|
filterset = filtersets.VLANFilterSet
|
||||||
table = tables.VLANTable
|
table = tables.VLANTable
|
||||||
|
|
||||||
@ -1106,14 +1111,14 @@ class ServiceTemplateBulkDeleteView(generic.BulkDeleteView):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class ServiceListView(generic.ObjectListView):
|
class ServiceListView(generic.ObjectListView):
|
||||||
queryset = Service.objects.all()
|
queryset = Service.objects.prefetch_related('device', 'virtual_machine')
|
||||||
filterset = filtersets.ServiceFilterSet
|
filterset = filtersets.ServiceFilterSet
|
||||||
filterset_form = forms.ServiceFilterForm
|
filterset_form = forms.ServiceFilterForm
|
||||||
table = tables.ServiceTable
|
table = tables.ServiceTable
|
||||||
|
|
||||||
|
|
||||||
class ServiceView(generic.ObjectView):
|
class ServiceView(generic.ObjectView):
|
||||||
queryset = Service.objects.prefetch_related('ipaddresses')
|
queryset = Service.objects.all()
|
||||||
|
|
||||||
|
|
||||||
class ServiceCreateView(generic.ObjectEditView):
|
class ServiceCreateView(generic.ObjectEditView):
|
||||||
@ -1123,7 +1128,7 @@ class ServiceCreateView(generic.ObjectEditView):
|
|||||||
|
|
||||||
|
|
||||||
class ServiceEditView(generic.ObjectEditView):
|
class ServiceEditView(generic.ObjectEditView):
|
||||||
queryset = Service.objects.prefetch_related('ipaddresses')
|
queryset = Service.objects.all()
|
||||||
form = forms.ServiceForm
|
form = forms.ServiceForm
|
||||||
template_name = 'ipam/service_edit.html'
|
template_name = 'ipam/service_edit.html'
|
||||||
|
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
from collections import OrderedDict
|
|
||||||
|
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
from netaddr import IPNetwork
|
from netaddr import IPNetwork
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
@ -48,10 +46,10 @@ class ChoiceField(serializers.Field):
|
|||||||
def to_representation(self, obj):
|
def to_representation(self, obj):
|
||||||
if obj == '':
|
if obj == '':
|
||||||
return None
|
return None
|
||||||
return OrderedDict([
|
return {
|
||||||
('value', obj),
|
'value': obj,
|
||||||
('label', self._choices[obj])
|
'label': self._choices[obj],
|
||||||
])
|
}
|
||||||
|
|
||||||
def to_internal_value(self, data):
|
def to_internal_value(self, data):
|
||||||
if data == '':
|
if data == '':
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from drf_yasg.utils import swagger_serializer_method
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from netbox.api.fields import ContentTypeField
|
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
|
from utilities.utils import content_type_identifier
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
@ -17,6 +20,7 @@ class GenericObjectSerializer(serializers.Serializer):
|
|||||||
queryset=ContentType.objects.all()
|
queryset=ContentType.objects.all()
|
||||||
)
|
)
|
||||||
object_id = serializers.IntegerField()
|
object_id = serializers.IntegerField()
|
||||||
|
object = serializers.SerializerMethodField(read_only=True)
|
||||||
|
|
||||||
def to_internal_value(self, data):
|
def to_internal_value(self, data):
|
||||||
data = super().to_internal_value(data)
|
data = super().to_internal_value(data)
|
||||||
@ -25,7 +29,17 @@ class GenericObjectSerializer(serializers.Serializer):
|
|||||||
|
|
||||||
def to_representation(self, instance):
|
def to_representation(self, instance):
|
||||||
ct = ContentType.objects.get_for_model(instance)
|
ct = ContentType.objects.get_for_model(instance)
|
||||||
return {
|
data = {
|
||||||
'object_type': content_type_identifier(ct),
|
'object_type': content_type_identifier(ct),
|
||||||
'object_id': instance.pk,
|
'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
|
import platform
|
||||||
from collections import OrderedDict
|
|
||||||
|
|
||||||
from django import __version__ as DJANGO_VERSION
|
from django import __version__ as DJANGO_VERSION
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
@ -26,18 +25,18 @@ class APIRootView(APIView):
|
|||||||
|
|
||||||
def get(self, request, format=None):
|
def get(self, request, format=None):
|
||||||
|
|
||||||
return Response(OrderedDict((
|
return Response({
|
||||||
('circuits', reverse('circuits-api:api-root', request=request, format=format)),
|
'circuits': reverse('circuits-api:api-root', request=request, format=format),
|
||||||
('dcim', reverse('dcim-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)),
|
'extras': reverse('extras-api:api-root', request=request, format=format),
|
||||||
('ipam', reverse('ipam-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)),
|
'plugins': reverse('plugins-api:api-root', request=request, format=format),
|
||||||
('status', reverse('api-status', request=request, format=format)),
|
'status': reverse('api-status', request=request, format=format),
|
||||||
('tenancy', reverse('tenancy-api:api-root', request=request, format=format)),
|
'tenancy': reverse('tenancy-api:api-root', request=request, format=format),
|
||||||
('users', reverse('users-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)),
|
'virtualization': reverse('virtualization-api:api-root', request=request, format=format),
|
||||||
('wireless', reverse('wireless-api:api-root', request=request, format=format)),
|
'wireless': reverse('wireless-api:api-root', request=request, format=format),
|
||||||
)))
|
})
|
||||||
|
|
||||||
|
|
||||||
class StatusView(APIView):
|
class StatusView(APIView):
|
||||||
|
58
netbox/netbox/denormalized.py
Normal file
58
netbox/netbox/denormalized.py
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
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.
|
||||||
|
"""
|
||||||
|
def _get_field_value(instance, field_name):
|
||||||
|
field = instance._meta.get_field(field_name)
|
||||||
|
return field.value_from_object(instance)
|
||||||
|
|
||||||
|
# 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: _get_field_value(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')
|
@ -29,7 +29,7 @@ django.utils.encoding.force_text = force_str
|
|||||||
# Environment setup
|
# Environment setup
|
||||||
#
|
#
|
||||||
|
|
||||||
VERSION = '3.3-beta1'
|
VERSION = '3.3-beta2'
|
||||||
|
|
||||||
# Hostname
|
# Hostname
|
||||||
HOSTNAME = platform.node()
|
HOSTNAME = platform.node()
|
||||||
|
@ -219,7 +219,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Path Status</th>
|
<th scope="row">Path Status</th>
|
||||||
<td>
|
<td>
|
||||||
{% if object.path.is_active %}
|
{% if object.path.is_complete and object.path.is_active %}
|
||||||
<span class="badge bg-success">Reachable</span>
|
<span class="badge bg-success">Reachable</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="badge bg-danger">Not Reachable</span>
|
<span class="badge bg-danger">Not Reachable</span>
|
||||||
|
@ -95,7 +95,7 @@ class TenantListView(generic.ObjectListView):
|
|||||||
|
|
||||||
|
|
||||||
class TenantView(generic.ObjectView):
|
class TenantView(generic.ObjectView):
|
||||||
queryset = Tenant.objects.prefetch_related('group')
|
queryset = Tenant.objects.all()
|
||||||
|
|
||||||
def get_extra_context(self, request, instance):
|
def get_extra_context(self, request, instance):
|
||||||
stats = {
|
stats = {
|
||||||
@ -140,14 +140,14 @@ class TenantBulkImportView(generic.BulkImportView):
|
|||||||
|
|
||||||
|
|
||||||
class TenantBulkEditView(generic.BulkEditView):
|
class TenantBulkEditView(generic.BulkEditView):
|
||||||
queryset = Tenant.objects.prefetch_related('group')
|
queryset = Tenant.objects.all()
|
||||||
filterset = filtersets.TenantFilterSet
|
filterset = filtersets.TenantFilterSet
|
||||||
table = tables.TenantTable
|
table = tables.TenantTable
|
||||||
form = forms.TenantBulkEditForm
|
form = forms.TenantBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
class TenantBulkDeleteView(generic.BulkDeleteView):
|
class TenantBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = Tenant.objects.prefetch_related('group')
|
queryset = Tenant.objects.all()
|
||||||
filterset = filtersets.TenantFilterSet
|
filterset = filtersets.TenantFilterSet
|
||||||
table = tables.TenantTable
|
table = tables.TenantTable
|
||||||
|
|
||||||
@ -337,14 +337,14 @@ class ContactBulkImportView(generic.BulkImportView):
|
|||||||
|
|
||||||
|
|
||||||
class ContactBulkEditView(generic.BulkEditView):
|
class ContactBulkEditView(generic.BulkEditView):
|
||||||
queryset = Contact.objects.prefetch_related('group')
|
queryset = Contact.objects.all()
|
||||||
filterset = filtersets.ContactFilterSet
|
filterset = filtersets.ContactFilterSet
|
||||||
table = tables.ContactTable
|
table = tables.ContactTable
|
||||||
form = forms.ContactBulkEditForm
|
form = forms.ContactBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
class ContactBulkDeleteView(generic.BulkDeleteView):
|
class ContactBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = Contact.objects.prefetch_related('group')
|
queryset = Contact.objects.all()
|
||||||
filterset = filtersets.ContactFilterSet
|
filterset = filtersets.ContactFilterSet
|
||||||
table = tables.ContactTable
|
table = tables.ContactTable
|
||||||
|
|
||||||
|
@ -1,8 +1,3 @@
|
|||||||
{% if utilization == 0 %}
|
|
||||||
<div class="progress align-items-center justify-content-center">
|
|
||||||
<span class="w-100 text-center">{{ utilization }}%</span>
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
<div class="progress">
|
<div class="progress">
|
||||||
<div
|
<div
|
||||||
role="progressbar"
|
role="progressbar"
|
||||||
@ -12,10 +7,9 @@
|
|||||||
class="progress-bar {{ bar_class }}"
|
class="progress-bar {{ bar_class }}"
|
||||||
style="width: {{ utilization }}%;"
|
style="width: {{ utilization }}%;"
|
||||||
>
|
>
|
||||||
{% if utilization >= 25 %}{{ utilization|floatformat:0 }}%{% endif %}
|
{% if utilization >= 35 %}{{ utilization|floatformat:1 }}%{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% if utilization < 25 %}
|
{% if utilization < 35 %}
|
||||||
<span class="ps-1">{{ utilization|floatformat:0 }}%</span>
|
<span class="ps-1">{{ utilization|floatformat:1 }}%</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import datetime
|
import datetime
|
||||||
import decimal
|
import decimal
|
||||||
import json
|
import json
|
||||||
from collections import OrderedDict
|
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from itertools import count, groupby
|
from itertools import count, groupby
|
||||||
|
|
||||||
@ -149,7 +148,7 @@ def serialize_object(obj, extra=None):
|
|||||||
# Include any tags. Check for tags cached on the instance; fall back to using the manager.
|
# Include any tags. Check for tags cached on the instance; fall back to using the manager.
|
||||||
if is_taggable(obj):
|
if is_taggable(obj):
|
||||||
tags = getattr(obj, '_tags', None) or obj.tags.all()
|
tags = getattr(obj, '_tags', None) or obj.tags.all()
|
||||||
data['tags'] = [tag.name for tag in tags]
|
data['tags'] = sorted([tag.name for tag in tags])
|
||||||
|
|
||||||
# Append any extra data
|
# Append any extra data
|
||||||
if extra is not None:
|
if extra is not None:
|
||||||
@ -218,7 +217,7 @@ def deepmerge(original, new):
|
|||||||
"""
|
"""
|
||||||
Deep merge two dictionaries (new into original) and return a new dict
|
Deep merge two dictionaries (new into original) and return a new dict
|
||||||
"""
|
"""
|
||||||
merged = OrderedDict(original)
|
merged = dict(original)
|
||||||
for key, val in new.items():
|
for key, val in new.items():
|
||||||
if key in original and isinstance(original[key], dict) and val and isinstance(val, dict):
|
if key in original and isinstance(original[key], dict) and val and isinstance(val, dict):
|
||||||
merged[key] = deepmerge(original[key], val)
|
merged[key] = deepmerge(original[key], val)
|
||||||
|
@ -54,6 +54,9 @@ class VirtualMachineTable(TenancyColumnsMixin, NetBoxTable):
|
|||||||
order_by=('primary_ip4', 'primary_ip6'),
|
order_by=('primary_ip4', 'primary_ip6'),
|
||||||
verbose_name='IP Address'
|
verbose_name='IP Address'
|
||||||
)
|
)
|
||||||
|
contacts = columns.ManyToManyColumn(
|
||||||
|
linkify_item=True
|
||||||
|
)
|
||||||
tags = columns.TagColumn(
|
tags = columns.TagColumn(
|
||||||
url_name='virtualization:virtualmachine_list'
|
url_name='virtualization:virtualmachine_list'
|
||||||
)
|
)
|
||||||
@ -62,8 +65,8 @@ class VirtualMachineTable(TenancyColumnsMixin, NetBoxTable):
|
|||||||
model = VirtualMachine
|
model = VirtualMachine
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'name', 'status', 'site', 'cluster', 'device', 'role', 'tenant', 'tenant_group', 'platform',
|
'pk', 'id', 'name', 'status', 'site', 'cluster', 'device', 'role', 'tenant', 'tenant_group', 'platform',
|
||||||
'vcpus', 'memory', 'disk', 'primary_ip4', 'primary_ip6', 'primary_ip', 'comments', 'tags', 'created',
|
'vcpus', 'memory', 'disk', 'primary_ip4', 'primary_ip6', 'primary_ip', 'comments', 'contacts', 'tags',
|
||||||
'last_updated',
|
'created', 'last_updated',
|
||||||
)
|
)
|
||||||
default_columns = (
|
default_columns = (
|
||||||
'pk', 'name', 'status', 'site', 'cluster', 'role', 'tenant', 'vcpus', 'memory', 'disk', 'primary_ip',
|
'pk', 'name', 'status', 'site', 'cluster', 'role', 'tenant', 'vcpus', 'memory', 'disk', 'primary_ip',
|
||||||
@ -84,9 +87,6 @@ class VMInterfaceTable(BaseInterfaceTable):
|
|||||||
vrf = tables.Column(
|
vrf = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
contacts = columns.ManyToManyColumn(
|
|
||||||
linkify_item=True
|
|
||||||
)
|
|
||||||
tags = columns.TagColumn(
|
tags = columns.TagColumn(
|
||||||
url_name='virtualization:vminterface_list'
|
url_name='virtualization:vminterface_list'
|
||||||
)
|
)
|
||||||
@ -95,8 +95,7 @@ class VMInterfaceTable(BaseInterfaceTable):
|
|||||||
model = VMInterface
|
model = VMInterface
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'name', 'virtual_machine', 'enabled', 'mac_address', 'mtu', 'mode', 'description', 'tags',
|
'pk', 'id', 'name', 'virtual_machine', 'enabled', 'mac_address', 'mtu', 'mode', 'description', 'tags',
|
||||||
'vrf', 'ip_addresses', 'fhrp_groups', 'untagged_vlan', 'tagged_vlans', 'contacts', 'created',
|
'vrf', 'ip_addresses', 'fhrp_groups', 'untagged_vlan', 'tagged_vlans', 'created', 'last_updated',
|
||||||
'last_updated',
|
|
||||||
)
|
)
|
||||||
default_columns = ('pk', 'name', 'virtual_machine', 'enabled', 'description')
|
default_columns = ('pk', 'name', 'virtual_machine', 'enabled', 'description')
|
||||||
|
|
||||||
|
@ -209,14 +209,14 @@ class ClusterBulkImportView(generic.BulkImportView):
|
|||||||
|
|
||||||
|
|
||||||
class ClusterBulkEditView(generic.BulkEditView):
|
class ClusterBulkEditView(generic.BulkEditView):
|
||||||
queryset = Cluster.objects.prefetch_related('type', 'group', 'site')
|
queryset = Cluster.objects.all()
|
||||||
filterset = filtersets.ClusterFilterSet
|
filterset = filtersets.ClusterFilterSet
|
||||||
table = tables.ClusterTable
|
table = tables.ClusterTable
|
||||||
form = forms.ClusterBulkEditForm
|
form = forms.ClusterBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
class ClusterBulkDeleteView(generic.BulkDeleteView):
|
class ClusterBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = Cluster.objects.prefetch_related('type', 'group', 'site')
|
queryset = Cluster.objects.all()
|
||||||
filterset = filtersets.ClusterFilterSet
|
filterset = filtersets.ClusterFilterSet
|
||||||
table = tables.ClusterTable
|
table = tables.ClusterTable
|
||||||
|
|
||||||
@ -308,7 +308,7 @@ class ClusterRemoveDevicesView(generic.ObjectEditView):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class VirtualMachineListView(generic.ObjectListView):
|
class VirtualMachineListView(generic.ObjectListView):
|
||||||
queryset = VirtualMachine.objects.all()
|
queryset = VirtualMachine.objects.prefetch_related('primary_ip4', 'primary_ip6')
|
||||||
filterset = filtersets.VirtualMachineFilterSet
|
filterset = filtersets.VirtualMachineFilterSet
|
||||||
filterset_form = forms.VirtualMachineFilterForm
|
filterset_form = forms.VirtualMachineFilterForm
|
||||||
table = tables.VirtualMachineTable
|
table = tables.VirtualMachineTable
|
||||||
@ -334,7 +334,8 @@ class VirtualMachineView(generic.ObjectView):
|
|||||||
services = Service.objects.restrict(request.user, 'view').filter(
|
services = Service.objects.restrict(request.user, 'view').filter(
|
||||||
virtual_machine=instance
|
virtual_machine=instance
|
||||||
).prefetch_related(
|
).prefetch_related(
|
||||||
Prefetch('ipaddresses', queryset=IPAddress.objects.restrict(request.user))
|
Prefetch('ipaddresses', queryset=IPAddress.objects.restrict(request.user)),
|
||||||
|
'virtual_machine'
|
||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -383,14 +384,14 @@ class VirtualMachineBulkImportView(generic.BulkImportView):
|
|||||||
|
|
||||||
|
|
||||||
class VirtualMachineBulkEditView(generic.BulkEditView):
|
class VirtualMachineBulkEditView(generic.BulkEditView):
|
||||||
queryset = VirtualMachine.objects.prefetch_related('cluster', 'tenant', 'role')
|
queryset = VirtualMachine.objects.prefetch_related('primary_ip4', 'primary_ip6')
|
||||||
filterset = filtersets.VirtualMachineFilterSet
|
filterset = filtersets.VirtualMachineFilterSet
|
||||||
table = tables.VirtualMachineTable
|
table = tables.VirtualMachineTable
|
||||||
form = forms.VirtualMachineBulkEditForm
|
form = forms.VirtualMachineBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
class VirtualMachineBulkDeleteView(generic.BulkDeleteView):
|
class VirtualMachineBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = VirtualMachine.objects.prefetch_related('cluster', 'tenant', 'role')
|
queryset = VirtualMachine.objects.prefetch_related('primary_ip4', 'primary_ip6')
|
||||||
filterset = filtersets.VirtualMachineFilterSet
|
filterset = filtersets.VirtualMachineFilterSet
|
||||||
table = tables.VirtualMachineTable
|
table = tables.VirtualMachineTable
|
||||||
|
|
||||||
@ -413,7 +414,7 @@ class VMInterfaceView(generic.ObjectView):
|
|||||||
def get_extra_context(self, request, instance):
|
def get_extra_context(self, request, instance):
|
||||||
# Get assigned IP addresses
|
# Get assigned IP addresses
|
||||||
ipaddress_table = AssignedIPAddressesTable(
|
ipaddress_table = AssignedIPAddressesTable(
|
||||||
data=instance.ip_addresses.restrict(request.user, 'view').prefetch_related('vrf', 'tenant'),
|
data=instance.ip_addresses.restrict(request.user, 'view'),
|
||||||
orderable=False
|
orderable=False
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
bleach==5.0.1
|
bleach==5.0.1
|
||||||
Django==4.0.6
|
Django==4.0.7
|
||||||
django-cors-headers==3.13.0
|
django-cors-headers==3.13.0
|
||||||
django-debug-toolbar==3.5.0
|
django-debug-toolbar==3.5.0
|
||||||
django-filter==22.1
|
django-filter==22.1
|
||||||
@ -14,19 +14,19 @@ django-tables2==2.4.1
|
|||||||
django-taggit==3.0.0
|
django-taggit==3.0.0
|
||||||
django-timezone-field==5.0
|
django-timezone-field==5.0
|
||||||
djangorestframework==3.13.1
|
djangorestframework==3.13.1
|
||||||
drf-yasg[validation]==1.20.0
|
drf-yasg[validation]==1.21.3
|
||||||
graphene-django==2.15.0
|
graphene-django==2.15.0
|
||||||
gunicorn==20.1.0
|
gunicorn==20.1.0
|
||||||
Jinja2==3.1.2
|
Jinja2==3.1.2
|
||||||
Markdown==3.3.7
|
Markdown==3.4.1
|
||||||
markdown-include==0.6.0
|
markdown-include==0.7.0
|
||||||
mkdocs-material==8.3.9
|
mkdocs-material==8.3.9
|
||||||
mkdocstrings[python-legacy]==0.19.0
|
mkdocstrings[python-legacy]==0.19.0
|
||||||
netaddr==0.8.0
|
netaddr==0.8.0
|
||||||
Pillow==9.2.0
|
Pillow==9.2.0
|
||||||
psycopg2-binary==2.9.3
|
psycopg2-binary==2.9.3
|
||||||
PyYAML==6.0
|
PyYAML==6.0
|
||||||
sentry-sdk==1.7.1
|
sentry-sdk==1.9.0
|
||||||
social-auth-app-django==5.0.0
|
social-auth-app-django==5.0.0
|
||||||
social-auth-core==4.3.0
|
social-auth-core==4.3.0
|
||||||
svgwrite==1.4.3
|
svgwrite==1.4.3
|
||||||
|
Loading…
Reference in New Issue
Block a user