Merge branch 'develop' into feature

This commit is contained in:
jeremystretch 2022-06-28 16:55:49 -04:00
commit cd3111ca8d
19 changed files with 197 additions and 38 deletions

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
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
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
Default: False

View File

@ -2,6 +2,19 @@
## v3.2.6 (FUTURE)
### 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
### 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
---
## v3.2.5 (2022-06-20)

View File

@ -5,6 +5,7 @@ from netbox.api.serializers import BaseModelSerializer, WritableNestedSerializer
__all__ = [
'ComponentNestedModuleSerializer',
'ModuleBayNestedModuleSerializer',
'NestedCableSerializer',
'NestedConsolePortSerializer',
'NestedConsolePortTemplateSerializer',
@ -281,6 +282,14 @@ class ModuleNestedModuleBaySerializer(WritableNestedSerializer):
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):
"""
Used by device component serializers.

View File

@ -912,12 +912,12 @@ class FrontPortSerializer(NetBoxModelSerializer, LinkTerminationSerializer):
class ModuleBaySerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:modulebay-detail')
device = NestedDeviceSerializer()
# installed_module = NestedModuleSerializer(required=False, allow_null=True)
installed_module = ModuleBayNestedModuleSerializer(required=False, allow_null=True)
class Meta:
model = ModuleBay
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',
]

View File

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

View File

@ -50,15 +50,6 @@ 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
#

View File

@ -996,6 +996,12 @@ class ModuleFilterSet(NetBoxModelFilterSet):
to_field_name='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(
queryset=Device.objects.all(),
label='Device (ID)',

View File

@ -525,13 +525,28 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
required=False,
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:
model = Device
fields = [
'name', 'device_role', 'device_type', 'serial', 'asset_tag', 'region', 'site_group', 'site', 'rack',
'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 = {
'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)),
('supply', models.CharField(default='ac', max_length=50)),
('phase', models.CharField(default='single-phase', max_length=50)),
('voltage', models.SmallIntegerField(default=120, validators=[utilities.validators.ExclusionValidator([0])])),
('amperage', models.PositiveSmallIntegerField(default=20, validators=[django.core.validators.MinValueValidator(1)])),
('max_utilization', models.PositiveSmallIntegerField(default=80, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100)])),
('voltage', models.SmallIntegerField(validators=[utilities.validators.ExclusionValidator([0])])),
('amperage', models.PositiveSmallIntegerField(validators=[django.core.validators.MinValueValidator(1)])),
('max_utilization', models.PositiveSmallIntegerField(validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100)])),
('available_power', models.PositiveIntegerField(default=0, editable=False)),
('comments', models.TextField(blank=True)),
],

View File

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

View File

@ -1853,6 +1853,11 @@ class ModuleTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'module_type': [module_types[0].model, module_types[1].model]}
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):
device_types = Device.objects.all()[:2]
params = {'device_id': [device_types[0].pk, device_types[1].pk]}

View File

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

View File

@ -1,7 +1,8 @@
from django import forms
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.constants import *
from ipam.models import *
@ -265,6 +266,7 @@ class IPAddressFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
('Attributes', ('parent', 'family', 'status', 'role', 'mask_length', 'assigned_to_interface')),
('VRF', ('vrf_id', 'present_in_vrf_id')),
('Tenant', ('tenant_group_id', 'tenant_id')),
('Device/VM', ('device_id', 'virtual_machine_id')),
)
parent = forms.CharField(
required=False,
@ -298,6 +300,16 @@ class IPAddressFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
required=False,
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(
choices=IPAddressStatusChoices,
required=False

View File

@ -680,13 +680,16 @@ class IPAddressView(generic.ObjectView):
service_filter = Q(ipaddresses=instance)
# Find services listening on all IPs on the assigned device/vm
if instance.assigned_object and instance.assigned_object.parent_object:
parent_object = instance.assigned_object.parent_object
try:
if instance.assigned_object and instance.assigned_object.parent_object:
parent_object = instance.assigned_object.parent_object
if isinstance(parent_object, VirtualMachine):
service_filter |= (Q(virtual_machine=parent_object) & Q(ipaddresses=None))
elif isinstance(parent_object, Device):
service_filter |= (Q(device=parent_object) & Q(ipaddresses=None))
if isinstance(parent_object, VirtualMachine):
service_filter |= (Q(virtual_machine=parent_object) & Q(ipaddresses=None))
elif isinstance(parent_object, Device):
service_filter |= (Q(device=parent_object) & Q(ipaddresses=None))
except AttributeError:
pass
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)
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
),
# 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
ConfigParam(
name='ALLOWED_URL_SCHEMES',

View File

@ -485,6 +485,19 @@ for param in dir(configuration):
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

View File

@ -86,6 +86,15 @@
{% render_field form.tenant %}
</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 %}
<div class="field-group my-5">
<div class="row mb-2">

View File

@ -1,6 +1,8 @@
from django.db import models
from timezone_field import TimeZoneField
from netbox.config import ConfigItem
SKIP_FIELDS = (
TimeZoneField,
@ -26,4 +28,9 @@ def custom_deconstruct(field):
for attr in EXEMPT_ATTRS:
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