Переглянути джерело

Merge branch 'develop' into feature

jeremystretch 2 роки тому
батько
коміт
15590f1f48

+ 1 - 1
.github/ISSUE_TEMPLATE/bug_report.yaml

@@ -14,7 +14,7 @@ body:
     attributes:
       label: NetBox version
       description: What version of NetBox are you currently running?
-      placeholder: v3.4.6
+      placeholder: v3.4.7
     validations:
       required: true
   - type: dropdown

+ 1 - 1
.github/ISSUE_TEMPLATE/feature_request.yaml

@@ -14,7 +14,7 @@ body:
     attributes:
       label: NetBox version
       description: What version of NetBox are you currently running?
-      placeholder: v3.4.6
+      placeholder: v3.4.7
     validations:
       required: true
   - type: dropdown

+ 2 - 1
base_requirements.txt

@@ -129,7 +129,8 @@ social-auth-core
 
 # Django app for social-auth-core
 # https://github.com/python-social-auth/social-app-django
-social-auth-app-django
+# See https://github.com/python-social-auth/social-app-django/issues/429
+social-auth-app-django==5.0.0
 
 # SVG image rendering (used for rack elevations)
 # https://github.com/mozman/svgwrite

+ 1 - 1
docs/configuration/remote-authentication.md

@@ -16,7 +16,7 @@ If true, NetBox will automatically create local accounts for users authenticated
 
 Default: `'netbox.authentication.RemoteUserBackend'`
 
-This is the Python path to the custom [Django authentication backend](https://docs.djangoproject.com/en/stable/topics/auth/customizing/) to use for external user authentication. NetBox provides two built-in backends (listed below), though custom authentication backends may also be provided by other packages or plugins.
+This is the Python path to the custom [Django authentication backend](https://docs.djangoproject.com/en/stable/topics/auth/customizing/) to use for external user authentication. NetBox provides two built-in backends (listed below), though custom authentication backends may also be provided by other packages or plugins. Provide a string for a single backend, or an iterable for multiple backends, which will be attempted in the order given.
 
 * `netbox.authentication.RemoteUserBackend`
 * `netbox.authentication.LDAPBackend`

+ 1 - 1
docs/configuration/system.md

@@ -38,7 +38,7 @@ In order to send email, NetBox needs an email server configured. The following i
 * `SERVER` - Hostname or IP address of the email server (use `localhost` if running locally)
 * `PORT` - TCP port to use for the connection (default: `25`)
 * `USERNAME` - Username with which to authenticate
-* `PASSSWORD` - Password with which to authenticate
+* `PASSWORD` - Password with which to authenticate
 * `USE_SSL` - Use SSL when connecting to the server (default: `False`)
 * `USE_TLS` - Use TLS when connecting to the server (default: `False`)
 * `SSL_CERTFILE` - Path to the PEM-formatted SSL certificate file (optional)

+ 17 - 1
docs/release-notes/version-3.4.md

@@ -1,15 +1,31 @@
 # NetBox v3.4
 
-## v3.4.7 (FUTURE)
+## v3.4.8 (FUTURE)
+
+---
+
+## v3.4.7 (2023-03-28)
 
 ### Enhancements
 
+* [#11645](https://github.com/netbox-community/netbox/issues/11645) - Automatically set the scheduled time when executing reports/scripts at a recurring interval
 * [#11833](https://github.com/netbox-community/netbox/issues/11833) - Add fieldset support for custom script forms
+* [#11973](https://github.com/netbox-community/netbox/issues/11833) - Use SSID for representing wireless links, if set
+* [#11977](https://github.com/netbox-community/netbox/issues/11977) - Support designating multiple backends via `REMOTE_AUTH_BACKEND` config parameter
+* [#11990](https://github.com/netbox-community/netbox/issues/11990) - Improve error reporting for duplicate CSV column headings
+* [#11991](https://github.com/netbox-community/netbox/issues/11991) - Enable VDC assignment during bulk import/edit of interfaces
 
 ### Bug Fixes
 
+* [#11914](https://github.com/netbox-community/netbox/issues/11914) - Include parameters when exporting saved filters
+* [#11933](https://github.com/netbox-community/netbox/issues/11933) - Fix cloning of saved filters
 * [#11984](https://github.com/netbox-community/netbox/issues/11984) - Remove erroneous 802.3az PoE type
 * [#11979](https://github.com/netbox-community/netbox/issues/11979) - Correct URL for tags in route targets list
+* [#12008](https://github.com/netbox-community/netbox/issues/12008) - Enable cloning of export templates
+* [#12029](https://github.com/netbox-community/netbox/issues/12029) - Restore missing description field on virtual chassis form
+* [#12038](https://github.com/netbox-community/netbox/issues/12038) - Correct display of zero values for virtual chassis member priority
+* [#12048](https://github.com/netbox-community/netbox/issues/12048) - Enable cloning of tags
+* [#12058](https://github.com/netbox-community/netbox/issues/12058) - Enable cloning of config contexts
 
 ---
 

+ 10 - 2
netbox/dcim/forms/bulk_edit.py

@@ -1160,6 +1160,14 @@ class InterfaceBulkEditForm(
         },
         label=_('LAG')
     )
+    vdcs = DynamicModelMultipleChoiceField(
+        queryset=VirtualDeviceContext.objects.all(),
+        required=False,
+        label='Virtual Device Contexts',
+        query_params={
+            'device_id': '$device',
+        }
+    )
     speed = forms.IntegerField(
         required=False,
         widget=SelectSpeedWidget(),
@@ -1222,14 +1230,14 @@ class InterfaceBulkEditForm(
     fieldsets = (
         (None, ('module', 'type', 'label', 'speed', 'duplex', 'description')),
         ('Addressing', ('vrf', 'mac_address', 'wwn')),
-        ('Operation', ('mtu', 'tx_power', 'enabled', 'mgmt_only', 'mark_connected')),
+        ('Operation', ('vdcs', 'mtu', 'tx_power', 'enabled', 'mgmt_only', 'mark_connected')),
         ('PoE', ('poe_mode', 'poe_type')),
         ('Related Interfaces', ('parent', 'bridge', 'lag')),
         ('802.1Q Switching', ('mode', 'vlan_group', 'untagged_vlan', 'tagged_vlans')),
         ('Wireless', ('rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width')),
     )
     nullable_fields = (
-        'module', 'label', 'parent', 'bridge', 'lag', 'speed', 'duplex', 'mac_address', 'wwn', 'mtu', 'description',
+        'module', 'label', 'parent', 'bridge', 'lag', 'speed', 'duplex', 'mac_address', 'wwn', 'vdcs', 'mtu', 'description',
         'poe_mode', 'poe_type', 'mode', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power',
         'vlan_group', 'untagged_vlan', 'tagged_vlans', 'vrf',
     )

+ 17 - 2
netbox/dcim/forms/bulk_import.py

@@ -12,7 +12,9 @@ from extras.models import ConfigTemplate
 from ipam.models import VRF
 from netbox.forms import NetBoxModelImportForm
 from tenancy.models import Tenant
-from utilities.forms import CSVChoiceField, CSVContentTypeField, CSVModelChoiceField, CSVTypedChoiceField, SlugField
+from utilities.forms import (
+    CSVChoiceField, CSVContentTypeField, CSVModelChoiceField, CSVTypedChoiceField, SlugField, CSVModelMultipleChoiceField
+)
 from virtualization.models import Cluster
 from wireless.choices import WirelessRoleChoices
 from .common import ModuleCommonForm
@@ -691,6 +693,12 @@ class InterfaceImportForm(NetBoxModelImportForm):
         to_field_name='name',
         help_text=_('Parent LAG interface')
     )
+    vdcs = CSVModelMultipleChoiceField(
+        queryset=VirtualDeviceContext.objects.all(),
+        required=False,
+        to_field_name='name',
+        help_text='VDC names separated by commas, encased with double quotes (e.g. "vdc1, vdc2, vdc3")'
+    )
     type = CSVChoiceField(
         choices=InterfaceTypeChoices,
         help_text=_('Physical medium')
@@ -730,7 +738,7 @@ class InterfaceImportForm(NetBoxModelImportForm):
         model = Interface
         fields = (
             'device', 'name', 'label', 'parent', 'bridge', 'lag', 'type', 'speed', 'duplex', 'enabled',
-            'mark_connected', 'mac_address', 'wwn', 'mtu', 'mgmt_only', 'description', 'poe_mode', 'poe_type', 'mode',
+            'mark_connected', 'mac_address', 'wwn', 'vdcs', 'mtu', 'mgmt_only', 'description', 'poe_mode', 'poe_type', 'mode',
             'vrf', 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'tags'
         )
 
@@ -746,6 +754,7 @@ class InterfaceImportForm(NetBoxModelImportForm):
                 self.fields['parent'].queryset = self.fields['parent'].queryset.filter(**params)
                 self.fields['bridge'].queryset = self.fields['bridge'].queryset.filter(**params)
                 self.fields['lag'].queryset = self.fields['lag'].queryset.filter(**params)
+                self.fields['vdcs'].queryset = self.fields['vdcs'].queryset.filter(**params)
 
     def clean_enabled(self):
         # Make sure enabled is True when it's not included in the uploaded data
@@ -754,6 +763,12 @@ class InterfaceImportForm(NetBoxModelImportForm):
         else:
             return self.cleaned_data['enabled']
 
+    def clean_vdcs(self):
+        for vdc in self.cleaned_data['vdcs']:
+            if vdc.device != self.cleaned_data['device']:
+                raise forms.ValidationError(f"VDC {vdc} is not assigned to device {self.cleaned_data['device']}")
+        return self.cleaned_data['vdcs']
+
 
 class FrontPortImportForm(NetBoxModelImportForm):
     device = CSVModelChoiceField(

+ 1 - 1
netbox/dcim/forms/object_create.py

@@ -359,7 +359,7 @@ class VirtualChassisCreateForm(NetBoxModelForm):
     class Meta:
         model = VirtualChassis
         fields = [
-            'name', 'domain', 'region', 'site_group', 'site', 'rack', 'members', 'initial_position', 'tags',
+            'name', 'domain', 'description', 'region', 'site_group', 'site', 'rack', 'members', 'initial_position', 'tags',
         ]
 
     def clean(self):

+ 12 - 6
netbox/extras/forms/model_forms.py

@@ -1,6 +1,7 @@
+import json
+
 from django import forms
 from django.contrib.contenttypes.models import ContentType
-from django.http import QueryDict
 from django.utils.translation import gettext as _
 
 from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup
@@ -147,11 +148,10 @@ class SavedFilterForm(BootstrapMixin, forms.ModelForm):
 
     def __init__(self, *args, initial=None, **kwargs):
 
-        # Convert any parameters delivered via initial data to a dictionary
+        # Convert any parameters delivered via initial data to JSON data
         if initial and 'parameters' in initial:
             if type(initial['parameters']) is str:
-                # TODO: Make a utility function for this
-                initial['parameters'] = dict(QueryDict(initial['parameters']).lists())
+                initial['parameters'] = json.loads(initial['parameters'])
 
         super().__init__(*args, initial=initial, **kwargs)
 
@@ -277,8 +277,14 @@ class ConfigContextForm(BootstrapMixin, SyncedDataMixin, forms.ModelForm):
             'tenants', 'tags', 'data_source', 'data_file',
         )
 
-    def __init__(self, *args, **kwargs):
-        super().__init__(*args, **kwargs)
+    def __init__(self, *args, initial=None, **kwargs):
+
+        # Convert data delivered via initial data to JSON data
+        if initial and 'data' in initial:
+            if type(initial['data']) is str:
+                initial['data'] = json.loads(initial['data'])
+
+        super().__init__(*args, initial=initial, **kwargs)
 
         # Disable data field when a DataFile has been set
         if self.instance.data_file:

+ 7 - 3
netbox/extras/forms/reports.py

@@ -25,12 +25,16 @@ class ReportForm(BootstrapMixin, forms.Form):
         help_text=_("Interval at which this report is re-run (in minutes)")
     )
 
-    def clean_schedule_at(self):
+    def clean(self):
         scheduled_time = self.cleaned_data['schedule_at']
-        if scheduled_time and scheduled_time < timezone.now():
+        if scheduled_time and scheduled_time < local_now():
             raise forms.ValidationError(_('Scheduled time must be in the future.'))
 
-        return scheduled_time
+        # When interval is used without schedule at, raise an exception
+        if self.cleaned_data['interval'] and not scheduled_time:
+            self.cleaned_data['schedule_at'] = local_now()
+
+        return self.cleaned_data
 
     def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)

+ 1 - 1
netbox/extras/forms/scripts.py

@@ -52,7 +52,7 @@ class ScriptForm(BootstrapMixin, forms.Form):
 
         # When interval is used without schedule at, raise an exception
         if self.cleaned_data['_interval'] and not scheduled_time:
-            raise forms.ValidationError(_('Scheduled time must be set when recurs is used.'))
+            self.cleaned_data['_schedule_at'] = local_now()
 
         return self.cleaned_data
 

+ 8 - 3
netbox/extras/models/configs.py

@@ -2,7 +2,6 @@ from django.conf import settings
 from django.core.validators import ValidationError
 from django.db import models
 from django.urls import reverse
-from django.utils import timezone
 from django.utils.translation import gettext as _
 from jinja2.loaders import BaseLoader
 from jinja2.sandbox import SandboxedEnvironment
@@ -10,7 +9,7 @@ from jinja2.sandbox import SandboxedEnvironment
 from extras.querysets import ConfigContextQuerySet
 from netbox.config import get_config
 from netbox.models import ChangeLoggedModel
-from netbox.models.features import ExportTemplatesMixin, SyncedDataMixin, TagsMixin
+from netbox.models.features import CloningMixin, ExportTemplatesMixin, SyncedDataMixin, TagsMixin
 from utilities.jinja2 import ConfigTemplateLoader
 from utilities.utils import deepmerge
 
@@ -25,7 +24,7 @@ __all__ = (
 # Config contexts
 #
 
-class ConfigContext(SyncedDataMixin, ChangeLoggedModel):
+class ConfigContext(SyncedDataMixin, CloningMixin, ChangeLoggedModel):
     """
     A ConfigContext represents a set of arbitrary data available to any Device or VirtualMachine matching its assigned
     qualifiers (region, site, etc.). For example, the data stored in a ConfigContext assigned to site A and tenant B
@@ -114,6 +113,12 @@ class ConfigContext(SyncedDataMixin, ChangeLoggedModel):
 
     objects = ConfigContextQuerySet.as_manager()
 
+    clone_fields = (
+        'weight', 'is_active', 'regions', 'site_groups', 'sites', 'locations', 'device_types',
+        'roles', 'platforms', 'cluster_types', 'cluster_groups', 'clusters', 'tenant_groups',
+        'tenants', 'tags', 'data',
+    )
+
     class Meta:
         ordering = ['weight', 'name']
 

+ 7 - 3
netbox/extras/models/models.py

@@ -250,7 +250,7 @@ class CustomLink(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
     )
 
     clone_fields = (
-        'enabled', 'weight', 'group_name', 'button_class', 'new_window',
+        'content_types', 'enabled', 'weight', 'group_name', 'button_class', 'new_window',
     )
 
     class Meta:
@@ -285,7 +285,7 @@ class CustomLink(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
         }
 
 
-class ExportTemplate(SyncedDataMixin, ExportTemplatesMixin, ChangeLoggedModel):
+class ExportTemplate(SyncedDataMixin, CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
     content_types = models.ManyToManyField(
         to=ContentType,
         related_name='export_templates',
@@ -318,6 +318,10 @@ class ExportTemplate(SyncedDataMixin, ExportTemplatesMixin, ChangeLoggedModel):
         help_text=_("Download file as attachment")
     )
 
+    clone_fields = (
+        'content_types', 'template_code', 'mime_type', 'file_extension', 'as_attachment',
+    )
+
     class Meta:
         ordering = ('name',)
 
@@ -417,7 +421,7 @@ class SavedFilter(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
     parameters = models.JSONField()
 
     clone_fields = (
-        'enabled', 'weight',
+        'content_types', 'weight', 'enabled', 'parameters',
     )
 
     class Meta:

+ 6 - 2
netbox/extras/models/tags.py

@@ -5,7 +5,7 @@ from django.utils.text import slugify
 from taggit.models import TagBase, GenericTaggedItemBase
 
 from netbox.models import ChangeLoggedModel
-from netbox.models.features import ExportTemplatesMixin
+from netbox.models.features import CloningMixin, ExportTemplatesMixin
 from utilities.choices import ColorChoices
 from utilities.fields import ColorField
 
@@ -19,7 +19,7 @@ __all__ = (
 # Tags
 #
 
-class Tag(ExportTemplatesMixin, ChangeLoggedModel, TagBase):
+class Tag(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel, TagBase):
     id = models.BigAutoField(
         primary_key=True
     )
@@ -31,6 +31,10 @@ class Tag(ExportTemplatesMixin, ChangeLoggedModel, TagBase):
         blank=True,
     )
 
+    clone_fields = (
+        'color', 'description',
+    )
+
     class Meta:
         ordering = ['name']
 

+ 6 - 1
netbox/netbox/models/features.py

@@ -1,3 +1,4 @@
+import json
 from collections import defaultdict
 from functools import cached_property
 
@@ -115,7 +116,11 @@ class CloningMixin(models.Model):
         for field_name in getattr(self, 'clone_fields', []):
             field = self._meta.get_field(field_name)
             field_value = field.value_from_object(self)
-            if field_value not in (None, ''):
+            if field_value and isinstance(field, models.ManyToManyField):
+                attrs[field_name] = [v.pk for v in field_value]
+            elif field_value and isinstance(field, models.JSONField):
+                attrs[field_name] = json.dumps(field_value)
+            elif field_value not in (None, ''):
                 attrs[field_name] = field_value
 
         # Include tags (if applicable)

+ 3 - 1
netbox/netbox/settings.py

@@ -394,8 +394,10 @@ TEMPLATES = [
 ]
 
 # Set up authentication backends
+if type(REMOTE_AUTH_BACKEND) not in (list, tuple):
+    REMOTE_AUTH_BACKEND = [REMOTE_AUTH_BACKEND]
 AUTHENTICATION_BACKENDS = [
-    REMOTE_AUTH_BACKEND,
+    *REMOTE_AUTH_BACKEND,
     'netbox.authentication.ObjectPermissionBackend',
 ]
 

+ 1 - 1
netbox/templates/dcim/device.html

@@ -141,7 +141,7 @@
                                       {% if object.virtual_chassis.master == vc_member %}<i class="mdi mdi-check-bold"></i>{% endif %}
                                     </td>
                                     <td>
-                                      {{ vc_member.vc_priority|default:"" }}
+                                      {{ vc_member.vc_priority|placeholder }}
                                     </td>
                                 </tr>
                             {% endfor %}

+ 1 - 0
netbox/templates/dcim/virtualchassis_add.html

@@ -8,6 +8,7 @@
     </div>
     {% render_field form.name %}
     {% render_field form.domain %}
+    {% render_field form.description %}
     {% render_field form.tags %}
   </div>
 

+ 6 - 6
requirements.txt

@@ -3,7 +3,7 @@ boto3==1.26.91
 Django==4.1.7
 django-cors-headers==3.14.0
 django-debug-toolbar==3.8.1
-django-filter==22.1
+django-filter==23.1
 django-graphiql-debug-toolbar==0.2.0
 django-mptt==0.14
 django-pglocks==1.0.4
@@ -21,18 +21,18 @@ graphene-django==3.0.0
 gunicorn==20.1.0
 Jinja2==3.1.2
 Markdown==3.3.7
-mkdocs-material==9.1.2
+mkdocs-material==9.1.4
 mkdocstrings[python-legacy]==0.20.0
 netaddr==0.8.0
 Pillow==9.4.0
 psycopg2-binary==2.9.5
 PyYAML==6.0
-sentry-sdk==1.16.0
+sentry-sdk==1.18.0
 social-auth-app-django==5.0.0
-social-auth-core[openidconnect]==4.3.0
+social-auth-core[openidconnect]==4.4.0
 svgwrite==1.4.3
-tablib==3.3.0
-tzdata==2022.7
+tablib==3.4.0
+tzdata==2023.2
 
 # Workaround for #7401
 jsonschema==3.2.0