diff --git a/docs/configuration/dynamic-settings.md b/docs/configuration/dynamic-settings.md index 2fa046fcf..d376dc5c4 100644 --- a/docs/configuration/dynamic-settings.md +++ b/docs/configuration/dynamic-settings.md @@ -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 diff --git a/docs/release-notes/version-3.2.md b/docs/release-notes/version-3.2.md index 059fc8924..57d965538 100644 --- a/docs/release-notes/version-3.2.md +++ b/docs/release-notes/version-3.2.md @@ -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) diff --git a/netbox/dcim/api/nested_serializers.py b/netbox/dcim/api/nested_serializers.py index 0ec0e07e0..1be8bb9dc 100644 --- a/netbox/dcim/api/nested_serializers.py +++ b/netbox/dcim/api/nested_serializers.py @@ -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. diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 8ac2aa738..debd7415b 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -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', ] diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index fdf53053e..0aa21eee4 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -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'] diff --git a/netbox/dcim/constants.py b/netbox/dcim/constants.py index 68bbd1dbe..b813b862d 100644 --- a/netbox/dcim/constants.py +++ b/netbox/dcim/constants.py @@ -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 # diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py index 628bd58f6..ee95fd75d 100644 --- a/netbox/dcim/filtersets.py +++ b/netbox/dcim/filtersets.py @@ -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)', diff --git a/netbox/dcim/forms/models.py b/netbox/dcim/forms/models.py index 7aa2a8584..aa573a4df 100644 --- a/netbox/dcim/forms/models.py +++ b/netbox/dcim/forms/models.py @@ -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", diff --git a/netbox/dcim/migrations/0001_squashed.py b/netbox/dcim/migrations/0001_squashed.py index bb99d199f..374d3bf45 100644 --- a/netbox/dcim/migrations/0001_squashed.py +++ b/netbox/dcim/migrations/0001_squashed.py @@ -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)), ], diff --git a/netbox/dcim/models/power.py b/netbox/dcim/models/power.py index 08f89e3b0..5978d86bd 100644 --- a/netbox/dcim/models/power.py +++ b/netbox/dcim/models/power.py @@ -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( diff --git a/netbox/dcim/tests/test_filtersets.py b/netbox/dcim/tests/test_filtersets.py index 9df75f4c0..b2fe2018b 100644 --- a/netbox/dcim/tests/test_filtersets.py +++ b/netbox/dcim/tests/test_filtersets.py @@ -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]} diff --git a/netbox/extras/admin.py b/netbox/extras/admin.py index 28902c323..01011b276 100644 --- a/netbox/extras/admin.py +++ b/netbox/extras/admin.py @@ -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'), }), diff --git a/netbox/ipam/forms/filtersets.py b/netbox/ipam/forms/filtersets.py index bbd6bb97b..3d67d4d37 100644 --- a/netbox/ipam/forms/filtersets.py +++ b/netbox/ipam/forms/filtersets.py @@ -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 diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index 6682fc920..df4d2d899 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -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) diff --git a/netbox/netbox/authentication.py b/netbox/netbox/authentication.py index a13e8d192..00fb3ee66 100644 --- a/netbox/netbox/authentication.py +++ b/netbox/netbox/authentication.py @@ -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") diff --git a/netbox/netbox/config/parameters.py b/netbox/netbox/config/parameters.py index 68c96b38a..e2295888f 100644 --- a/netbox/netbox/config/parameters.py +++ b/netbox/netbox/config/parameters.py @@ -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', diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 446198c61..b776650dc 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -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 diff --git a/netbox/templates/dcim/device_edit.html b/netbox/templates/dcim/device_edit.html index 7cbb224c9..38125e83c 100644 --- a/netbox/templates/dcim/device_edit.html +++ b/netbox/templates/dcim/device_edit.html @@ -86,6 +86,15 @@ {% render_field form.tenant %} +