From 78d68373751ffe77de819c671921fbab6c7be89c Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 20 Jun 2023 14:27:18 -0400 Subject: [PATCH 01/37] PRVB --- docs/release-notes/version-3.5.md | 4 ++++ netbox/netbox/settings.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/release-notes/version-3.5.md b/docs/release-notes/version-3.5.md index 49d2e6347..7ad333e47 100644 --- a/docs/release-notes/version-3.5.md +++ b/docs/release-notes/version-3.5.md @@ -1,5 +1,9 @@ # NetBox v3.5 +## v3.5.5 (FUTURE) + +--- + ## v3.5.4 (2023-06-20) ### Enhancements diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 22193dbac..31363144f 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -25,7 +25,7 @@ from netbox.constants import RQ_QUEUE_DEFAULT, RQ_QUEUE_HIGH, RQ_QUEUE_LOW # Environment setup # -VERSION = '3.5.4' +VERSION = '3.5.5-dev' # Hostname HOSTNAME = platform.node() From 58bab9368919ba297fa962b76471863a05d52866 Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Fri, 23 Jun 2023 23:29:28 +0530 Subject: [PATCH 02/37] fixes device type and module type tag import #12949 --- netbox/dcim/forms/bulk_import.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/netbox/dcim/forms/bulk_import.py b/netbox/dcim/forms/bulk_import.py index de7575acb..8c8c4f79a 100644 --- a/netbox/dcim/forms/bulk_import.py +++ b/netbox/dcim/forms/bulk_import.py @@ -306,7 +306,7 @@ class DeviceTypeImportForm(NetBoxModelImportForm): model = DeviceType fields = [ 'manufacturer', 'default_platform', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', - 'subdevice_role', 'airflow', 'description', 'weight', 'weight_unit', 'comments', + 'subdevice_role', 'airflow', 'description', 'weight', 'weight_unit', 'comments', 'tags', ] @@ -327,7 +327,7 @@ class ModuleTypeImportForm(NetBoxModelImportForm): class Meta: model = ModuleType - fields = ['manufacturer', 'model', 'part_number', 'description', 'weight', 'weight_unit', 'comments'] + fields = ['manufacturer', 'model', 'part_number', 'description', 'weight', 'weight_unit', 'comments', 'tags'] class DeviceRoleImportForm(NetBoxModelImportForm): From a3a9fbec108d607c2511ea257d02c8613e936caf Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Fri, 23 Jun 2023 22:23:20 +0530 Subject: [PATCH 03/37] fixes most schema warnings #12257 --- netbox/ipam/filtersets.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/netbox/ipam/filtersets.py b/netbox/ipam/filtersets.py index a128b6acc..d011472d9 100644 --- a/netbox/ipam/filtersets.py +++ b/netbox/ipam/filtersets.py @@ -4,6 +4,8 @@ from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ValidationError from django.db.models import Q from django.utils.translation import gettext as _ +from drf_spectacular.utils import extend_schema_field +from drf_spectacular.types import OpenApiTypes from netaddr.core import AddrFormatError from dcim.models import Device, Interface, Region, Site, SiteGroup @@ -414,6 +416,7 @@ class PrefixFilterSet(NetBoxModelFilterSet, TenancyFilterSet): except (AddrFormatError, ValueError): return queryset.none() + @extend_schema_field(OpenApiTypes.STR) def filter_present_in_vrf(self, queryset, name, vrf): if vrf is None: return queryset.none @@ -659,6 +662,7 @@ class IPAddressFilterSet(NetBoxModelFilterSet, TenancyFilterSet): return queryset return queryset.filter(address__net_mask_length=value) + @extend_schema_field(OpenApiTypes.STR) def filter_present_in_vrf(self, queryset, name, vrf): if vrf is None: return queryset.none @@ -727,6 +731,7 @@ class FHRPGroupFilterSet(NetBoxModelFilterSet): Q(name__icontains=value) ) + @extend_schema_field(OpenApiTypes.STR) def filter_related_ip(self, queryset, name, value): """ Filter by VRF & prefix of assigned IP addresses. @@ -941,9 +946,11 @@ class VLANFilterSet(NetBoxModelFilterSet, TenancyFilterSet): pass return queryset.filter(qs_filter) + @extend_schema_field(OpenApiTypes.STR) def get_for_device(self, queryset, name, value): return queryset.get_for_device(value) + @extend_schema_field(OpenApiTypes.STR) def get_for_virtualmachine(self, queryset, name, value): return queryset.get_for_virtualmachine(value) From 7a530548e8378ac96b1df159436b6ffba7fe768f Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Fri, 23 Jun 2023 21:31:30 +0530 Subject: [PATCH 04/37] fixes choice field schema for response #12960 --- netbox/core/api/schema.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/netbox/core/api/schema.py b/netbox/core/api/schema.py index 9550df3ea..b7e537c23 100644 --- a/netbox/core/api/schema.py +++ b/netbox/core/api/schema.py @@ -1,5 +1,6 @@ import re import typing +from collections import OrderedDict from drf_spectacular.extensions import OpenApiSerializerFieldExtension from drf_spectacular.openapi import AutoSchema @@ -28,14 +29,19 @@ class ChoiceFieldFix(OpenApiSerializerFieldExtension): target_class = 'netbox.api.fields.ChoiceField' def map_serializer_field(self, auto_schema, direction): + build_cf = build_choice_field(self.target) + if direction == 'request': - return build_choice_field(self.target) + return build_cf elif direction == "response": + value = build_cf + label = {**build_basic_type(OpenApiTypes.STR), "enum": list(OrderedDict.fromkeys(self.target.choices.values()))} + return build_object_type( properties={ - "value": build_basic_type(OpenApiTypes.STR), - "label": build_basic_type(OpenApiTypes.STR), + "value": value, + "label": label } ) From abaf20ae7b32948a2f41b9652cf3d8fb54404997 Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Fri, 23 Jun 2023 20:39:57 +0530 Subject: [PATCH 05/37] fixes device interface serializer #12533 --- netbox/dcim/api/serializers.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 3f6d55da7..c4aa838ea 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -880,12 +880,12 @@ class InterfaceSerializer(NetBoxModelSerializer, CabledObjectSerializer, Connect parent = NestedInterfaceSerializer(required=False, allow_null=True) bridge = NestedInterfaceSerializer(required=False, allow_null=True) lag = NestedInterfaceSerializer(required=False, allow_null=True) - mode = ChoiceField(choices=InterfaceModeChoices, required=False, allow_blank=True, allow_null=True) + mode = ChoiceField(choices=InterfaceModeChoices, required=False, allow_blank=True) duplex = ChoiceField(choices=InterfaceDuplexChoices, required=False, allow_blank=True, allow_null=True) - rf_role = ChoiceField(choices=WirelessRoleChoices, required=False, allow_blank=True, allow_null=True) - rf_channel = ChoiceField(choices=WirelessChannelChoices, required=False, allow_blank=True, allow_null=True) - poe_mode = ChoiceField(choices=InterfacePoEModeChoices, required=False, allow_blank=True, allow_null=True) - poe_type = ChoiceField(choices=InterfacePoETypeChoices, required=False, allow_blank=True, allow_null=True) + rf_role = ChoiceField(choices=WirelessRoleChoices, required=False, allow_blank=True) + rf_channel = ChoiceField(choices=WirelessChannelChoices, required=False, allow_blank=True) + poe_mode = ChoiceField(choices=InterfacePoEModeChoices, required=False, allow_blank=True) + poe_type = ChoiceField(choices=InterfacePoETypeChoices, required=False, allow_blank=True) untagged_vlan = NestedVLANSerializer(required=False, allow_null=True) tagged_vlans = SerializedPKRelatedField( queryset=VLAN.objects.all(), @@ -907,9 +907,10 @@ class InterfaceSerializer(NetBoxModelSerializer, CabledObjectSerializer, Connect mac_address = serializers.CharField( required=False, default=None, + allow_blank=True, allow_null=True ) - wwn = serializers.CharField(required=False, default=None) + wwn = serializers.CharField(required=False, default=None, allow_blank=True, allow_null=True) class Meta: model = Interface From 99e08a73878f48dba3a043c66ba89e93d300a1d5 Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Fri, 23 Jun 2023 18:11:32 +0530 Subject: [PATCH 06/37] fix db maintenance mode exception #12966 --- netbox/netbox/middleware.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/netbox/middleware.py b/netbox/netbox/middleware.py index 76b3e42a8..ba6967f1f 100644 --- a/netbox/netbox/middleware.py +++ b/netbox/netbox/middleware.py @@ -203,7 +203,7 @@ class MaintenanceModeMiddleware: """ Prevent any write-related database operations if an exception is raised. """ - if isinstance(exception, InternalError): + if get_config().MAINTENANCE_MODE and isinstance(exception, InternalError): error_message = 'NetBox is currently operating in maintenance mode and is unable to perform write ' \ 'operations. Please try again later.' From 6b267fe1667ebad5987d21f047fba04c68485dfa Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Fri, 23 Jun 2023 18:08:20 +0530 Subject: [PATCH 07/37] fix ip address assignment #12953 --- netbox/ipam/forms/model_forms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/ipam/forms/model_forms.py b/netbox/ipam/forms/model_forms.py index b0b08e4e0..6fa0f95ea 100644 --- a/netbox/ipam/forms/model_forms.py +++ b/netbox/ipam/forms/model_forms.py @@ -345,7 +345,7 @@ class IPAddressForm(TenancyForm, NetBoxModelForm): }) elif selected_objects: assigned_object = self.cleaned_data[selected_objects[0]] - if self.cleaned_data['primary_for_parent'] and assigned_object != self.instance.assigned_object: + if self.instance.pk and self.cleaned_data['primary_for_parent'] and assigned_object != self.instance.assigned_object: raise ValidationError( "Cannot reassign IP address while it is designated as the primary IP for the parent object" ) From f28563ec0413dad0544ee9843ae0b66d6164ef0a Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 23 Jun 2023 15:23:40 -0400 Subject: [PATCH 08/37] Fixes #12975: Correct URL for VirtualDeviceContext API serializer --- netbox/dcim/api/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index c4aa838ea..2f854d3e4 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -707,7 +707,7 @@ class DeviceWithConfigContextSerializer(DeviceSerializer): class VirtualDeviceContextSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:device-detail') + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualdevicecontext-detail') device = NestedDeviceSerializer() tenant = NestedTenantSerializer(required=False, allow_null=True, default=None) primary_ip = NestedIPAddressSerializer(read_only=True, allow_null=True) From dffa1d4934ea7b299d70bcb60ec80fc9538bb473 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 23 Jun 2023 15:26:48 -0400 Subject: [PATCH 09/37] Changelog for #12533, #12960, #12963, #12966, #12975, #12989 --- docs/release-notes/version-3.5.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/release-notes/version-3.5.md b/docs/release-notes/version-3.5.md index 7ad333e47..3d4c7381c 100644 --- a/docs/release-notes/version-3.5.md +++ b/docs/release-notes/version-3.5.md @@ -2,6 +2,15 @@ ## v3.5.5 (FUTURE) +### Bug Fixes + +* [#12533](https://github.com/netbox-community/netbox/issues/12533) - Fix REST API validation of null values for several interface attributes +* [#12960](https://github.com/netbox-community/netbox/issues/12960) - Fix OpenAPI schema for various choice fields +* [#12963](https://github.com/netbox-community/netbox/issues/12963) - Fix designation of primary IP addresses during interface assignment +* [#12966](https://github.com/netbox-community/netbox/issues/12966) - Avoid catching database exceptions when maintenance mode is disabled +* [#12975](https://github.com/netbox-community/netbox/issues/12975) - Correct URL for VirtualDeviceContext API serializer +* [#12989](https://github.com/netbox-community/netbox/issues/12989) - Fix bulk import of tags for devie & module types + --- ## v3.5.4 (2023-06-20) From 36676088eefe75c8eba08a3641978ae66f9ac190 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 23 Jun 2023 15:31:18 -0400 Subject: [PATCH 10/37] Correct changelog for #12953 --- docs/release-notes/version-3.5.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/release-notes/version-3.5.md b/docs/release-notes/version-3.5.md index 3d4c7381c..96623f66f 100644 --- a/docs/release-notes/version-3.5.md +++ b/docs/release-notes/version-3.5.md @@ -5,11 +5,11 @@ ### Bug Fixes * [#12533](https://github.com/netbox-community/netbox/issues/12533) - Fix REST API validation of null values for several interface attributes +* [#12953](https://github.com/netbox-community/netbox/issues/12953) - Fix designation of primary IP addresses during interface assignment * [#12960](https://github.com/netbox-community/netbox/issues/12960) - Fix OpenAPI schema for various choice fields -* [#12963](https://github.com/netbox-community/netbox/issues/12963) - Fix designation of primary IP addresses during interface assignment * [#12966](https://github.com/netbox-community/netbox/issues/12966) - Avoid catching database exceptions when maintenance mode is disabled * [#12975](https://github.com/netbox-community/netbox/issues/12975) - Correct URL for VirtualDeviceContext API serializer -* [#12989](https://github.com/netbox-community/netbox/issues/12989) - Fix bulk import of tags for devie & module types +* [#12989](https://github.com/netbox-community/netbox/issues/12989) - Fix bulk import of tags for device & module types --- From f41646fd0af6e96751c6d33c7be6e5676e32bb48 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 23 Jun 2023 15:54:40 -0400 Subject: [PATCH 11/37] Fixes #12961: Set correct return_url for object contacts tabs --- netbox/templates/tenancy/object_contacts.html | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/netbox/templates/tenancy/object_contacts.html b/netbox/templates/tenancy/object_contacts.html index bbe2d9bc1..e13fedc43 100644 --- a/netbox/templates/tenancy/object_contacts.html +++ b/netbox/templates/tenancy/object_contacts.html @@ -2,10 +2,12 @@ {% load helpers %} {% block extra_controls %} - {% if perms.tenancy.add_contactassignment %} - + {% if perms.tenancy.add_contactassignment %} + {% with viewname=object|viewname:"contacts" %} + Add a contact - + + {% endwith %} {% endif %} {% endblock %} From b1c1b1bbbd682a1c74627461812c1987f6315ef9 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 26 Jun 2023 14:21:26 -0400 Subject: [PATCH 12/37] Fixes #12977: Fix URL parameters for object count dashboard widgets (#12991) * Fixes #12977: Introduce dict_to_querydict() to ensure proper handling of QueryDicts * Remove unused import --- netbox/extras/dashboard/widgets.py | 6 ++---- netbox/extras/models/models.py | 7 +++---- netbox/utilities/templatetags/builtins/tags.py | 5 +++-- netbox/utilities/utils.py | 16 +++++++++++++++- 4 files changed, 23 insertions(+), 11 deletions(-) diff --git a/netbox/extras/dashboard/widgets.py b/netbox/extras/dashboard/widgets.py index b3a4d090c..31e7cb2d3 100644 --- a/netbox/extras/dashboard/widgets.py +++ b/netbox/extras/dashboard/widgets.py @@ -10,7 +10,6 @@ from django.conf import settings from django.contrib.contenttypes.models import ContentType from django.core.cache import cache from django.db.models import Q -from django.http import QueryDict from django.template.loader import render_to_string from django.urls import NoReverseMatch, resolve, reverse from django.utils.translation import gettext as _ @@ -19,7 +18,7 @@ from extras.utils import FeatureQuery from utilities.forms import BootstrapMixin from utilities.permissions import get_permission_for_model from utilities.templatetags.builtins.filters import render_markdown -from utilities.utils import content_type_identifier, content_type_name, get_viewname +from utilities.utils import content_type_identifier, content_type_name, dict_to_querydict, get_viewname from .utils import register_widget __all__ = ( @@ -170,8 +169,7 @@ class ObjectCountsWidget(DashboardWidget): qs = model.objects.restrict(request.user, 'view') # Apply any specified filters if filters := self.config.get('filters'): - params = QueryDict(mutable=True) - params.update(filters) + params = dict_to_querydict(filters) filterset = getattr(resolve(url).func.view_class, 'filterset', None) qs = filterset(params, qs).qs url = f'{url}?{params.urlencode()}' diff --git a/netbox/extras/models/models.py b/netbox/extras/models/models.py index b7034645f..4b4b6fbfd 100644 --- a/netbox/extras/models/models.py +++ b/netbox/extras/models/models.py @@ -9,7 +9,7 @@ from django.contrib.contenttypes.models import ContentType from django.core.cache import cache from django.core.validators import ValidationError from django.db import models -from django.http import HttpResponse, QueryDict +from django.http import HttpResponse from django.urls import reverse from django.utils import timezone from django.utils.formats import date_format @@ -26,7 +26,7 @@ from netbox.models.features import ( CloningMixin, CustomFieldsMixin, CustomLinksMixin, ExportTemplatesMixin, SyncedDataMixin, TagsMixin, ) from utilities.querysets import RestrictedQuerySet -from utilities.utils import clean_html, render_jinja2 +from utilities.utils import clean_html, dict_to_querydict, render_jinja2 __all__ = ( 'ConfigRevision', @@ -462,8 +462,7 @@ class SavedFilter(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel): @property def url_params(self): - qd = QueryDict(mutable=True) - qd.update(self.parameters) + qd = dict_to_querydict(self.parameters) return qd.urlencode() diff --git a/netbox/utilities/templatetags/builtins/tags.py b/netbox/utilities/templatetags/builtins/tags.py index dc86586e7..f9fe5f4e3 100644 --- a/netbox/utilities/templatetags/builtins/tags.py +++ b/netbox/utilities/templatetags/builtins/tags.py @@ -1,6 +1,8 @@ from django import template from django.http import QueryDict +from utilities.utils import dict_to_querydict + __all__ = ( 'badge', 'checkmark', @@ -87,8 +89,7 @@ def htmx_table(context, viewname, return_url=None, **kwargs): viewname: The name of the view to use for the HTMX request (e.g. `dcim:site_list`) return_url: The URL to pass as the `return_url`. If not provided, the current request's path will be used. """ - url_params = QueryDict(mutable=True) - url_params.update(kwargs) + url_params = dict_to_querydict(kwargs) url_params['return_url'] = return_url or context['request'].path return { 'viewname': viewname, diff --git a/netbox/utilities/utils.py b/netbox/utilities/utils.py index 4b4a2631a..114397dae 100644 --- a/netbox/utilities/utils.py +++ b/netbox/utilities/utils.py @@ -11,8 +11,9 @@ from django.core import serializers from django.db.models import Count, OuterRef, Subquery from django.db.models.functions import Coalesce from django.http import QueryDict -from django.utils.html import escape from django.utils import timezone +from django.utils.datastructures import MultiValueDict +from django.utils.html import escape from django.utils.timezone import localtime from jinja2.sandbox import SandboxedEnvironment from mptt.models import MPTTModel @@ -231,6 +232,19 @@ def dict_to_filter_params(d, prefix=''): return params +def dict_to_querydict(d, mutable=True): + """ + Create a QueryDict instance from a regular Python dictionary. + """ + qd = QueryDict(mutable=True) + for k, v in d.items(): + item = MultiValueDict({k: v}) if isinstance(v, (list, tuple, set)) else {k: v} + qd.update(item) + if not mutable: + qd._mutable = False + return qd + + def normalize_querydict(querydict): """ Convert a QueryDict to a normal, mutable dictionary, preserving list values. For example, From 06403730ecdec43dfd0f08b21b078e7ffafb11d9 Mon Sep 17 00:00:00 2001 From: Alexander Votteler Date: Tue, 27 Jun 2023 07:57:59 +0200 Subject: [PATCH 13/37] Fixes #13011: Allow comma in custom links --- netbox/extras/models/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/extras/models/models.py b/netbox/extras/models/models.py index 4b4b6fbfd..fcf5c26a2 100644 --- a/netbox/extras/models/models.py +++ b/netbox/extras/models/models.py @@ -285,7 +285,7 @@ class CustomLink(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel): text = clean_html(text, allowed_schemes) # Sanitize link - link = urllib.parse.quote(link, safe='/:?&=%+[]@#') + link = urllib.parse.quote(link, safe='/:?&=%+[]@#,') # Verify link scheme is allowed result = urllib.parse.urlparse(link) From fce98233563cd609b41043a5a9545c7b41c01cac Mon Sep 17 00:00:00 2001 From: pobradovic08 Date: Tue, 27 Jun 2023 01:38:33 +0200 Subject: [PATCH 14/37] Add QSFP-DD (100GE) interface type --- netbox/dcim/choices.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/netbox/dcim/choices.py b/netbox/dcim/choices.py index cc388b750..f2f401718 100644 --- a/netbox/dcim/choices.py +++ b/netbox/dcim/choices.py @@ -810,6 +810,7 @@ class InterfaceTypeChoices(ChoiceSet): TYPE_100GE_CXP = '100gbase-x-cxp' TYPE_100GE_CPAK = '100gbase-x-cpak' TYPE_100GE_QSFP28 = '100gbase-x-qsfp28' + TYPE_100GE_QSFP_DD = '100gbase-x-qsfpdd' TYPE_200GE_CFP2 = '200gbase-x-cfp2' TYPE_200GE_QSFP56 = '200gbase-x-qsfp56' TYPE_200GE_QSFP_DD = '200gbase-x-qsfpdd' @@ -959,6 +960,7 @@ class InterfaceTypeChoices(ChoiceSet): (TYPE_100GE_CXP, 'CXP (100GE)'), (TYPE_100GE_CPAK, 'Cisco CPAK (100GE)'), (TYPE_100GE_QSFP28, 'QSFP28 (100GE)'), + (TYPE_100GE_QSFP_DD, 'QSFP-DD (100GE)'), (TYPE_200GE_QSFP56, 'QSFP56 (200GE)'), (TYPE_200GE_QSFP_DD, 'QSFP-DD (200GE)'), (TYPE_400GE_QSFP_DD, 'QSFP-DD (400GE)'), From 2972d6fe0e5d514d51ea0f4cccbbee3d2a7e9538 Mon Sep 17 00:00:00 2001 From: rmanyari Date: Mon, 26 Jun 2023 14:39:38 -0400 Subject: [PATCH 15/37] fix remote auth backend check --- netbox/netbox/api/authentication.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/netbox/api/authentication.py b/netbox/netbox/api/authentication.py index 814ca1ed6..f0bd5fd27 100644 --- a/netbox/netbox/api/authentication.py +++ b/netbox/netbox/api/authentication.py @@ -60,7 +60,7 @@ class TokenAuthentication(authentication.TokenAuthentication): user = token.user # When LDAP authentication is active try to load user data from LDAP directory - if settings.REMOTE_AUTH_BACKEND == 'netbox.authentication.LDAPBackend': + if 'netbox.authentication.LDAPBackend' in settings.REMOTE_AUTH_BACKEND: from netbox.authentication import LDAPBackend ldap_backend = LDAPBackend() From 3f9bf0b896c9d93db576db5bb57e6a61452555a3 Mon Sep 17 00:00:00 2001 From: Harm Geerts Date: Mon, 26 Jun 2023 14:07:29 +0200 Subject: [PATCH 16/37] Fix #12983 bulk edit of M2M fields when nothing is selected Partially revert f56098e39f973ec55def737c2e2f18b6fa89cac3 to restore bulk edit with m2m fields. The m2m cleaned_data yields a empty queryset when nothing is selected. By setting the m2m relation unless set null is checked even when nothing is selected the m2m relation is always cleared. This commit only sets the m2m relation when a selection is made. --- netbox/netbox/views/generic/bulk_views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/netbox/views/generic/bulk_views.py b/netbox/netbox/views/generic/bulk_views.py index e66e79a7a..35caa31b3 100644 --- a/netbox/netbox/views/generic/bulk_views.py +++ b/netbox/netbox/views/generic/bulk_views.py @@ -551,7 +551,7 @@ class BulkEditView(GetReturnURLMixin, BaseMultiObjectView): for name, m2m_field in m2m_fields.items(): if name in form.nullable_fields and name in nullified_fields: getattr(obj, name).clear() - else: + elif form.cleaned_data[name]: getattr(obj, name).set(form.cleaned_data[name]) # Add/remove tags From 286446494648cfc96e6dd057f9fe9ab40e37d914 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 27 Jun 2023 08:39:51 -0400 Subject: [PATCH 17/37] Changelog for #12849, #12945, #12961, #12977, #12983, #13011 --- docs/release-notes/version-3.5.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/release-notes/version-3.5.md b/docs/release-notes/version-3.5.md index 96623f66f..7b82ed07c 100644 --- a/docs/release-notes/version-3.5.md +++ b/docs/release-notes/version-3.5.md @@ -2,14 +2,23 @@ ## v3.5.5 (FUTURE) +### Enhancements + +* [#12945](https://github.com/netbox-community/netbox/issues/12945) - Add 100GE QSFP-DD interface type + ### Bug Fixes * [#12533](https://github.com/netbox-community/netbox/issues/12533) - Fix REST API validation of null values for several interface attributes +* [#12849](https://github.com/netbox-community/netbox/issues/12849) - Fix LDAP group permissions assignment for API clients * [#12953](https://github.com/netbox-community/netbox/issues/12953) - Fix designation of primary IP addresses during interface assignment * [#12960](https://github.com/netbox-community/netbox/issues/12960) - Fix OpenAPI schema for various choice fields +* [#12961](https://github.com/netbox-community/netbox/issues/12961) - Set correct return URL for object contacts tabs * [#12966](https://github.com/netbox-community/netbox/issues/12966) - Avoid catching database exceptions when maintenance mode is disabled * [#12975](https://github.com/netbox-community/netbox/issues/12975) - Correct URL for VirtualDeviceContext API serializer +* [#12977](https://github.com/netbox-community/netbox/issues/12977) - Fix URL parameters for object count dashboard widgets +* [#12983](https://github.com/netbox-community/netbox/issues/12983) - Avoid erroneously clearing many-to-many assignments during bulk edit * [#12989](https://github.com/netbox-community/netbox/issues/12989) - Fix bulk import of tags for device & module types +* [#13011](https://github.com/netbox-community/netbox/issues/13011) - Do not escape commas when rendering custom links --- From eba0f7bb9a7bf64260a0ec8cb9a2ae6b39419231 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 27 Jun 2023 10:19:12 -0400 Subject: [PATCH 18/37] Fix Repography charts --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 6e2b34fb8..b7dfc9e43 100644 --- a/README.md +++ b/README.md @@ -52,10 +52,10 @@ as the cornerstone for network automation in thousands of organizations. ## Project Stats
- Timeline graph - Issues graph - Pull requests graph - Top contributors + Timeline graph + Issues graph + Pull requests graph + Top contributors
Stats via Repography
From c310aa8c7ef0ae7873821742e27fc92decd39c94 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 29 Jun 2023 14:47:10 -0400 Subject: [PATCH 19/37] Add OneMind Services as a sponsor --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b7dfc9e43..54b3e727e 100644 --- a/README.md +++ b/README.md @@ -66,10 +66,12 @@ as the cornerstone for network automation in thousands of organizations. [![NetBox Labs](https://raw.githubusercontent.com/wiki/netbox-community/netbox/images/sponsors/netbox_labs.png)](https://netboxlabs.com)            [![DigitalOcean](https://raw.githubusercontent.com/wiki/netbox-community/netbox/images/sponsors/digitalocean.png)](https://try.digitalocean.com/developer-cloud) -
- [![Sentry](https://raw.githubusercontent.com/wiki/netbox-community/netbox/images/sponsors/sentry.png)](https://sentry.io)            + [![Sentry](https://raw.githubusercontent.com/wiki/netbox-community/netbox/images/sponsors/sentry.png)](https://sentry.io) +
[![Equinix Metal](https://raw.githubusercontent.com/wiki/netbox-community/netbox/images/sponsors/equinix.png)](https://metal.equinix.com) +            + [![OneMind Services](https://raw.githubusercontent.com/wiki/netbox-community/netbox/images/sponsors/onemind_services.png)](https://onemindservices.com) From 373fbdd3c584a1fbf7e3b9471b5bdea9a4daaeac Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Thu, 29 Jun 2023 20:24:23 +0530 Subject: [PATCH 20/37] adds contact accessor fields on assignment table #12955 --- netbox/tenancy/tables/contacts.py | 34 +++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/netbox/tenancy/tables/contacts.py b/netbox/tenancy/tables/contacts.py index 0c697af79..7de8ffceb 100644 --- a/netbox/tenancy/tables/contacts.py +++ b/netbox/tenancy/tables/contacts.py @@ -1,4 +1,5 @@ import django_tables2 as tables +from django_tables2.utils import Accessor from netbox.tables import NetBoxTable, columns from tenancy.models import * @@ -90,11 +91,40 @@ class ContactAssignmentTable(NetBoxTable): role = tables.Column( linkify=True ) + contact_title = tables.Column( + accessor=Accessor('contact__title'), + verbose_name='Contact Title' + ) + contact_phone = tables.Column( + accessor=Accessor('contact__phone'), + verbose_name='Contact Phone' + ) + contact_email = tables.Column( + accessor=Accessor('contact__email'), + verbose_name='Contact Email' + ) + contact_address = tables.Column( + accessor=Accessor('contact__address'), + verbose_name='Contact Address' + ) + contact_link = tables.Column( + accessor=Accessor('contact__link'), + verbose_name='Contact Link' + ) + contact_description = tables.Column( + accessor=Accessor('contact__description'), + verbose_name='Contact Description' + ) actions = columns.ActionsColumn( actions=('edit', 'delete') ) class Meta(NetBoxTable.Meta): model = ContactAssignment - fields = ('pk', 'content_type', 'object', 'contact', 'role', 'priority', 'actions') - default_columns = ('pk', 'content_type', 'object', 'contact', 'role', 'priority') + fields = ( + 'pk', 'content_type', 'object', 'contact', 'role', 'priority', 'contact_title', 'contact_phone', + 'contact_email', 'contact_address', 'contact_link', 'contact_description', 'actions' + ) + default_columns = ( + 'pk', 'content_type', 'object', 'contact', 'role', 'priority', 'contact_email', 'contact_phone' + ) From b5549ece8c62178898e6ef1eb5dc2d8364ccbeac Mon Sep 17 00:00:00 2001 From: pobradovic08 Date: Thu, 29 Jun 2023 21:48:00 +0200 Subject: [PATCH 21/37] List multiple devices in Terminations (#13030) --- netbox/templates/dcim/inc/cable_termination.html | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/netbox/templates/dcim/inc/cable_termination.html b/netbox/templates/dcim/inc/cable_termination.html index 0ee4c1ccf..bb28be9a2 100644 --- a/netbox/templates/dcim/inc/cable_termination.html +++ b/netbox/templates/dcim/inc/cable_termination.html @@ -15,15 +15,14 @@ Rack {{ terminations.0.device.rack|linkify|placeholder }} - - Device - {{ terminations.0.device|linkify }} - {{ terminations.0|meta:"verbose_name"|capfirst }} {% for term in terminations %} - {{ term|linkify }}{% if not forloop.last %},{% endif %} + {{term.device|linkify}} + + {{ term|linkify }} + {% if not forloop.last %}
{% endif %} {% endfor %} From 8836074b5a34121fa1f279dbdb50e910ef696533 Mon Sep 17 00:00:00 2001 From: Arthur Date: Tue, 27 Jun 2023 08:44:54 -0700 Subject: [PATCH 22/37] 12809 document not to use underscores in model names --- docs/plugins/development/models.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/plugins/development/models.md b/docs/plugins/development/models.md index b3bcb292a..c51d025f4 100644 --- a/docs/plugins/development/models.md +++ b/docs/plugins/development/models.md @@ -19,6 +19,9 @@ class MyModel(models.Model): Every model includes by default a numeric primary key. This value is generated automatically by the database, and can be referenced as `pk` or `id`. +!!! note + Model names should adhere to [PEP8](https://www.python.org/dev/peps/pep-0008/#class-names) standards and be CapWords (no underscores). Using underscores in model names will result in problems with permissions. + ## Enabling NetBox Features Plugin models can leverage certain NetBox features by inheriting from NetBox's `NetBoxModel` class. This class extends the plugin model to enable features unique to NetBox, including: From 94642f4400413e677ee2fccb254904f64bd06d06 Mon Sep 17 00:00:00 2001 From: Daniel Sheppard Date: Tue, 27 Jun 2023 08:48:27 -0500 Subject: [PATCH 23/37] Fixes #12760 - Adds Vary header to cause cache to be keyed based on URL and the HX-Request header (or lack thereof) --- netbox/netbox/middleware.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/netbox/netbox/middleware.py b/netbox/netbox/middleware.py index ba6967f1f..18f350fd7 100644 --- a/netbox/netbox/middleware.py +++ b/netbox/netbox/middleware.py @@ -49,6 +49,9 @@ class CoreMiddleware: # Attach the unique request ID as an HTTP header. response['X-Request-ID'] = request.id + # Enable the Vary header to help with caching of HTMX responses + response['Vary'] = 'HX-Request' + # If this is an API request, attach an HTTP header annotating the API version (e.g. '3.5'). if is_api_request(request): response['API-Version'] = settings.REST_FRAMEWORK_VERSION From a762e1e8088a08f4eff61f8470177dbd3c3c63ac Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Fri, 30 Jun 2023 01:32:39 +0530 Subject: [PATCH 24/37] Fixes syntax error on reports (#12997) * fixes syntax error on reports #12842 * remove the extra filter #12842 --- netbox/extras/models/reports.py | 7 +- netbox/templates/extras/report_list.html | 128 ++++++++++++----------- 2 files changed, 72 insertions(+), 63 deletions(-) diff --git a/netbox/extras/models/reports.py b/netbox/extras/models/reports.py index aaa785696..f1e336df5 100644 --- a/netbox/extras/models/reports.py +++ b/netbox/extras/models/reports.py @@ -1,7 +1,7 @@ import inspect +import logging from functools import cached_property -from django.contrib.contenttypes.fields import GenericRelation from django.db import models from django.urls import reverse @@ -12,6 +12,8 @@ from netbox.models.features import JobsMixin, WebhooksMixin from utilities.querysets import RestrictedQuerySet from .mixins import PythonModuleMixin +logger = logging.getLogger('netbox.reports') + __all__ = ( 'Report', 'ReportModule', @@ -56,7 +58,8 @@ class ReportModule(PythonModuleMixin, JobsMixin, ManagedFile): try: module = self.get_module() - except ImportError: + except (ImportError, SyntaxError) as e: + logger.error(f"Unable to load report module {self.name}, exception: {e}") return {} reports = {} ordered = getattr(module, 'report_order', []) diff --git a/netbox/templates/extras/report_list.html b/netbox/templates/extras/report_list.html index 0c27eefda..7867fdbbe 100644 --- a/netbox/templates/extras/report_list.html +++ b/netbox/templates/extras/report_list.html @@ -38,71 +38,77 @@
{% include 'inc/sync_warning.html' with object=module %} - - - - - - - - - - - - {% with jobs=module.get_latest_jobs %} - {% for report_name, report in module.reports.items %} - {% with last_job=jobs|get_key:report.name %} - - - - {% if last_job %} - - - {% else %} - - - {% endif %} - - - {% for method, stats in last_job.data.items %} + {% if module.reports %} +
NameDescriptionLast RunStatus
- {{ report.name }} - {{ report.description|markdown|placeholder }} - {{ last_job.created|annotated_date }} - - {% badge last_job.get_status_display last_job.get_status_color %} - Never{{ ''|placeholder }} - {% if perms.extras.run_report %} -
-
- {% csrf_token %} - -
-
- {% endif %} -
+ + + + + + + + + + + {% with jobs=module.get_latest_jobs %} + {% for report_name, report in module.reports.items %} + {% with last_job=jobs|get_key:report.name %} - - + {% if last_job %} + + + {% else %} + + + {% endif %} + - {% endfor %} - {% endwith %} - {% endfor %} - {% endwith %} - -
NameDescriptionLast RunStatus
- {{ method }} + + {{ report.name }} - {{ stats.success }} - {{ stats.info }} - {{ stats.warning }} - {{ stats.failure }} + {{ report.description|markdown|placeholder }} + {{ last_job.created|annotated_date }} + + {% badge last_job.get_status_display last_job.get_status_color %} + Never{{ ''|placeholder }} + {% if perms.extras.run_report %} +
+
+ {% csrf_token %} + +
+
+ {% endif %}
+ {% for method, stats in last_job.data.items %} + + + {{ method }} + + + {{ stats.success }} + {{ stats.info }} + {{ stats.warning }} + {{ stats.failure }} + + + {% endfor %} + {% endwith %} + {% endfor %} + {% endwith %} + + + {% else %} + + {% endif %}
{% empty %} From b7ab379276dea8fa189b1c553976d1b61f3adb59 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 5 Jul 2023 11:39:35 -0400 Subject: [PATCH 25/37] Fixes #11335: Default manager for ObjectChange should filter by installed apps (#11709) * Fixes #11335: Default manager for ObjectChange should filter by installed apps * Employ canonical model discovery mechanism * Move filtering logic to valid_models() queryset method * fixed import to avoid content type does not exist * Cleanup --------- Co-authored-by: Abhimanyu Saharan --- netbox/extras/api/views.py | 2 +- netbox/extras/models/change_logging.py | 4 ++-- netbox/extras/querysets.py | 13 +++++++++++++ netbox/extras/tests/test_api.py | 3 ++- netbox/extras/views.py | 8 ++++---- netbox/users/views.py | 4 +++- 6 files changed, 25 insertions(+), 9 deletions(-) diff --git a/netbox/extras/api/views.py b/netbox/extras/api/views.py index 3f796d7f8..f4b5a1433 100644 --- a/netbox/extras/api/views.py +++ b/netbox/extras/api/views.py @@ -368,7 +368,7 @@ class ObjectChangeViewSet(ReadOnlyModelViewSet): Retrieve a list of recent changes. """ metadata_class = ContentTypeMetadata - queryset = ObjectChange.objects.prefetch_related('user') + queryset = ObjectChange.objects.valid_models().prefetch_related('user') serializer_class = serializers.ObjectChangeSerializer filterset_class = filtersets.ObjectChangeFilterSet diff --git a/netbox/extras/models/change_logging.py b/netbox/extras/models/change_logging.py index e2b118b84..2cb53ed01 100644 --- a/netbox/extras/models/change_logging.py +++ b/netbox/extras/models/change_logging.py @@ -5,7 +5,7 @@ from django.db import models from django.urls import reverse from extras.choices import * -from utilities.querysets import RestrictedQuerySet +from ..querysets import ObjectChangeQuerySet __all__ = ( 'ObjectChange', @@ -82,7 +82,7 @@ class ObjectChange(models.Model): null=True ) - objects = RestrictedQuerySet.as_manager() + objects = ObjectChangeQuerySet.as_manager() class Meta: ordering = ['-time'] diff --git a/netbox/extras/querysets.py b/netbox/extras/querysets.py index 2b97af0fb..2e6f93b93 100644 --- a/netbox/extras/querysets.py +++ b/netbox/extras/querysets.py @@ -1,3 +1,5 @@ +from django.apps import apps +from django.contrib.contenttypes.models import ContentType from django.contrib.postgres.aggregates import JSONBAgg from django.db.models import OuterRef, Subquery, Q @@ -151,3 +153,14 @@ class ConfigContextModelQuerySet(RestrictedQuerySet): ) return base_query + + +class ObjectChangeQuerySet(RestrictedQuerySet): + + def valid_models(self): + # Exclude any change records which refer to an instance of a model that's no longer installed. This + # can happen when a plugin is removed but its data remains in the database, for example. + content_type_ids = set( + ct.pk for ct in ContentType.objects.get_for_models(*apps.get_models()).values() + ) + return self.filter(changed_object_type_id__in=content_type_ids) diff --git a/netbox/extras/tests/test_api.py b/netbox/extras/tests/test_api.py index b59481a36..086c8e246 100644 --- a/netbox/extras/tests/test_api.py +++ b/netbox/extras/tests/test_api.py @@ -8,7 +8,6 @@ from rest_framework import status from core.choices import ManagedFileRootPathChoices from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Rack, Location, RackRole, Site -from extras.api.views import ReportViewSet, ScriptViewSet from extras.models import * from extras.reports import Report from extras.scripts import BooleanVar, IntegerVar, Script, StringVar @@ -579,6 +578,7 @@ class ReportTest(APITestCase): super().setUp() # Monkey-patch the API viewset's _get_report() method to return our test Report above + from extras.api.views import ReportViewSet ReportViewSet._get_report = self.get_test_report def test_get_report(self): @@ -621,6 +621,7 @@ class ScriptTest(APITestCase): super().setUp() # Monkey-patch the API viewset's _get_script() method to return our test Script above + from extras.api.views import ScriptViewSet ScriptViewSet._get_script = self.get_test_script def test_get_script(self): diff --git a/netbox/extras/views.py b/netbox/extras/views.py index 6cbadf09d..6ba63ab58 100644 --- a/netbox/extras/views.py +++ b/netbox/extras/views.py @@ -511,7 +511,7 @@ class ConfigTemplateBulkSyncDataView(generic.BulkSyncDataView): # class ObjectChangeListView(generic.ObjectListView): - queryset = ObjectChange.objects.all() + queryset = ObjectChange.objects.valid_models() filterset = filtersets.ObjectChangeFilterSet filterset_form = forms.ObjectChangeFilterForm table = tables.ObjectChangeTable @@ -521,10 +521,10 @@ class ObjectChangeListView(generic.ObjectListView): @register_model_view(ObjectChange) class ObjectChangeView(generic.ObjectView): - queryset = ObjectChange.objects.all() + queryset = ObjectChange.objects.valid_models() def get_extra_context(self, request, instance): - related_changes = ObjectChange.objects.restrict(request.user, 'view').filter( + related_changes = ObjectChange.objects.valid_models().restrict(request.user, 'view').filter( request_id=instance.request_id ).exclude( pk=instance.pk @@ -534,7 +534,7 @@ class ObjectChangeView(generic.ObjectView): orderable=False ) - objectchanges = ObjectChange.objects.restrict(request.user, 'view').filter( + objectchanges = ObjectChange.objects.valid_models().restrict(request.user, 'view').filter( changed_object_type=instance.changed_object_type, changed_object_id=instance.changed_object_id, ) diff --git a/netbox/users/views.py b/netbox/users/views.py index a82620914..05648e2e3 100644 --- a/netbox/users/views.py +++ b/netbox/users/views.py @@ -159,7 +159,9 @@ class ProfileView(LoginRequiredMixin, View): def get(self, request): # Compile changelog table - changelog = ObjectChange.objects.restrict(request.user, 'view').filter(user=request.user).prefetch_related( + changelog = ObjectChange.objects.valid_models().restrict(request.user, 'view').filter( + user=request.user + ).prefetch_related( 'changed_object_type' )[:20] changelog_table = ObjectChangeTable(changelog) From 22f7cd375b8182149aac35480605b2f26d5b868c Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 5 Jul 2023 11:43:53 -0400 Subject: [PATCH 26/37] Changelog for #11335, #12760, #12842, #12951, #12955 --- docs/release-notes/version-3.5.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/release-notes/version-3.5.md b/docs/release-notes/version-3.5.md index 7b82ed07c..beb0a1991 100644 --- a/docs/release-notes/version-3.5.md +++ b/docs/release-notes/version-3.5.md @@ -5,11 +5,16 @@ ### Enhancements * [#12945](https://github.com/netbox-community/netbox/issues/12945) - Add 100GE QSFP-DD interface type +* [#12955](https://github.com/netbox-community/netbox/issues/12955) - Include additional contact details on contact assignments table ### Bug Fixes +* [#11335](https://github.com/netbox-community/netbox/issues/11335) - Exclude stale content types when retrieving changelog records * [#12533](https://github.com/netbox-community/netbox/issues/12533) - Fix REST API validation of null values for several interface attributes +* [#12760](https://github.com/netbox-community/netbox/issues/12760) - Avoid rendering partial HTMX responses when restoring browser tabs +* [#12842](https://github.com/netbox-community/netbox/issues/12842) - Improve handling of exceptions when loading reports * [#12849](https://github.com/netbox-community/netbox/issues/12849) - Fix LDAP group permissions assignment for API clients +* [#12951](https://github.com/netbox-community/netbox/issues/12951) - Display consistent parent information for each termination under cable view * [#12953](https://github.com/netbox-community/netbox/issues/12953) - Fix designation of primary IP addresses during interface assignment * [#12960](https://github.com/netbox-community/netbox/issues/12960) - Fix OpenAPI schema for various choice fields * [#12961](https://github.com/netbox-community/netbox/issues/12961) - Set correct return URL for object contacts tabs From 0e384af1abc69437c7fdc5701e2d5945e4612b8a Mon Sep 17 00:00:00 2001 From: Arthur Date: Thu, 6 Jul 2023 12:45:11 +0700 Subject: [PATCH 27/37] 12092 allow setnull for bulk edit power port maximum and allocated draw --- netbox/dcim/forms/bulk_edit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/dcim/forms/bulk_edit.py b/netbox/dcim/forms/bulk_edit.py index bc9693afb..f1abdef1d 100644 --- a/netbox/dcim/forms/bulk_edit.py +++ b/netbox/dcim/forms/bulk_edit.py @@ -1106,7 +1106,7 @@ class PowerPortBulkEditForm( (None, ('module', 'type', 'label', 'description', 'mark_connected')), ('Power', ('maximum_draw', 'allocated_draw')), ) - nullable_fields = ('module', 'label', 'description') + nullable_fields = ('module', 'label', 'description', 'maximum_draw', 'allocated_draw') class PowerOutletBulkEditForm( From 2f3b6d6c7098bbec82dc92b6839a562591dbc45c Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 29 Jun 2023 15:35:53 -0400 Subject: [PATCH 28/37] Fixes #13047: Add annotate_asn_count() to ASNRange manager --- netbox/ipam/models/asns.py | 3 +++ netbox/ipam/querysets.py | 26 +++++++++++++++++++++++++- netbox/ipam/tables/asn.py | 9 ++++----- netbox/ipam/views.py | 10 +++------- 4 files changed, 35 insertions(+), 13 deletions(-) diff --git a/netbox/ipam/models/asns.py b/netbox/ipam/models/asns.py index a07cbb789..6c0b5231b 100644 --- a/netbox/ipam/models/asns.py +++ b/netbox/ipam/models/asns.py @@ -4,6 +4,7 @@ from django.urls import reverse from django.utils.translation import gettext as _ from ipam.fields import ASNField +from ipam.querysets import ASNRangeQuerySet from netbox.models import OrganizationalModel, PrimaryModel __all__ = ( @@ -37,6 +38,8 @@ class ASNRange(OrganizationalModel): null=True ) + objects = ASNRangeQuerySet.as_manager() + class Meta: ordering = ('name',) verbose_name = 'ASN range' diff --git a/netbox/ipam/querysets.py b/netbox/ipam/querysets.py index 9f4463f61..d6b23b843 100644 --- a/netbox/ipam/querysets.py +++ b/netbox/ipam/querysets.py @@ -1,9 +1,33 @@ from django.contrib.contenttypes.models import ContentType -from django.db.models import Q +from django.db.models import Count, OuterRef, Q, Subquery, Value from django.db.models.expressions import RawSQL from utilities.querysets import RestrictedQuerySet +__all__ = ( + 'ASNRangeQuerySet', + 'PrefixQuerySet', + 'VLANQuerySet', +) + + +class ASNRangeQuerySet(RestrictedQuerySet): + + def annotate_asn_counts(self): + """ + Annotate the number of ASNs which appear within each range. + """ + from .models import ASN + + # Because ASN does not have a foreign key to ASNRange, we create a fake column "_" with a consistent value + # that we can use to count ASNs and return a single value per ASNRange. + asns = ASN.objects.filter( + asn__gte=OuterRef('start'), + asn__lte=OuterRef('end') + ).order_by().annotate(_=Value(1)).values('_').annotate(c=Count('*')).values('c') + + return self.annotate(asn_count=Subquery(asns)) + class PrefixQuerySet(RestrictedQuerySet): diff --git a/netbox/ipam/tables/asn.py b/netbox/ipam/tables/asn.py index 511e914ec..356f2fc17 100644 --- a/netbox/ipam/tables/asn.py +++ b/netbox/ipam/tables/asn.py @@ -21,10 +21,8 @@ class ASNRangeTable(TenancyColumnsMixin, NetBoxTable): tags = columns.TagColumn( url_name='ipam:asnrange_list' ) - asn_count = columns.LinkedCountColumn( - viewname='ipam:asn_list', - url_params={'asn_id': 'pk'}, - verbose_name=_('ASN Count') + asn_count = tables.Column( + verbose_name=_('ASNs') ) class Meta(NetBoxTable.Meta): @@ -59,7 +57,8 @@ class ASNTable(TenancyColumnsMixin, NetBoxTable): verbose_name=_('Provider Count') ) sites = columns.ManyToManyColumn( - linkify_item=True + linkify_item=True, + verbose_name=_('Sites') ) comments = columns.MarkdownColumn() tags = columns.TagColumn( diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index 6b73a061b..6efaef8ea 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -198,7 +198,7 @@ class RIRBulkDeleteView(generic.BulkDeleteView): # class ASNRangeListView(generic.ObjectListView): - queryset = ASNRange.objects.all() + queryset = ASNRange.objects.annotate_asn_counts() filterset = filtersets.ASNRangeFilterSet filterset_form = forms.ASNRangeFilterForm table = tables.ASNRangeTable @@ -247,18 +247,14 @@ class ASNRangeBulkImportView(generic.BulkImportView): class ASNRangeBulkEditView(generic.BulkEditView): - queryset = ASNRange.objects.annotate( - site_count=count_related(Site, 'asns') - ) + queryset = ASNRange.objects.annotate_asn_counts() filterset = filtersets.ASNRangeFilterSet table = tables.ASNRangeTable form = forms.ASNRangeBulkEditForm class ASNRangeBulkDeleteView(generic.BulkDeleteView): - queryset = ASNRange.objects.annotate( - site_count=count_related(Site, 'asns') - ) + queryset = ASNRange.objects.annotate_asn_counts() filterset = filtersets.ASNRangeFilterSet table = tables.ASNRangeTable From 0c136108661b372071a900cc1cb4060665f101cd Mon Sep 17 00:00:00 2001 From: Anthony Brissonnet <137606620+netopsab@users.noreply.github.com> Date: Thu, 6 Jul 2023 15:28:45 +0200 Subject: [PATCH 29/37] Fix #12579 create cable and add another error (#13007) * fix create cable and add another error #12579 * fix return proper parent object field * improve code and wokflow --------- Co-authored-by: netopsab --- netbox/dcim/views.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index b52e0afa5..008db382a 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -3131,6 +3131,19 @@ class CableEditView(generic.ObjectEditView): return obj + def get_extra_addanother_params(self, request): + + params = { + 'a_terminations_type': request.GET.get('a_terminations_type'), + 'b_terminations_type': request.GET.get('b_terminations_type') + } + + for key in request.POST: + if 'device' in key or 'power_panel' in key or 'circuit' in key: + params.update({key: request.POST.get(key)}) + + return params + @register_model_view(Cable, 'delete') class CableDeleteView(generic.ObjectDeleteView): From 8c878638601c4cc088e44d050a4d23d42d494c31 Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Sat, 24 Jun 2023 00:29:37 +0530 Subject: [PATCH 30/37] fixes prechange snapshot #12617 --- netbox/ipam/forms/model_forms.py | 1 + 1 file changed, 1 insertion(+) diff --git a/netbox/ipam/forms/model_forms.py b/netbox/ipam/forms/model_forms.py index 6fa0f95ea..a3c218fc9 100644 --- a/netbox/ipam/forms/model_forms.py +++ b/netbox/ipam/forms/model_forms.py @@ -379,6 +379,7 @@ class IPAddressForm(TenancyForm, NetBoxModelForm): interface = self.instance.assigned_object if type(interface) in (Interface, VMInterface): parent = interface.parent_object + parent.snapshot() if self.cleaned_data['primary_for_parent']: if ipaddress.address.version == 4: parent.primary_ip4 = ipaddress From 85ff70c0689776a2fee1a2c0511732c65180fba8 Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Thu, 6 Jul 2023 18:41:42 +0530 Subject: [PATCH 31/37] fixes search for vdc #13100 --- netbox/dcim/filtersets.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py index e784be8e8..5ddaf9a9a 100644 --- a/netbox/dcim/filtersets.py +++ b/netbox/dcim/filtersets.py @@ -1077,10 +1077,13 @@ class VirtualDeviceContextFilterSet(NetBoxModelFilterSet, TenancyFilterSet): def search(self, queryset, name, value): if not value.strip(): return queryset - return queryset.filter( - Q(name__icontains=value) | - Q(identifier=value.strip()) - ).distinct() + + qs_filter = Q(name__icontains=value) + try: + qs_filter |= Q(identifier=int(value)) + except ValueError: + pass + return queryset.filter(qs_filter).distinct() def _has_primary_ip(self, queryset, name, value): params = Q(primary_ip4__isnull=False) | Q(primary_ip6__isnull=False) From 244d39387f8bbcb407bd2a01f78c253a3d7c8ea9 Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Thu, 6 Jul 2023 18:51:51 +0530 Subject: [PATCH 32/37] adds object change for contact assignment #13065 --- netbox/tenancy/models/contacts.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/netbox/tenancy/models/contacts.py b/netbox/tenancy/models/contacts.py index 440541b5f..1df5e3305 100644 --- a/netbox/tenancy/models/contacts.py +++ b/netbox/tenancy/models/contacts.py @@ -136,3 +136,8 @@ class ContactAssignment(ChangeLoggedModel): def get_absolute_url(self): return reverse('tenancy:contact', args=[self.contact.pk]) + + def to_objectchange(self, action): + objectchange = super().to_objectchange(action) + objectchange.related_object = self.object + return objectchange From 404714f61821338c92832349b2370acfcad4d483 Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Thu, 6 Jul 2023 23:49:55 +0530 Subject: [PATCH 33/37] Adds copy content button (#12584) * adds copy content button #12499 * adds newline * Omit hash mark from target string * Clean up HTML element IDs --------- Co-authored-by: Jeremy Stretch --- netbox/ipam/tables/ip.py | 31 ++++++++++++++++-- netbox/project-static/dist/netbox.js | Bin 530632 -> 530613 bytes netbox/project-static/dist/netbox.js.map | Bin 450874 -> 450868 bytes netbox/project-static/src/clipboard.ts | 2 +- netbox/templates/core/datafile.html | 8 ++--- netbox/templates/dcim/device.html | 6 ++-- .../templates/dcim/virtualdevicecontext.html | 14 ++++++-- netbox/templates/users/api_token.html | 6 ++-- .../virtualization/virtualmachine.html | 6 ++-- netbox/users/tables.py | 4 +-- .../templates/builtins/copy_content.html | 3 ++ .../utilities/templatetags/builtins/tags.py | 12 +++++++ 12 files changed, 69 insertions(+), 23 deletions(-) create mode 100644 netbox/utilities/templates/builtins/copy_content.html diff --git a/netbox/ipam/tables/ip.py b/netbox/ipam/tables/ip.py index 86d1a3775..aff090f3a 100644 --- a/netbox/ipam/tables/ip.py +++ b/netbox/ipam/tables/ip.py @@ -19,14 +19,22 @@ __all__ = ( AVAILABLE_LABEL = mark_safe('Available') +AGGREGATE_COPY_BUTTON = """ +{% copy_content record.pk prefix="aggregate_" %} +""" + PREFIX_LINK = """ {% if record.pk %} - {{ record.prefix }} + {{ record.prefix }} {% else %} {{ record.prefix }} {% endif %} """ +PREFIX_COPY_BUTTON = """ +{% copy_content record.pk prefix="prefix_" %} +""" + PREFIX_LINK_WITH_DEPTH = """ {% load helpers %} {% if record.depth %} @@ -40,7 +48,7 @@ PREFIX_LINK_WITH_DEPTH = """ IPADDRESS_LINK = """ {% if record.pk %} - {{ record.address }} + {{ record.address }} {% elif perms.ipam.add_ipaddress %} {% if record.0 <= 65536 %}{{ record.0 }}{% else %}Many{% endif %} IP{{ record.0|pluralize }} available {% else %} @@ -48,6 +56,10 @@ IPADDRESS_LINK = """ {% endif %} """ +IPADDRESS_COPY_BUTTON = """ +{% copy_content record.pk prefix="ipaddress_" %} +""" + IPADDRESS_ASSIGN_LINK = """ {{ record }} """ @@ -99,7 +111,11 @@ class RIRTable(NetBoxTable): class AggregateTable(TenancyColumnsMixin, NetBoxTable): prefix = tables.Column( linkify=True, - verbose_name='Aggregate' + verbose_name='Aggregate', + attrs={ + # Allow the aggregate to be copied to the clipboard + 'a': {'id': lambda record: f"aggregate_{record.pk}"} + } ) date_added = tables.DateColumn( format="Y-m-d", @@ -116,6 +132,9 @@ class AggregateTable(TenancyColumnsMixin, NetBoxTable): tags = columns.TagColumn( url_name='ipam:aggregate_list' ) + actions = columns.ActionsColumn( + extra_buttons=AGGREGATE_COPY_BUTTON + ) class Meta(NetBoxTable.Meta): model = Aggregate @@ -242,6 +261,9 @@ class PrefixTable(TenancyColumnsMixin, NetBoxTable): tags = columns.TagColumn( url_name='ipam:prefix_list' ) + actions = columns.ActionsColumn( + extra_buttons=PREFIX_COPY_BUTTON + ) class Meta(NetBoxTable.Meta): model = Prefix @@ -348,6 +370,9 @@ class IPAddressTable(TenancyColumnsMixin, NetBoxTable): tags = columns.TagColumn( url_name='ipam:ipaddress_list' ) + actions = columns.ActionsColumn( + extra_buttons=IPADDRESS_COPY_BUTTON + ) class Meta(NetBoxTable.Meta): model = IPAddress diff --git a/netbox/project-static/dist/netbox.js b/netbox/project-static/dist/netbox.js index 9642d15851c5238db66f5a46ac81880ce2f10e1f..b62436d757a2b299e5bf0f33dcc4fbd67ec21e8c 100644 GIT binary patch delta 40 wcmX@HQDN&wg@zW!7N!>F7M2#)7Pc+yaw2TW`FSO&dF|#R?Ay&nI3}_H03@djzW@LL delta 59 zcmdnGQQ^c!g@zW!7N!>F7M2#)7Pc+yaw3u?`Pr#?N;*nOr6nc#d3wqD1(mwRsmVpD P?J6Sd+f_t3Cb9qkYoQZ* diff --git a/netbox/project-static/dist/netbox.js.map b/netbox/project-static/dist/netbox.js.map index f86d50148d419322456611b15cb949883cc3d9c2..ed3833f982a5d0f0d81c6bcf7e90a86aeb99d789 100644 GIT binary patch delta 34 qcmdmWNP5d5>4p}@7N!>F7M2#)Eo^FBY}rnZ&Kb_zjkwr2t^)wp@(S<( delta 40 wcmdmTNP5>H>4p}@7N!>F7M2#)Eo^FBJgJV3=}tPuPL9qQF57ju*f_2O03L1(wEzGB diff --git a/netbox/project-static/src/clipboard.ts b/netbox/project-static/src/clipboard.ts index a04acba39..46ca5e36c 100644 --- a/netbox/project-static/src/clipboard.ts +++ b/netbox/project-static/src/clipboard.ts @@ -2,7 +2,7 @@ import Clipboard from 'clipboard'; import { getElements } from './util'; export function initClipboard(): void { - for (const element of getElements('a.copy-token', 'button.copy-secret')) { + for (const element of getElements('a.copy-content')) { new Clipboard(element); } } diff --git a/netbox/templates/core/datafile.html b/netbox/templates/core/datafile.html index 3d79d17e2..785617ae5 100644 --- a/netbox/templates/core/datafile.html +++ b/netbox/templates/core/datafile.html @@ -39,9 +39,7 @@ Path {{ object.path }} - - - + {% copy_content "datafile_path" %} @@ -56,9 +54,7 @@ SHA256 Hash {{ object.hash }} - - - + {% copy_content "datafile_hash" %} diff --git a/netbox/templates/dcim/device.html b/netbox/templates/dcim/device.html index b0e67269c..df5209add 100644 --- a/netbox/templates/dcim/device.html +++ b/netbox/templates/dcim/device.html @@ -194,12 +194,13 @@ Primary IPv4 {% if object.primary_ip4 %} - {{ object.primary_ip4.address.ip }} + {{ object.primary_ip4.address.ip }} {% if object.primary_ip4.nat_inside %} (NAT for {{ object.primary_ip4.nat_inside.address.ip }}) {% elif object.primary_ip4.nat_outside.exists %} (NAT: {% for nat in object.primary_ip4.nat_outside.all %}{{ nat.address.ip }}{% if not forloop.last %}, {% endif %}{% endfor %}) {% endif %} + {% copy_content "primary_ip4" %} {% else %} {{ ''|placeholder }} {% endif %} @@ -209,12 +210,13 @@ Primary IPv6 {% if object.primary_ip6 %} - {{ object.primary_ip6.address.ip }} + {{ object.primary_ip6.address.ip }} {% if object.primary_ip6.nat_inside %} (NAT for {{ object.primary_ip6.nat_inside.address.ip }}) {% elif object.primary_ip6.nat_outside.exists %} (NAT: {% for nat in object.primary_ip6.nat_outside.all %}{{ nat.address.ip }}{% if not forloop.last %}, {% endif %}{% endfor %}) {% endif %} + {% copy_content "primary_ip6" %} {% else %} {{ ''|placeholder }} {% endif %} diff --git a/netbox/templates/dcim/virtualdevicecontext.html b/netbox/templates/dcim/virtualdevicecontext.html index d6e3e0c63..1caf05bd2 100644 --- a/netbox/templates/dcim/virtualdevicecontext.html +++ b/netbox/templates/dcim/virtualdevicecontext.html @@ -31,13 +31,23 @@ Primary IPv4 - {{ object.primary_ip4|linkify|placeholder }} + {% if object.primary_ip4 %} + {{ object.primary_ip4 }} + {% copy_content "primary_ip4" %} + {% else %} + + {% endif %} Primary IPv6 - {{ object.primary_ip6|linkify|placeholder }} + {% if object.primary_ip6 %} + {{ object.primary_ip6 }} + {% copy_content "primary_ip6" %} + {% else %} + + {% endif %} diff --git a/netbox/templates/users/api_token.html b/netbox/templates/users/api_token.html index 1a9296704..7fd6f064d 100644 --- a/netbox/templates/users/api_token.html +++ b/netbox/templates/users/api_token.html @@ -8,7 +8,7 @@
{% if not settings.ALLOW_TOKEN_RETRIEVAL %} {% endif %}
@@ -19,9 +19,7 @@ Key
- - - + {% copy_content "token_id" %}
{{ key }}
diff --git a/netbox/templates/virtualization/virtualmachine.html b/netbox/templates/virtualization/virtualmachine.html index 51fd8aa80..3d3b498ad 100644 --- a/netbox/templates/virtualization/virtualmachine.html +++ b/netbox/templates/virtualization/virtualmachine.html @@ -46,12 +46,13 @@ Primary IPv4 {% if object.primary_ip4 %} - {{ object.primary_ip4.address.ip }} + {{ object.primary_ip4.address.ip }} {% if object.primary_ip4.nat_inside %} (NAT for {{ object.primary_ip4.nat_inside.address.ip }}) {% elif object.primary_ip4.nat_outside.exists %} (NAT: {% for nat in object.primary_ip4.nat_outside.all %}{{ nat.address.ip }}{% if not forloop.last %}, {% endif %}{% endfor %}) {% endif %} + {% copy_content "primary_ip4" %} {% else %} {{ ''|placeholder }} {% endif %} @@ -61,12 +62,13 @@ Primary IPv6 {% if object.primary_ip6 %} - {{ object.primary_ip6.address.ip }} + {{ object.primary_ip6.address.ip }} {% if object.primary_ip6.nat_inside %} (NAT for {{ object.primary_ip6.nat_inside.address.ip }}) {% elif object.primary_ip6.nat_outside.exists %} (NAT: {% for nat in object.primary_ip6.nat_outside.all %}{{ nat.address.ip }}{% if not forloop.last %}, {% endif %}{% endfor %}) {% endif %} + {% copy_content "primary_ip6" %} {% else %} {{ ''|placeholder }} {% endif %} diff --git a/netbox/users/tables.py b/netbox/users/tables.py index 0f1484887..cea50b10f 100644 --- a/netbox/users/tables.py +++ b/netbox/users/tables.py @@ -12,9 +12,7 @@ ALLOWED_IPS = """{{ value|join:", " }}""" COPY_BUTTON = """ {% if settings.ALLOW_TOKEN_RETRIEVAL %} - - - + {% copy_content record.pk prefix="token_" color="success" %} {% endif %} """ diff --git a/netbox/utilities/templates/builtins/copy_content.html b/netbox/utilities/templates/builtins/copy_content.html new file mode 100644 index 000000000..9025a71a1 --- /dev/null +++ b/netbox/utilities/templates/builtins/copy_content.html @@ -0,0 +1,3 @@ + + + diff --git a/netbox/utilities/templatetags/builtins/tags.py b/netbox/utilities/templatetags/builtins/tags.py index f9fe5f4e3..35aec1000 100644 --- a/netbox/utilities/templatetags/builtins/tags.py +++ b/netbox/utilities/templatetags/builtins/tags.py @@ -6,6 +6,7 @@ from utilities.utils import dict_to_querydict __all__ = ( 'badge', 'checkmark', + 'copy_content', 'customfield_value', 'tag', ) @@ -79,6 +80,17 @@ def checkmark(value, show_false=True, true='Yes', false='No'): } +@register.inclusion_tag('builtins/copy_content.html') +def copy_content(target, prefix=None, color='primary'): + """ + Display a copy button to copy the content of a field. + """ + return { + 'target': f'#{prefix or ""}{target}', + 'color': f'btn-{color}' + } + + @register.inclusion_tag('builtins/htmx_table.html', takes_context=True) def htmx_table(context, viewname, return_url=None, **kwargs): """ From 1d404cc1d344f2d2436e2b518a9146aa479f51c8 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 6 Jul 2023 14:51:28 -0400 Subject: [PATCH 34/37] Closes #11738: Annotate utilization on VLAN groups (#13108) * Update serializers.py * Update vlans.py * Update vlans.py * Update vlangroup.html * Update vlans.py * Update vlans.py * Update serializers.py * adds db annotation to calculate utilization * optimize queries * merge fix * adds round function for utilization to limit decimal * fixed object view annotation * consolidated queryset for utilization * lint fixes * Renamed manager method to annotate_utilization() for consistency with other managers --------- Co-authored-by: Abhimanyu Saharan --- netbox/ipam/api/serializers.py | 3 ++- netbox/ipam/api/views.py | 6 +++--- netbox/ipam/models/vlans.py | 4 +++- netbox/ipam/querysets.py | 15 ++++++++++++++- netbox/ipam/tables/vlans.py | 8 ++++++-- netbox/ipam/views.py | 17 ++++++----------- netbox/templates/ipam/vlangroup.html | 4 ++++ 7 files changed, 38 insertions(+), 19 deletions(-) diff --git a/netbox/ipam/api/serializers.py b/netbox/ipam/api/serializers.py index 064452667..1501f16dc 100644 --- a/netbox/ipam/api/serializers.py +++ b/netbox/ipam/api/serializers.py @@ -218,12 +218,13 @@ class VLANGroupSerializer(NetBoxModelSerializer): scope_id = serializers.IntegerField(allow_null=True, required=False, default=None) scope = serializers.SerializerMethodField(read_only=True) vlan_count = serializers.IntegerField(read_only=True) + utilization = serializers.CharField(read_only=True) class Meta: model = VLANGroup fields = [ 'id', 'url', 'display', 'name', 'slug', 'scope_type', 'scope_id', 'scope', 'min_vid', 'max_vid', - 'description', 'tags', 'custom_fields', 'created', 'last_updated', 'vlan_count', + 'description', 'tags', 'custom_fields', 'created', 'last_updated', 'vlan_count', 'utilization' ] validators = [] diff --git a/netbox/ipam/api/views.py b/netbox/ipam/api/views.py index f432e0e6b..99b4c023d 100644 --- a/netbox/ipam/api/views.py +++ b/netbox/ipam/api/views.py @@ -1,5 +1,7 @@ from django.core.exceptions import ObjectDoesNotExist, PermissionDenied from django.db import transaction +from django.db.models import F +from django.db.models.functions import Round from django.shortcuts import get_object_or_404 from django_pglocks import advisory_lock from drf_spectacular.utils import extend_schema @@ -145,9 +147,7 @@ class FHRPGroupAssignmentViewSet(NetBoxModelViewSet): class VLANGroupViewSet(NetBoxModelViewSet): - queryset = VLANGroup.objects.annotate( - vlan_count=count_related(VLAN, 'group') - ).prefetch_related('tags') + queryset = VLANGroup.objects.annotate_utilization().prefetch_related('tags') serializer_class = serializers.VLANGroupSerializer filterset_class = filtersets.VLANGroupFilterSet diff --git a/netbox/ipam/models/vlans.py b/netbox/ipam/models/vlans.py index 7d4777da9..da504ded2 100644 --- a/netbox/ipam/models/vlans.py +++ b/netbox/ipam/models/vlans.py @@ -9,7 +9,7 @@ from django.utils.translation import gettext as _ from dcim.models import Interface from ipam.choices import * from ipam.constants import * -from ipam.querysets import VLANQuerySet +from ipam.querysets import VLANQuerySet, VLANGroupQuerySet from netbox.models import OrganizationalModel, PrimaryModel from virtualization.models import VMInterface @@ -63,6 +63,8 @@ class VLANGroup(OrganizationalModel): help_text=_('Highest permissible ID of a child VLAN') ) + objects = VLANGroupQuerySet.as_manager() + class Meta: ordering = ('name', 'pk') # Name may be non-unique constraints = ( diff --git a/netbox/ipam/querysets.py b/netbox/ipam/querysets.py index d6b23b843..39da0c3a2 100644 --- a/netbox/ipam/querysets.py +++ b/netbox/ipam/querysets.py @@ -1,8 +1,10 @@ from django.contrib.contenttypes.models import ContentType -from django.db.models import Count, OuterRef, Q, Subquery, Value +from django.db.models import Count, F, OuterRef, Q, Subquery, Value from django.db.models.expressions import RawSQL +from django.db.models.functions import Round from utilities.querysets import RestrictedQuerySet +from utilities.utils import count_related __all__ = ( 'ASNRangeQuerySet', @@ -54,6 +56,17 @@ class PrefixQuerySet(RestrictedQuerySet): ) +class VLANGroupQuerySet(RestrictedQuerySet): + + def annotate_utilization(self): + from .models import VLAN + + return self.annotate( + vlan_count=count_related(VLAN, 'group'), + utilization=Round(F('vlan_count') / (F('max_vid') - F('min_vid') + 1.0) * 100, 2) + ) + + class VLANQuerySet(RestrictedQuerySet): def get_for_device(self, device): diff --git a/netbox/ipam/tables/vlans.py b/netbox/ipam/tables/vlans.py index 6fa2cd2da..5d9828531 100644 --- a/netbox/ipam/tables/vlans.py +++ b/netbox/ipam/tables/vlans.py @@ -70,6 +70,10 @@ class VLANGroupTable(NetBoxTable): url_params={'group_id': 'pk'}, verbose_name='VLANs' ) + utilization = columns.UtilizationColumn( + orderable=False, + verbose_name='Utilization' + ) tags = columns.TagColumn( url_name='ipam:vlangroup_list' ) @@ -81,9 +85,9 @@ class VLANGroupTable(NetBoxTable): model = VLANGroup fields = ( 'pk', 'id', 'name', 'scope_type', 'scope', 'min_vid', 'max_vid', 'vlan_count', 'slug', 'description', - 'tags', 'created', 'last_updated', 'actions', + 'tags', 'created', 'last_updated', 'actions', 'utilization', ) - default_columns = ('pk', 'name', 'scope_type', 'scope', 'vlan_count', 'description') + default_columns = ('pk', 'name', 'scope_type', 'scope', 'vlan_count', 'utilization', 'description') # diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index 6efaef8ea..32badd2d5 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -1,6 +1,7 @@ from django.contrib.contenttypes.models import ContentType -from django.db.models import Prefetch +from django.db.models import F, Prefetch from django.db.models.expressions import RawSQL +from django.db.models.functions import Round from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse from django.utils.translation import gettext as _ @@ -882,9 +883,7 @@ class IPAddressRelatedIPsView(generic.ObjectChildrenView): # class VLANGroupListView(generic.ObjectListView): - queryset = VLANGroup.objects.annotate( - vlan_count=count_related(VLAN, 'group') - ) + queryset = VLANGroup.objects.annotate_utilization().prefetch_related('tags') filterset = filtersets.VLANGroupFilterSet filterset_form = forms.VLANGroupFilterForm table = tables.VLANGroupTable @@ -892,7 +891,7 @@ class VLANGroupListView(generic.ObjectListView): @register_model_view(VLANGroup) class VLANGroupView(generic.ObjectView): - queryset = VLANGroup.objects.all() + queryset = VLANGroup.objects.annotate_utilization().prefetch_related('tags') def get_extra_context(self, request, instance): related_models = ( @@ -934,18 +933,14 @@ class VLANGroupBulkImportView(generic.BulkImportView): class VLANGroupBulkEditView(generic.BulkEditView): - queryset = VLANGroup.objects.annotate( - vlan_count=count_related(VLAN, 'group') - ) + queryset = VLANGroup.objects.annotate_utilization().prefetch_related('tags') filterset = filtersets.VLANGroupFilterSet table = tables.VLANGroupTable form = forms.VLANGroupBulkEditForm class VLANGroupBulkDeleteView(generic.BulkDeleteView): - queryset = VLANGroup.objects.annotate( - vlan_count=count_related(VLAN, 'group') - ) + queryset = VLANGroup.objects.annotate_utilization().prefetch_related('tags') filterset = filtersets.VLANGroupFilterSet table = tables.VLANGroupTable diff --git a/netbox/templates/ipam/vlangroup.html b/netbox/templates/ipam/vlangroup.html index 2917536be..e474cbd84 100644 --- a/netbox/templates/ipam/vlangroup.html +++ b/netbox/templates/ipam/vlangroup.html @@ -42,6 +42,10 @@ Permitted VIDs {{ object.min_vid }} - {{ object.max_vid }} + + Utilization + {% utilization_graph object.utilization %} +
From 60c710f1f7173bfe14ef269bf850d31f75bdba51 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 6 Jul 2023 14:54:37 -0400 Subject: [PATCH 35/37] Change log for #11738, #12499, #12579, #12617, #13047, #13065, #13092, #13100 --- docs/release-notes/version-3.5.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/release-notes/version-3.5.md b/docs/release-notes/version-3.5.md index beb0a1991..194d44922 100644 --- a/docs/release-notes/version-3.5.md +++ b/docs/release-notes/version-3.5.md @@ -4,13 +4,18 @@ ### Enhancements +* [#11738](https://github.com/netbox-community/netbox/issues/11738) - Annotate VLAN group utilization +* [#12499](https://github.com/netbox-community/netbox/issues/12499) - Add "copy to clipboard" buttons in UI for IP addresses * [#12945](https://github.com/netbox-community/netbox/issues/12945) - Add 100GE QSFP-DD interface type * [#12955](https://github.com/netbox-community/netbox/issues/12955) - Include additional contact details on contact assignments table +* [#13065](https://github.com/netbox-community/netbox/issues/13065) - Associate contact assignments with their objects in the change log ### Bug Fixes * [#11335](https://github.com/netbox-community/netbox/issues/11335) - Exclude stale content types when retrieving changelog records * [#12533](https://github.com/netbox-community/netbox/issues/12533) - Fix REST API validation of null values for several interface attributes +* [#12579](https://github.com/netbox-community/netbox/issues/12579) - Fix exception when clicking "create and add another" to add a cable +* [#12617](https://github.com/netbox-community/netbox/issues/12617) - Populate prechange snapshot on parent object when assigning/removing primary IP address * [#12760](https://github.com/netbox-community/netbox/issues/12760) - Avoid rendering partial HTMX responses when restoring browser tabs * [#12842](https://github.com/netbox-community/netbox/issues/12842) - Improve handling of exceptions when loading reports * [#12849](https://github.com/netbox-community/netbox/issues/12849) - Fix LDAP group permissions assignment for API clients @@ -24,6 +29,9 @@ * [#12983](https://github.com/netbox-community/netbox/issues/12983) - Avoid erroneously clearing many-to-many assignments during bulk edit * [#12989](https://github.com/netbox-community/netbox/issues/12989) - Fix bulk import of tags for device & module types * [#13011](https://github.com/netbox-community/netbox/issues/13011) - Do not escape commas when rendering custom links +* [#13047](https://github.com/netbox-community/netbox/issues/13047) - Correct ASN count under ASN ranges list +* [#13092](https://github.com/netbox-community/netbox/issues/13092) - Allow nullifying power port max & allocated draw values during bulk edit +* [#13100](https://github.com/netbox-community/netbox/issues/13100) - Fix ValueError exception when searching for virtual device context for non-numeric values --- From cebb1e51462e58fc86dabcf2bdca8701d04b26d1 Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Fri, 7 Jul 2023 00:43:02 +0530 Subject: [PATCH 36/37] adds config_template to device serializer #13056 --- netbox/dcim/api/serializers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 2f854d3e4..9cf30fdd4 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -698,7 +698,8 @@ class DeviceWithConfigContextSerializer(DeviceSerializer): 'id', 'url', 'display', 'name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'asset_tag', 'site', 'location', 'rack', 'position', 'face', 'parent_device', 'status', 'airflow', 'primary_ip', 'primary_ip4', 'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'description', - 'comments', 'local_context_data', 'tags', 'custom_fields', 'config_context', 'created', 'last_updated', + 'comments', 'local_context_data', 'tags', 'custom_fields', 'config_context', 'config_template', + 'created', 'last_updated', ] @extend_schema_field(serializers.JSONField(allow_null=True)) From 52daa5e76071ff757087872239824e631aa39171 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 6 Jul 2023 16:20:14 -0400 Subject: [PATCH 37/37] Release v3.5.5 --- .github/ISSUE_TEMPLATE/bug_report.yaml | 2 +- .github/ISSUE_TEMPLATE/feature_request.yaml | 2 +- docs/release-notes/version-3.5.md | 3 ++- netbox/netbox/settings.py | 2 +- requirements.txt | 16 ++++++++-------- 5 files changed, 13 insertions(+), 12 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index b3dd583ca..a43f754bf 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.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.5.4 + placeholder: v3.5.5 validations: required: true - type: dropdown diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml index bd93001e7..c79b2fe9c 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yaml +++ b/.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.5.4 + placeholder: v3.5.5 validations: required: true - type: dropdown diff --git a/docs/release-notes/version-3.5.md b/docs/release-notes/version-3.5.md index 194d44922..6087f895f 100644 --- a/docs/release-notes/version-3.5.md +++ b/docs/release-notes/version-3.5.md @@ -1,6 +1,6 @@ # NetBox v3.5 -## v3.5.5 (FUTURE) +## v3.5.5 (2023-07-06) ### Enhancements @@ -30,6 +30,7 @@ * [#12989](https://github.com/netbox-community/netbox/issues/12989) - Fix bulk import of tags for device & module types * [#13011](https://github.com/netbox-community/netbox/issues/13011) - Do not escape commas when rendering custom links * [#13047](https://github.com/netbox-community/netbox/issues/13047) - Correct ASN count under ASN ranges list +* [#13056](https://github.com/netbox-community/netbox/issues/13056) - Add `config_template` field to device API serializer * [#13092](https://github.com/netbox-community/netbox/issues/13092) - Allow nullifying power port max & allocated draw values during bulk edit * [#13100](https://github.com/netbox-community/netbox/issues/13100) - Fix ValueError exception when searching for virtual device context for non-numeric values diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 31363144f..8a7133c07 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -25,7 +25,7 @@ from netbox.constants import RQ_QUEUE_DEFAULT, RQ_QUEUE_HIGH, RQ_QUEUE_LOW # Environment setup # -VERSION = '3.5.5-dev' +VERSION = '3.5.5' # Hostname HOSTNAME = platform.node() diff --git a/requirements.txt b/requirements.txt index e6e56ce56..df729d30f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ bleach==6.0.0 -boto3==1.26.156 -Django==4.1.9 +boto3==1.27.1 +Django==4.1.10 django-cors-headers==4.1.0 django-debug-toolbar==4.1.0 django-filter==23.2 @@ -11,25 +11,25 @@ django-prometheus==2.3.1 django-redis==5.3.0 django-rich==1.6.0 django-rq==2.8.1 -django-tables2==2.5.3 +django-tables2==2.6.0 django-taggit==4.0.0 django-timezone-field==5.1 djangorestframework==3.14.0 -drf-spectacular==0.26.2 -drf-spectacular-sidecar==2023.6.1 +drf-spectacular==0.26.3 +drf-spectacular-sidecar==2023.7.1 dulwich==0.21.5 feedparser==6.0.10 graphene-django==3.0.0 gunicorn==20.1.0 Jinja2==3.1.2 Markdown==3.3.7 -mkdocs-material==9.1.16 +mkdocs-material==9.1.18 mkdocstrings[python-legacy]==0.22.0 netaddr==0.8.0 -Pillow==9.5.0 +Pillow==10.0.0 psycopg2-binary==2.9.6 PyYAML==6.0 -sentry-sdk==1.25.1 +sentry-sdk==1.27.1 social-auth-app-django==5.2.0 social-auth-core[openidconnect]==4.4.2 svgwrite==1.4.3