Merge pull request #9706 from netbox-community/develop

Release v3.2.6
This commit is contained in:
Jeremy Stretch 2022-07-11 12:09:21 -04:00 committed by GitHub
commit b72793a85a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
51 changed files with 437 additions and 184 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.2.5 placeholder: v3.2.6
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.2.5 placeholder: v3.2.6
validations: validations:
required: true required: true
- type: dropdown - type: dropdown

6
NOTICE
View File

@ -1 +1,7 @@
Copyrighted and licensed under Apache License 2.0 by DigitalOcean, LLC. Copyrighted and licensed under Apache License 2.0 by DigitalOcean, LLC.
This project contains code developed expressly for NetBox, and its reuse in
other projects may introduce issues affecting performance, data integrity,
and security.
For more information, please see https://github.com/netbox-community/netbox.

View File

@ -1,3 +1,7 @@
# HTML sanitizer
# https://github.com/mozilla/bleach
bleach
# The Python web framework on which NetBox is built # The Python web framework on which NetBox is built
# https://github.com/django/django # https://github.com/django/django
Django Django
@ -126,7 +130,3 @@ tablib
# Timezone data (required by django-timezone-field on Python 3.9+) # Timezone data (required by django-timezone-field on Python 3.9+)
# https://github.com/python/tzdata # https://github.com/python/tzdata
tzdata tzdata
# HTML sanitizer
# https://github.com/mozilla/bleach
bleach

View File

@ -43,18 +43,6 @@ changes in the database indefinitely.
--- ---
## JOBRESULT_RETENTION
Default: 90
The number of days to retain job results (scripts and reports). Set this to `0` to retain
job results in the database indefinitely.
!!! warning
If enabling indefinite job results retention, it is recommended to periodically delete old entries. Otherwise, the database may eventually exceed capacity.
---
## CUSTOM_VALIDATORS ## CUSTOM_VALIDATORS
This is a mapping of models to [custom validators](../customization/custom-validation.md) that have been defined locally to enforce custom validation logic. An example is provided below: This is a mapping of models to [custom validators](../customization/custom-validation.md) that have been defined locally to enforce custom validation logic. An example is provided below:
@ -110,6 +98,18 @@ Setting this to False will disable the GraphQL API.
--- ---
## JOBRESULT_RETENTION
Default: 90
The number of days to retain job results (scripts and reports). Set this to `0` to retain
job results in the database indefinitely.
!!! warning
If enabling indefinite job results retention, it is recommended to periodically delete old entries. Otherwise, the database may eventually exceed capacity.
---
## MAINTENANCE_MODE ## MAINTENANCE_MODE
Default: False Default: False
@ -185,6 +185,30 @@ The default maximum number of objects to display per page within each list of ob
--- ---
## POWERFEED_DEFAULT_AMPERAGE
Default: 15
The default value for the `amperage` field when creating new power feeds.
---
## POWERFEED_DEFAULT_MAX_UTILIZATION
Default: 80
The default value (percentage) for the `max_utilization` field when creating new power feeds.
---
## POWERFEED_DEFAULT_VOLTAGE
Default: 120
The default value for the `voltage` field when creating new power feeds.
---
## PREFER_IPV4 ## PREFER_IPV4
Default: False Default: False

View File

@ -10,7 +10,7 @@ Within the database, custom fields are stored as JSON data directly alongside ea
Custom fields may be created by navigating to Customization > Custom Fields. NetBox supports six types of custom field: Custom fields may be created by navigating to Customization > Custom Fields. NetBox supports six types of custom field:
* Text: Free-form text (up to 255 characters) * Text: Free-form text (intended for single-line use)
* Long text: Free-form of any length; supports Markdown rendering * Long text: Free-form of any length; supports Markdown rendering
* Integer: A whole number (positive or negative) * Integer: A whole number (positive or negative)
* Boolean: True or false * Boolean: True or false

View File

@ -1,5 +1,28 @@
# NetBox v3.2 # NetBox v3.2
## v3.2.6 (2022-07-11)
### Enhancements
* [#7702](https://github.com/netbox-community/netbox/issues/7702) - Enable dynamic configuration for default powerfeed attributes
* [#9396](https://github.com/netbox-community/netbox/issues/9396) - Allow filtering modules by bay ID
* [#9403](https://github.com/netbox-community/netbox/issues/9403) - Enable modifying virtual chassis properties when creating/editing a device
* [#9540](https://github.com/netbox-community/netbox/issues/9540) - Add filters for assigned device & VM to IP addresses list
* [#9686](https://github.com/netbox-community/netbox/issues/9686) - Add tenant group column for all object tables with tenant assignments
### Bug Fixes
* [#8854](https://github.com/netbox-community/netbox/issues/8854) - Fix `REMOTE_AUTH_DEFAULT_GROUPS` for social-auth backends
* [#9575](https://github.com/netbox-community/netbox/issues/9575) - Fix AttributeError exception for FHRP group with an IP address assigned
* [#9597](https://github.com/netbox-community/netbox/issues/9597) - Include `installed_module` in module bay REST API serializer
* [#9632](https://github.com/netbox-community/netbox/issues/9632) - Automatically focus on search box when expanding dropdowns
* [#9657](https://github.com/netbox-community/netbox/issues/9657) - Fix filtering for custom fields and webhooks in the UI
* [#9682](https://github.com/netbox-community/netbox/issues/9682) - Fix bulk assignment of ASNs to sites
* [#9687](https://github.com/netbox-community/netbox/issues/9687) - Don't restrict custom text field lengths when entering via UI form
* [#9704](https://github.com/netbox-community/netbox/issues/9704) - Include `last_updated` field on JournalEntry REST API serializer
---
## v3.2.5 (2022-06-20) ## v3.2.5 (2022-06-20)
### Enhancements ### Enhancements
@ -25,7 +48,7 @@
* [#9484](https://github.com/netbox-community/netbox/issues/9484) - Include services listening on "all IPs" under IP address view * [#9484](https://github.com/netbox-community/netbox/issues/9484) - Include services listening on "all IPs" under IP address view
* [#9486](https://github.com/netbox-community/netbox/issues/9486) - Fix redirect URL when adding device components from the module view * [#9486](https://github.com/netbox-community/netbox/issues/9486) - Fix redirect URL when adding device components from the module view
* [#9495](https://github.com/netbox-community/netbox/issues/9495) - Correct link to contacts in contact groups table column * [#9495](https://github.com/netbox-community/netbox/issues/9495) - Correct link to contacts in contact groups table column
* [#9503](https://github.com/netbox-community/netbox/issues/9503) - Hyperlinks in ack elevation SVGs must always use absolute URLs * [#9503](https://github.com/netbox-community/netbox/issues/9503) - Hyperlinks in rack elevation SVGs must always use absolute URLs
* [#9512](https://github.com/netbox-community/netbox/issues/9512) - Fix duplicate site results when searching by ASN * [#9512](https://github.com/netbox-community/netbox/issues/9512) - Fix duplicate site results when searching by ASN
* [#9524](https://github.com/netbox-community/netbox/issues/9524) - Correct order of VLAN fields under VM interface creation form * [#9524](https://github.com/netbox-community/netbox/issues/9524) - Correct order of VLAN fields under VM interface creation form
* [#9537](https://github.com/netbox-community/netbox/issues/9537) - Ensure consistent use of placeholder tag throughout UI * [#9537](https://github.com/netbox-community/netbox/issues/9537) - Ensure consistent use of placeholder tag throughout UI

View File

@ -2,7 +2,7 @@ import django_tables2 as tables
from circuits.models import * from circuits.models import *
from netbox.tables import NetBoxTable, columns from netbox.tables import NetBoxTable, columns
from tenancy.tables import TenantColumn from tenancy.tables import TenancyColumnsMixin
from .columns import CommitRateColumn from .columns import CommitRateColumn
__all__ = ( __all__ = (
@ -39,7 +39,7 @@ class CircuitTypeTable(NetBoxTable):
default_columns = ('pk', 'name', 'circuit_count', 'description', 'slug') default_columns = ('pk', 'name', 'circuit_count', 'description', 'slug')
class CircuitTable(NetBoxTable): class CircuitTable(TenancyColumnsMixin, NetBoxTable):
cid = tables.Column( cid = tables.Column(
linkify=True, linkify=True,
verbose_name='Circuit ID' verbose_name='Circuit ID'
@ -48,7 +48,6 @@ class CircuitTable(NetBoxTable):
linkify=True linkify=True
) )
status = columns.ChoiceFieldColumn() status = columns.ChoiceFieldColumn()
tenant = TenantColumn()
termination_a = tables.TemplateColumn( termination_a = tables.TemplateColumn(
template_code=CIRCUITTERMINATION_LINK, template_code=CIRCUITTERMINATION_LINK,
verbose_name='Side A' verbose_name='Side A'
@ -69,7 +68,7 @@ class CircuitTable(NetBoxTable):
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = Circuit model = Circuit
fields = ( fields = (
'pk', 'id', 'cid', 'provider', 'type', 'status', 'tenant', 'termination_a', 'termination_z', 'install_date', 'pk', 'id', 'cid', 'provider', 'type', 'status', 'tenant', 'tenant_group', 'termination_a', 'termination_z', 'install_date',
'commit_rate', 'description', 'comments', 'contacts', 'tags', 'created', 'last_updated', 'commit_rate', 'description', 'comments', 'contacts', 'tags', 'created', 'last_updated',
) )
default_columns = ( default_columns = (

View File

@ -30,7 +30,7 @@ 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', 'terminations__site' 'type', 'tenant', 'tenant__group', 'terminations__site'
) )
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 +91,7 @@ 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', 'terminations__site' 'type', 'tenant', 'tenant__group', 'terminations__site'
) )
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 +192,7 @@ 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', 'termination_a', 'termination_z' 'provider', 'type', 'tenant', 'tenant__group', 'termination_a', 'termination_z'
) )
filterset = filtersets.CircuitFilterSet filterset = filtersets.CircuitFilterSet
filterset_form = forms.CircuitFilterForm filterset_form = forms.CircuitFilterForm

View File

@ -5,6 +5,7 @@ from netbox.api.serializers import BaseModelSerializer, WritableNestedSerializer
__all__ = [ __all__ = [
'ComponentNestedModuleSerializer', 'ComponentNestedModuleSerializer',
'ModuleBayNestedModuleSerializer',
'NestedCableSerializer', 'NestedCableSerializer',
'NestedConsolePortSerializer', 'NestedConsolePortSerializer',
'NestedConsolePortTemplateSerializer', 'NestedConsolePortTemplateSerializer',
@ -281,6 +282,14 @@ class ModuleNestedModuleBaySerializer(WritableNestedSerializer):
fields = ['id', 'url', 'display', 'name'] fields = ['id', 'url', 'display', 'name']
class ModuleBayNestedModuleSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:module-detail')
class Meta:
model = models.Module
fields = ['id', 'url', 'display', 'serial']
class ComponentNestedModuleSerializer(WritableNestedSerializer): class ComponentNestedModuleSerializer(WritableNestedSerializer):
""" """
Used by device component serializers. Used by device component serializers.

View File

@ -886,12 +886,12 @@ class FrontPortSerializer(NetBoxModelSerializer, LinkTerminationSerializer):
class ModuleBaySerializer(NetBoxModelSerializer): class ModuleBaySerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:modulebay-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:modulebay-detail')
device = NestedDeviceSerializer() device = NestedDeviceSerializer()
# installed_module = NestedModuleSerializer(required=False, allow_null=True) installed_module = ModuleBayNestedModuleSerializer(required=False, allow_null=True)
class Meta: class Meta:
model = ModuleBay model = ModuleBay
fields = [ fields = [
'id', 'url', 'display', 'device', 'name', 'label', 'position', 'description', 'tags', 'custom_fields', 'id', 'url', 'display', 'device', 'name', 'installed_module', 'label', 'position', 'description', 'tags', 'custom_fields',
'created', 'last_updated', 'created', 'last_updated',
] ]

View File

@ -611,7 +611,7 @@ class RearPortViewSet(PassThroughPortMixin, NetBoxModelViewSet):
class ModuleBayViewSet(NetBoxModelViewSet): class ModuleBayViewSet(NetBoxModelViewSet):
queryset = ModuleBay.objects.prefetch_related('tags') queryset = ModuleBay.objects.prefetch_related('tags', 'installed_module')
serializer_class = serializers.ModuleBaySerializer serializer_class = serializers.ModuleBaySerializer
filterset_class = filtersets.ModuleBayFilterSet filterset_class = filtersets.ModuleBayFilterSet
brief_prefetch_fields = ['device'] brief_prefetch_fields = ['device']

View File

@ -49,15 +49,6 @@ WIRELESS_IFACE_TYPES = [
NONCONNECTABLE_IFACE_TYPES = VIRTUAL_IFACE_TYPES + WIRELESS_IFACE_TYPES NONCONNECTABLE_IFACE_TYPES = VIRTUAL_IFACE_TYPES + WIRELESS_IFACE_TYPES
#
# Power feeds
#
POWERFEED_VOLTAGE_DEFAULT = 120
POWERFEED_AMPERAGE_DEFAULT = 20
POWERFEED_MAX_UTILIZATION_DEFAULT = 80 # Percentage
# #
# Device components # Device components
# #

View File

@ -992,6 +992,12 @@ class ModuleFilterSet(NetBoxModelFilterSet):
to_field_name='model', to_field_name='model',
label='Module type (model)', label='Module type (model)',
) )
module_bay_id = django_filters.ModelMultipleChoiceFilter(
field_name='module_bay',
queryset=ModuleBay.objects.all(),
to_field_name='id',
label='Module Bay (ID)'
)
device_id = django_filters.ModelMultipleChoiceFilter( device_id = django_filters.ModelMultipleChoiceFilter(
queryset=Device.objects.all(), queryset=Device.objects.all(),
label='Device (ID)', label='Device (ID)',

View File

@ -521,13 +521,28 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
required=False, required=False,
label='' label=''
) )
virtual_chassis = DynamicModelChoiceField(
queryset=VirtualChassis.objects.all(),
required=False
)
vc_position = forms.IntegerField(
required=False,
label='Position',
help_text="The position in the virtual chassis this device is identified by"
)
vc_priority = forms.IntegerField(
required=False,
label='Priority',
help_text="The priority of the device in the virtual chassis"
)
class Meta: class Meta:
model = Device model = Device
fields = [ fields = [
'name', 'device_role', 'device_type', 'serial', 'asset_tag', 'region', 'site_group', 'site', 'rack', 'name', 'device_role', 'device_type', 'serial', 'asset_tag', 'region', 'site_group', 'site', 'rack',
'location', 'position', 'face', 'status', 'airflow', 'platform', 'primary_ip4', 'primary_ip6', 'location', 'position', 'face', 'status', 'airflow', 'platform', 'primary_ip4', 'primary_ip6',
'cluster_group', 'cluster', 'tenant_group', 'tenant', 'comments', 'tags', 'local_context_data' 'cluster_group', 'cluster', 'tenant_group', 'tenant', 'virtual_chassis', 'vc_position', 'vc_priority',
'comments', 'tags', 'local_context_data'
] ]
help_texts = { help_texts = {
'device_role': "The function this device serves", 'device_role': "The function this device serves",

View File

@ -386,9 +386,9 @@ class Migration(migrations.Migration):
('type', models.CharField(default='primary', max_length=50)), ('type', models.CharField(default='primary', max_length=50)),
('supply', models.CharField(default='ac', max_length=50)), ('supply', models.CharField(default='ac', max_length=50)),
('phase', models.CharField(default='single-phase', max_length=50)), ('phase', models.CharField(default='single-phase', max_length=50)),
('voltage', models.SmallIntegerField(default=120, validators=[utilities.validators.ExclusionValidator([0])])), ('voltage', models.SmallIntegerField(validators=[utilities.validators.ExclusionValidator([0])])),
('amperage', models.PositiveSmallIntegerField(default=20, validators=[django.core.validators.MinValueValidator(1)])), ('amperage', models.PositiveSmallIntegerField(validators=[django.core.validators.MinValueValidator(1)])),
('max_utilization', models.PositiveSmallIntegerField(default=80, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100)])), ('max_utilization', models.PositiveSmallIntegerField(validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100)])),
('available_power', models.PositiveIntegerField(default=0, editable=False)), ('available_power', models.PositiveIntegerField(default=0, editable=False)),
('comments', models.TextField(blank=True)), ('comments', models.TextField(blank=True)),
], ],

View File

@ -95,8 +95,7 @@ class ModularComponentModel(ComponentModel):
inventory_items = GenericRelation( inventory_items = GenericRelation(
to='dcim.InventoryItem', to='dcim.InventoryItem',
content_type_field='component_type', content_type_field='component_type',
object_id_field='component_id', object_id_field='component_id'
related_name='%(class)ss',
) )
class Meta: class Meta:

View File

@ -6,6 +6,7 @@ from django.urls import reverse
from dcim.choices import * from dcim.choices import *
from dcim.constants import * from dcim.constants import *
from netbox.config import ConfigItem
from netbox.models import NetBoxModel from netbox.models import NetBoxModel
from utilities.validators import ExclusionValidator from utilities.validators import ExclusionValidator
from .device_components import LinkTermination, PathEndpoint from .device_components import LinkTermination, PathEndpoint
@ -105,16 +106,16 @@ class PowerFeed(NetBoxModel, PathEndpoint, LinkTermination):
default=PowerFeedPhaseChoices.PHASE_SINGLE default=PowerFeedPhaseChoices.PHASE_SINGLE
) )
voltage = models.SmallIntegerField( voltage = models.SmallIntegerField(
default=POWERFEED_VOLTAGE_DEFAULT, default=ConfigItem('POWERFEED_DEFAULT_VOLTAGE'),
validators=[ExclusionValidator([0])] validators=[ExclusionValidator([0])]
) )
amperage = models.PositiveSmallIntegerField( amperage = models.PositiveSmallIntegerField(
validators=[MinValueValidator(1)], validators=[MinValueValidator(1)],
default=POWERFEED_AMPERAGE_DEFAULT default=ConfigItem('POWERFEED_DEFAULT_AMPERAGE')
) )
max_utilization = models.PositiveSmallIntegerField( max_utilization = models.PositiveSmallIntegerField(
validators=[MinValueValidator(1), MaxValueValidator(100)], validators=[MinValueValidator(1), MaxValueValidator(100)],
default=POWERFEED_MAX_UTILIZATION_DEFAULT, default=ConfigItem('POWERFEED_DEFAULT_MAX_UTILIZATION'),
help_text="Maximum permissible draw (percentage)" help_text="Maximum permissible draw (percentage)"
) )
available_power = models.PositiveIntegerField( available_power = models.PositiveIntegerField(

View File

@ -3,7 +3,7 @@ from django_tables2.utils import Accessor
from dcim.models import Cable from dcim.models import Cable
from netbox.tables import NetBoxTable, columns from netbox.tables import NetBoxTable, columns
from tenancy.tables import TenantColumn from tenancy.tables import TenancyColumnsMixin
from .template_code import CABLE_LENGTH, CABLE_TERMINATION_PARENT from .template_code import CABLE_LENGTH, CABLE_TERMINATION_PARENT
__all__ = ( __all__ = (
@ -15,7 +15,7 @@ __all__ = (
# Cables # Cables
# #
class CableTable(NetBoxTable): class CableTable(TenancyColumnsMixin, NetBoxTable):
termination_a_parent = tables.TemplateColumn( termination_a_parent = tables.TemplateColumn(
template_code=CABLE_TERMINATION_PARENT, template_code=CABLE_TERMINATION_PARENT,
accessor=Accessor('termination_a'), accessor=Accessor('termination_a'),
@ -53,7 +53,6 @@ class CableTable(NetBoxTable):
verbose_name='Termination B' verbose_name='Termination B'
) )
status = columns.ChoiceFieldColumn() status = columns.ChoiceFieldColumn()
tenant = TenantColumn()
length = columns.TemplateColumn( length = columns.TemplateColumn(
template_code=CABLE_LENGTH, template_code=CABLE_LENGTH,
order_by=('_abs_length', 'length_unit') order_by=('_abs_length', 'length_unit')
@ -67,7 +66,7 @@ class CableTable(NetBoxTable):
model = Cable model = Cable
fields = ( fields = (
'pk', 'id', 'label', 'termination_a_parent', 'rack_a', 'termination_a', 'termination_b_parent', 'rack_b', 'termination_b', 'pk', 'id', 'label', 'termination_a_parent', 'rack_a', 'termination_a', 'termination_b_parent', 'rack_b', 'termination_b',
'status', 'type', 'tenant', 'color', 'length', 'tags', 'created', 'last_updated', 'status', 'type', 'tenant', 'tenant_group', 'color', 'length', 'tags', 'created', 'last_updated',
) )
default_columns = ( default_columns = (
'pk', 'id', 'label', 'termination_a_parent', 'termination_a', 'termination_b_parent', 'termination_b', 'pk', 'id', 'label', 'termination_a_parent', 'termination_a', 'termination_b_parent', 'termination_b',

View File

@ -6,7 +6,7 @@ from dcim.models import (
InventoryItemRole, ModuleBay, Platform, PowerOutlet, PowerPort, RearPort, VirtualChassis, InventoryItemRole, ModuleBay, Platform, PowerOutlet, PowerPort, RearPort, VirtualChassis,
) )
from netbox.tables import NetBoxTable, columns from netbox.tables import NetBoxTable, columns
from tenancy.tables import TenantColumn from tenancy.tables import TenancyColumnsMixin
from .template_code import * from .template_code import *
__all__ = ( __all__ = (
@ -137,13 +137,12 @@ class PlatformTable(NetBoxTable):
# Devices # Devices
# #
class DeviceTable(NetBoxTable): class DeviceTable(TenancyColumnsMixin, NetBoxTable):
name = tables.TemplateColumn( name = tables.TemplateColumn(
order_by=('_name',), order_by=('_name',),
template_code=DEVICE_LINK template_code=DEVICE_LINK
) )
status = columns.ChoiceFieldColumn() status = columns.ChoiceFieldColumn()
tenant = TenantColumn()
site = tables.Column( site = tables.Column(
linkify=True linkify=True
) )
@ -200,7 +199,7 @@ class DeviceTable(NetBoxTable):
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = Device model = Device
fields = ( fields = (
'pk', 'id', 'name', 'status', 'tenant', 'device_role', 'manufacturer', 'device_type', 'platform', 'serial', 'pk', 'id', 'name', 'status', 'tenant', 'tenant_group', 'device_role', 'manufacturer', 'device_type', 'platform', 'serial',
'asset_tag', 'site', 'location', 'rack', 'position', 'face', 'primary_ip', 'airflow', 'primary_ip4', 'asset_tag', 'site', 'location', 'rack', 'position', 'face', 'primary_ip', 'airflow', 'primary_ip4',
'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'contacts', 'tags', 'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'contacts', 'tags',
'created', 'last_updated', 'created', 'last_updated',
@ -211,12 +210,11 @@ class DeviceTable(NetBoxTable):
) )
class DeviceImportTable(NetBoxTable): class DeviceImportTable(TenancyColumnsMixin, NetBoxTable):
name = tables.TemplateColumn( name = tables.TemplateColumn(
template_code=DEVICE_LINK template_code=DEVICE_LINK
) )
status = columns.ChoiceFieldColumn() status = columns.ChoiceFieldColumn()
tenant = TenantColumn()
site = tables.Column( site = tables.Column(
linkify=True linkify=True
) )
@ -232,7 +230,7 @@ class DeviceImportTable(NetBoxTable):
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = Device model = Device
fields = ('id', 'name', 'status', 'tenant', 'site', 'rack', 'position', 'device_role', 'device_type') fields = ('id', 'name', 'status', 'tenant', 'tenant_group', 'site', 'rack', 'position', 'device_role', 'device_type')
empty_text = False empty_text = False

View File

@ -3,7 +3,7 @@ from django_tables2.utils import Accessor
from dcim.models import Rack, RackReservation, RackRole from dcim.models import Rack, RackReservation, RackRole
from netbox.tables import NetBoxTable, columns from netbox.tables import NetBoxTable, columns
from tenancy.tables import TenantColumn from tenancy.tables import TenancyColumnsMixin
__all__ = ( __all__ = (
'RackTable', 'RackTable',
@ -37,7 +37,7 @@ class RackRoleTable(NetBoxTable):
# Racks # Racks
# #
class RackTable(NetBoxTable): class RackTable(TenancyColumnsMixin, NetBoxTable):
name = tables.Column( name = tables.Column(
order_by=('_name',), order_by=('_name',),
linkify=True linkify=True
@ -48,7 +48,6 @@ class RackTable(NetBoxTable):
site = tables.Column( site = tables.Column(
linkify=True linkify=True
) )
tenant = TenantColumn()
status = columns.ChoiceFieldColumn() status = columns.ChoiceFieldColumn()
role = columns.ColoredLabelColumn() role = columns.ColoredLabelColumn()
u_height = tables.TemplateColumn( u_height = tables.TemplateColumn(
@ -87,7 +86,7 @@ class RackTable(NetBoxTable):
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = Rack model = Rack
fields = ( fields = (
'pk', 'id', 'name', 'site', 'location', 'status', 'facility_id', 'tenant', 'role', 'serial', 'asset_tag', 'pk', 'id', 'name', 'site', 'location', 'status', 'facility_id', 'tenant', 'tenant_group', 'role', 'serial', 'asset_tag',
'type', 'width', 'outer_width', 'outer_depth', 'u_height', 'comments', 'device_count', 'get_utilization', 'type', 'width', 'outer_width', 'outer_depth', 'u_height', 'comments', 'device_count', 'get_utilization',
'get_power_utilization', 'contacts', 'tags', 'created', 'last_updated', 'get_power_utilization', 'contacts', 'tags', 'created', 'last_updated',
) )
@ -101,7 +100,7 @@ class RackTable(NetBoxTable):
# Rack reservations # Rack reservations
# #
class RackReservationTable(NetBoxTable): class RackReservationTable(TenancyColumnsMixin, NetBoxTable):
reservation = tables.Column( reservation = tables.Column(
accessor='pk', accessor='pk',
linkify=True linkify=True
@ -110,7 +109,6 @@ class RackReservationTable(NetBoxTable):
accessor=Accessor('rack__site'), accessor=Accessor('rack__site'),
linkify=True linkify=True
) )
tenant = TenantColumn()
rack = tables.Column( rack = tables.Column(
linkify=True linkify=True
) )
@ -125,7 +123,7 @@ class RackReservationTable(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', 'description', 'tags', 'pk', 'id', 'reservation', 'site', '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

@ -2,7 +2,7 @@ import django_tables2 as tables
from dcim.models import Location, Region, Site, SiteGroup from dcim.models import Location, Region, Site, SiteGroup
from netbox.tables import NetBoxTable, columns from netbox.tables import NetBoxTable, columns
from tenancy.tables import TenantColumn from tenancy.tables import TenancyColumnsMixin
from .template_code import LOCATION_BUTTONS from .template_code import LOCATION_BUTTONS
__all__ = ( __all__ = (
@ -75,7 +75,7 @@ class SiteGroupTable(NetBoxTable):
# Sites # Sites
# #
class SiteTable(NetBoxTable): class SiteTable(TenancyColumnsMixin, NetBoxTable):
name = tables.Column( name = tables.Column(
linkify=True linkify=True
) )
@ -96,7 +96,6 @@ class SiteTable(NetBoxTable):
url_params={'site_id': 'pk'}, url_params={'site_id': 'pk'},
verbose_name='ASN Count' verbose_name='ASN Count'
) )
tenant = TenantColumn()
comments = columns.MarkdownColumn() comments = columns.MarkdownColumn()
contacts = columns.ManyToManyColumn( contacts = columns.ManyToManyColumn(
linkify_item=True linkify_item=True
@ -108,7 +107,7 @@ class SiteTable(NetBoxTable):
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = Site model = Site
fields = ( fields = (
'pk', 'id', 'name', 'slug', 'status', 'facility', 'region', 'group', 'tenant', 'asns', 'asn_count', 'pk', 'id', 'name', 'slug', 'status', 'facility', 'region', 'group', 'tenant', 'tenant_group', 'asns', 'asn_count',
'time_zone', 'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'comments', 'time_zone', 'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'comments',
'contacts', 'tags', 'created', 'last_updated', 'actions', 'contacts', 'tags', 'created', 'last_updated', 'actions',
) )
@ -119,14 +118,13 @@ class SiteTable(NetBoxTable):
# Locations # Locations
# #
class LocationTable(NetBoxTable): class LocationTable(TenancyColumnsMixin, NetBoxTable):
name = columns.MPTTColumn( name = columns.MPTTColumn(
linkify=True linkify=True
) )
site = tables.Column( site = tables.Column(
linkify=True linkify=True
) )
tenant = TenantColumn()
rack_count = columns.LinkedCountColumn( rack_count = columns.LinkedCountColumn(
viewname='dcim:rack_list', viewname='dcim:rack_list',
url_params={'location_id': 'pk'}, url_params={'location_id': 'pk'},
@ -150,7 +148,7 @@ class LocationTable(NetBoxTable):
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = Location model = Location
fields = ( fields = (
'pk', 'id', 'name', 'site', 'tenant', 'rack_count', 'device_count', 'description', 'slug', 'contacts', 'pk', 'id', 'name', 'site', 'tenant', 'tenant_group', 'rack_count', 'device_count', 'description', 'slug', 'contacts',
'tags', 'actions', 'created', 'last_updated', 'tags', 'actions', 'created', 'last_updated',
) )
default_columns = ('pk', 'name', 'site', 'tenant', 'rack_count', 'device_count', 'description') default_columns = ('pk', 'name', 'site', 'tenant', 'rack_count', 'device_count', 'description')

View File

@ -1849,6 +1849,11 @@ class ModuleTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'module_type': [module_types[0].model, module_types[1].model]} params = {'module_type': [module_types[0].model, module_types[1].model]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
def test_module_bay(self):
module_bays = ModuleBay.objects.all()[:2]
params = {'module_bay_id': [module_bays[0].pk, module_bays[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_device(self): def test_device(self):
device_types = Device.objects.all()[:2] device_types = Device.objects.all()[:2]
params = {'device_id': [device_types[0].pk, device_types[1].pk]} params = {'device_id': [device_types[0].pk, device_types[1].pk]}

View File

@ -561,7 +561,7 @@ class RackRoleBulkDeleteView(generic.BulkDeleteView):
class RackListView(generic.ObjectListView): class RackListView(generic.ObjectListView):
queryset = Rack.objects.prefetch_related( queryset = Rack.objects.prefetch_related(
'site', 'location', 'tenant', 'role', 'devices__device_type' 'site', 'location', 'tenant', 'tenant_group', 'role', 'devices__device_type'
).annotate( ).annotate(
device_count=count_related(Device, 'rack') device_count=count_related(Device, 'rack')
) )

View File

@ -15,6 +15,9 @@ class ConfigRevisionAdmin(admin.ModelAdmin):
('Rack Elevations', { ('Rack Elevations', {
'fields': ('RACK_ELEVATION_DEFAULT_UNIT_HEIGHT', 'RACK_ELEVATION_DEFAULT_UNIT_WIDTH'), 'fields': ('RACK_ELEVATION_DEFAULT_UNIT_HEIGHT', 'RACK_ELEVATION_DEFAULT_UNIT_WIDTH'),
}), }),
('Power', {
'fields': ('POWERFEED_DEFAULT_VOLTAGE', 'POWERFEED_DEFAULT_AMPERAGE', 'POWERFEED_DEFAULT_MAX_UTILIZATION')
}),
('IPAM', { ('IPAM', {
'fields': ('ENFORCE_GLOBAL_UNIQUE', 'PREFER_IPV4'), 'fields': ('ENFORCE_GLOBAL_UNIQUE', 'PREFER_IPV4'),
}), }),

View File

@ -221,7 +221,7 @@ class JournalEntrySerializer(NetBoxModelSerializer):
model = JournalEntry model = JournalEntry
fields = [ fields = [
'id', 'url', 'display', 'assigned_object_type', 'assigned_object_id', 'assigned_object', 'created', 'id', 'url', 'display', 'assigned_object_type', 'assigned_object_id', 'assigned_object', 'created',
'created_by', 'kind', 'comments', 'tags', 'custom_fields', 'created_by', 'kind', 'comments', 'tags', 'custom_fields', 'last_updated',
] ]
def validate(self, data): def validate(self, data):

View File

@ -32,6 +32,9 @@ class WebhookFilterSet(BaseFilterSet):
method='search', method='search',
label='Search', label='Search',
) )
content_type_id = MultiValueNumberFilter(
field_name='content_types__id'
)
content_types = ContentTypeFilter() content_types = ContentTypeFilter()
http_method = django_filters.MultipleChoiceFilter( http_method = django_filters.MultipleChoiceFilter(
choices=WebhookHttpMethodChoices choices=WebhookHttpMethodChoices
@ -40,8 +43,8 @@ class WebhookFilterSet(BaseFilterSet):
class Meta: class Meta:
model = Webhook model = Webhook
fields = [ fields = [
'id', 'content_types', 'name', 'type_create', 'type_update', 'type_delete', 'payload_url', 'enabled', 'id', 'name', 'type_create', 'type_update', 'type_delete', 'payload_url', 'enabled', 'http_method',
'http_method', 'http_content_type', 'secret', 'ssl_verification', 'ca_file_path', 'http_content_type', 'secret', 'ssl_verification', 'ca_file_path',
] ]
def search(self, queryset, name, value): def search(self, queryset, name, value):
@ -58,11 +61,17 @@ class CustomFieldFilterSet(BaseFilterSet):
method='search', method='search',
label='Search', label='Search',
) )
type = django_filters.MultipleChoiceFilter(
choices=CustomFieldTypeChoices
)
content_type_id = MultiValueNumberFilter(
field_name='content_types__id'
)
content_types = ContentTypeFilter() content_types = ContentTypeFilter()
class Meta: class Meta:
model = CustomField model = CustomField
fields = ['id', 'content_types', 'name', 'required', 'filter_logic', 'weight', 'description'] fields = ['id', 'name', 'required', 'filter_logic', 'weight', 'description']
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():

View File

@ -32,12 +32,13 @@ __all__ = (
class CustomFieldFilterForm(FilterForm): class CustomFieldFilterForm(FilterForm):
fieldsets = ( fieldsets = (
(None, ('q',)), (None, ('q',)),
('Attributes', ('type', 'content_types', 'weight', 'required')), ('Attributes', ('type', 'content_type_id', 'weight', 'required')),
) )
content_types = ContentTypeMultipleChoiceField( content_type_id = ContentTypeMultipleChoiceField(
queryset=ContentType.objects.all(), queryset=ContentType.objects.all(),
limit_choices_to=FeatureQuery('custom_fields'), limit_choices_to=FeatureQuery('custom_fields'),
required=False required=False,
label='Object type'
) )
type = MultipleChoiceField( type = MultipleChoiceField(
choices=CustomFieldTypeChoices, choices=CustomFieldTypeChoices,
@ -110,13 +111,14 @@ class ExportTemplateFilterForm(FilterForm):
class WebhookFilterForm(FilterForm): class WebhookFilterForm(FilterForm):
fieldsets = ( fieldsets = (
(None, ('q',)), (None, ('q',)),
('Attributes', ('content_types', 'http_method', 'enabled')), ('Attributes', ('content_type_id', 'http_method', 'enabled')),
('Events', ('type_create', 'type_update', 'type_delete')), ('Events', ('type_create', 'type_update', 'type_delete')),
) )
content_types = ContentTypeMultipleChoiceField( content_type_id = ContentTypeMultipleChoiceField(
queryset=ContentType.objects.all(), queryset=ContentType.objects.all(),
limit_choices_to=FeatureQuery('webhooks'), limit_choices_to=FeatureQuery('webhooks'),
required=False required=False,
label='Object type'
) )
http_method = MultipleChoiceField( http_method = MultipleChoiceField(
choices=WebhookHttpMethodChoices, choices=WebhookHttpMethodChoices,

View File

@ -365,13 +365,8 @@ class CustomField(ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel):
# Text # Text
else: else:
if self.type == CustomFieldTypeChoices.TYPE_LONGTEXT: widget = forms.Textarea if self.type == CustomFieldTypeChoices.TYPE_LONGTEXT else None
max_length = None field = forms.CharField(required=required, initial=initial, widget=widget)
widget = forms.Textarea
else:
max_length = 255
widget = None
field = forms.CharField(max_length=max_length, required=required, initial=initial, widget=widget)
if self.validation_regex: if self.validation_regex:
field.validators = [ field.validators = [
RegexValidator( RegexValidator(

View File

@ -7,7 +7,9 @@ from django.test import TestCase
from circuits.models import Provider from circuits.models import Provider
from dcim.models import DeviceRole, DeviceType, Manufacturer, Platform, Rack, Region, Site, SiteGroup from dcim.models import DeviceRole, DeviceType, Manufacturer, Platform, Rack, Region, Site, SiteGroup
from extras.choices import JournalEntryKindChoices, ObjectChangeActionChoices from extras.choices import (
CustomFieldTypeChoices, CustomFieldFilterLogicChoices, JournalEntryKindChoices, ObjectChangeActionChoices,
)
from extras.filtersets import * from extras.filtersets import *
from extras.models import * from extras.models import *
from ipam.models import IPAddress from ipam.models import IPAddress
@ -16,6 +18,65 @@ from utilities.testing import BaseFilterSetTests, ChangeLoggedFilterSetTests, cr
from virtualization.models import Cluster, ClusterGroup, ClusterType from virtualization.models import Cluster, ClusterGroup, ClusterType
class CustomFieldTestCase(TestCase, BaseFilterSetTests):
queryset = CustomField.objects.all()
filterset = CustomFieldFilterSet
@classmethod
def setUpTestData(cls):
content_types = ContentType.objects.filter(model__in=['site', 'rack', 'device'])
custom_fields = (
CustomField(
name='Custom Field 1',
type=CustomFieldTypeChoices.TYPE_TEXT,
required=True,
weight=100,
filter_logic=CustomFieldFilterLogicChoices.FILTER_LOOSE
),
CustomField(
name='Custom Field 2',
type=CustomFieldTypeChoices.TYPE_INTEGER,
required=False,
weight=200,
filter_logic=CustomFieldFilterLogicChoices.FILTER_EXACT
),
CustomField(
name='Custom Field 3',
type=CustomFieldTypeChoices.TYPE_BOOLEAN,
required=False,
weight=300,
filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED
),
)
CustomField.objects.bulk_create(custom_fields)
custom_fields[0].content_types.add(content_types[0])
custom_fields[1].content_types.add(content_types[1])
custom_fields[2].content_types.add(content_types[2])
def test_name(self):
params = {'name': ['Custom Field 1', 'Custom Field 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_content_types(self):
params = {'content_types': 'dcim.site'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
params = {'content_type_id': [ContentType.objects.get_for_model(Site).pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_required(self):
params = {'required': True}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_weight(self):
params = {'weight': [100, 200]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_filter_logic(self):
params = {'filter_logic': CustomFieldFilterLogicChoices.FILTER_LOOSE}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
class WebhookTestCase(TestCase, BaseFilterSetTests): class WebhookTestCase(TestCase, BaseFilterSetTests):
queryset = Webhook.objects.all() queryset = Webhook.objects.all()
filterset = WebhookFilterSet filterset = WebhookFilterSet
@ -62,6 +123,8 @@ class WebhookTestCase(TestCase, BaseFilterSetTests):
def test_content_types(self): def test_content_types(self):
params = {'content_types': 'dcim.site'} params = {'content_types': 'dcim.site'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
params = {'content_type_id': [ContentType.objects.get_for_model(Site).pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_type_create(self): def test_type_create(self):
params = {'type_create': True} params = {'type_create': True}

View File

@ -1,7 +1,8 @@
from django import forms from django import forms
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from dcim.models import Location, Rack, Region, Site, SiteGroup from dcim.models import Location, Rack, Region, Site, SiteGroup, Device
from virtualization.models import VirtualMachine
from ipam.choices import * from ipam.choices import *
from ipam.constants import * from ipam.constants import *
from ipam.models import * from ipam.models import *
@ -265,6 +266,7 @@ class IPAddressFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
('Attributes', ('parent', 'family', 'status', 'role', 'mask_length', 'assigned_to_interface')), ('Attributes', ('parent', 'family', 'status', 'role', 'mask_length', 'assigned_to_interface')),
('VRF', ('vrf_id', 'present_in_vrf_id')), ('VRF', ('vrf_id', 'present_in_vrf_id')),
('Tenant', ('tenant_group_id', 'tenant_id')), ('Tenant', ('tenant_group_id', 'tenant_id')),
('Device/VM', ('device_id', 'virtual_machine_id')),
) )
parent = forms.CharField( parent = forms.CharField(
required=False, required=False,
@ -298,6 +300,16 @@ class IPAddressFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
required=False, required=False,
label=_('Present in VRF') label=_('Present in VRF')
) )
device_id = DynamicModelMultipleChoiceField(
queryset=Device.objects.all(),
required=False,
label=_('Assigned Device'),
)
virtual_machine_id = DynamicModelMultipleChoiceField(
queryset=VirtualMachine.objects.all(),
required=False,
label=_('Assigned VM'),
)
status = MultipleChoiceField( status = MultipleChoiceField(
choices=IPAddressStatusChoices, choices=IPAddressStatusChoices,
required=False required=False

View File

@ -4,7 +4,7 @@ from django_tables2.utils import Accessor
from ipam.models import * from ipam.models import *
from netbox.tables import NetBoxTable, columns from netbox.tables import NetBoxTable, columns
from tenancy.tables import TenantColumn from tenancy.tables import TenancyColumnsMixin, TenantColumn
__all__ = ( __all__ = (
'AggregateTable', 'AggregateTable',
@ -99,7 +99,7 @@ class RIRTable(NetBoxTable):
# ASNs # ASNs
# #
class ASNTable(NetBoxTable): class ASNTable(TenancyColumnsMixin, NetBoxTable):
asn = tables.Column( asn = tables.Column(
linkify=True linkify=True
) )
@ -122,7 +122,6 @@ class ASNTable(NetBoxTable):
linkify_item=True, linkify_item=True,
verbose_name='Sites' verbose_name='Sites'
) )
tenant = TenantColumn()
tags = columns.TagColumn( tags = columns.TagColumn(
url_name='ipam:asn_list' url_name='ipam:asn_list'
) )
@ -130,7 +129,7 @@ class ASNTable(NetBoxTable):
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = ASN model = ASN
fields = ( fields = (
'pk', 'asn', 'asn_asdot', 'rir', 'site_count', 'provider_count', 'tenant', 'description', 'sites', 'tags', 'pk', 'asn', 'asn_asdot', 'rir', 'site_count', 'provider_count', 'tenant', 'tenant_group', 'description', 'sites', 'tags',
'created', 'last_updated', 'actions', 'created', 'last_updated', 'actions',
) )
default_columns = ('pk', 'asn', 'rir', 'site_count', 'provider_count', 'sites', 'description', 'tenant') default_columns = ('pk', 'asn', 'rir', 'site_count', 'provider_count', 'sites', 'description', 'tenant')
@ -140,12 +139,11 @@ class ASNTable(NetBoxTable):
# Aggregates # Aggregates
# #
class AggregateTable(NetBoxTable): class AggregateTable(TenancyColumnsMixin, NetBoxTable):
prefix = tables.Column( prefix = tables.Column(
linkify=True, linkify=True,
verbose_name='Aggregate' verbose_name='Aggregate'
) )
tenant = TenantColumn()
date_added = tables.DateColumn( date_added = tables.DateColumn(
format="Y-m-d", format="Y-m-d",
verbose_name='Added' verbose_name='Added'
@ -164,7 +162,7 @@ class AggregateTable(NetBoxTable):
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = Aggregate model = Aggregate
fields = ( fields = (
'pk', 'id', 'prefix', 'rir', 'tenant', 'child_count', 'utilization', 'date_added', 'description', 'tags', 'pk', 'id', 'prefix', 'rir', 'tenant', 'tenant_group', 'child_count', 'utilization', 'date_added', 'description', 'tags',
'created', 'last_updated', 'created', 'last_updated',
) )
default_columns = ('pk', 'prefix', 'rir', 'tenant', 'child_count', 'utilization', 'date_added', 'description') default_columns = ('pk', 'prefix', 'rir', 'tenant', 'child_count', 'utilization', 'date_added', 'description')
@ -225,7 +223,7 @@ class PrefixUtilizationColumn(columns.UtilizationColumn):
""" """
class PrefixTable(NetBoxTable): class PrefixTable(TenancyColumnsMixin, NetBoxTable):
prefix = columns.TemplateColumn( prefix = columns.TemplateColumn(
template_code=PREFIX_LINK, template_code=PREFIX_LINK,
export_raw=True, export_raw=True,
@ -256,7 +254,6 @@ class PrefixTable(NetBoxTable):
template_code=VRF_LINK, template_code=VRF_LINK,
verbose_name='VRF' verbose_name='VRF'
) )
tenant = TenantColumn()
site = tables.Column( site = tables.Column(
linkify=True linkify=True
) )
@ -289,7 +286,7 @@ class PrefixTable(NetBoxTable):
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = Prefix model = Prefix
fields = ( fields = (
'pk', 'id', 'prefix', 'prefix_flat', 'status', 'children', 'vrf', 'utilization', 'tenant', 'site', 'pk', 'id', 'prefix', 'prefix_flat', 'status', 'children', 'vrf', 'utilization', 'tenant', 'tenant_group', 'site',
'vlan_group', 'vlan', 'role', 'is_pool', 'mark_utilized', 'description', 'tags', 'created', 'last_updated', 'vlan_group', 'vlan', 'role', 'is_pool', 'mark_utilized', 'description', 'tags', 'created', 'last_updated',
) )
default_columns = ( default_columns = (
@ -303,7 +300,7 @@ class PrefixTable(NetBoxTable):
# #
# IP ranges # IP ranges
# #
class IPRangeTable(NetBoxTable): class IPRangeTable(TenancyColumnsMixin, NetBoxTable):
start_address = tables.Column( start_address = tables.Column(
linkify=True linkify=True
) )
@ -317,7 +314,6 @@ class IPRangeTable(NetBoxTable):
role = tables.Column( role = tables.Column(
linkify=True linkify=True
) )
tenant = TenantColumn()
utilization = columns.UtilizationColumn( utilization = columns.UtilizationColumn(
accessor='utilization', accessor='utilization',
orderable=False orderable=False
@ -329,7 +325,7 @@ class IPRangeTable(NetBoxTable):
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = IPRange model = IPRange
fields = ( fields = (
'pk', 'id', 'start_address', 'end_address', 'size', 'vrf', 'status', 'role', 'tenant', 'description', 'pk', 'id', 'start_address', 'end_address', 'size', 'vrf', 'status', 'role', 'tenant', 'tenant_group', 'description',
'utilization', 'tags', 'created', 'last_updated', 'utilization', 'tags', 'created', 'last_updated',
) )
default_columns = ( default_columns = (
@ -344,7 +340,7 @@ class IPRangeTable(NetBoxTable):
# IPAddresses # IPAddresses
# #
class IPAddressTable(NetBoxTable): class IPAddressTable(TenancyColumnsMixin, NetBoxTable):
address = tables.TemplateColumn( address = tables.TemplateColumn(
template_code=IPADDRESS_LINK, template_code=IPADDRESS_LINK,
verbose_name='IP Address' verbose_name='IP Address'
@ -357,7 +353,6 @@ class IPAddressTable(NetBoxTable):
default=AVAILABLE_LABEL default=AVAILABLE_LABEL
) )
role = columns.ChoiceFieldColumn() role = columns.ChoiceFieldColumn()
tenant = TenantColumn()
assigned_object = tables.Column( assigned_object = tables.Column(
linkify=True, linkify=True,
orderable=False, orderable=False,
@ -386,7 +381,7 @@ class IPAddressTable(NetBoxTable):
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = IPAddress model = IPAddress
fields = ( fields = (
'pk', 'id', 'address', 'vrf', 'status', 'role', 'tenant', 'nat_inside', 'assigned', 'dns_name', 'description', 'pk', 'id', 'address', 'vrf', 'status', 'role', 'tenant', 'tenant_group', 'nat_inside', 'assigned', 'dns_name', 'description',
'tags', 'created', 'last_updated', 'tags', 'created', 'last_updated',
) )
default_columns = ( default_columns = (

View File

@ -5,7 +5,7 @@ from django_tables2.utils import Accessor
from dcim.models import Interface from dcim.models import Interface
from ipam.models import * from ipam.models import *
from netbox.tables import NetBoxTable, columns from netbox.tables import NetBoxTable, columns
from tenancy.tables import TenantColumn from tenancy.tables import TenancyColumnsMixin, TenantColumn
from virtualization.models import VMInterface from virtualization.models import VMInterface
__all__ = ( __all__ = (
@ -90,7 +90,7 @@ class VLANGroupTable(NetBoxTable):
# VLANs # VLANs
# #
class VLANTable(NetBoxTable): class VLANTable(TenancyColumnsMixin, NetBoxTable):
vid = tables.TemplateColumn( vid = tables.TemplateColumn(
template_code=VLAN_LINK, template_code=VLAN_LINK,
verbose_name='VID' verbose_name='VID'
@ -104,7 +104,6 @@ class VLANTable(NetBoxTable):
group = tables.Column( group = tables.Column(
linkify=True linkify=True
) )
tenant = TenantColumn()
status = columns.ChoiceFieldColumn( status = columns.ChoiceFieldColumn(
default=AVAILABLE_LABEL default=AVAILABLE_LABEL
) )
@ -123,7 +122,7 @@ class VLANTable(NetBoxTable):
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = VLAN model = VLAN
fields = ( fields = (
'pk', 'id', 'vid', 'name', 'site', 'group', 'prefixes', 'tenant', 'status', 'role', 'description', 'tags', 'pk', 'id', 'vid', 'name', 'site', 'group', 'prefixes', 'tenant', 'tenant_group', 'status', 'role', 'description', 'tags',
'created', 'last_updated', 'created', 'last_updated',
) )
default_columns = ('pk', 'vid', 'name', 'site', 'group', 'prefixes', 'tenant', 'status', 'role', 'description') default_columns = ('pk', 'vid', 'name', 'site', 'group', 'prefixes', 'tenant', 'status', 'role', 'description')

View File

@ -2,7 +2,7 @@ import django_tables2 as tables
from ipam.models import * from ipam.models import *
from netbox.tables import NetBoxTable, columns from netbox.tables import NetBoxTable, columns
from tenancy.tables import TenantColumn from tenancy.tables import TenancyColumnsMixin
__all__ = ( __all__ = (
'RouteTargetTable', 'RouteTargetTable',
@ -20,14 +20,13 @@ VRF_TARGETS = """
# VRFs # VRFs
# #
class VRFTable(NetBoxTable): class VRFTable(TenancyColumnsMixin, NetBoxTable):
name = tables.Column( name = tables.Column(
linkify=True linkify=True
) )
rd = tables.Column( rd = tables.Column(
verbose_name='RD' verbose_name='RD'
) )
tenant = TenantColumn()
enforce_unique = columns.BooleanColumn( enforce_unique = columns.BooleanColumn(
verbose_name='Unique' verbose_name='Unique'
) )
@ -46,7 +45,7 @@ class VRFTable(NetBoxTable):
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = VRF model = VRF
fields = ( fields = (
'pk', 'id', 'name', 'rd', 'tenant', 'enforce_unique', 'description', 'import_targets', 'export_targets', 'pk', 'id', 'name', 'rd', 'tenant', 'tenant_group', 'enforce_unique', 'description', 'import_targets', 'export_targets',
'tags', 'created', 'last_updated', 'tags', 'created', 'last_updated',
) )
default_columns = ('pk', 'name', 'rd', 'tenant', 'description') default_columns = ('pk', 'name', 'rd', 'tenant', 'description')
@ -56,16 +55,15 @@ class VRFTable(NetBoxTable):
# Route targets # Route targets
# #
class RouteTargetTable(NetBoxTable): class RouteTargetTable(TenancyColumnsMixin, NetBoxTable):
name = tables.Column( name = tables.Column(
linkify=True linkify=True
) )
tenant = TenantColumn()
tags = columns.TagColumn( tags = columns.TagColumn(
url_name='ipam:vrf_list' url_name='ipam:vrf_list'
) )
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = RouteTarget model = RouteTarget
fields = ('pk', 'id', 'name', 'tenant', 'description', 'tags', 'created', 'last_updated',) fields = ('pk', 'id', 'name', 'tenant', 'tenant_group', 'description', 'tags', 'created', 'last_updated',)
default_columns = ('pk', 'name', 'tenant', 'description') default_columns = ('pk', 'name', 'tenant', 'description')

View File

@ -298,7 +298,7 @@ class AggregatePrefixesView(generic.ObjectChildrenView):
def get_children(self, request, parent): def get_children(self, request, parent):
return Prefix.objects.restrict(request.user, 'view').filter( return Prefix.objects.restrict(request.user, 'view').filter(
prefix__net_contained_or_equal=str(parent.prefix) prefix__net_contained_or_equal=str(parent.prefix)
).prefetch_related('site', 'role', 'tenant', 'vlan') ).prefetch_related('site', 'role', 'tenant', 'tenant__group', 'vlan')
def prep_table_data(self, request, queryset, parent): def prep_table_data(self, request, queryset, parent):
# Determine whether to show assigned prefixes, available prefixes, or both # Determine whether to show assigned prefixes, available prefixes, or both
@ -470,7 +470,7 @@ class PrefixPrefixesView(generic.ObjectChildrenView):
def get_children(self, request, parent): def get_children(self, request, parent):
return parent.get_child_prefixes().restrict(request.user, 'view').prefetch_related( return parent.get_child_prefixes().restrict(request.user, 'view').prefetch_related(
'site', 'vrf', 'vlan', 'role', 'tenant', 'site', 'vrf', 'vlan', 'role', 'tenant', 'tenant__group'
) )
def prep_table_data(self, request, queryset, parent): def prep_table_data(self, request, queryset, parent):
@ -499,7 +499,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', 'vrf', 'role', 'tenant', 'tenant__group',
) )
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
@ -587,7 +587,7 @@ class IPRangeIPAddressesView(generic.ObjectChildrenView):
def get_children(self, request, parent): def get_children(self, request, parent):
return parent.get_child_ips().restrict(request.user, 'view').prefetch_related( return parent.get_child_ips().restrict(request.user, 'view').prefetch_related(
'vrf', 'role', 'tenant', 'vrf', 'role', 'tenant', 'tenant__group',
) )
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
@ -680,13 +680,16 @@ class IPAddressView(generic.ObjectView):
service_filter = Q(ipaddresses=instance) service_filter = Q(ipaddresses=instance)
# Find services listening on all IPs on the assigned device/vm # Find services listening on all IPs on the assigned device/vm
if instance.assigned_object and instance.assigned_object.parent_object: try:
parent_object = instance.assigned_object.parent_object if instance.assigned_object and instance.assigned_object.parent_object:
parent_object = instance.assigned_object.parent_object
if isinstance(parent_object, VirtualMachine): if isinstance(parent_object, VirtualMachine):
service_filter |= (Q(virtual_machine=parent_object) & Q(ipaddresses=None)) service_filter |= (Q(virtual_machine=parent_object) & Q(ipaddresses=None))
elif isinstance(parent_object, Device): elif isinstance(parent_object, Device):
service_filter |= (Q(device=parent_object) & Q(ipaddresses=None)) service_filter |= (Q(device=parent_object) & Q(ipaddresses=None))
except AttributeError:
pass
services = Service.objects.restrict(request.user, 'view').filter(service_filter) services = Service.objects.restrict(request.user, 'view').filter(service_filter)

View File

@ -348,3 +348,26 @@ class LDAPBackend:
ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER) ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER)
return obj return obj
# Custom Social Auth Pipeline Handlers
def user_default_groups_handler(backend, user, response, *args, **kwargs):
"""
Custom pipeline handler which adds remote auth users to the default group specified in the
configuration file.
"""
logger = logging.getLogger('netbox.auth.user_default_groups_handler')
if settings.REMOTE_AUTH_DEFAULT_GROUPS:
# Assign default groups to the user
group_list = []
for name in settings.REMOTE_AUTH_DEFAULT_GROUPS:
try:
group_list.append(Group.objects.get(name=name))
except Group.DoesNotExist:
logging.error(
f"Could not assign group {name} to remotely-authenticated user {user}: Group not found")
if group_list:
user.groups.add(*group_list)
else:
user.groups.clear()
logger.debug(f"Stripping user {user} from Groups")

View File

@ -82,6 +82,31 @@ PARAMS = (
field=forms.IntegerField field=forms.IntegerField
), ),
# Power
ConfigParam(
name='POWERFEED_DEFAULT_VOLTAGE',
label='Powerfeed voltage',
default=120,
description="Default voltage for powerfeeds",
field=forms.IntegerField
),
ConfigParam(
name='POWERFEED_DEFAULT_AMPERAGE',
label='Powerfeed amperage',
default=15,
description="Default amperage for powerfeeds",
field=forms.IntegerField
),
ConfigParam(
name='POWERFEED_DEFAULT_MAX_UTILIZATION',
label='Powerfeed max utilization',
default=80,
description="Default max utilization for powerfeeds",
field=forms.IntegerField
),
# Security # Security
ConfigParam( ConfigParam(
name='ALLOWED_URL_SCHEMES', name='ALLOWED_URL_SCHEMES',

View File

@ -34,7 +34,7 @@ CIRCUIT_TYPES = OrderedDict(
}), }),
('circuit', { ('circuit', {
'queryset': Circuit.objects.prefetch_related( 'queryset': Circuit.objects.prefetch_related(
'type', 'provider', 'tenant', 'terminations__site' 'type', 'provider', 'tenant', 'tenant__group', 'terminations__site'
), ),
'filterset': circuits.filtersets.CircuitFilterSet, 'filterset': circuits.filtersets.CircuitFilterSet,
'table': circuits.tables.CircuitTable, 'table': circuits.tables.CircuitTable,
@ -53,13 +53,13 @@ CIRCUIT_TYPES = OrderedDict(
DCIM_TYPES = OrderedDict( DCIM_TYPES = OrderedDict(
( (
('site', { ('site', {
'queryset': Site.objects.prefetch_related('region', 'tenant'), 'queryset': Site.objects.prefetch_related('region', 'tenant', 'tenant__group'),
'filterset': dcim.filtersets.SiteFilterSet, 'filterset': dcim.filtersets.SiteFilterSet,
'table': dcim.tables.SiteTable, 'table': dcim.tables.SiteTable,
'url': 'dcim:site_list', 'url': 'dcim:site_list',
}), }),
('rack', { ('rack', {
'queryset': Rack.objects.prefetch_related('site', 'location', 'tenant', 'role').annotate( 'queryset': Rack.objects.prefetch_related('site', 'location', 'tenant', 'tenant__group', 'role').annotate(
device_count=count_related(Device, 'rack') device_count=count_related(Device, 'rack')
), ),
'filterset': dcim.filtersets.RackFilterSet, 'filterset': dcim.filtersets.RackFilterSet,
@ -100,7 +100,7 @@ DCIM_TYPES = OrderedDict(
}), }),
('device', { ('device', {
'queryset': Device.objects.prefetch_related( 'queryset': Device.objects.prefetch_related(
'device_type__manufacturer', 'device_role', 'tenant', 'site', 'rack', 'primary_ip4', 'primary_ip6', 'device_type__manufacturer', 'device_role', 'tenant', 'tenant__group', 'site', 'rack', 'primary_ip4', 'primary_ip6',
), ),
'filterset': dcim.filtersets.DeviceFilterSet, 'filterset': dcim.filtersets.DeviceFilterSet,
'table': dcim.tables.DeviceTable, 'table': dcim.tables.DeviceTable,
@ -148,7 +148,7 @@ DCIM_TYPES = OrderedDict(
IPAM_TYPES = OrderedDict( IPAM_TYPES = OrderedDict(
( (
('vrf', { ('vrf', {
'queryset': VRF.objects.prefetch_related('tenant'), 'queryset': VRF.objects.prefetch_related('tenant', 'tenant__group'),
'filterset': ipam.filtersets.VRFFilterSet, 'filterset': ipam.filtersets.VRFFilterSet,
'table': ipam.tables.VRFTable, 'table': ipam.tables.VRFTable,
'url': 'ipam:vrf_list', 'url': 'ipam:vrf_list',
@ -160,25 +160,25 @@ IPAM_TYPES = OrderedDict(
'url': 'ipam:aggregate_list', 'url': 'ipam:aggregate_list',
}), }),
('prefix', { ('prefix', {
'queryset': Prefix.objects.prefetch_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role'), 'queryset': Prefix.objects.prefetch_related('site', 'vrf__tenant', 'tenant', 'tenant__group', 'vlan', 'role'),
'filterset': ipam.filtersets.PrefixFilterSet, 'filterset': ipam.filtersets.PrefixFilterSet,
'table': ipam.tables.PrefixTable, 'table': ipam.tables.PrefixTable,
'url': 'ipam:prefix_list', 'url': 'ipam:prefix_list',
}), }),
('ipaddress', { ('ipaddress', {
'queryset': IPAddress.objects.prefetch_related('vrf__tenant', 'tenant'), 'queryset': IPAddress.objects.prefetch_related('vrf__tenant', 'tenant', 'tenant__group'),
'filterset': ipam.filtersets.IPAddressFilterSet, 'filterset': ipam.filtersets.IPAddressFilterSet,
'table': ipam.tables.IPAddressTable, 'table': ipam.tables.IPAddressTable,
'url': 'ipam:ipaddress_list', 'url': 'ipam:ipaddress_list',
}), }),
('vlan', { ('vlan', {
'queryset': VLAN.objects.prefetch_related('site', 'group', 'tenant', 'role'), 'queryset': VLAN.objects.prefetch_related('site', 'group', 'tenant', 'tenant__group', 'role'),
'filterset': ipam.filtersets.VLANFilterSet, 'filterset': ipam.filtersets.VLANFilterSet,
'table': ipam.tables.VLANTable, 'table': ipam.tables.VLANTable,
'url': 'ipam:vlan_list', 'url': 'ipam:vlan_list',
}), }),
('asn', { ('asn', {
'queryset': ASN.objects.prefetch_related('rir', 'tenant'), 'queryset': ASN.objects.prefetch_related('rir', 'tenant', 'tenant__group'),
'filterset': ipam.filtersets.ASNFilterSet, 'filterset': ipam.filtersets.ASNFilterSet,
'table': ipam.tables.ASNTable, 'table': ipam.tables.ASNTable,
'url': 'ipam:asn_list', 'url': 'ipam:asn_list',
@ -223,7 +223,7 @@ VIRTUALIZATION_TYPES = OrderedDict(
}), }),
('virtualmachine', { ('virtualmachine', {
'queryset': VirtualMachine.objects.prefetch_related( 'queryset': VirtualMachine.objects.prefetch_related(
'cluster', 'tenant', 'platform', 'primary_ip4', 'primary_ip6', 'cluster', 'tenant', 'tenant__group', 'platform', 'primary_ip4', 'primary_ip6',
), ),
'filterset': virtualization.filtersets.VirtualMachineFilterSet, 'filterset': virtualization.filtersets.VirtualMachineFilterSet,
'table': virtualization.tables.VirtualMachineTable, 'table': virtualization.tables.VirtualMachineTable,

View File

@ -29,7 +29,7 @@ django.utils.encoding.force_text = force_str
# Environment setup # Environment setup
# #
VERSION = '3.2.5' VERSION = '3.2.6'
# Hostname # Hostname
HOSTNAME = platform.node() HOSTNAME = platform.node()
@ -483,6 +483,19 @@ for param in dir(configuration):
SOCIAL_AUTH_JSONFIELD_ENABLED = True SOCIAL_AUTH_JSONFIELD_ENABLED = True
SOCIAL_AUTH_PIPELINE = (
'social_core.pipeline.social_auth.social_details',
'social_core.pipeline.social_auth.social_uid',
'social_core.pipeline.social_auth.social_user',
'social_core.pipeline.user.get_username',
'social_core.pipeline.social_auth.associate_by_email',
'social_core.pipeline.user.create_user',
'social_core.pipeline.social_auth.associate_user',
'netbox.authentication.user_default_groups_handler',
'social_core.pipeline.social_auth.load_extra_data',
'social_core.pipeline.user.user_details',
)
# #
# Django Prometheus # Django Prometheus

View File

@ -8,6 +8,7 @@ from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import FieldDoesNotExist, ValidationError from django.core.exceptions import FieldDoesNotExist, ValidationError
from django.db import transaction, IntegrityError from django.db import transaction, IntegrityError
from django.db.models import ManyToManyField, ProtectedError from django.db.models import ManyToManyField, ProtectedError
from django.db.models.fields.reverse_related import ManyToManyRel
from django.forms import Form, ModelMultipleChoiceField, MultipleHiddenInput from django.forms import Form, ModelMultipleChoiceField, MultipleHiddenInput
from django.http import HttpResponse from django.http import HttpResponse
from django.shortcuts import get_object_or_404, redirect, render from django.shortcuts import get_object_or_404, redirect, render
@ -484,7 +485,7 @@ class BulkEditView(GetReturnURLMixin, BaseMultiObjectView):
setattr(obj, name, None if model_field.null else '') setattr(obj, name, None if model_field.null else '')
# ManyToManyFields # ManyToManyFields
elif isinstance(model_field, ManyToManyField): elif isinstance(model_field, (ManyToManyField, ManyToManyRel)):
if form.cleaned_data[name]: if form.cleaned_data[name]:
getattr(obj, name).set(form.cleaned_data[name]) getattr(obj, name).set(form.cleaned_data[name])
# Normal fields # Normal fields

Binary file not shown.

Binary file not shown.

View File

@ -411,6 +411,7 @@ export class APISelect {
} finally { } finally {
this.setOptionStyles(); this.setOptionStyles();
this.enable(); this.enable();
this.slim.slim.search.input.focus();
this.base.dispatchEvent(this.loadEvent); this.base.dispatchEvent(this.loadEvent);
} }
} }

View File

@ -46,13 +46,7 @@
</tr> </tr>
<tr> <tr>
<th scope="row">Rack</th> <th scope="row">Rack</th>
<td> <td>{{ object.rack|linkify|placeholder }}</td>
{% if object.rack %}
<a href="{% url 'dcim:rack' pk=object.rack.pk %}">{{ object.rack }}</a>
{% else %}
{{ ''|placeholder }}
{% endif %}
</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Position</th> <th scope="row">Position</th>
@ -161,9 +155,7 @@
</tr> </tr>
<tr> <tr>
<th scope="row">Role</th> <th scope="row">Role</th>
<td> <td>{{ object.device_role|linkify }}</td>
<a href="{% url 'dcim:device_list' %}?role={{ object.device_role.slug }}">{{ object.device_role }}</a>
</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Platform</th> <th scope="row">Platform</th>
@ -173,7 +165,7 @@
<th scope="row">Primary IPv4</th> <th scope="row">Primary IPv4</th>
<td> <td>
{% if object.primary_ip4 %} {% if object.primary_ip4 %}
<a href="{% url 'ipam:ipaddress' pk=object.primary_ip4.pk %}">{{ object.primary_ip4.address.ip }}</a> <a href="{{ object.primary_ip4.get_absolute_url }}">{{ object.primary_ip4.address.ip }}</a>
{% if object.primary_ip4.nat_inside %} {% if object.primary_ip4.nat_inside %}
(NAT for {{ object.primary_ip4.nat_inside.address.ip|linkify }}) (NAT for {{ object.primary_ip4.nat_inside.address.ip|linkify }})
{% elif object.primary_ip4.nat_outside %} {% elif object.primary_ip4.nat_outside %}
@ -188,7 +180,7 @@
<th scope="row">Primary IPv6</th> <th scope="row">Primary IPv6</th>
<td> <td>
{% if object.primary_ip6 %} {% if object.primary_ip6 %}
<a href="{% url 'ipam:ipaddress' pk=object.primary_ip6.pk %}">{{ object.primary_ip6.address.ip }}</a> <a href="{{ object.primary_ip6.get_absolute_url }}">{{ object.primary_ip6.address.ip }}</a>
{% if object.primary_ip6.nat_inside %} {% if object.primary_ip6.nat_inside %}
(NAT for {{ object.primary_ip6.nat_inside.address.ip|linkify }}) (NAT for {{ object.primary_ip6.nat_inside.address.ip|linkify }})
{% elif object.primary_ip6.nat_outside %} {% elif object.primary_ip6.nat_outside %}

View File

@ -86,6 +86,15 @@
{% render_field form.tenant %} {% render_field form.tenant %}
</div> </div>
<div class="field-group my-5">
<div class="row mb-2">
<h5 class="offset-sm-3">Virtual Chassis</h5>
</div>
{% render_field form.virtual_chassis %}
{% render_field form.vc_position %}
{% render_field form.vc_priority %}
</div>
{% if form.custom_fields %} {% if form.custom_fields %}
<div class="field-group my-5"> <div class="field-group my-5">
<div class="row mb-2"> <div class="row mb-2">

View File

@ -5,25 +5,29 @@
{% render_errors form %} {% render_errors form %}
{% block content %} {% block content %}
{% if perms.extras.add_journalentry %}
<form action="{% url 'extras:journalentry_add' %}" method="post" enctype="multipart/form-data">
<div class="container">
<div class="field-group">
<h4>New Journal Entry</h4>
{% csrf_token %}
{% render_form form %}
</div>
<div class="col col-md-12 text-end my-3">
<a href="{{ object.get_absolute_url }}" class="btn btn-outline-danger">Cancel</a>
<button type="submit" class="btn btn-primary">Save</button>
</div>
</div>
</form>
{% endif %}
<div class="card"> <div class="card">
<div class="card-body table-responsive"> <div class="card-body table-responsive">
{% render_table table 'inc/table.html' %} {% render_table table 'inc/table.html' %}
{% include 'inc/paginator.html' with paginator=table.paginator page=table.page %} {% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
</div> </div>
</div> </div>
{% if perms.extras.add_journalentry %}
<div class="card">
<div class="card-body table-responsive">
<h4 class="card-header">New Journal Entry</h4>
<form action="{% url 'extras:journalentry_add' %}" method="post" enctype="multipart/form-data">
<div class="container">
<div class="field-group">
{% csrf_token %}
{% render_form form %}
</div>
<div class="col col-md-12 text-end my-3">
<a href="{{ object.get_absolute_url }}" class="btn btn-outline-danger">Cancel</a>
<button type="submit" class="btn btn-primary">Save</button>
</div>
</div>
</form>
</div>
</div>
{% endif %}
{% endblock %} {% endblock %}

View File

@ -2,6 +2,8 @@ import django_tables2 as tables
__all__ = ( __all__ = (
'TenantColumn', 'TenantColumn',
'TenantGroupColumn',
'TenancyColumnsMixin',
) )
@ -24,3 +26,32 @@ class TenantColumn(tables.TemplateColumn):
def value(self, value): def value(self, value):
return str(value) if value else None return str(value) if value else None
class TenantGroupColumn(tables.TemplateColumn):
"""
Include the tenant group description.
"""
template_code = """
{% if record.tenant and record.tenant.group %}
<a href="{{ record.tenant.group.get_absolute_url }}" title="{{ record.tenant.group.description }}">{{ record.tenant.group }}</a>
{% elif record.vrf.tenant and record.vrf.tenant.group %}
<a href="{{ record.vrf.tenant.group.get_absolute_url }}" title="{{ record.vrf.tenant.group.description }}">{{ record.vrf.tenant.group }}</a>*
{% else %}
&mdash;
{% endif %}
"""
def __init__(self, accessor=tables.A('tenant__group'), *args, **kwargs):
if 'verbose_name' not in kwargs:
kwargs['verbose_name'] = 'Tenant Group'
super().__init__(template_code=self.template_code, accessor=accessor, *args, **kwargs)
def value(self, value):
return str(value) if value else None
class TenancyColumnsMixin(tables.Table):
tenant_group = TenantGroupColumn()
tenant = TenantColumn()

View File

@ -1,6 +1,8 @@
from django.db import models from django.db import models
from timezone_field import TimeZoneField from timezone_field import TimeZoneField
from netbox.config import ConfigItem
SKIP_FIELDS = ( SKIP_FIELDS = (
TimeZoneField, TimeZoneField,
@ -26,4 +28,9 @@ def custom_deconstruct(field):
for attr in EXEMPT_ATTRS: for attr in EXEMPT_ATTRS:
kwargs.pop(attr, None) kwargs.pop(attr, None)
# Ignore any field defaults which reference a ConfigItem
kwargs = {
k: v for k, v in kwargs.items() if not isinstance(v, ConfigItem)
}
return name, path, args, kwargs return name, path, args, kwargs

View File

@ -1,6 +1,7 @@
import django_tables2 as tables import django_tables2 as tables
from netbox.tables import NetBoxTable, columns from netbox.tables import NetBoxTable, columns
from tenancy.tables import TenancyColumnsMixin
from virtualization.models import Cluster, ClusterGroup, ClusterType from virtualization.models import Cluster, ClusterGroup, ClusterType
__all__ = ( __all__ = (
@ -56,7 +57,7 @@ class ClusterGroupTable(NetBoxTable):
default_columns = ('pk', 'name', 'cluster_count', 'description') default_columns = ('pk', 'name', 'cluster_count', 'description')
class ClusterTable(NetBoxTable): class ClusterTable(TenancyColumnsMixin, NetBoxTable):
name = tables.Column( name = tables.Column(
linkify=True linkify=True
) )
@ -66,9 +67,6 @@ class ClusterTable(NetBoxTable):
group = tables.Column( group = tables.Column(
linkify=True linkify=True
) )
tenant = tables.Column(
linkify=True
)
site = tables.Column( site = tables.Column(
linkify=True linkify=True
) )
@ -93,7 +91,7 @@ class ClusterTable(NetBoxTable):
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = Cluster model = Cluster
fields = ( fields = (
'pk', 'id', 'name', 'type', 'group', 'tenant', 'site', 'comments', 'device_count', 'vm_count', 'contacts', 'pk', 'id', 'name', 'type', 'group', 'tenant', 'tenant_group', 'site', 'comments', 'device_count', 'vm_count', 'contacts',
'tags', 'created', 'last_updated', 'tags', 'created', 'last_updated',
) )
default_columns = ('pk', 'name', 'type', 'group', 'tenant', 'site', 'device_count', 'vm_count') default_columns = ('pk', 'name', 'type', 'group', 'tenant', 'site', 'device_count', 'vm_count')

View File

@ -2,7 +2,7 @@ import django_tables2 as tables
from dcim.tables.devices import BaseInterfaceTable from dcim.tables.devices import BaseInterfaceTable
from netbox.tables import NetBoxTable, columns from netbox.tables import NetBoxTable, columns
from tenancy.tables import TenantColumn from tenancy.tables import TenancyColumnsMixin
from virtualization.models import VirtualMachine, VMInterface from virtualization.models import VirtualMachine, VMInterface
__all__ = ( __all__ = (
@ -24,7 +24,7 @@ VMINTERFACE_BUTTONS = """
# Virtual machines # Virtual machines
# #
class VirtualMachineTable(NetBoxTable): class VirtualMachineTable(TenancyColumnsMixin, NetBoxTable):
name = tables.Column( name = tables.Column(
order_by=('_name',), order_by=('_name',),
linkify=True linkify=True
@ -34,7 +34,6 @@ class VirtualMachineTable(NetBoxTable):
linkify=True linkify=True
) )
role = columns.ColoredLabelColumn() role = columns.ColoredLabelColumn()
tenant = TenantColumn()
comments = columns.MarkdownColumn() comments = columns.MarkdownColumn()
primary_ip4 = tables.Column( primary_ip4 = tables.Column(
linkify=True, linkify=True,
@ -56,7 +55,7 @@ class VirtualMachineTable(NetBoxTable):
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = VirtualMachine model = VirtualMachine
fields = ( fields = (
'pk', 'id', 'name', 'status', 'cluster', 'role', 'tenant', 'platform', 'vcpus', 'memory', 'disk', 'pk', 'id', 'name', 'status', 'cluster', 'role', 'tenant', 'tenant_group', 'platform', 'vcpus', 'memory', 'disk',
'primary_ip4', 'primary_ip6', 'primary_ip', 'comments', 'tags', 'created', 'last_updated', 'primary_ip4', 'primary_ip6', 'primary_ip', 'comments', 'tags', 'created', 'last_updated',
) )
default_columns = ( default_columns = (

View File

@ -1,7 +1,7 @@
bleach==5.0.0 bleach==5.0.1
Django==4.0.5 Django==4.0.6
django-cors-headers==3.13.0 django-cors-headers==3.13.0
django-debug-toolbar==3.4.0 django-debug-toolbar==3.5.0
django-filter==22.1 django-filter==22.1
django-graphiql-debug-toolbar==0.2.0 django-graphiql-debug-toolbar==0.2.0
django-mptt==0.13.4 django-mptt==0.13.4
@ -19,13 +19,13 @@ gunicorn==20.1.0
Jinja2==3.1.2 Jinja2==3.1.2
Markdown==3.3.7 Markdown==3.3.7
markdown-include==0.6.0 markdown-include==0.6.0
mkdocs-material==8.3.6 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.1.1 Pillow==9.2.0
psycopg2-binary==2.9.3 psycopg2-binary==2.9.3
PyYAML==6.0 PyYAML==6.0
sentry-sdk==1.5.12 sentry-sdk==1.7.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.2 svgwrite==1.4.2