Просмотр исходного кода

Merge branch 'develop' into feature

jeremystretch 3 лет назад
Родитель
Сommit
cd3111ca8d

+ 36 - 12
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

+ 13 - 0
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)

+ 9 - 0
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.

+ 2 - 2
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',
         ]
 

+ 1 - 1
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']

+ 0 - 9
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
 #

+ 6 - 0
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)',

+ 16 - 1
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",

+ 3 - 3
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)),
             ],

+ 4 - 3
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(

+ 5 - 0
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]}

+ 3 - 0
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'),
         }),

+ 13 - 1
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

+ 10 - 7
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
-
-            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))
+        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))
+        except AttributeError:
+            pass
 
         services = Service.objects.restrict(request.user, 'view').filter(service_filter)
 

+ 23 - 0
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")

+ 25 - 0
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',

+ 13 - 0
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

+ 9 - 0
netbox/templates/dcim/device_edit.html

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

+ 7 - 0
netbox/utilities/management/commands/__init__.py

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