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

Merge branch 'develop' into feature

Jeremy Stretch 2 лет назад
Родитель
Сommit
b83fcc6077

+ 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.6.5
+      placeholder: v3.6.6
     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.6.5
+      placeholder: v3.6.6
     validations:
       required: true
   - type: dropdown

+ 24 - 1
docs/release-notes/version-3.6.md

@@ -1,6 +1,29 @@
 # NetBox v3.6
 
-## v3.6.6 (FUTURE)
+## v3.6.7 (FUTURE)
+
+---
+
+## v3.6.6 (2023-11-29)
+
+### Enhancements
+
+* [#13735](https://github.com/netbox-community/netbox/issues/13735) - Show complete region hierarchy in UI for all relevant objects
+
+### Bug Fixes
+
+* [#14056](https://github.com/netbox-community/netbox/issues/14056) - Record a pre-change snapshot when bulk editing objects via CSV
+* [#14187](https://github.com/netbox-community/netbox/issues/14187) - Raise a validation error when attempting to create a duplicate script or report
+* [#14199](https://github.com/netbox-community/netbox/issues/14199) - Fix jobs list for reports with a custom name
+* [#14239](https://github.com/netbox-community/netbox/issues/14239) - Fix CustomFieldChoiceSet search filter
+* [#14242](https://github.com/netbox-community/netbox/issues/14242) - Enable export templates for contact assignments
+* [#14299](https://github.com/netbox-community/netbox/issues/14299) - Webhook timestamps should be in proper ISO 8601 format
+* [#14325](https://github.com/netbox-community/netbox/issues/14325) - Fix numeric ordering of service ports
+* [#14339](https://github.com/netbox-community/netbox/issues/14339) - Correctly hash local user password when set via REST API
+* [#14343](https://github.com/netbox-community/netbox/issues/14343) - Fix ordering ASN table by ASDOT column
+* [#14346](https://github.com/netbox-community/netbox/issues/14346) - Fix running reports via REST API
+* [#14349](https://github.com/netbox-community/netbox/issues/14349) - Fix custom validation support for remote data sources
+* [#14363](https://github.com/netbox-community/netbox/issues/14363) - Fix bulk editing of interfaces assigned to VM with no cluster
 
 ---
 

+ 1 - 0
netbox/core/models/data.py

@@ -116,6 +116,7 @@ class DataSource(JobsMixin, PrimaryModel):
         )
 
     def clean(self):
+        super().clean()
 
         # Validate data backend type
         if self.type and self.type not in registry['data_backends']:

+ 9 - 0
netbox/core/models/files.py

@@ -2,6 +2,7 @@ import logging
 import os
 
 from django.conf import settings
+from django.core.exceptions import ValidationError
 from django.db import models
 from django.urls import reverse
 from django.utils.translation import gettext as _
@@ -85,6 +86,14 @@ class ManagedFile(SyncedDataMixin, models.Model):
             self.file_path = os.path.basename(self.data_path)
             self.data_file.write_to_disk(self.full_path, overwrite=True)
 
+    def clean(self):
+        super().clean()
+
+        # Ensure that the file root and path make a unique pair
+        if self._meta.model.objects.filter(file_root=self.file_root, file_path=self.file_path).exclude(pk=self.pk).exists():
+            raise ValidationError(
+                f"A {self._meta.verbose_name.lower()} with this file path already exists ({self.file_root}/{self.file_path}).")
+
     def delete(self, *args, **kwargs):
         # Delete file from disk
         try:

+ 1 - 1
netbox/core/models/jobs.py

@@ -244,7 +244,7 @@ class Job(models.Model):
                 model_name=self.object_type.model,
                 event=event,
                 data=self.data,
-                timestamp=str(timezone.now()),
+                timestamp=timezone.now().isoformat(),
                 username=self.user.username,
                 retry=get_rq_retry()
             )

+ 1 - 1
netbox/extras/api/views.py

@@ -283,7 +283,7 @@ class ReportViewSet(ViewSet):
 
         # Retrieve and run the Report. This will create a new Job.
         module, report_cls = self._get_report(pk)
-        report = report_cls()
+        report = report_cls
         input_serializer = serializers.ReportInputSerializer(
             data=request.data,
             context={'report': report}

+ 1 - 2
netbox/extras/filtersets.py

@@ -121,8 +121,7 @@ class CustomFieldChoiceSetFilterSet(BaseFilterSet):
             return queryset
         return queryset.filter(
             Q(name__icontains=value) |
-            Q(description__icontains=value) |
-            Q(extra_choices__contains=value)
+            Q(description__icontains=value)
         )
 
     def filter_by_choice(self, queryset, name, value):

+ 1 - 1
netbox/extras/views.py

@@ -1093,7 +1093,7 @@ class ReportJobsView(ContentTypePermissionRequiredMixin, View):
         jobs = Job.objects.filter(
             object_type=object_type,
             object_id=module.pk,
-            name=report.name
+            name=report.class_name
         )
 
         jobs_table = JobTable(

+ 1 - 1
netbox/extras/webhooks.py

@@ -115,7 +115,7 @@ def flush_webhooks(queue):
                 event=data['event'],
                 data=data['data'],
                 snapshots=data['snapshots'],
-                timestamp=str(timezone.now()),
+                timestamp=timezone.now().isoformat(),
                 username=data['username'],
                 request_id=data['request_id'],
                 retry=get_rq_retry()

+ 1 - 0
netbox/ipam/tables/asn.py

@@ -48,6 +48,7 @@ class ASNTable(TenancyColumnsMixin, NetBoxTable):
     asn_asdot = tables.Column(
         accessor=tables.A('asn_asdot'),
         linkify=True,
+        order_by=tables.A('asn'),
         verbose_name=_('ASDOT')
     )
     site_count = columns.LinkedCountColumn(

+ 1 - 1
netbox/netbox/settings.py

@@ -27,7 +27,7 @@ from netbox.plugins import PluginConfig
 # Environment setup
 #
 
-VERSION = '3.6.6-dev'
+VERSION = '3.6.7-dev'
 
 # Hostname
 HOSTNAME = platform.node()

+ 4 - 0
netbox/netbox/views/generic/bulk_views.py

@@ -394,6 +394,10 @@ class BulkImportView(GetReturnURLMixin, BaseMultiObjectView):
                     form.add_error('data', f"Row {i}: Object with ID {object_id} does not exist")
                     raise ValidationError('')
 
+                # Take a snapshot for change logging
+                if instance.pk and hasattr(instance, 'snapshot'):
+                    instance.snapshot()
+
             # Instantiate the model form for the object
             model_form_kwargs = {
                 'data': record,

+ 3 - 20
netbox/templates/dcim/device.html

@@ -5,6 +5,7 @@
 {% load helpers %}
 {% load plugins %}
 {% load i18n %}
+{% load mptt %}
 
 {% block content %}
     <div class="row">
@@ -15,16 +16,7 @@
                     <table class="table table-hover attr-table">
                         <tr>
                             <th scope="row">{% trans "Region" %}</th>
-                            <td>
-                                {% if object.site.region %}
-                                    {% for region in object.site.region.get_ancestors %}
-                                        {{ region|linkify }} /
-                                    {% endfor %}
-                                    {{ object.site.region|linkify }}
-                                {% else %}
-                                    {{ ''|placeholder }}
-                                {% endif %}
-                            </td>
+                            <td>{% nested_tree object.site.region %}</td>
                         </tr>
                         <tr>
                             <th scope="row">{% trans "Site" %}</th>
@@ -32,16 +24,7 @@
                         </tr>
                         <tr>
                             <th scope="row">{% trans "Location" %}</th>
-                            <td>
-                            {% if object.location %}
-                                {% for location in object.location.get_ancestors %}
-                                    {{ location|linkify }} /
-                                {% endfor %}
-                                {{ object.location|linkify }}
-                            {% else %}
-                                {{ ''|placeholder }}
-                            {% endif %}
-                            </td>
+                            <td>{% nested_tree object.location %}</td>
                         </tr>
                         <tr>
                             <th scope="row">{% trans "Rack" %}</th>

+ 8 - 15
netbox/templates/dcim/rack.html

@@ -4,6 +4,7 @@
 {% load static %}
 {% load plugins %}
 {% load i18n %}
+{% load mptt %}
 
 {% block content %}
   <div class="row">
@@ -15,26 +16,18 @@
             <div class="card-body">
                 <table class="table table-hover attr-table">
                     <tr>
-                        <th scope="row">{% trans "Site" %}</th>
+                        <th scope="row">{% trans "Region" %}</th>
                         <td>
-                            {% if object.site.region %}
-                                {{ object.site.region|linkify }} /
-                            {% endif %}
-                            {{ object.site|linkify }}
+                            {% nested_tree object.site.region %}
                         </td>
                     </tr>
+                    <tr>
+                        <th scope="row">{% trans "Site" %}</th>
+                        <td>{{ object.site|linkify }}</td>
+                    </tr>
                     <tr>
                         <th scope="row">{% trans "Location" %}</th>
-                        <td>
-                            {% if object.location %}
-                                {% for location in object.location.get_ancestors %}
-                                    {{ location|linkify }} /
-                                {% endfor %}
-                                {{ object.location|linkify }}
-                            {% else %}
-                                {{ ''|placeholder }}
-                            {% endif %}
-                        </td>
+                        <td>{% nested_tree object.location %}</td>
                     </tr>
                     <tr>
                         <th scope="row">{% trans "Facility ID" %}</th>

+ 19 - 19
netbox/templates/dcim/rackreservation.html

@@ -4,6 +4,7 @@
 {% load static %}
 {% load plugins %}
 {% load i18n %}
+{% load mptt %}
 
 {% block breadcrumbs %}
   {{ block.super }}
@@ -20,25 +21,24 @@
             </h5>
             <div class="card-body">
                 <table class="table table-hover attr-table">
-                    {% with rack=object.rack %}
-                        <tr>
-                            <th scope="row">{% trans "Site" %}</th>
-                            <td>
-                                {% if rack.site.region %}
-                                    {{ rack.site.region|linkify }} /
-                                {% endif %}
-                                {{ rack.site|linkify }}
-                            </td>
-                        </tr>
-                        <tr>
-                            <th scope="row">{% trans "Location" %}</th>
-                            <td>{{ rack.location|linkify|placeholder }}</td>
-                        </tr>
-                        <tr>
-                            <th scope="row">{% trans "Rack" %}</th>
-                            <td>{{ rack|linkify }}</td>
-                        </tr>
-                    {% endwith %}
+                    <tr>
+                        <th scope="row">{% trans "Region" %}</th>
+                        <td>
+                            {% nested_tree object.rack.site.region %}
+                        </td>
+                    </tr>
+                    <tr>
+                        <th scope="row">{% trans "Site" %}</th>
+                        <td>{{ object.rack.site|linkify }}</td>
+                    </tr>
+                    <tr>
+                        <th scope="row">{% trans "Location" %}</th>
+                        <td>{{ object.rack.location|linkify|placeholder }}</td>
+                    </tr>
+                    <tr>
+                        <th scope="row">{% trans "Rack" %}</th>
+                        <td>{{ object.rack|linkify }}</td>
+                    </tr>
                 </table>
             </div>
         </div>

+ 3 - 16
netbox/templates/dcim/site.html

@@ -3,6 +3,7 @@
 {% load plugins %}
 {% load tz %}
 {% load i18n %}
+{% load mptt %}
 
 {% block breadcrumbs %}
   {{ block.super }}
@@ -29,27 +30,13 @@
           <tr>
             <th scope="row">{% trans "Region" %}</th>
             <td>
-              {% if object.region %}
-                {% for region in object.region.get_ancestors %}
-                  {{ region|linkify }} /
-                {% endfor %}
-                {{ object.region|linkify }}
-              {% else %}
-                {{ ''|placeholder }}
-              {% endif %}
+              {% nested_tree object.region %}
             </td>
           </tr>
           <tr>
             <th scope="row">{% trans "Group" %}</th>
             <td>
-              {% if object.group %}
-                {% for group in object.group.get_ancestors %}
-                  {{ group|linkify }} /
-                {% endfor %}
-                {{ object.group|linkify }}
-              {% else %}
-                {{ ''|placeholder }}
-              {% endif %}
+              {% nested_tree object.group %}
             </td>
           </tr>
           <tr>

+ 10 - 10
netbox/templates/ipam/prefix.html

@@ -3,6 +3,7 @@
 {% load helpers %}
 {% load plugins %}
 {% load i18n %}
+{% load mptt %}
 
 {% block content %}
 <div class="row">
@@ -44,18 +45,17 @@
               {% endif %}
             </td>
           </tr>
+          {% if object.site.region %}
+            <tr>
+              <th scope="row">{% trans "Region" %}</th>
+              <td>
+                {% nested_tree object.site.region %}
+              </td>
+            </tr>
+          {% endif %}
           <tr>
             <th scope="row">{% trans "Site" %}</th>
-            <td>
-              {% if object.site %}
-                {% if object.site.region %}
-                  {{ object.site.region|linkify }} /
-                {% endif %}
-                {{ object.site|linkify }}
-              {% else %}
-                {{ ''|placeholder }}
-              {% endif %}
-            </td>
+            <td>{{ object.site|linkify|placeholder }}</td>
           </tr>
           <tr>
             <th scope="row">{% trans "VLAN" %}</th>

+ 10 - 10
netbox/templates/ipam/vlan.html

@@ -3,6 +3,7 @@
 {% load render_table from django_tables2 %}
 {% load plugins %}
 {% load i18n %}
+{% load mptt %}
 
 {% block content %}
     <div class="row">
@@ -13,18 +14,17 @@
                 </h5>
                 <div class="card-body">
                     <table class="table table-hover attr-table">
+                        {% if object.site.region %}
+                            <tr>
+                                <th scope="row">{% trans "Region" %}</th>
+                                <td>
+                                    {% nested_tree object.site.region %}
+                                </td>
+                            </tr>
+                        {% endif %}
                         <tr>
                             <th scope="row">{% trans "Site" %}</th>
-                            <td>
-                                {% if object.site %}
-                                    {% if object.site.region %}
-                                        {{ object.site.region|linkify }} /
-                                    {% endif %}
-                                    {{ object.site|linkify }}
-                                {% else %}
-                                    {{ ''|placeholder }}
-                                {% endif %}
-                            </td>
+                            <td>{{ object.site|linkify|placeholder }}</td>
                         </tr>
                         <tr>
                             <th scope="row">{% trans "Group" %}</th>

+ 2 - 2
netbox/tenancy/models/contacts.py

@@ -6,7 +6,7 @@ from django.utils.translation import gettext_lazy as _
 
 from core.models import ContentType
 from netbox.models import ChangeLoggedModel, NestedGroupModel, OrganizationalModel, PrimaryModel
-from netbox.models.features import CustomFieldsMixin, TagsMixin
+from netbox.models.features import CustomFieldsMixin, ExportTemplatesMixin, TagsMixin
 from tenancy.choices import *
 
 __all__ = (
@@ -110,7 +110,7 @@ class Contact(PrimaryModel):
         return reverse('tenancy:contact', args=[self.pk])
 
 
-class ContactAssignment(CustomFieldsMixin, TagsMixin, ChangeLoggedModel):
+class ContactAssignment(CustomFieldsMixin, ExportTemplatesMixin, TagsMixin, ChangeLoggedModel):
     content_type = models.ForeignKey(
         to='contenttypes.ContentType',
         on_delete=models.CASCADE

+ 10 - 0
netbox/users/api/serializers.py

@@ -52,6 +52,16 @@ class UserSerializer(ValidatedModelSerializer):
 
         return user
 
+    def update(self, instance, validated_data):
+        """
+        Ensure proper updated password hash generation.
+        """
+        password = validated_data.pop('password', None)
+        if password is not None:
+            instance.set_password(password)
+
+        return super().update(instance, validated_data)
+
     @extend_schema_field(OpenApiTypes.STR)
     def get_display(self, obj):
         if full_name := obj.get_full_name():

+ 32 - 0
netbox/users/tests/test_api.py

@@ -54,6 +54,38 @@ class UserTest(APIViewTestCases.APIViewTestCase):
         )
         User.objects.bulk_create(users)
 
+    def test_that_password_is_changed(self):
+        """
+        Test that password is changed
+        """
+
+        obj_perm = ObjectPermission(
+            name='Test permission',
+            actions=['change']
+        )
+        obj_perm.save()
+        obj_perm.users.add(self.user)
+        obj_perm.object_types.add(ContentType.objects.get_for_model(self.model))
+
+        user_credentials = {
+            'username': 'user1',
+            'password': 'abc123',
+        }
+        user = User.objects.create_user(**user_credentials)
+
+        data = {
+            'password': 'newpassword'
+        }
+        url = reverse('users-api:user-detail', kwargs={'pk': user.id})
+
+        response = self.client.patch(url, data, format='json', **self.header)
+
+        self.assertEqual(response.status_code, 200)
+
+        updated_user = User.objects.get(id=user.id)
+
+        self.assertTrue(updated_user.check_password(data['password']))
+
 
 class GroupTest(APIViewTestCases.APIViewTestCase):
     model = Group

+ 1 - 1
netbox/utilities/forms/utils.py

@@ -40,7 +40,7 @@ def parse_numeric_range(string, base=10):
         except ValueError:
             raise forms.ValidationError(f'Range "{dash_range}" is invalid.')
         values.extend(range(begin, end))
-    return list(set(values))
+    return sorted(set(values))
 
 
 def parse_alphanumeric_range(string):

+ 20 - 0
netbox/utilities/templatetags/mptt.py

@@ -0,0 +1,20 @@
+from django import template
+from django.utils.safestring import mark_safe
+
+register = template.Library()
+
+
+@register.simple_tag()
+def nested_tree(obj):
+    """
+    Renders the entire hierarchy of a recursively-nested object (such as Region or SiteGroup).
+    """
+    if not obj:
+        return mark_safe('&mdash;')
+
+    nodes = obj.get_ancestors(include_self=True)
+    return mark_safe(
+        ' / '.join(
+            f'<a href="{node.get_absolute_url()}">{node}</a>' for node in nodes
+        )
+    )

+ 3 - 2
netbox/virtualization/forms/bulk_edit.py

@@ -296,9 +296,10 @@ class VMInterfaceBulkEditForm(NetBoxModelBulkEditForm):
                 # Check interface sites.  First interface should set site, further interfaces will either continue the
                 # loop or reset back to no site and break the loop.
                 for interface in interfaces:
+                    vm_site = interface.virtual_machine.site or interface.virtual_machine.cluster.site
                     if site is None:
-                        site = interface.virtual_machine.cluster.site
-                    elif interface.virtual_machine.cluster.site is not site:
+                        site = vm_site
+                    elif vm_site is not site:
                         site = None
                         break
 

+ 8 - 8
requirements.txt

@@ -1,18 +1,18 @@
 bleach==6.1.0
 Django==4.2.7
-django-cors-headers==4.3.0
+django-cors-headers==4.3.1
 django-debug-toolbar==4.2.0
-django-filter==23.3
+django-filter==23.4
 django-graphiql-debug-toolbar==0.2.0
 django-mptt==0.14.0
 django-pglocks==1.0.4
 django-prometheus==2.3.1
 django-redis==5.4.0
 django-rich==1.8.0
-django-rq==2.8.1
+django-rq==2.9.0
 django-tables2==2.6.0
 django-taggit==4.0.0
-django-timezone-field==6.0.1
+django-timezone-field==6.1.0
 djangorestframework==3.14.0
 drf-spectacular==0.26.5
 drf-spectacular-sidecar==2023.10.1
@@ -21,15 +21,15 @@ graphene-django==3.0.0
 gunicorn==21.2.0
 Jinja2==3.1.2
 Markdown==3.3.7
-mkdocs-material==9.4.8
-mkdocstrings[python-legacy]==0.23.0
+mkdocs-material==9.4.14
+mkdocstrings[python-legacy]==0.24.0
 netaddr==0.9.0
 Pillow==10.1.0
-psycopg[binary,pool]==3.1.12
+psycopg[binary,pool]==3.1.13
 PyYAML==6.0.1
 requests==2.31.0
 social-auth-app-django==5.4.0
-social-auth-core[openidconnect]==4.5.0
+social-auth-core[openidconnect]==4.5.1
 svgwrite==1.4.3
 tablib==3.5.0
 tzdata==2023.3