From eb3d3dcbc4b65c819a0cb92de4794491f59f7687 Mon Sep 17 00:00:00 2001 From: Arthur Date: Thu, 18 Aug 2022 13:58:40 -0700 Subject: [PATCH 01/41] #6454 add prerequisite alert --- netbox/dcim/models/racks.py | 5 +++++ netbox/dcim/views.py | 1 + netbox/netbox/models/__init__.py | 4 ++++ netbox/netbox/views/generic/bulk_views.py | 15 +++++++++++++++ netbox/netbox/views/generic/object_views.py | 12 ++++++++++-- netbox/netbox/views/generic/utils.py | 12 ++++++++++++ netbox/templates/generic/object_edit.html | 4 ++++ netbox/templates/generic/object_list.html | 6 ++++++ netbox/templates/inc/missing_prerequisites.html | 5 +++++ 9 files changed, 62 insertions(+), 2 deletions(-) create mode 100644 netbox/netbox/views/generic/utils.py create mode 100644 netbox/templates/inc/missing_prerequisites.html diff --git a/netbox/dcim/models/racks.py b/netbox/dcim/models/racks.py index 81d699b11..e57934353 100644 --- a/netbox/dcim/models/racks.py +++ b/netbox/dcim/models/racks.py @@ -1,5 +1,6 @@ from collections import OrderedDict +from django.apps import apps from django.contrib.auth.models import User from django.contrib.contenttypes.fields import GenericRelation from django.contrib.contenttypes.models import ContentType @@ -202,6 +203,10 @@ class Rack(NetBoxModel): return f'{self.name} ({self.facility_id})' return self.name + @classmethod + def get_prerequisite_models(cls): + return [apps.get_model('dcim.Site'), ] + def get_absolute_url(self): return reverse('dcim:rack', args=[self.pk]) diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 966d90876..6e77d4396 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -560,6 +560,7 @@ class RackRoleBulkDeleteView(generic.BulkDeleteView): # class RackListView(generic.ObjectListView): + required_prerequisites = [Site] queryset = Rack.objects.prefetch_related('devices__device_type').annotate( device_count=count_related(Device, 'rack') ) diff --git a/netbox/netbox/models/__init__.py b/netbox/netbox/models/__init__.py index ea2feb8de..2524c7c9b 100644 --- a/netbox/netbox/models/__init__.py +++ b/netbox/netbox/models/__init__.py @@ -52,6 +52,10 @@ class NetBoxModel(NetBoxFeatureSet, models.Model): class Meta: abstract = True + @classmethod + def get_prerequisite_models(cls): + return [] + class NestedGroupModel(NetBoxFeatureSet, MPTTModel): """ diff --git a/netbox/netbox/views/generic/bulk_views.py b/netbox/netbox/views/generic/bulk_views.py index 5aea9c469..29007985c 100644 --- a/netbox/netbox/views/generic/bulk_views.py +++ b/netbox/netbox/views/generic/bulk_views.py @@ -25,6 +25,7 @@ from utilities.htmx import is_htmx from utilities.permissions import get_permission_for_model from utilities.views import GetReturnURLMixin from .base import BaseMultiObjectView +from .utils import get_prerequisite_model __all__ = ( 'BulkComponentCreateView', @@ -143,6 +144,7 @@ class ObjectListView(BaseMultiObjectView): """ model = self.queryset.model content_type = ContentType.objects.get_for_model(model) + requirement = get_prerequisite_model(self.queryset) if self.filterset: self.queryset = self.filterset(request.GET, self.queryset).qs @@ -198,6 +200,8 @@ class ObjectListView(BaseMultiObjectView): 'filter_form': self.filterset_form(request.GET, label_suffix='') if self.filterset_form else None, **self.get_extra_context(request), } + if requirement: + context['required_model'] = requirement return render(request, self.template_name, context) @@ -256,6 +260,17 @@ class BulkCreateView(GetReturnURLMixin, BaseMultiObjectView): form = self.form() model_form = self.model_form(initial=initial) + context = { + 'obj_type': self.model_form._meta.model._meta.verbose_name, + 'form': form, + 'model_form': model_form, + 'return_url': self.get_return_url(request), + **self.get_extra_context(request), + } + + if requirement: + context['required_model'] = requirement + return render(request, self.template_name, { 'obj_type': self.model_form._meta.model._meta.verbose_name, 'form': form, diff --git a/netbox/netbox/views/generic/object_views.py b/netbox/netbox/views/generic/object_views.py index 88e078ae3..878f293a0 100644 --- a/netbox/netbox/views/generic/object_views.py +++ b/netbox/netbox/views/generic/object_views.py @@ -20,6 +20,7 @@ from utilities.permissions import get_permission_for_model from utilities.utils import get_viewname, normalize_querydict, prepare_cloned_fields from utilities.views import GetReturnURLMixin from .base import BaseObjectView +from .utils import get_prerequisite_model __all__ = ( 'ComponentCreateView', @@ -342,12 +343,19 @@ class ObjectEditView(GetReturnURLMixin, BaseObjectView): form = self.form(instance=obj, initial=initial_data) restrict_form_fields(form, request.user) - return render(request, self.template_name, { + context = { 'object': obj, 'form': form, 'return_url': self.get_return_url(request, obj), **self.get_extra_context(request, obj), - }) + } + + requirement = get_prerequisite_model(self.queryset) + if requirement: + context['required_model'] = requirement + context['model'] = self.queryset.model + + return render(request, self.template_name, context) def post(self, request, *args, **kwargs): """ diff --git a/netbox/netbox/views/generic/utils.py b/netbox/netbox/views/generic/utils.py new file mode 100644 index 000000000..57c7b5eba --- /dev/null +++ b/netbox/netbox/views/generic/utils.py @@ -0,0 +1,12 @@ +def get_prerequisite_model(queryset): + requirement = None + model = queryset.model + + if not queryset.count(): + prerequisites = model.get_prerequisite_models() + if prerequisites: + for prereq in prerequisites: + if not prereq.objects.count(): + requirement = prereq + + return requirement diff --git a/netbox/templates/generic/object_edit.html b/netbox/templates/generic/object_edit.html index 892c7d2b1..73e9727bb 100644 --- a/netbox/templates/generic/object_edit.html +++ b/netbox/templates/generic/object_edit.html @@ -40,6 +40,10 @@ Context: {% endif %} + {% if required_model %} + {% include 'inc/missing_prerequisites.html' %} + {% endif %} +
{% csrf_token %} diff --git a/netbox/templates/generic/object_list.html b/netbox/templates/generic/object_list.html index 1e2ae796f..6910aa116 100644 --- a/netbox/templates/generic/object_list.html +++ b/netbox/templates/generic/object_list.html @@ -100,6 +100,12 @@ Context: {# Object table #} + + {% if required_model %} + {% include 'inc/missing_prerequisites.html' %} + {% endif %} + +
{% include 'htmx/table.html' %} diff --git a/netbox/templates/inc/missing_prerequisites.html b/netbox/templates/inc/missing_prerequisites.html new file mode 100644 index 000000000..c12b157d0 --- /dev/null +++ b/netbox/templates/inc/missing_prerequisites.html @@ -0,0 +1,5 @@ +{% load buttons %} + + From c811eb069d1d83c8735890199800eb3b2945db90 Mon Sep 17 00:00:00 2001 From: atownson <52260120+atownson@users.noreply.github.com> Date: Thu, 18 Aug 2022 16:05:29 -0500 Subject: [PATCH 02/41] netbox-community#10055 - Add loop for NAT Outside --- netbox/templates/dcim/device.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/netbox/templates/dcim/device.html b/netbox/templates/dcim/device.html index 8286f2c61..217362311 100644 --- a/netbox/templates/dcim/device.html +++ b/netbox/templates/dcim/device.html @@ -179,8 +179,8 @@ {{ object.primary_ip4.address.ip }} {% if object.primary_ip4.nat_inside %} (NAT for {{ object.primary_ip4.nat_inside.address.ip|linkify }}) - {% elif object.primary_ip4.nat_outside %} - (NAT: {{ object.primary_ip4.nat_outside.address.ip|linkify }}) + {% elif object.primary_ip4.nat_outside.count > 0 %} + (NAT for {% for nat in object.primary_ip4.nat_outside.all %}{{ nat.address.ip }}{% if not forloop.last %}, {% endif %}{% endfor %}) {% endif %} {% else %} {{ ''|placeholder }} @@ -194,8 +194,8 @@ {{ object.primary_ip6.address.ip }} {% if object.primary_ip6.nat_inside %} (NAT for {{ object.primary_ip6.nat_inside.address.ip|linkify }}) - {% elif object.primary_ip6.nat_outside %} - (NAT: {{ object.primary_ip6.nat_outside.address.ip|linkify }}) + {% elif object.primary_ip6.nat_outside.count > 0 %} + (NAT for {% for nat in object.primary_ip6.nat_outside.all %}{{ nat.address.ip }}{% if not forloop.last %}, {% endif %}{% endfor %}) {% endif %} {% else %} {{ ''|placeholder }} From a687aa1de6fe702d496a5d25435bd38af4340ddb Mon Sep 17 00:00:00 2001 From: atownson <52260120+atownson@users.noreply.github.com> Date: Thu, 18 Aug 2022 16:09:36 -0500 Subject: [PATCH 03/41] netbox-community#10055 - Add loop for NAT Outside --- netbox/templates/virtualization/virtualmachine.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/netbox/templates/virtualization/virtualmachine.html b/netbox/templates/virtualization/virtualmachine.html index f62da6fed..3826e0cf2 100644 --- a/netbox/templates/virtualization/virtualmachine.html +++ b/netbox/templates/virtualization/virtualmachine.html @@ -45,8 +45,8 @@ {{ 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 %} - (NAT: {{ object.primary_ip4.nat_outside.address.ip }}) + {% elif object.primary_ip4.nat_outside.count > 0 %} + (NAT for {% for nat in object.primary_ip4.nat_outside.all %}{{ nat.address.ip }}{% if not forloop.last %}, {% endif %}{% endfor %}) {% endif %} {% else %} {{ ''|placeholder }} @@ -60,8 +60,8 @@ {{ 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 %} - (NAT: {{ object.primary_ip6.nat_outside.address.ip }}) + {% elif object.primary_ip4.nat_outside.count > 0 %} + (NAT for {% for nat in object.primary_ip4.nat_outside.all %}{{ nat.address.ip }}{% if not forloop.last %}, {% endif %}{% endfor %}) {% endif %} {% else %} {{ ''|placeholder }} From 3f40e15ed5612b0567e0987b43e7e0f93447df7d Mon Sep 17 00:00:00 2001 From: atownson <52260120+atownson@users.noreply.github.com> Date: Thu, 18 Aug 2022 16:18:29 -0500 Subject: [PATCH 04/41] netbox-community#10055 - Add template for NAT Outside Fixes 'ipam.IPAddress.None' text --- netbox/ipam/tables/ip.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/netbox/ipam/tables/ip.py b/netbox/ipam/tables/ip.py index 20e63fe55..493488dac 100644 --- a/netbox/ipam/tables/ip.py +++ b/netbox/ipam/tables/ip.py @@ -56,6 +56,12 @@ VRF_LINK = """ {% endif %} """ +NAT_OUTSIDE_LINK = """ +{% if record.nat_outside.count > 0 %} + {% for nat in record.nat_outside.all %}{{ nat.address.ip }}{% if not forloop.last %}, {% endif %}{% endfor %} +{% endif %} +""" + # # RIRs @@ -360,8 +366,8 @@ class IPAddressTable(TenancyColumnsMixin, NetBoxTable): orderable=False, verbose_name='NAT (Inside)' ) - nat_outside = tables.Column( - linkify=True, + nat_outside = tables.TemplateColumn( + template_code=NAT_OUTSIDE_LINK, orderable=False, verbose_name='NAT (Outside)' ) From 0bdee1d6d8a352c2142b666ef53df6d738d0115b Mon Sep 17 00:00:00 2001 From: atownson <52260120+atownson@users.noreply.github.com> Date: Thu, 18 Aug 2022 16:22:22 -0500 Subject: [PATCH 05/41] netbox-community#10055 - Align NAT Outside with NAT Inside --- netbox/templates/ipam/ipaddress.html | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/netbox/templates/ipam/ipaddress.html b/netbox/templates/ipam/ipaddress.html index 8b628c2f7..ba0f0c5e6 100644 --- a/netbox/templates/ipam/ipaddress.html +++ b/netbox/templates/ipam/ipaddress.html @@ -91,10 +91,13 @@ - Outside NAT IPs + NAT (Outside) {% for ip in object.nat_outside.all %} - {{ ip|linkify }}
+ {{ ip|linkify }} + {% if ip.assigned_object %} + ({{ ip.assigned_object.parent_object|linkify }}) + {% endif %}
{% empty %} {{ ''|placeholder }} {% endfor %} From 928dff6b6879bf56c8953e0b137bbdaaa0a065e8 Mon Sep 17 00:00:00 2001 From: Arthur Date: Thu, 18 Aug 2022 15:11:03 -0700 Subject: [PATCH 06/41] #6454 add prerequisite alert --- netbox/circuits/models/circuits.py | 5 +++++ netbox/dcim/models/devices.py | 14 ++++++++++++++ netbox/dcim/models/power.py | 9 +++++++++ netbox/dcim/models/racks.py | 4 ++++ netbox/dcim/models/sites.py | 4 ++++ netbox/ipam/models/ip.py | 8 ++++++++ netbox/netbox/models/__init__.py | 8 ++++++++ netbox/netbox/views/generic/utils.py | 14 +++++++------- netbox/templates/inc/missing_prerequisites.html | 3 ++- netbox/virtualization/models.py | 8 ++++++++ netbox/wireless/models.py | 5 +++++ 11 files changed, 74 insertions(+), 8 deletions(-) diff --git a/netbox/circuits/models/circuits.py b/netbox/circuits/models/circuits.py index 02ba5209d..c14e365e1 100644 --- a/netbox/circuits/models/circuits.py +++ b/netbox/circuits/models/circuits.py @@ -1,3 +1,4 @@ +from django.apps import apps from django.contrib.contenttypes.fields import GenericRelation from django.core.exceptions import ValidationError from django.db import models @@ -129,6 +130,10 @@ class Circuit(NetBoxModel): def __str__(self): return self.cid + @classmethod + def get_prerequisite_models(cls): + return [apps.get_model('circuits.Provider'), CircuitType] + def get_absolute_url(self): return reverse('circuits:circuit', args=[self.pk]) diff --git a/netbox/dcim/models/devices.py b/netbox/dcim/models/devices.py index 91227f1cf..8d524dcb1 100644 --- a/netbox/dcim/models/devices.py +++ b/netbox/dcim/models/devices.py @@ -1,4 +1,6 @@ import yaml + +from django.apps import apps from django.contrib.contenttypes.fields import GenericRelation from django.core.exceptions import ValidationError from django.core.validators import MaxValueValidator, MinValueValidator @@ -155,6 +157,10 @@ class DeviceType(NetBoxModel): self._original_front_image = self.front_image self._original_rear_image = self.rear_image + @classmethod + def get_prerequisite_models(cls): + return [Manufacturer, ] + def get_absolute_url(self): return reverse('dcim:devicetype', args=[self.pk]) @@ -328,6 +334,10 @@ class ModuleType(NetBoxModel): def __str__(self): return self.model + @classmethod + def get_prerequisite_models(cls): + return [Manufacturer, ] + def get_absolute_url(self): return reverse('dcim:moduletype', args=[self.pk]) @@ -645,6 +655,10 @@ class Device(NetBoxModel, ConfigContextModel): return f'{self.device_type.manufacturer} {self.device_type.model} ({self.pk})' return super().__str__() + @classmethod + def get_prerequisite_models(cls): + return [apps.get_model('dcim.Site'), DeviceRole, DeviceType, ] + def get_absolute_url(self): return reverse('dcim:device', args=[self.pk]) diff --git a/netbox/dcim/models/power.py b/netbox/dcim/models/power.py index 5978d86bd..5e355ce42 100644 --- a/netbox/dcim/models/power.py +++ b/netbox/dcim/models/power.py @@ -1,3 +1,4 @@ +from django.apps import apps from django.contrib.contenttypes.fields import GenericRelation from django.core.exceptions import ValidationError from django.core.validators import MaxValueValidator, MinValueValidator @@ -54,6 +55,10 @@ class PowerPanel(NetBoxModel): def __str__(self): return self.name + @classmethod + def get_prerequisite_models(cls): + return [apps.get_model('dcim.Site'), ] + def get_absolute_url(self): return reverse('dcim:powerpanel', args=[self.pk]) @@ -138,6 +143,10 @@ class PowerFeed(NetBoxModel, PathEndpoint, LinkTermination): def __str__(self): return self.name + @classmethod + def get_prerequisite_models(cls): + return [PowerPanel, ] + def get_absolute_url(self): return reverse('dcim:powerfeed', args=[self.pk]) diff --git a/netbox/dcim/models/racks.py b/netbox/dcim/models/racks.py index e57934353..d0600e987 100644 --- a/netbox/dcim/models/racks.py +++ b/netbox/dcim/models/racks.py @@ -473,6 +473,10 @@ class RackReservation(NetBoxModel): def __str__(self): return "Reservation for rack {}".format(self.rack) + @classmethod + def get_prerequisite_models(cls): + return [apps.get_model('dcim.Site'), Rack, ] + def get_absolute_url(self): return reverse('dcim:rackreservation', args=[self.pk]) diff --git a/netbox/dcim/models/sites.py b/netbox/dcim/models/sites.py index d02bd0932..70b4e6421 100644 --- a/netbox/dcim/models/sites.py +++ b/netbox/dcim/models/sites.py @@ -406,6 +406,10 @@ class Location(NestedGroupModel): super().validate_unique(exclude=exclude) + @classmethod + def get_prerequisite_models(cls): + return [Site, ] + def get_absolute_url(self): return reverse('dcim:location', args=[self.pk]) diff --git a/netbox/ipam/models/ip.py b/netbox/ipam/models/ip.py index d1538953a..ec054339c 100644 --- a/netbox/ipam/models/ip.py +++ b/netbox/ipam/models/ip.py @@ -124,6 +124,10 @@ class ASN(NetBoxModel): def __str__(self): return f'AS{self.asn_with_asdot}' + @classmethod + def get_prerequisite_models(cls): + return [RIR, ] + def get_absolute_url(self): return reverse('ipam:asn', args=[self.pk]) @@ -185,6 +189,10 @@ class Aggregate(GetAvailablePrefixesMixin, NetBoxModel): def __str__(self): return str(self.prefix) + @classmethod + def get_prerequisite_models(cls): + return [RIR, ] + def get_absolute_url(self): return reverse('ipam:aggregate', args=[self.pk]) diff --git a/netbox/netbox/models/__init__.py b/netbox/netbox/models/__init__.py index 2524c7c9b..675103d06 100644 --- a/netbox/netbox/models/__init__.py +++ b/netbox/netbox/models/__init__.py @@ -89,6 +89,10 @@ class NestedGroupModel(NetBoxFeatureSet, MPTTModel): def __str__(self): return self.name + @classmethod + def get_prerequisite_models(cls): + return [] + def clean(self): super().clean() @@ -126,3 +130,7 @@ class OrganizationalModel(NetBoxFeatureSet, models.Model): class Meta: abstract = True ordering = ('name',) + + @classmethod + def get_prerequisite_models(cls): + return [] diff --git a/netbox/netbox/views/generic/utils.py b/netbox/netbox/views/generic/utils.py index 57c7b5eba..c682181a1 100644 --- a/netbox/netbox/views/generic/utils.py +++ b/netbox/netbox/views/generic/utils.py @@ -1,12 +1,12 @@ def get_prerequisite_model(queryset): - requirement = None model = queryset.model if not queryset.count(): - prerequisites = model.get_prerequisite_models() - if prerequisites: - for prereq in prerequisites: - if not prereq.objects.count(): - requirement = prereq + if hasattr(model, 'get_prerequisite_models'): + prerequisites = model.get_prerequisite_models() + if prerequisites: + for prereq in prerequisites: + if not prereq.objects.count(): + return prereq - return requirement + return None diff --git a/netbox/templates/inc/missing_prerequisites.html b/netbox/templates/inc/missing_prerequisites.html index c12b157d0..04043fc9c 100644 --- a/netbox/templates/inc/missing_prerequisites.html +++ b/netbox/templates/inc/missing_prerequisites.html @@ -1,5 +1,6 @@ {% load buttons %} diff --git a/netbox/virtualization/models.py b/netbox/virtualization/models.py index 586bb8a9e..b7151a1f0 100644 --- a/netbox/virtualization/models.py +++ b/netbox/virtualization/models.py @@ -162,6 +162,10 @@ class Cluster(NetBoxModel): def __str__(self): return self.name + @classmethod + def get_prerequisite_models(cls): + return [ClusterType, ] + def get_absolute_url(self): return reverse('virtualization:cluster', args=[self.pk]) @@ -288,6 +292,10 @@ class VirtualMachine(NetBoxModel, ConfigContextModel): def __str__(self): return self.name + @classmethod + def get_prerequisite_models(cls): + return [Cluster, ] + def get_absolute_url(self): return reverse('virtualization:virtualmachine', args=[self.pk]) diff --git a/netbox/wireless/models.py b/netbox/wireless/models.py index 0543e5621..f9838c0c7 100644 --- a/netbox/wireless/models.py +++ b/netbox/wireless/models.py @@ -1,3 +1,4 @@ +from django.apps import apps from django.core.exceptions import ValidationError from django.db import models from django.urls import reverse @@ -174,6 +175,10 @@ class WirelessLink(WirelessAuthenticationBase, NetBoxModel): def __str__(self): return f'#{self.pk}' + @classmethod + def get_prerequisite_models(cls): + return [apps.get_model('dcim.Interface'), ] + def get_absolute_url(self): return reverse('wireless:wirelesslink', args=[self.pk]) From 43ad8e80b9bfab3eda91b0a771e4247726f54209 Mon Sep 17 00:00:00 2001 From: atownson <52260120+atownson@users.noreply.github.com> Date: Thu, 18 Aug 2022 17:12:44 -0500 Subject: [PATCH 07/41] netbox-community#10055: Added empty text --- netbox/ipam/tables/ip.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/netbox/ipam/tables/ip.py b/netbox/ipam/tables/ip.py index 493488dac..52b1c4393 100644 --- a/netbox/ipam/tables/ip.py +++ b/netbox/ipam/tables/ip.py @@ -59,6 +59,8 @@ VRF_LINK = """ NAT_OUTSIDE_LINK = """ {% if record.nat_outside.count > 0 %} {% for nat in record.nat_outside.all %}{{ nat.address.ip }}{% if not forloop.last %}, {% endif %}{% endfor %} +{% else %} + — {% endif %} """ From c65a29169878ef44e131a7601578781c595c8cf3 Mon Sep 17 00:00:00 2001 From: Arthur Date: Thu, 18 Aug 2022 16:00:17 -0700 Subject: [PATCH 08/41] #6454 add L2VPN check --- netbox/ipam/models/l2vpn.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/netbox/ipam/models/l2vpn.py b/netbox/ipam/models/l2vpn.py index 0e948b18e..809007033 100644 --- a/netbox/ipam/models/l2vpn.py +++ b/netbox/ipam/models/l2vpn.py @@ -1,3 +1,4 @@ +from django.apps import apps from django.contrib.contenttypes.fields import GenericRelation, GenericForeignKey from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ValidationError @@ -103,6 +104,10 @@ class L2VPNTermination(NetBoxModel): return f'{self.assigned_object} <> {self.l2vpn}' return super().__str__() + @classmethod + def get_prerequisite_models(cls): + return [apps.get_model('ipam.L2VPN'), ] + def get_absolute_url(self): return reverse('ipam:l2vpntermination', args=[self.pk]) From 0a38c16cc2ee136705ac2841d45a66203ba7dc99 Mon Sep 17 00:00:00 2001 From: Arthur Date: Thu, 18 Aug 2022 16:06:57 -0700 Subject: [PATCH 09/41] Fix for #10056 --- netbox/templates/dcim/interface.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/templates/dcim/interface.html b/netbox/templates/dcim/interface.html index 7503e1be2..1216f3e88 100644 --- a/netbox/templates/dcim/interface.html +++ b/netbox/templates/dcim/interface.html @@ -74,7 +74,7 @@ {{ object.get_poe_mode_display|placeholder }} - PoE Mode + PoE Type {{ object.get_poe_type_display|placeholder }} From 3a7ea62874d0e6251dfe40358b653e9fb0cea6d8 Mon Sep 17 00:00:00 2001 From: Arthur Date: Thu, 18 Aug 2022 16:20:24 -0700 Subject: [PATCH 10/41] fix for #10057 --- netbox/netbox/search.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/netbox/search.py b/netbox/netbox/search.py index ef0c4fd87..32a9cfb1d 100644 --- a/netbox/netbox/search.py +++ b/netbox/netbox/search.py @@ -62,7 +62,7 @@ DCIM_TYPES = { 'url': 'dcim:rack_list', }, 'rackreservation': { - 'queryset': RackReservation.objects.prefetch_related('site', 'rack', 'user'), + 'queryset': RackReservation.objects.prefetch_related('rack', 'user'), 'filterset': dcim.filtersets.RackReservationFilterSet, 'table': dcim.tables.RackReservationTable, 'url': 'dcim:rackreservation_list', From c14a5973c7c1f3a1508a4f62ae4b21a918333680 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 22 Aug 2022 11:14:36 -0400 Subject: [PATCH 11/41] Fixes #10089: linkify template filter should escape object representation --- docs/release-notes/version-3.3.md | 1 + netbox/utilities/templatetags/builtins/filters.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/release-notes/version-3.3.md b/docs/release-notes/version-3.3.md index 382d6c29e..0d61e43be 100644 --- a/docs/release-notes/version-3.3.md +++ b/docs/release-notes/version-3.3.md @@ -10,6 +10,7 @@ * [#10040](https://github.com/netbox-community/netbox/issues/10040) - Fix exception when ordering prefixes by flat representation * [#10053](https://github.com/netbox-community/netbox/issues/10053) - Custom fields header should not be displayed when editing circuit terminations with no custom fields +* [#10089](https://github.com/netbox-community/netbox/issues/10089) - `linkify` template filter should escape object representation --- diff --git a/netbox/utilities/templatetags/builtins/filters.py b/netbox/utilities/templatetags/builtins/filters.py index bc395e438..6b548a89d 100644 --- a/netbox/utilities/templatetags/builtins/filters.py +++ b/netbox/utilities/templatetags/builtins/filters.py @@ -5,7 +5,7 @@ import re import yaml from django import template from django.contrib.contenttypes.models import ContentType -from django.utils.html import strip_tags +from django.utils.html import escape from django.utils.safestring import mark_safe from markdown import markdown @@ -35,7 +35,7 @@ def linkify(instance, attr=None): text = getattr(instance, attr) if attr is not None else str(instance) try: url = instance.get_absolute_url() - return mark_safe(f'{text}') + return mark_safe(f'{escape(text)}') except (AttributeError, TypeError): return text From 2ef9e2d6fce664821e276b6d3ef0c18d362471de Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 22 Aug 2022 11:17:40 -0400 Subject: [PATCH 12/41] Closes #10066: Use fixed column widths for custom field values in UI --- docs/release-notes/version-3.3.md | 1 + netbox/templates/inc/panels/custom_fields.html | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/release-notes/version-3.3.md b/docs/release-notes/version-3.3.md index 0d61e43be..2651948a5 100644 --- a/docs/release-notes/version-3.3.md +++ b/docs/release-notes/version-3.3.md @@ -5,6 +5,7 @@ ### Enhancements * [#10061](https://github.com/netbox-community/netbox/issues/10061) - Replicate type when cloning L2VPN instances +* [#10066](https://github.com/netbox-community/netbox/issues/10066) - Use fixed column widths for custom field values in UI ### Bug Fixes diff --git a/netbox/templates/inc/panels/custom_fields.html b/netbox/templates/inc/panels/custom_fields.html index 616b1c712..45843eea5 100644 --- a/netbox/templates/inc/panels/custom_fields.html +++ b/netbox/templates/inc/panels/custom_fields.html @@ -12,9 +12,9 @@ {% for field, value in fields.items %} - From 917439725ad3cb6195159ca51266ce67a8175c72 Mon Sep 17 00:00:00 2001 From: Arthur Date: Mon, 22 Aug 2022 09:08:16 -0700 Subject: [PATCH 13/41] fix for #10059 - add identifier to L2VPN table --- netbox/ipam/tables/l2vpn.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/ipam/tables/l2vpn.py b/netbox/ipam/tables/l2vpn.py index 168c8ca89..077c6eb77 100644 --- a/netbox/ipam/tables/l2vpn.py +++ b/netbox/ipam/tables/l2vpn.py @@ -34,7 +34,7 @@ class L2VPNTable(TenancyColumnsMixin, NetBoxTable): model = L2VPN fields = ( 'pk', 'name', 'slug', 'type', 'description', 'import_targets', 'export_targets', 'tenant', 'tenant_group', - 'actions', + 'actions', 'identifier', ) default_columns = ('pk', 'name', 'type', 'description', 'actions') From ea1467add7ce308fc3203468cf0262e5cd75b2e0 Mon Sep 17 00:00:00 2001 From: Arthur Date: Mon, 22 Aug 2022 09:24:52 -0700 Subject: [PATCH 14/41] fix for #10086 - change capitalization on wireless link table for Interface A, B and Auth Type --- netbox/wireless/models.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/netbox/wireless/models.py b/netbox/wireless/models.py index 0540e9c45..fe3cdc1e2 100644 --- a/netbox/wireless/models.py +++ b/netbox/wireless/models.py @@ -23,7 +23,8 @@ class WirelessAuthenticationBase(models.Model): auth_type = models.CharField( max_length=50, choices=WirelessAuthTypeChoices, - blank=True + blank=True, + verbose_name="Auth Type", ) auth_cipher = models.CharField( max_length=50, @@ -134,13 +135,15 @@ class WirelessLink(WirelessAuthenticationBase, NetBoxModel): to='dcim.Interface', limit_choices_to={'type__in': WIRELESS_IFACE_TYPES}, on_delete=models.PROTECT, - related_name='+' + related_name='+', + verbose_name="Interface A", ) interface_b = models.ForeignKey( to='dcim.Interface', limit_choices_to={'type__in': WIRELESS_IFACE_TYPES}, on_delete=models.PROTECT, - related_name='+' + related_name='+', + verbose_name="Interface B", ) ssid = models.CharField( max_length=SSID_MAX_LENGTH, From e8f62eb1f97050c4cd310042307f7b64e686527e Mon Sep 17 00:00:00 2001 From: Arthur Date: Mon, 22 Aug 2022 11:17:01 -0700 Subject: [PATCH 15/41] #10059 change ordering of identifier column --- netbox/ipam/tables/l2vpn.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/netbox/ipam/tables/l2vpn.py b/netbox/ipam/tables/l2vpn.py index 077c6eb77..4a6af7c9b 100644 --- a/netbox/ipam/tables/l2vpn.py +++ b/netbox/ipam/tables/l2vpn.py @@ -33,10 +33,10 @@ class L2VPNTable(TenancyColumnsMixin, NetBoxTable): class Meta(NetBoxTable.Meta): model = L2VPN fields = ( - 'pk', 'name', 'slug', 'type', 'description', 'import_targets', 'export_targets', 'tenant', 'tenant_group', - 'actions', 'identifier', + 'pk', 'name', 'slug', 'identifier', 'type', 'description', 'import_targets', 'export_targets', 'tenant', 'tenant_group', + 'actions', ) - default_columns = ('pk', 'name', 'type', 'description', 'actions') + default_columns = ('pk', 'name', 'identifier', 'type', 'description', 'actions') class L2VPNTerminationTable(NetBoxTable): From a972174706c2b4e857ca7019b4524c7216520fac Mon Sep 17 00:00:00 2001 From: Arthur Date: Mon, 22 Aug 2022 11:46:41 -0700 Subject: [PATCH 16/41] #6454 changes from PR review --- netbox/dcim/views.py | 1 - netbox/netbox/models/__init__.py | 20 ++++++++----------- netbox/netbox/views/generic/bulk_views.py | 15 +------------- netbox/netbox/views/generic/object_views.py | 15 ++++++-------- netbox/netbox/views/generic/utils.py | 4 ++-- netbox/templates/generic/object_edit.html | 2 +- netbox/templates/generic/object_list.html | 2 +- .../templates/inc/missing_prerequisites.html | 4 ++-- 8 files changed, 21 insertions(+), 42 deletions(-) diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index c807176d4..a31eabc5e 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -570,7 +570,6 @@ class RackRoleBulkDeleteView(generic.BulkDeleteView): # class RackListView(generic.ObjectListView): - required_prerequisites = [Site] queryset = Rack.objects.annotate( device_count=count_related(Device, 'rack') ) diff --git a/netbox/netbox/models/__init__.py b/netbox/netbox/models/__init__.py index c36d36f46..4c65094ca 100644 --- a/netbox/netbox/models/__init__.py +++ b/netbox/netbox/models/__init__.py @@ -28,6 +28,14 @@ class NetBoxFeatureSet( class Meta: abstract = True + @classmethod + def get_prerequisite_models(cls): + """ + Return a list of model types that are required to create this model or empty list if none. This is used for + showing prequisite warnings in the UI on the list and detail views. + """ + return [] + # # Base model classes @@ -53,10 +61,6 @@ class NetBoxModel(NetBoxFeatureSet, models.Model): class Meta: abstract = True - @classmethod - def get_prerequisite_models(cls): - return [] - def clone(self): """ Return a dictionary of attributes suitable for creating a copy of the current instance. This is used for pre- @@ -109,10 +113,6 @@ class NestedGroupModel(NetBoxFeatureSet, MPTTModel): def __str__(self): return self.name - @classmethod - def get_prerequisite_models(cls): - return [] - def clean(self): super().clean() @@ -150,7 +150,3 @@ class OrganizationalModel(NetBoxFeatureSet, models.Model): class Meta: abstract = True ordering = ('name',) - - @classmethod - def get_prerequisite_models(cls): - return [] diff --git a/netbox/netbox/views/generic/bulk_views.py b/netbox/netbox/views/generic/bulk_views.py index d30d67468..8fe0ad518 100644 --- a/netbox/netbox/views/generic/bulk_views.py +++ b/netbox/netbox/views/generic/bulk_views.py @@ -172,12 +172,10 @@ class ObjectListView(BaseMultiObjectView, ActionsMixin, TableMixin): 'table': table, 'actions': actions, 'filter_form': self.filterset_form(request.GET, label_suffix='') if self.filterset_form else None, + 'prerequisite_model': requirement if requirement else None, **self.get_extra_context(request), } - if requirement: - context['required_model'] = requirement - return render(request, self.template_name, context) @@ -235,17 +233,6 @@ class BulkCreateView(GetReturnURLMixin, BaseMultiObjectView): form = self.form() model_form = self.model_form(initial=initial) - context = { - 'obj_type': self.model_form._meta.model._meta.verbose_name, - 'form': form, - 'model_form': model_form, - 'return_url': self.get_return_url(request), - **self.get_extra_context(request), - } - - if requirement: - context['required_model'] = requirement - return render(request, self.template_name, { 'obj_type': self.model_form._meta.model._meta.verbose_name, 'form': form, diff --git a/netbox/netbox/views/generic/object_views.py b/netbox/netbox/views/generic/object_views.py index 7c63b2ec6..c9ff738d7 100644 --- a/netbox/netbox/views/generic/object_views.py +++ b/netbox/netbox/views/generic/object_views.py @@ -341,24 +341,21 @@ class ObjectEditView(GetReturnURLMixin, BaseObjectView): """ obj = self.get_object(**kwargs) obj = self.alter_object(obj, request, args, kwargs) + model = self.queryset.model initial_data = normalize_querydict(request.GET) form = self.form(instance=obj, initial=initial_data) restrict_form_fields(form, request.user) - context = { + requirement = get_prerequisite_model(self.queryset) + return render(request, self.template_name, { + 'model': model, 'object': obj, 'form': form, 'return_url': self.get_return_url(request, obj), + 'prerequisite_model': requirement if requirement else None, **self.get_extra_context(request, obj), - } - - requirement = get_prerequisite_model(self.queryset) - if requirement: - context['required_model'] = requirement - context['model'] = self.queryset.model - - return render(request, self.template_name, context) + }) def post(self, request, *args, **kwargs): """ diff --git a/netbox/netbox/views/generic/utils.py b/netbox/netbox/views/generic/utils.py index c682181a1..61c6dc242 100644 --- a/netbox/netbox/views/generic/utils.py +++ b/netbox/netbox/views/generic/utils.py @@ -1,12 +1,12 @@ def get_prerequisite_model(queryset): model = queryset.model - if not queryset.count(): + if not queryset.exists(): if hasattr(model, 'get_prerequisite_models'): prerequisites = model.get_prerequisite_models() if prerequisites: for prereq in prerequisites: - if not prereq.objects.count(): + if not prereq.objects.exists(): return prereq return None diff --git a/netbox/templates/generic/object_edit.html b/netbox/templates/generic/object_edit.html index 73e9727bb..8047dc59d 100644 --- a/netbox/templates/generic/object_edit.html +++ b/netbox/templates/generic/object_edit.html @@ -40,7 +40,7 @@ Context: {% endif %} - {% if required_model %} + {% if prerequisite_model %} {% include 'inc/missing_prerequisites.html' %} {% endif %} diff --git a/netbox/templates/generic/object_list.html b/netbox/templates/generic/object_list.html index 6910aa116..9d3952a28 100644 --- a/netbox/templates/generic/object_list.html +++ b/netbox/templates/generic/object_list.html @@ -101,7 +101,7 @@ Context: {# Object table #} - {% if required_model %} + {% if prerequisite_model %} {% include 'inc/missing_prerequisites.html' %} {% endif %} diff --git a/netbox/templates/inc/missing_prerequisites.html b/netbox/templates/inc/missing_prerequisites.html index 04043fc9c..5814b72eb 100644 --- a/netbox/templates/inc/missing_prerequisites.html +++ b/netbox/templates/inc/missing_prerequisites.html @@ -1,6 +1,6 @@ {% load buttons %} From 25ec624e4e97026088950e213bc5ec6dcd1d0bff Mon Sep 17 00:00:00 2001 From: Arthur Date: Mon, 22 Aug 2022 12:59:26 -0700 Subject: [PATCH 17/41] #6454 suggested review changes --- netbox/netbox/views/generic/bulk_views.py | 3 +-- netbox/netbox/views/generic/object_views.py | 3 +-- netbox/templates/generic/object_list.html | 1 - 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/netbox/netbox/views/generic/bulk_views.py b/netbox/netbox/views/generic/bulk_views.py index 8fe0ad518..7340ea2a0 100644 --- a/netbox/netbox/views/generic/bulk_views.py +++ b/netbox/netbox/views/generic/bulk_views.py @@ -124,7 +124,6 @@ class ObjectListView(BaseMultiObjectView, ActionsMixin, TableMixin): """ model = self.queryset.model content_type = ContentType.objects.get_for_model(model) - requirement = get_prerequisite_model(self.queryset) if self.filterset: self.queryset = self.filterset(request.GET, self.queryset).qs @@ -172,7 +171,7 @@ class ObjectListView(BaseMultiObjectView, ActionsMixin, TableMixin): 'table': table, 'actions': actions, 'filter_form': self.filterset_form(request.GET, label_suffix='') if self.filterset_form else None, - 'prerequisite_model': requirement if requirement else None, + 'prerequisite_model': get_prerequisite_model(self.queryset), **self.get_extra_context(request), } diff --git a/netbox/netbox/views/generic/object_views.py b/netbox/netbox/views/generic/object_views.py index c9ff738d7..19401f79a 100644 --- a/netbox/netbox/views/generic/object_views.py +++ b/netbox/netbox/views/generic/object_views.py @@ -347,13 +347,12 @@ class ObjectEditView(GetReturnURLMixin, BaseObjectView): form = self.form(instance=obj, initial=initial_data) restrict_form_fields(form, request.user) - requirement = get_prerequisite_model(self.queryset) return render(request, self.template_name, { 'model': model, 'object': obj, 'form': form, 'return_url': self.get_return_url(request, obj), - 'prerequisite_model': requirement if requirement else None, + 'prerequisite_model': get_prerequisite_model(self.queryset), **self.get_extra_context(request, obj), }) diff --git a/netbox/templates/generic/object_list.html b/netbox/templates/generic/object_list.html index 9d3952a28..60eba6097 100644 --- a/netbox/templates/generic/object_list.html +++ b/netbox/templates/generic/object_list.html @@ -105,7 +105,6 @@ Context: {% include 'inc/missing_prerequisites.html' %} {% endif %} -
{% include 'htmx/table.html' %} From 069c2d2fd2f3c2a81b882c12cc62a55ed857b33b Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 22 Aug 2022 16:11:35 -0400 Subject: [PATCH 18/41] Changelog for #6454, #10057, #10059 --- docs/release-notes/version-3.3.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/release-notes/version-3.3.md b/docs/release-notes/version-3.3.md index 2651948a5..75eb7208d 100644 --- a/docs/release-notes/version-3.3.md +++ b/docs/release-notes/version-3.3.md @@ -4,6 +4,7 @@ ### Enhancements +* [#6454](https://github.com/netbox-community/netbox/issues/6454) - Include contextual help when creating first objects in UI * [#10061](https://github.com/netbox-community/netbox/issues/10061) - Replicate type when cloning L2VPN instances * [#10066](https://github.com/netbox-community/netbox/issues/10066) - Use fixed column widths for custom field values in UI @@ -11,6 +12,8 @@ * [#10040](https://github.com/netbox-community/netbox/issues/10040) - Fix exception when ordering prefixes by flat representation * [#10053](https://github.com/netbox-community/netbox/issues/10053) - Custom fields header should not be displayed when editing circuit terminations with no custom fields +* [#10057](https://github.com/netbox-community/netbox/issues/10057) - Fix AttributeError exception when global search results include rack reservations +* [#10059](https://github.com/netbox-community/netbox/issues/10059) - Add identifier column to L2VPN table * [#10089](https://github.com/netbox-community/netbox/issues/10089) - `linkify` template filter should escape object representation --- From 71bf5f4697ebbd00d2ac87e7972961f5f6289851 Mon Sep 17 00:00:00 2001 From: atownson <52260120+atownson@users.noreply.github.com> Date: Mon, 22 Aug 2022 15:17:35 -0500 Subject: [PATCH 19/41] Updated exists evaluation --- netbox/templates/dcim/device.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/netbox/templates/dcim/device.html b/netbox/templates/dcim/device.html index 217362311..a798a34b0 100644 --- a/netbox/templates/dcim/device.html +++ b/netbox/templates/dcim/device.html @@ -179,7 +179,7 @@ {{ object.primary_ip4.address.ip }} {% if object.primary_ip4.nat_inside %} (NAT for {{ object.primary_ip4.nat_inside.address.ip|linkify }}) - {% elif object.primary_ip4.nat_outside.count > 0 %} + {% elif object.primary_ip4.nat_outside.exists %} (NAT for {% for nat in object.primary_ip4.nat_outside.all %}{{ nat.address.ip }}{% if not forloop.last %}, {% endif %}{% endfor %}) {% endif %} {% else %} @@ -194,7 +194,7 @@ {{ object.primary_ip6.address.ip }} {% if object.primary_ip6.nat_inside %} (NAT for {{ object.primary_ip6.nat_inside.address.ip|linkify }}) - {% elif object.primary_ip6.nat_outside.count > 0 %} + {% elif object.primary_ip6.nat_outside.exists %} (NAT for {% for nat in object.primary_ip6.nat_outside.all %}{{ nat.address.ip }}{% if not forloop.last %}, {% endif %}{% endfor %}) {% endif %} {% else %} From 2bb79e1346bdae419497176619edfe8942df1231 Mon Sep 17 00:00:00 2001 From: atownson <52260120+atownson@users.noreply.github.com> Date: Mon, 22 Aug 2022 15:18:25 -0500 Subject: [PATCH 20/41] Updated exists evaluation --- netbox/templates/virtualization/virtualmachine.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/netbox/templates/virtualization/virtualmachine.html b/netbox/templates/virtualization/virtualmachine.html index 3826e0cf2..8b69374f1 100644 --- a/netbox/templates/virtualization/virtualmachine.html +++ b/netbox/templates/virtualization/virtualmachine.html @@ -45,7 +45,7 @@ {{ 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.count > 0 %} + {% elif object.primary_ip4.nat_outside.exists %} (NAT for {% for nat in object.primary_ip4.nat_outside.all %}{{ nat.address.ip }}{% if not forloop.last %}, {% endif %}{% endfor %}) {% endif %} {% else %} @@ -60,7 +60,7 @@ {{ object.primary_ip6.address.ip }} {% if object.primary_ip6.nat_inside %} (NAT for {{ object.primary_ip6.nat_inside.address.ip }}) - {% elif object.primary_ip4.nat_outside.count > 0 %} + {% elif object.primary_ip4.nat_outside.exists %} (NAT for {% for nat in object.primary_ip4.nat_outside.all %}{{ nat.address.ip }}{% if not forloop.last %}, {% endif %}{% endfor %}) {% endif %} {% else %} From 6179686c81c5a33fe264046214e4b9a005b94a42 Mon Sep 17 00:00:00 2001 From: atownson <52260120+atownson@users.noreply.github.com> Date: Mon, 22 Aug 2022 15:22:53 -0500 Subject: [PATCH 21/41] Corrected IPv6 family --- netbox/templates/virtualization/virtualmachine.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/netbox/templates/virtualization/virtualmachine.html b/netbox/templates/virtualization/virtualmachine.html index 8b69374f1..5756d939a 100644 --- a/netbox/templates/virtualization/virtualmachine.html +++ b/netbox/templates/virtualization/virtualmachine.html @@ -60,8 +60,8 @@ {{ object.primary_ip6.address.ip }} {% if object.primary_ip6.nat_inside %} (NAT for {{ object.primary_ip6.nat_inside.address.ip }}) - {% elif object.primary_ip4.nat_outside.exists %} - (NAT for {% for nat in object.primary_ip4.nat_outside.all %}{{ nat.address.ip }}{% if not forloop.last %}, {% endif %}{% endfor %}) + {% elif object.primary_ip6.nat_outside.exists %} + (NAT for {% for nat in object.primary_ip6.nat_outside.all %}{{ nat.address.ip }}{% if not forloop.last %}, {% endif %}{% endfor %}) {% endif %} {% else %} {{ ''|placeholder }} From 0c7c61b685560cfa886ff10fbb18853a27ad7587 Mon Sep 17 00:00:00 2001 From: Arthur Date: Mon, 22 Aug 2022 14:56:31 -0700 Subject: [PATCH 22/41] #10037 add Child Interface to context menu --- netbox/dcim/tables/template_code.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/netbox/dcim/tables/template_code.py b/netbox/dcim/tables/template_code.py index 3403f9392..62a189b63 100644 --- a/netbox/dcim/tables/template_code.py +++ b/netbox/dcim/tables/template_code.py @@ -238,6 +238,9 @@ INTERFACE_BUTTONS = """ {% if perms.dcim.add_inventoryitem %}
  • Inventory Item
  • {% endif %} + {% if perms.dcim.add_interface %} +
  • Child Interface
  • + {% endif %} {% endif %} From 9fddd193b92b4cb5b035502a9f8d5dc60fadf49d Mon Sep 17 00:00:00 2001 From: Arthur Date: Mon, 22 Aug 2022 16:31:41 -0700 Subject: [PATCH 23/41] #10094 fix Contact AddAnother --- netbox/netbox/views/generic/object_views.py | 4 ++++ netbox/tenancy/views.py | 10 ++++++++++ 2 files changed, 14 insertions(+) diff --git a/netbox/netbox/views/generic/object_views.py b/netbox/netbox/views/generic/object_views.py index 5ff0cfdff..433e70b63 100644 --- a/netbox/netbox/views/generic/object_views.py +++ b/netbox/netbox/views/generic/object_views.py @@ -327,6 +327,9 @@ class ObjectEditView(GetReturnURLMixin, BaseObjectView): """ return obj + def get_extra_addanother_params(self, request, params: dict): + return params + # # Request handlers # @@ -399,6 +402,7 @@ class ObjectEditView(GetReturnURLMixin, BaseObjectView): # If cloning is supported, pre-populate a new instance of the form params = prepare_cloned_fields(obj) + params = self.get_extra_addanother_params(request, params) if params: if 'return_url' in request.GET: params['return_url'] = request.GET.get('return_url') diff --git a/netbox/tenancy/views.py b/netbox/tenancy/views.py index 9a2fe6ab9..8b0f90f88 100644 --- a/netbox/tenancy/views.py +++ b/netbox/tenancy/views.py @@ -1,4 +1,5 @@ from django.contrib.contenttypes.models import ContentType +from django.http import QueryDict from django.shortcuts import get_object_or_404 from circuits.models import Circuit @@ -365,6 +366,15 @@ class ContactAssignmentEditView(generic.ObjectEditView): instance.object = get_object_or_404(content_type.model_class(), pk=request.GET.get('object_id')) return instance + def get_extra_addanother_params(self, request, params: dict): + if not params: + params = QueryDict(mutable=True) + + params['content_type'] = request.GET.get('content_type') + params['object_id'] = request.GET.get('object_id') + + return params + class ContactAssignmentDeleteView(generic.ObjectDeleteView): queryset = ContactAssignment.objects.all() From 41499b189c29bf9c7b07b3d7601637312b13d141 Mon Sep 17 00:00:00 2001 From: Arthur Date: Mon, 22 Aug 2022 16:33:50 -0700 Subject: [PATCH 24/41] #10094 fix Contact AddAnother --- netbox/utilities/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/utilities/utils.py b/netbox/utilities/utils.py index 1dece76c8..69ab615fc 100644 --- a/netbox/utilities/utils.py +++ b/netbox/utilities/utils.py @@ -285,7 +285,7 @@ def prepare_cloned_fields(instance): """ # Generate the clone attributes from the instance if not hasattr(instance, 'clone'): - return QueryDict() + return QueryDict(mutable=True) attrs = instance.clone() # Prepare querydict parameters From f48aaf1c465d0048606f486d9530631a33920a65 Mon Sep 17 00:00:00 2001 From: Arthur Date: Mon, 22 Aug 2022 16:47:40 -0700 Subject: [PATCH 25/41] #10094 fix Contact AddAnother --- netbox/netbox/views/generic/object_views.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/netbox/netbox/views/generic/object_views.py b/netbox/netbox/views/generic/object_views.py index 433e70b63..89f52e475 100644 --- a/netbox/netbox/views/generic/object_views.py +++ b/netbox/netbox/views/generic/object_views.py @@ -327,7 +327,10 @@ class ObjectEditView(GetReturnURLMixin, BaseObjectView): """ return obj - def get_extra_addanother_params(self, request, params: dict): + def get_extra_addanother_params(self, request, params): + """ + Return a QueryDict of extra params to use on the Add Another button. + """ return params # From 63e8faeed931f9ed2d589c5341976fa1b4f82324 Mon Sep 17 00:00:00 2001 From: atownson <52260120+atownson@users.noreply.github.com> Date: Mon, 22 Aug 2022 20:34:44 -0500 Subject: [PATCH 26/41] Changed nat_outside to ManyToManyColumn --- netbox/ipam/tables/ip.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/netbox/ipam/tables/ip.py b/netbox/ipam/tables/ip.py index 52b1c4393..82f4686c0 100644 --- a/netbox/ipam/tables/ip.py +++ b/netbox/ipam/tables/ip.py @@ -56,14 +56,6 @@ VRF_LINK = """ {% endif %} """ -NAT_OUTSIDE_LINK = """ -{% if record.nat_outside.count > 0 %} - {% for nat in record.nat_outside.all %}{{ nat.address.ip }}{% if not forloop.last %}, {% endif %}{% endfor %} -{% else %} - — -{% endif %} -""" - # # RIRs @@ -368,8 +360,8 @@ class IPAddressTable(TenancyColumnsMixin, NetBoxTable): orderable=False, verbose_name='NAT (Inside)' ) - nat_outside = tables.TemplateColumn( - template_code=NAT_OUTSIDE_LINK, + nat_outside = tables.ManyToManyColumn( + linkify_item=True, orderable=False, verbose_name='NAT (Outside)' ) From 984d8b8ee6adc6a6e1b7713a133af312d67b883d Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 23 Aug 2022 09:17:12 -0400 Subject: [PATCH 27/41] Fixes #10108: Linkify inside NAT IPs for primary device IPs in UI --- docs/release-notes/version-3.3.md | 2 ++ netbox/templates/dcim/device.html | 8 +++----- netbox/templates/ipam/ipaddress.html | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/release-notes/version-3.3.md b/docs/release-notes/version-3.3.md index 75eb7208d..33fce0a3c 100644 --- a/docs/release-notes/version-3.3.md +++ b/docs/release-notes/version-3.3.md @@ -12,9 +12,11 @@ * [#10040](https://github.com/netbox-community/netbox/issues/10040) - Fix exception when ordering prefixes by flat representation * [#10053](https://github.com/netbox-community/netbox/issues/10053) - Custom fields header should not be displayed when editing circuit terminations with no custom fields +* [#10055](https://github.com/netbox-community/netbox/issues/10055) - Fix extraneous NAT indicator by device primary IP * [#10057](https://github.com/netbox-community/netbox/issues/10057) - Fix AttributeError exception when global search results include rack reservations * [#10059](https://github.com/netbox-community/netbox/issues/10059) - Add identifier column to L2VPN table * [#10089](https://github.com/netbox-community/netbox/issues/10089) - `linkify` template filter should escape object representation +* [#10108](https://github.com/netbox-community/netbox/issues/10108) - Linkify inside NAT IPs for primary device IPs in UI --- diff --git a/netbox/templates/dcim/device.html b/netbox/templates/dcim/device.html index a798a34b0..2df2407b5 100644 --- a/netbox/templates/dcim/device.html +++ b/netbox/templates/dcim/device.html @@ -155,9 +155,7 @@
    -
    - Management -
    +
    Management
    + {{ field }} - + {% customfield_value field value %}
    @@ -178,7 +176,7 @@ {% if object.primary_ip4 %} {{ object.primary_ip4.address.ip }} {% if object.primary_ip4.nat_inside %} - (NAT for {{ object.primary_ip4.nat_inside.address.ip|linkify }}) + (NAT for {{ object.primary_ip4.nat_inside.address.ip }}) {% elif object.primary_ip4.nat_outside.exists %} (NAT for {% for nat in object.primary_ip4.nat_outside.all %}{{ nat.address.ip }}{% if not forloop.last %}, {% endif %}{% endfor %}) {% endif %} @@ -193,7 +191,7 @@ {% if object.primary_ip6 %} {{ object.primary_ip6.address.ip }} {% if object.primary_ip6.nat_inside %} - (NAT for {{ object.primary_ip6.nat_inside.address.ip|linkify }}) + (NAT for {{ object.primary_ip6.nat_inside.address.ip }}) {% elif object.primary_ip6.nat_outside.exists %} (NAT for {% for nat in object.primary_ip6.nat_outside.all %}{{ nat.address.ip }}{% if not forloop.last %}, {% endif %}{% endfor %}) {% endif %} diff --git a/netbox/templates/ipam/ipaddress.html b/netbox/templates/ipam/ipaddress.html index ba0f0c5e6..7f77e8137 100644 --- a/netbox/templates/ipam/ipaddress.html +++ b/netbox/templates/ipam/ipaddress.html @@ -95,9 +95,9 @@
    {% for ip in object.nat_outside.all %} {{ ip|linkify }} - {% if ip.assigned_object %} - ({{ ip.assigned_object.parent_object|linkify }}) - {% endif %}
    + {% if ip.assigned_object %} + ({{ ip.assigned_object.parent_object|linkify }}) + {% endif %}
    {% empty %} {{ ''|placeholder }} {% endfor %} From f3906dd7c43c10be4b44ab8a755f42c83a55660d Mon Sep 17 00:00:00 2001 From: Daniel Sheppard Date: Tue, 23 Aug 2022 09:33:36 -0500 Subject: [PATCH 28/41] Fixes #10111 - Wrap search QS to catch ValueError on identifier field --- docs/release-notes/version-3.3.md | 1 + netbox/ipam/filtersets.py | 12 +++++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/docs/release-notes/version-3.3.md b/docs/release-notes/version-3.3.md index 33fce0a3c..16a62b8cd 100644 --- a/docs/release-notes/version-3.3.md +++ b/docs/release-notes/version-3.3.md @@ -17,6 +17,7 @@ * [#10059](https://github.com/netbox-community/netbox/issues/10059) - Add identifier column to L2VPN table * [#10089](https://github.com/netbox-community/netbox/issues/10089) - `linkify` template filter should escape object representation * [#10108](https://github.com/netbox-community/netbox/issues/10108) - Linkify inside NAT IPs for primary device IPs in UI +* [#10111](https://github.com/netbox-community/netbox/issues/10111) - Wrap search QS to catch ValueError on identifier field --- diff --git a/netbox/ipam/filtersets.py b/netbox/ipam/filtersets.py index 49ec15fc1..3c0ab1ac8 100644 --- a/netbox/ipam/filtersets.py +++ b/netbox/ipam/filtersets.py @@ -965,7 +965,11 @@ class L2VPNFilterSet(NetBoxModelFilterSet, TenancyFilterSet): def search(self, queryset, name, value): if not value.strip(): return queryset - qs_filter = Q(identifier=value) | Q(name__icontains=value) | Q(description__icontains=value) + qs_filter = Q(name__icontains=value) | Q(description__icontains=value) + try: + qs_filter |= Q(identifier=int(value)) + except ValueError: + pass return queryset.filter(qs_filter) @@ -1071,6 +1075,12 @@ class L2VPNTerminationFilterSet(NetBoxModelFilterSet): qs_filter = Q(l2vpn__name__icontains=value) return queryset.filter(qs_filter) + def filter_assigned_object(self, queryset, name, value): + qs = queryset.filter( + Q(**{'{}__in'.format(name): value}) + ) + return qs + def filter_site(self, queryset, name, value): qs = queryset.filter( Q( From 7ba0b420f181cffac86a9de384a1ec9d8a9a07dd Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 23 Aug 2022 10:32:21 -0400 Subject: [PATCH 29/41] Fixes #10109: Fix available prefixes calculation for container prefixes in the global table --- docs/release-notes/version-3.3.md | 1 + netbox/ipam/models/ip.py | 13 ++++++++----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/docs/release-notes/version-3.3.md b/docs/release-notes/version-3.3.md index 16a62b8cd..7ed635bc2 100644 --- a/docs/release-notes/version-3.3.md +++ b/docs/release-notes/version-3.3.md @@ -17,6 +17,7 @@ * [#10059](https://github.com/netbox-community/netbox/issues/10059) - Add identifier column to L2VPN table * [#10089](https://github.com/netbox-community/netbox/issues/10089) - `linkify` template filter should escape object representation * [#10108](https://github.com/netbox-community/netbox/issues/10108) - Linkify inside NAT IPs for primary device IPs in UI +* [#10109](https://github.com/netbox-community/netbox/issues/10109) - Fix available prefixes calculation for container prefixes in the global table * [#10111](https://github.com/netbox-community/netbox/issues/10111) - Wrap search QS to catch ValueError on identifier field --- diff --git a/netbox/ipam/models/ip.py b/netbox/ipam/models/ip.py index 2d3f4d291..456bab4f0 100644 --- a/netbox/ipam/models/ip.py +++ b/netbox/ipam/models/ip.py @@ -35,13 +35,16 @@ class GetAvailablePrefixesMixin: def get_available_prefixes(self): """ - Return all available Prefixes within this aggregate as an IPSet. + Return all available prefixes within this Aggregate or Prefix as an IPSet. """ - prefix = netaddr.IPSet(self.prefix) - child_prefixes = netaddr.IPSet([child.prefix for child in self.get_child_prefixes()]) - available_prefixes = prefix - child_prefixes + params = { + 'prefix__net_contained': str(self.prefix) + } + if hasattr(self, 'vrf'): + params['vrf'] = self.vrf - return available_prefixes + child_prefixes = Prefix.objects.filter(**params).values_list('prefix', flat=True) + return netaddr.IPSet(self.prefix) - netaddr.IPSet(child_prefixes) def get_first_available_prefix(self): """ From c11ca543e2f31723da7ce29c7062a4f99339398f Mon Sep 17 00:00:00 2001 From: Arthur Date: Tue, 23 Aug 2022 09:16:48 -0700 Subject: [PATCH 30/41] #10037 default type to virtual --- netbox/dcim/tables/template_code.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/dcim/tables/template_code.py b/netbox/dcim/tables/template_code.py index 62a189b63..8c23f327c 100644 --- a/netbox/dcim/tables/template_code.py +++ b/netbox/dcim/tables/template_code.py @@ -239,7 +239,7 @@ INTERFACE_BUTTONS = """
  • Inventory Item
  • {% endif %} {% if perms.dcim.add_interface %} -
  • Child Interface
  • +
  • Child Interface
  • {% endif %} From 8b1a462a6070cb6054af8bb59589c9a2e785afc2 Mon Sep 17 00:00:00 2001 From: Arthur Date: Tue, 23 Aug 2022 09:29:55 -0700 Subject: [PATCH 31/41] #10094 changes from code review --- netbox/netbox/views/generic/object_views.py | 6 +++--- netbox/tenancy/views.py | 13 +++++-------- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/netbox/netbox/views/generic/object_views.py b/netbox/netbox/views/generic/object_views.py index 89f52e475..6ef88bb2f 100644 --- a/netbox/netbox/views/generic/object_views.py +++ b/netbox/netbox/views/generic/object_views.py @@ -329,9 +329,9 @@ class ObjectEditView(GetReturnURLMixin, BaseObjectView): def get_extra_addanother_params(self, request, params): """ - Return a QueryDict of extra params to use on the Add Another button. + Return a dictionary of extra parameters to use on the Add Another button. """ - return params + return {} # # Request handlers @@ -405,7 +405,7 @@ class ObjectEditView(GetReturnURLMixin, BaseObjectView): # If cloning is supported, pre-populate a new instance of the form params = prepare_cloned_fields(obj) - params = self.get_extra_addanother_params(request, params) + params.update(self.get_extra_addanother_params(request)) if params: if 'return_url' in request.GET: params['return_url'] = request.GET.get('return_url') diff --git a/netbox/tenancy/views.py b/netbox/tenancy/views.py index 8b0f90f88..e582c15d1 100644 --- a/netbox/tenancy/views.py +++ b/netbox/tenancy/views.py @@ -366,14 +366,11 @@ class ContactAssignmentEditView(generic.ObjectEditView): instance.object = get_object_or_404(content_type.model_class(), pk=request.GET.get('object_id')) return instance - def get_extra_addanother_params(self, request, params: dict): - if not params: - params = QueryDict(mutable=True) - - params['content_type'] = request.GET.get('content_type') - params['object_id'] = request.GET.get('object_id') - - return params + def get_extra_addanother_params(self, request): + return { + 'content_type': request.GET.get('content_type'), + 'object_id': request.GET.get('object_id'), + } class ContactAssignmentDeleteView(generic.ObjectDeleteView): From 374abe52149c5804c605a781aee73b9542f6ac36 Mon Sep 17 00:00:00 2001 From: Arthur Date: Tue, 23 Aug 2022 10:34:06 -0700 Subject: [PATCH 32/41] #10033 disable Add a Termination button if 2 terminations on L2VPN P2P --- netbox/ipam/models/l2vpn.py | 6 ++++++ netbox/templates/ipam/l2vpn.html | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/netbox/ipam/models/l2vpn.py b/netbox/ipam/models/l2vpn.py index 0e948b18e..db6f47924 100644 --- a/netbox/ipam/models/l2vpn.py +++ b/netbox/ipam/models/l2vpn.py @@ -67,6 +67,12 @@ class L2VPN(NetBoxModel): def get_absolute_url(self): return reverse('ipam:l2vpn', args=[self.pk]) + def can_add_termination(self): + if self.type in L2VPNTypeChoices.P2P and self.terminations.count() >= 2: + return False + else: + return True + class L2VPNTermination(NetBoxModel): l2vpn = models.ForeignKey( diff --git a/netbox/templates/ipam/l2vpn.html b/netbox/templates/ipam/l2vpn.html index 44a1da818..32013400b 100644 --- a/netbox/templates/ipam/l2vpn.html +++ b/netbox/templates/ipam/l2vpn.html @@ -59,7 +59,7 @@ {% if perms.ipam.add_l2vpntermination %} From 439cf1a30874bffce105e0d0dea05b416f603487 Mon Sep 17 00:00:00 2001 From: Arthur Date: Tue, 23 Aug 2022 16:17:40 -0700 Subject: [PATCH 33/41] #10033 changes from code review --- netbox/ipam/models/l2vpn.py | 2 ++ netbox/templates/ipam/l2vpn.html | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/netbox/ipam/models/l2vpn.py b/netbox/ipam/models/l2vpn.py index db6f47924..ab29ab048 100644 --- a/netbox/ipam/models/l2vpn.py +++ b/netbox/ipam/models/l2vpn.py @@ -3,6 +3,7 @@ from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ValidationError from django.db import models from django.urls import reverse +from django.utils.functional import cached_property from ipam.choices import L2VPNTypeChoices from ipam.constants import L2VPN_ASSIGNMENT_MODELS @@ -67,6 +68,7 @@ class L2VPN(NetBoxModel): def get_absolute_url(self): return reverse('ipam:l2vpn', args=[self.pk]) + @cached_property def can_add_termination(self): if self.type in L2VPNTypeChoices.P2P and self.terminations.count() >= 2: return False diff --git a/netbox/templates/ipam/l2vpn.html b/netbox/templates/ipam/l2vpn.html index 32013400b..c19363d33 100644 --- a/netbox/templates/ipam/l2vpn.html +++ b/netbox/templates/ipam/l2vpn.html @@ -59,7 +59,7 @@ {% if perms.ipam.add_l2vpntermination %} From 1c46102c4a672079e4e61f33d5405ccb505fe54f Mon Sep 17 00:00:00 2001 From: Arthur Date: Tue, 23 Aug 2022 16:19:43 -0700 Subject: [PATCH 34/41] #10094 changes from code review --- netbox/netbox/views/generic/object_views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/netbox/views/generic/object_views.py b/netbox/netbox/views/generic/object_views.py index 6ef88bb2f..ece299f21 100644 --- a/netbox/netbox/views/generic/object_views.py +++ b/netbox/netbox/views/generic/object_views.py @@ -327,7 +327,7 @@ class ObjectEditView(GetReturnURLMixin, BaseObjectView): """ return obj - def get_extra_addanother_params(self, request, params): + def get_extra_addanother_params(self, request): """ Return a dictionary of extra parameters to use on the Add Another button. """ From 18d5576997d029f3c67c323e3e88912e08d1f542 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 24 Aug 2022 08:59:40 -0400 Subject: [PATCH 35/41] Changelog for #10033, #10037, #10094 --- docs/release-notes/version-3.3.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/release-notes/version-3.3.md b/docs/release-notes/version-3.3.md index 7ed635bc2..4d7c482e5 100644 --- a/docs/release-notes/version-3.3.md +++ b/docs/release-notes/version-3.3.md @@ -5,6 +5,8 @@ ### Enhancements * [#6454](https://github.com/netbox-community/netbox/issues/6454) - Include contextual help when creating first objects in UI +* [#10033](https://github.com/netbox-community/netbox/issues/10033) - Disable "add termination" button for point-to-point L2VPNs with two terminations +* [#10037](https://github.com/netbox-community/netbox/issues/10037) - Add link to create child interface to interface context menu * [#10061](https://github.com/netbox-community/netbox/issues/10061) - Replicate type when cloning L2VPN instances * [#10066](https://github.com/netbox-community/netbox/issues/10066) - Use fixed column widths for custom field values in UI @@ -16,6 +18,7 @@ * [#10057](https://github.com/netbox-community/netbox/issues/10057) - Fix AttributeError exception when global search results include rack reservations * [#10059](https://github.com/netbox-community/netbox/issues/10059) - Add identifier column to L2VPN table * [#10089](https://github.com/netbox-community/netbox/issues/10089) - `linkify` template filter should escape object representation +* [#10094](https://github.com/netbox-community/netbox/issues/10094) - Fix 404 when using "create and add another" to add contact assignments * [#10108](https://github.com/netbox-community/netbox/issues/10108) - Linkify inside NAT IPs for primary device IPs in UI * [#10109](https://github.com/netbox-community/netbox/issues/10109) - Fix available prefixes calculation for container prefixes in the global table * [#10111](https://github.com/netbox-community/netbox/issues/10111) - Wrap search QS to catch ValueError on identifier field From 36729fb6aec9ee856945a1ac5d48b75df8aa7178 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 24 Aug 2022 13:08:21 -0400 Subject: [PATCH 36/41] Fixes #10134: Custom fields data serializer should return a 400 response for invalid data --- docs/release-notes/version-3.3.md | 1 + netbox/extras/api/customfields.py | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/docs/release-notes/version-3.3.md b/docs/release-notes/version-3.3.md index 4d7c482e5..e23438478 100644 --- a/docs/release-notes/version-3.3.md +++ b/docs/release-notes/version-3.3.md @@ -22,6 +22,7 @@ * [#10108](https://github.com/netbox-community/netbox/issues/10108) - Linkify inside NAT IPs for primary device IPs in UI * [#10109](https://github.com/netbox-community/netbox/issues/10109) - Fix available prefixes calculation for container prefixes in the global table * [#10111](https://github.com/netbox-community/netbox/issues/10111) - Wrap search QS to catch ValueError on identifier field +* [#10134](https://github.com/netbox-community/netbox/issues/10134) - Custom fields data serializer should return a 400 response for invalid data --- diff --git a/netbox/extras/api/customfields.py b/netbox/extras/api/customfields.py index b7fd1e129..cb35b4e73 100644 --- a/netbox/extras/api/customfields.py +++ b/netbox/extras/api/customfields.py @@ -1,5 +1,6 @@ from django.contrib.contenttypes.models import ContentType from rest_framework.fields import Field +from rest_framework.serializers import ValidationError from extras.choices import CustomFieldTypeChoices from extras.models import CustomField @@ -62,6 +63,12 @@ class CustomFieldsDataField(Field): return data def to_internal_value(self, data): + if type(data) is not dict: + raise ValidationError( + "Invalid data format. Custom field data must be passed as a dictionary mapping field names to their " + "values." + ) + # If updating an existing instance, start with existing custom_field_data if self.parent.instance: data = {**self.parent.instance.custom_field_data, **data} From c2c8bd0a761dde32bc2438abf6c05fd27fab58c3 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 24 Aug 2022 13:25:54 -0400 Subject: [PATCH 37/41] Closes #10133: Enable nullifying device location during bulk edit --- docs/release-notes/version-3.3.md | 1 + netbox/dcim/forms/bulk_edit.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/release-notes/version-3.3.md b/docs/release-notes/version-3.3.md index e23438478..5c4c8c654 100644 --- a/docs/release-notes/version-3.3.md +++ b/docs/release-notes/version-3.3.md @@ -9,6 +9,7 @@ * [#10037](https://github.com/netbox-community/netbox/issues/10037) - Add link to create child interface to interface context menu * [#10061](https://github.com/netbox-community/netbox/issues/10061) - Replicate type when cloning L2VPN instances * [#10066](https://github.com/netbox-community/netbox/issues/10066) - Use fixed column widths for custom field values in UI +* [#10133](https://github.com/netbox-community/netbox/issues/10133) - Enable nullifying device location during bulk edit ### Bug Fixes diff --git a/netbox/dcim/forms/bulk_edit.py b/netbox/dcim/forms/bulk_edit.py index 8f765ae9b..396f7e59b 100644 --- a/netbox/dcim/forms/bulk_edit.py +++ b/netbox/dcim/forms/bulk_edit.py @@ -480,7 +480,7 @@ class DeviceBulkEditForm(NetBoxModelBulkEditForm): ('Hardware', ('manufacturer', 'device_type', 'airflow', 'serial')), ) nullable_fields = ( - 'tenant', 'platform', 'serial', 'airflow', + 'location', 'tenant', 'platform', 'serial', 'airflow', ) From 2baf06e012032ea0915a32df13d953acc5bf8500 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 24 Aug 2022 14:46:42 -0400 Subject: [PATCH 38/41] Add unique slugs to L2VPNs in relevant tests --- netbox/ipam/tests/test_api.py | 6 +++--- netbox/ipam/tests/test_filtersets.py | 6 +++--- netbox/ipam/tests/test_models.py | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/netbox/ipam/tests/test_api.py b/netbox/ipam/tests/test_api.py index 3fef04194..4c07e0a90 100644 --- a/netbox/ipam/tests/test_api.py +++ b/netbox/ipam/tests/test_api.py @@ -973,9 +973,9 @@ class L2VPNTerminationTest(APIViewTestCases.APIViewTestCase): VLAN.objects.bulk_create(vlans) l2vpns = ( - L2VPN(name='L2VPN 1', type='vxlan', identifier=650001), - L2VPN(name='L2VPN 2', type='vpws', identifier=650002), - L2VPN(name='L2VPN 3', type='vpls'), # No RD + L2VPN(name='L2VPN 1', slug='l2vpn-1', type='vxlan', identifier=650001), + L2VPN(name='L2VPN 2', slug='l2vpn-2', type='vpws', identifier=650002), + L2VPN(name='L2VPN 3', slug='l2vpn-3', type='vpls'), # No RD ) L2VPN.objects.bulk_create(l2vpns) diff --git a/netbox/ipam/tests/test_filtersets.py b/netbox/ipam/tests/test_filtersets.py index 081f6e11d..5c4113786 100644 --- a/netbox/ipam/tests/test_filtersets.py +++ b/netbox/ipam/tests/test_filtersets.py @@ -1485,9 +1485,9 @@ class L2VPNTestCase(TestCase, ChangeLoggedFilterSetTests): RouteTarget.objects.bulk_create(route_targets) l2vpns = ( - L2VPN(name='L2VPN 1', type=L2VPNTypeChoices.TYPE_VXLAN, identifier=65001), - L2VPN(name='L2VPN 2', type=L2VPNTypeChoices.TYPE_VPWS, identifier=65002), - L2VPN(name='L2VPN 3', type=L2VPNTypeChoices.TYPE_VPLS), + L2VPN(name='L2VPN 1', slug='l2vpn-1', type=L2VPNTypeChoices.TYPE_VXLAN, identifier=65001), + L2VPN(name='L2VPN 2', slug='l2vpn-2', type=L2VPNTypeChoices.TYPE_VPWS, identifier=65002), + L2VPN(name='L2VPN 3', slug='l2vpn-3', type=L2VPNTypeChoices.TYPE_VPLS), ) L2VPN.objects.bulk_create(l2vpns) l2vpns[0].import_targets.add(route_targets[0]) diff --git a/netbox/ipam/tests/test_models.py b/netbox/ipam/tests/test_models.py index 3bd7e8ccb..94a315be5 100644 --- a/netbox/ipam/tests/test_models.py +++ b/netbox/ipam/tests/test_models.py @@ -581,9 +581,9 @@ class TestL2VPNTermination(TestCase): VLAN.objects.bulk_create(vlans) l2vpns = ( - L2VPN(name='L2VPN 1', type='vxlan', identifier=650001), - L2VPN(name='L2VPN 2', type='vpws', identifier=650002), - L2VPN(name='L2VPN 3', type='vpls'), # No RD + L2VPN(name='L2VPN 1', slug='l2vpn-1', type='vxlan', identifier=650001), + L2VPN(name='L2VPN 2', slug='l2vpn-2', type='vpws', identifier=650002), + L2VPN(name='L2VPN 3', slug='l2vpn-3', type='vpls'), # No RD ) L2VPN.objects.bulk_create(l2vpns) From bfbf97aec9119539f7f42cf16f52d0ca8203ba60 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 24 Aug 2022 15:49:36 -0400 Subject: [PATCH 39/41] Closes #10031: Enforce 'application/json' content type for REST API requests --- docs/release-notes/version-3.3.md | 1 + netbox/ipam/tests/test_api.py | 4 ++-- netbox/netbox/settings.py | 4 +++- netbox/users/tests/test_api.py | 4 ++-- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/docs/release-notes/version-3.3.md b/docs/release-notes/version-3.3.md index 5c4c8c654..765944950 100644 --- a/docs/release-notes/version-3.3.md +++ b/docs/release-notes/version-3.3.md @@ -5,6 +5,7 @@ ### Enhancements * [#6454](https://github.com/netbox-community/netbox/issues/6454) - Include contextual help when creating first objects in UI +* [#10031](https://github.com/netbox-community/netbox/issues/10031) - Enforce `application/json` content type for REST API requests * [#10033](https://github.com/netbox-community/netbox/issues/10033) - Disable "add termination" button for point-to-point L2VPNs with two terminations * [#10037](https://github.com/netbox-community/netbox/issues/10037) - Add link to create child interface to interface context menu * [#10061](https://github.com/netbox-community/netbox/issues/10061) - Replicate type when cloning L2VPN instances diff --git a/netbox/ipam/tests/test_api.py b/netbox/ipam/tests/test_api.py index 4c07e0a90..5dc708cd0 100644 --- a/netbox/ipam/tests/test_api.py +++ b/netbox/ipam/tests/test_api.py @@ -390,7 +390,7 @@ class PrefixTest(APIViewTestCases.APIViewTestCase): self.assertEqual(response.data['description'], data['description']) # Try to create one more IP - response = self.client.post(url, {}, **self.header) + response = self.client.post(url, {}, format='json', **self.header) self.assertHttpStatus(response, status.HTTP_409_CONFLICT) self.assertIn('detail', response.data) @@ -487,7 +487,7 @@ class IPRangeTest(APIViewTestCases.APIViewTestCase): self.assertEqual(response.data['description'], data['description']) # Try to create one more IP - response = self.client.post(url, {}, **self.header) + response = self.client.post(url, {}, format='json', **self.header) self.assertHttpStatus(response, status.HTTP_409_CONFLICT) self.assertIn('detail', response.data) diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 0edce8f69..4438d338b 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -533,6 +533,9 @@ REST_FRAMEWORK = { ), 'DEFAULT_METADATA_CLASS': 'netbox.api.metadata.BulkOperationMetadata', 'DEFAULT_PAGINATION_CLASS': 'netbox.api.pagination.OptionalLimitOffsetPagination', + 'DEFAULT_PARSER_CLASSES': ( + 'rest_framework.parsers.JSONParser', + ), 'DEFAULT_PERMISSION_CLASSES': ( 'netbox.api.authentication.TokenPermissions', ), @@ -542,7 +545,6 @@ REST_FRAMEWORK = { ), 'DEFAULT_VERSION': REST_FRAMEWORK_VERSION, 'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.AcceptHeaderVersioning', - # 'PAGE_SIZE': PAGINATE_COUNT, 'SCHEMA_COERCE_METHOD_NAMES': { # Default mappings 'retrieve': 'read', diff --git a/netbox/users/tests/test_api.py b/netbox/users/tests/test_api.py index bcfc9cf14..a0bf8a49e 100644 --- a/netbox/users/tests/test_api.py +++ b/netbox/users/tests/test_api.py @@ -124,7 +124,7 @@ class TokenTest( user = User.objects.create_user(**data) url = reverse('users-api:token_provision') - response = self.client.post(url, **self.header, data=data) + response = self.client.post(url, data, format='json', **self.header) self.assertEqual(response.status_code, 201) self.assertIn('key', response.data) self.assertEqual(len(response.data['key']), 40) @@ -141,7 +141,7 @@ class TokenTest( } url = reverse('users-api:token_provision') - response = self.client.post(url, **self.header, data=data) + response = self.client.post(url, data, format='json', **self.header) self.assertEqual(response.status_code, 403) From eb2bf3469ec2b28e687c2dbfb35191bd256f80bb Mon Sep 17 00:00:00 2001 From: Arthur Hanson Date: Wed, 24 Aug 2022 13:36:38 -0700 Subject: [PATCH 40/41] #9935 add new wireless choices for interfaces (#10116) * #9935 add new wireless choices for interfaces * #9935 add new wireless interfaces to constants * #9935 oops - remove login.html changes --- netbox/dcim/choices.py | 4 +++ netbox/dcim/constants.py | 3 +++ ...alter_wirelesslink_interface_a_and_more.py | 25 +++++++++++++++++++ 3 files changed, 32 insertions(+) create mode 100644 netbox/wireless/migrations/0005_alter_wirelesslink_interface_a_and_more.py diff --git a/netbox/dcim/choices.py b/netbox/dcim/choices.py index 79049384a..019ae09a4 100644 --- a/netbox/dcim/choices.py +++ b/netbox/dcim/choices.py @@ -790,7 +790,9 @@ class InterfaceTypeChoices(ChoiceSet): TYPE_80211AC = 'ieee802.11ac' TYPE_80211AD = 'ieee802.11ad' TYPE_80211AX = 'ieee802.11ax' + TYPE_80211AY = 'ieee802.11ay' TYPE_802151 = 'ieee802.15.1' + TYPE_OTHER_WIRELESS = 'other-wireless' # Cellular TYPE_GSM = 'gsm' @@ -918,7 +920,9 @@ class InterfaceTypeChoices(ChoiceSet): (TYPE_80211AC, 'IEEE 802.11ac'), (TYPE_80211AD, 'IEEE 802.11ad'), (TYPE_80211AX, 'IEEE 802.11ax'), + (TYPE_80211AY, 'IEEE 802.11ay'), (TYPE_802151, 'IEEE 802.15.1 (Bluetooth)'), + (TYPE_OTHER_WIRELESS, 'Other (Wireless)'), ) ), ( diff --git a/netbox/dcim/constants.py b/netbox/dcim/constants.py index 9e41ed113..80d7558c9 100644 --- a/netbox/dcim/constants.py +++ b/netbox/dcim/constants.py @@ -45,6 +45,9 @@ WIRELESS_IFACE_TYPES = [ InterfaceTypeChoices.TYPE_80211AC, InterfaceTypeChoices.TYPE_80211AD, InterfaceTypeChoices.TYPE_80211AX, + InterfaceTypeChoices.TYPE_80211AY, + InterfaceTypeChoices.TYPE_802151, + InterfaceTypeChoices.TYPE_OTHER_WIRELESS, ] NONCONNECTABLE_IFACE_TYPES = VIRTUAL_IFACE_TYPES + WIRELESS_IFACE_TYPES diff --git a/netbox/wireless/migrations/0005_alter_wirelesslink_interface_a_and_more.py b/netbox/wireless/migrations/0005_alter_wirelesslink_interface_a_and_more.py new file mode 100644 index 000000000..64e375e43 --- /dev/null +++ b/netbox/wireless/migrations/0005_alter_wirelesslink_interface_a_and_more.py @@ -0,0 +1,25 @@ +# Generated by Django 4.0.7 on 2022-08-24 17:18 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('dcim', '0161_cabling_cleanup'), + ('wireless', '0004_wireless_tenancy'), + ] + + operations = [ + migrations.AlterField( + model_name='wirelesslink', + name='interface_a', + field=models.ForeignKey(limit_choices_to={'type__in': ['ieee802.11a', 'ieee802.11g', 'ieee802.11n', 'ieee802.11ac', 'ieee802.11ad', 'ieee802.11ax', 'ieee802.11ay', 'ieee802.15.1', 'other-wireless']}, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='dcim.interface'), + ), + migrations.AlterField( + model_name='wirelesslink', + name='interface_b', + field=models.ForeignKey(limit_choices_to={'type__in': ['ieee802.11a', 'ieee802.11g', 'ieee802.11n', 'ieee802.11ac', 'ieee802.11ad', 'ieee802.11ax', 'ieee802.11ay', 'ieee802.15.1', 'other-wireless']}, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='dcim.interface'), + ), + ] From f70ef7a585d142813850c14853ff2cba122cb31f Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 24 Aug 2022 16:44:24 -0400 Subject: [PATCH 41/41] Changelog and cleanup for #9935 --- docs/release-notes/version-3.3.md | 1 + ...alter_wirelesslink_interface_a_and_more.py | 25 ------------------- .../0005_wirelesslink_interface_types.py | 24 ++++++++++++++++++ netbox/wireless/models.py | 10 ++++++-- 4 files changed, 33 insertions(+), 27 deletions(-) delete mode 100644 netbox/wireless/migrations/0005_alter_wirelesslink_interface_a_and_more.py create mode 100644 netbox/wireless/migrations/0005_wirelesslink_interface_types.py diff --git a/docs/release-notes/version-3.3.md b/docs/release-notes/version-3.3.md index 765944950..1421bb2c7 100644 --- a/docs/release-notes/version-3.3.md +++ b/docs/release-notes/version-3.3.md @@ -5,6 +5,7 @@ ### Enhancements * [#6454](https://github.com/netbox-community/netbox/issues/6454) - Include contextual help when creating first objects in UI +* [#9935](https://github.com/netbox-community/netbox/issues/9935) - Add 802.11ay and "other" wireless interface types * [#10031](https://github.com/netbox-community/netbox/issues/10031) - Enforce `application/json` content type for REST API requests * [#10033](https://github.com/netbox-community/netbox/issues/10033) - Disable "add termination" button for point-to-point L2VPNs with two terminations * [#10037](https://github.com/netbox-community/netbox/issues/10037) - Add link to create child interface to interface context menu diff --git a/netbox/wireless/migrations/0005_alter_wirelesslink_interface_a_and_more.py b/netbox/wireless/migrations/0005_alter_wirelesslink_interface_a_and_more.py deleted file mode 100644 index 64e375e43..000000000 --- a/netbox/wireless/migrations/0005_alter_wirelesslink_interface_a_and_more.py +++ /dev/null @@ -1,25 +0,0 @@ -# Generated by Django 4.0.7 on 2022-08-24 17:18 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('dcim', '0161_cabling_cleanup'), - ('wireless', '0004_wireless_tenancy'), - ] - - operations = [ - migrations.AlterField( - model_name='wirelesslink', - name='interface_a', - field=models.ForeignKey(limit_choices_to={'type__in': ['ieee802.11a', 'ieee802.11g', 'ieee802.11n', 'ieee802.11ac', 'ieee802.11ad', 'ieee802.11ax', 'ieee802.11ay', 'ieee802.15.1', 'other-wireless']}, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='dcim.interface'), - ), - migrations.AlterField( - model_name='wirelesslink', - name='interface_b', - field=models.ForeignKey(limit_choices_to={'type__in': ['ieee802.11a', 'ieee802.11g', 'ieee802.11n', 'ieee802.11ac', 'ieee802.11ad', 'ieee802.11ax', 'ieee802.11ay', 'ieee802.15.1', 'other-wireless']}, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='dcim.interface'), - ), - ] diff --git a/netbox/wireless/migrations/0005_wirelesslink_interface_types.py b/netbox/wireless/migrations/0005_wirelesslink_interface_types.py new file mode 100644 index 000000000..0b3f88c5b --- /dev/null +++ b/netbox/wireless/migrations/0005_wirelesslink_interface_types.py @@ -0,0 +1,24 @@ +from django.db import migrations, models +import django.db.models.deletion +import wireless.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dcim', '0161_cabling_cleanup'), + ('wireless', '0004_wireless_tenancy'), + ] + + operations = [ + migrations.AlterField( + model_name='wirelesslink', + name='interface_a', + field=models.ForeignKey(limit_choices_to=wireless.models.get_wireless_interface_types, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='dcim.interface'), + ), + migrations.AlterField( + model_name='wirelesslink', + name='interface_b', + field=models.ForeignKey(limit_choices_to=wireless.models.get_wireless_interface_types, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='dcim.interface'), + ), + ] diff --git a/netbox/wireless/models.py b/netbox/wireless/models.py index d8166fe9a..c383ad642 100644 --- a/netbox/wireless/models.py +++ b/netbox/wireless/models.py @@ -128,20 +128,26 @@ class WirelessLAN(WirelessAuthenticationBase, NetBoxModel): return reverse('wireless:wirelesslan', args=[self.pk]) +def get_wireless_interface_types(): + # Wrap choices in a callable to avoid generating dummy migrations + # when the choices are updated. + return {'type__in': WIRELESS_IFACE_TYPES} + + class WirelessLink(WirelessAuthenticationBase, NetBoxModel): """ A point-to-point connection between two wireless Interfaces. """ interface_a = models.ForeignKey( to='dcim.Interface', - limit_choices_to={'type__in': WIRELESS_IFACE_TYPES}, + limit_choices_to=get_wireless_interface_types, on_delete=models.PROTECT, related_name='+', verbose_name="Interface A", ) interface_b = models.ForeignKey( to='dcim.Interface', - limit_choices_to={'type__in': WIRELESS_IFACE_TYPES}, + limit_choices_to=get_wireless_interface_types, on_delete=models.PROTECT, related_name='+', verbose_name="Interface B",