Merge branch 'feature' into docs-refresh

This commit is contained in:
jeremystretch 2022-08-04 13:13:22 -04:00
commit a7bf7bf7a5
54 changed files with 708 additions and 438 deletions

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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',
})

View File

@ -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,

View File

@ -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')),
) )

View File

@ -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

View File

@ -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)
nodes.append(prefetched[ct_id][object_id]) try:
nodes.append(prefetched[ct_id][object_id])
except KeyError:
# Ignore stale (deleted) object IDs
pass
path.append(nodes) path.append(nodes)
return path return path

View File

@ -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):
""" """

View File

@ -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

View File

@ -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)

View File

@ -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()

View File

@ -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(

View File

@ -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',
) )

View File

@ -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')

View File

@ -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')

View File

@ -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'])

View File

@ -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):

View File

@ -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

View File

@ -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'}),
} }

View File

@ -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):

View File

@ -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)

View File

@ -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

View File

@ -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]

View File

@ -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:

View File

@ -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):

View File

@ -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']

View File

@ -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

View File

@ -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,
]) }
# #

View File

@ -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

View File

@ -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')
) )

View File

@ -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']:

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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 = (

View File

@ -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')

View File

@ -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)

View File

@ -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'

View File

@ -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 == '':

View File

@ -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

View File

@ -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):

View 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')

View File

@ -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()

View File

@ -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>

View File

@ -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

View File

@ -1,21 +1,15 @@
{% if utilization == 0 %} <div class="progress">
<div class="progress align-items-center justify-content-center"> <div
<span class="w-100 text-center">{{ utilization }}%</span> role="progressbar"
aria-valuemin="0"
aria-valuemax="100"
aria-valuenow="{{ utilization }}"
class="progress-bar {{ bar_class }}"
style="width: {{ utilization }}%;"
>
{% if utilization >= 35 %}{{ utilization|floatformat:1 }}%{% endif %}
</div> </div>
{% else %} {% if utilization < 35 %}
<div class="progress"> <span class="ps-1">{{ utilization|floatformat:1 }}%</span>
<div {% endif %}
role="progressbar" </div>
aria-valuemin="0"
aria-valuemax="100"
aria-valuenow="{{ utilization }}"
class="progress-bar {{ bar_class }}"
style="width: {{ utilization }}%;"
>
{% if utilization >= 25 %}{{ utilization|floatformat:0 }}%{% endif %}
</div>
{% if utilization < 25 %}
<span class="ps-1">{{ utilization|floatformat:0 }}%</span>
{% endif %}
</div>
{% endif %}

View File

@ -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)

View File

@ -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')

View File

@ -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
) )

View File

@ -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