mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-15 19:52:52 -06:00
Merge branch 'develop' into feature
This commit is contained in:
commit
cd3111ca8d
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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.
|
||||
|
@ -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',
|
||||
]
|
||||
|
||||
|
@ -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']
|
||||
|
@ -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
|
||||
#
|
||||
|
@ -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)',
|
||||
|
@ -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",
|
||||
|
@ -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)),
|
||||
],
|
||||
|
@ -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(
|
||||
|
@ -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]}
|
||||
|
@ -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'),
|
||||
}),
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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")
|
||||
|
@ -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',
|
||||
|
@ -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
|
||||
|
@ -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">
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user