From 02e8979178783d562cc8a47cd6df097403b5fb6b Mon Sep 17 00:00:00 2001 From: Chris Mills Date: Fri, 22 Jan 2021 16:45:08 +0000 Subject: [PATCH 001/182] Changes to template, view and CablePath class to indicate to users whether the cable length is accurate or not. --- netbox/dcim/models/cables.py | 8 ++++++-- netbox/dcim/views.py | 6 +++++- netbox/templates/dcim/cable_trace.html | 2 +- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/netbox/dcim/models/cables.py b/netbox/dcim/models/cables.py index 6a530bb49..898e73b4c 100644 --- a/netbox/dcim/models/cables.py +++ b/netbox/dcim/models/cables.py @@ -480,13 +480,17 @@ class CablePath(models.Model): def get_total_length(self): """ - Return the sum of the length of each cable in the path. + Return a tuple containing the sum of the length of each cable in the path + and a flag indicating whether the length is definitive. """ cable_ids = [ # Starting from the first element, every third element in the path should be a Cable decompile_path_node(self.path[i])[1] for i in range(0, len(self.path), 3) ] - return Cable.objects.filter(id__in=cable_ids).aggregate(total=Sum('_abs_length'))['total'] + cables = Cable.objects.filter(id__in=cable_ids, _abs_length__isnull=False) + total_length = cables.aggregate(total=Sum('_abs_length'))['total'] + is_definitive = len(cables) == len(cable_ids) + return (total_length, is_definitive) def get_split_nodes(self): """ diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index b092be612..faec98cb7 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -2134,10 +2134,14 @@ class PathTraceView(generic.ObjectView): else: path = related_paths.first() + # Get the total length of the cable and whether the length is definitive (fully defined) + total_length, is_definitive = path.get_total_length if path else (None, False) + return { 'path': path, 'related_paths': related_paths, - 'total_length': path.get_total_length() if path else None, + 'total_length': total_length, + 'is_definitive': is_definitive } diff --git a/netbox/templates/dcim/cable_trace.html b/netbox/templates/dcim/cable_trace.html index a39ada1ce..060fe2076 100644 --- a/netbox/templates/dcim/cable_trace.html +++ b/netbox/templates/dcim/cable_trace.html @@ -69,7 +69,7 @@
Total segments: {{ traced_path|length }}
Total length: {% if total_length %} - {{ total_length|floatformat:"-2" }} Meters / + {% if not is_definitive %}>{% endif %}{{ total_length|floatformat:"-2" }} Meters / {{ total_length|meters_to_feet|floatformat:"-2" }} Feet {% else %} N/A From 04964cc52b90a567a2b8a26e966d24bdc9ca4e07 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 10 Mar 2021 17:00:35 -0500 Subject: [PATCH 002/182] Fixes #5595: Restore ability to delete an uploaded device type image --- docs/release-notes/version-2.10.md | 10 ++++++++++ netbox/dcim/forms.py | 4 ++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/docs/release-notes/version-2.10.md b/docs/release-notes/version-2.10.md index 2b3792204..1ed8cf288 100644 --- a/docs/release-notes/version-2.10.md +++ b/docs/release-notes/version-2.10.md @@ -1,5 +1,13 @@ # NetBox v2.10 +## v2.10.7 (FUTURE) + +### Bug Fixes + +* [#5595](https://github.com/netbox-community/netbox/issues/5595) - Restore ability to delete an uploaded device type image + +--- + ## v2.10.6 (2021-03-09) ### Enhancements @@ -19,6 +27,8 @@ * [#5935](https://github.com/netbox-community/netbox/issues/5935) - Fix filtering prefixes list by multiple prefix values * [#5948](https://github.com/netbox-community/netbox/issues/5948) - Invalidate cached queries when running `renaturalize` +--- + ## v2.10.5 (2021-02-24) ### Bug Fixes diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 40c16d59f..e06b2ae8a 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -944,10 +944,10 @@ class DeviceTypeForm(BootstrapMixin, CustomFieldModelForm): widgets = { 'subdevice_role': StaticSelect2(), # Exclude SVG images (unsupported by PIL) - 'front_image': forms.FileInput(attrs={ + 'front_image': forms.ClearableFileInput(attrs={ 'accept': 'image/bmp,image/gif,image/jpeg,image/png,image/tiff' }), - 'rear_image': forms.FileInput(attrs={ + 'rear_image': forms.ClearableFileInput(attrs={ 'accept': 'image/bmp,image/gif,image/jpeg,image/png,image/tiff' }) } From cb9478e0eaa5853e73718ef6bc52c1f1171678e5 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 10 Mar 2021 17:08:11 -0500 Subject: [PATCH 003/182] Closes #5950: Use TimeZoneSerializerField from django-timezone-field --- netbox/dcim/api/serializers.py | 6 +++--- netbox/netbox/api/__init__.py | 3 +-- netbox/netbox/api/fields.py | 15 --------------- 3 files changed, 4 insertions(+), 20 deletions(-) diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 6f497bfa6..dc730488c 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -3,6 +3,7 @@ from django.contrib.contenttypes.models import ContentType from drf_yasg.utils import swagger_serializer_method from rest_framework import serializers from rest_framework.validators import UniqueTogetherValidator +from timezone_field.rest_framework import TimeZoneSerializerField from dcim.choices import * from dcim.constants import * @@ -13,13 +14,12 @@ from dcim.models import ( PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site, VirtualChassis, ) -from dcim.utils import decompile_path_node from extras.api.customfields import CustomFieldModelSerializer from extras.api.serializers import TaggedObjectSerializer from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedVLANSerializer from ipam.models import VLAN from netbox.api import ( - ChoiceField, ContentTypeField, SerializedPKRelatedField, TimeZoneField, ValidatedModelSerializer, + ChoiceField, ContentTypeField, SerializedPKRelatedField, ValidatedModelSerializer, WritableNestedSerializer, ) from tenancy.api.nested_serializers import NestedTenantSerializer @@ -98,7 +98,7 @@ class SiteSerializer(TaggedObjectSerializer, CustomFieldModelSerializer): status = ChoiceField(choices=SiteStatusChoices, required=False) region = NestedRegionSerializer(required=False, allow_null=True) tenant = NestedTenantSerializer(required=False, allow_null=True) - time_zone = TimeZoneField(required=False) + time_zone = TimeZoneSerializerField(required=False) circuit_count = serializers.IntegerField(read_only=True) device_count = serializers.IntegerField(read_only=True) prefix_count = serializers.IntegerField(read_only=True) diff --git a/netbox/netbox/api/__init__.py b/netbox/netbox/api/__init__.py index 78ab7431d..334ee09f7 100644 --- a/netbox/netbox/api/__init__.py +++ b/netbox/netbox/api/__init__.py @@ -1,4 +1,4 @@ -from .fields import ChoiceField, ContentTypeField, SerializedPKRelatedField, TimeZoneField +from .fields import ChoiceField, ContentTypeField, SerializedPKRelatedField from .routers import OrderedDefaultRouter from .serializers import BulkOperationSerializer, ValidatedModelSerializer, WritableNestedSerializer @@ -9,7 +9,6 @@ __all__ = ( 'ContentTypeField', 'OrderedDefaultRouter', 'SerializedPKRelatedField', - 'TimeZoneField', 'ValidatedModelSerializer', 'WritableNestedSerializer', ) diff --git a/netbox/netbox/api/fields.py b/netbox/netbox/api/fields.py index fb3eef76f..d73cbcac2 100644 --- a/netbox/netbox/api/fields.py +++ b/netbox/netbox/api/fields.py @@ -104,21 +104,6 @@ class ContentTypeField(RelatedField): return f"{obj.app_label}.{obj.model}" -class TimeZoneField(serializers.Field): - """ - Represent a pytz time zone. - """ - def to_representation(self, obj): - return obj.zone if obj else None - - def to_internal_value(self, data): - if not data: - return "" - if data not in pytz.common_timezones: - raise ValidationError('Unknown time zone "{}" (see pytz.common_timezones for all options)'.format(data)) - return pytz.timezone(data) - - class SerializedPKRelatedField(PrimaryKeyRelatedField): """ Extends PrimaryKeyRelatedField to return a serialized object on read. This is useful for representing related From 132b1ff47969341d1b243223582c33f10bdd6ecf Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 11 Mar 2021 13:42:26 -0500 Subject: [PATCH 004/182] Fixes #5962: Ensure consistent display of change log action labels --- docs/release-notes/version-2.10.md | 1 + netbox/templates/home.html | 8 +------- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/docs/release-notes/version-2.10.md b/docs/release-notes/version-2.10.md index 1ed8cf288..fee73023c 100644 --- a/docs/release-notes/version-2.10.md +++ b/docs/release-notes/version-2.10.md @@ -5,6 +5,7 @@ ### Bug Fixes * [#5595](https://github.com/netbox-community/netbox/issues/5595) - Restore ability to delete an uploaded device type image +* [#5962](https://github.com/netbox-community/netbox/issues/5962) - Ensure consistent display of change log action labels --- diff --git a/netbox/templates/home.html b/netbox/templates/home.html index 47e0e8a1a..e9180eca3 100644 --- a/netbox/templates/home.html +++ b/netbox/templates/home.html @@ -304,13 +304,7 @@ {% for change in changelog %} {% with action=change.get_action_display|lower %}
- {% if action == 'created' %} - Created - {% elif action == 'updated' %} - Modified - {% elif action == 'deleted' %} - Deleted - {% endif %} + {{ change.get_action_display }} {{ change.changed_object_type.name|bettertitle }} {% if change.changed_object.get_absolute_url %} {{ change.changed_object }} From d58291d119cc65b248f7391a2426c95e88ce9d62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20R=C3=B8dvand?= Date: Thu, 11 Mar 2021 22:27:43 +0100 Subject: [PATCH 005/182] Closes #5953: Adds Markdown rendering of Custom Scripts' descriptions --- netbox/templates/extras/script.html | 2 +- netbox/templates/extras/script_list.html | 2 +- netbox/templates/extras/script_result.html | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/netbox/templates/extras/script.html b/netbox/templates/extras/script.html index 5808f707f..3f0839512 100644 --- a/netbox/templates/extras/script.html +++ b/netbox/templates/extras/script.html @@ -16,7 +16,7 @@

{{ script }}

-

{{ script.Meta.description }}

+

{{ script.Meta.description|render_markdown }}

diff --git a/netbox/utilities/templatetags/buttons.py b/netbox/utilities/templatetags/buttons.py index c486dd2e5..e7ebd26a0 100644 --- a/netbox/utilities/templatetags/buttons.py +++ b/netbox/utilities/templatetags/buttons.py @@ -82,13 +82,18 @@ def import_button(url): @register.inclusion_tag('buttons/export.html', takes_context=True) def export_button(context, content_type=None): + add_exporttemplate_link = None + if content_type is not None: user = context['request'].user export_templates = ExportTemplate.objects.restrict(user, 'view').filter(content_type=content_type) + if user.is_staff and user.has_perm('extras.add_exporttemplate'): + add_exporttemplate_link = f"{reverse('admin:extras_exporttemplate_add')}?content_type={content_type.pk}" else: export_templates = [] return { 'url_params': context['request'].GET, 'export_templates': export_templates, + 'add_exporttemplate_link': add_exporttemplate_link, } From a313b675a687fab7c13e889e58a55c662aee531c Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 5 Apr 2021 15:24:57 -0400 Subject: [PATCH 126/182] Simplify CircuitTermination display in circuits table --- netbox/circuits/tables.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/netbox/circuits/tables.py b/netbox/circuits/tables.py index ba599757a..41a3aed7f 100644 --- a/netbox/circuits/tables.py +++ b/netbox/circuits/tables.py @@ -6,6 +6,15 @@ from utilities.tables import BaseTable, ButtonsColumn, ChoiceFieldColumn, TagCol from .models import * +CIRCUITTERMINATION_LINK = """ +{% if value.site %} + {{ value.site }} +{% elif value.provider_network %} + {{ value.provider_network }} +{% endif %} +""" + + # # Providers # @@ -88,12 +97,12 @@ class CircuitTable(BaseTable): ) status = ChoiceFieldColumn() tenant = TenantColumn() - termination_a = tables.Column( - linkify=True, + termination_a = tables.TemplateColumn( + template_code=CIRCUITTERMINATION_LINK, verbose_name='Side A' ) - termination_z = tables.Column( - linkify=True, + termination_z = tables.TemplateColumn( + template_code=CIRCUITTERMINATION_LINK, verbose_name='Side Z' ) tags = TagColumn( From 71022d58d30ca6469308f17256248deef23b1fe9 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 5 Apr 2021 16:35:01 -0400 Subject: [PATCH 127/182] Site is required when creating devices --- netbox/dcim/forms.py | 1 - 1 file changed, 1 deletion(-) diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 207dee0a8..52c2e9ec7 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -2036,7 +2036,6 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): ) site = DynamicModelChoiceField( queryset=Site.objects.all(), - required=False, query_params={ 'region_id': '$region', 'group_id': '$site_group', From d42b0691b2a3ea836ee4efc8ccb7ba287bedda8b Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 5 Apr 2021 17:13:32 -0400 Subject: [PATCH 128/182] Fix 'select all' widget --- netbox/templates/generic/object_list.html | 89 ++++++++++++----------- 1 file changed, 46 insertions(+), 43 deletions(-) diff --git a/netbox/templates/generic/object_list.html b/netbox/templates/generic/object_list.html index 738cbca49..b3b9944bb 100644 --- a/netbox/templates/generic/object_list.html +++ b/netbox/templates/generic/object_list.html @@ -1,6 +1,7 @@ {% extends 'base.html' %} {% load buttons %} {% load helpers %} +{% load render_table from django_tables2 %} {% load static %} {% block content %} @@ -28,54 +29,56 @@ {% block sidebar %}{% endblock %} {% endif %} +
{% with bulk_edit_url=content_type.model_class|validated_viewname:"bulk_edit" bulk_delete_url=content_type.model_class|validated_viewname:"bulk_delete" %} - {% if permissions.change or permissions.delete %} - - {% csrf_token %} - - {% if table.paginator.num_pages > 1 %} - + {% include 'inc/custom_fields_panel.html' %} {% plugin_left_page object %}
- {% include 'inc/custom_fields_panel.html' %} +
+
+ Child Regions +
+ {% include 'inc/table.html' with table=child_regions_table %} + {% if perms.dcim.add_region %} + + {% endif %} +
{% plugin_right_page object %}
diff --git a/netbox/templates/dcim/sitegroup.html b/netbox/templates/dcim/sitegroup.html index 95cba7aeb..2de17c025 100644 --- a/netbox/templates/dcim/sitegroup.html +++ b/netbox/templates/dcim/sitegroup.html @@ -44,10 +44,23 @@ + {% include 'inc/custom_fields_panel.html' %} {% plugin_left_page object %}
- {% include 'inc/custom_fields_panel.html' %} +
+
+ Child Groups +
+ {% include 'inc/table.html' with table=child_groups_table %} + {% if perms.dcim.add_sitegroup %} + + {% endif %} +
{% plugin_right_page object %}
@@ -65,9 +78,9 @@ {% endif %} - - {% include 'inc/paginator.html' with paginator=sites_table.paginator page=sites_table.page %} - {% plugin_full_width_page object %} + + {% include 'inc/paginator.html' with paginator=sites_table.paginator page=sites_table.page %} + {% plugin_full_width_page object %} {% endblock %} From 8f674aede9900dea4430b2d87b639b905827c722 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 6 Apr 2021 09:45:59 -0400 Subject: [PATCH 131/182] Bump Django to 3.2 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 9b883b554..0e20388fe 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -Django==3.2b1 +Django==3.2 django-cacheops==5.1 django-cors-headers==3.7.0 django-debug-toolbar==3.2 From 0635e7ae1e57ec9abc1138e95c7e3c2905f314f0 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 6 Apr 2021 11:36:30 -0400 Subject: [PATCH 132/182] Update dependencies for v2.11-beta1 --- requirements.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index 0e20388fe..cb277e916 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,17 +6,17 @@ django-filter==2.4.0 django-mptt==0.12.0 django-pglocks==1.0.4 django-prometheus==2.1.0 -django-rq==2.4.0 +django-rq==2.4.1 django-tables2==2.3.4 django-taggit==1.3.0 django-timezone-field==4.1.2 djangorestframework==3.12.4 drf-yasg[validation]==1.20.0 -gunicorn==20.0.4 +gunicorn==20.1.0 Jinja2==2.11.3 Markdown==3.3.4 netaddr==0.8.0 -Pillow==8.1.2 +Pillow==8.2.0 psycopg2-binary==2.8.6 pycryptodome==3.10.1 PyYAML==5.4.1 From f1e2b994569062b054d27232a1a2a6a7199e75e1 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 6 Apr 2021 11:45:32 -0400 Subject: [PATCH 133/182] Release v2.11-beta1 --- docs/release-notes/version-2.11.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/release-notes/version-2.11.md b/docs/release-notes/version-2.11.md index 9fa97cf96..bf1914ba4 100644 --- a/docs/release-notes/version-2.11.md +++ b/docs/release-notes/version-2.11.md @@ -1,6 +1,6 @@ # NetBox v2.11 -## v2.11-beta1 (FUTURE) +## v2.11-beta1 (2021-04-07) **WARNING:** This is a beta release and is not suitable for production use. It is intended for development and evaluation purposes only. No upgrade path to the final v2.11 release will be provided from this beta, and users should assume that all data entered into the application will be lost. From b5ad29e3f27e48d96936faddda5894b29a217913 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 7 Apr 2021 15:17:02 -0400 Subject: [PATCH 134/182] Fixes #6100: Fix VM interfaces table "add interfaces" link --- docs/release-notes/version-2.11.md | 10 +++++++++- netbox/netbox/settings.py | 2 +- .../virtualization/virtualmachine/interfaces.html | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/docs/release-notes/version-2.11.md b/docs/release-notes/version-2.11.md index bf1914ba4..b3f68f8e9 100644 --- a/docs/release-notes/version-2.11.md +++ b/docs/release-notes/version-2.11.md @@ -1,6 +1,14 @@ # NetBox v2.11 -## v2.11-beta1 (2021-04-07) +## v2.11.0 (FUTURE) + +### Bug Fixes (from Beta) + +* [#6100](https://github.com/netbox-community/netbox/issues/6100) - Fix VM interfaces table "add interfaces" link + +--- + +## v2.11-beta1 (2021-04-06) **WARNING:** This is a beta release and is not suitable for production use. It is intended for development and evaluation purposes only. No upgrade path to the final v2.11 release will be provided from this beta, and users should assume that all data entered into the application will be lost. diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index bd9b0e44c..dd3de6fa0 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -16,7 +16,7 @@ from django.core.validators import URLValidator # Environment setup # -VERSION = '2.11-beta1' +VERSION = '2.11.0-dev' # Hostname HOSTNAME = platform.node() diff --git a/netbox/templates/virtualization/virtualmachine/interfaces.html b/netbox/templates/virtualization/virtualmachine/interfaces.html index 15d07310c..f45c990eb 100644 --- a/netbox/templates/virtualization/virtualmachine/interfaces.html +++ b/netbox/templates/virtualization/virtualmachine/interfaces.html @@ -35,7 +35,7 @@ {% endif %} {% if perms.virtualization.add_vminterface %} From 59e185b781fcab297fa0d21a8efcbcf7695dfecf Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 7 Apr 2021 15:40:03 -0400 Subject: [PATCH 135/182] Fixes #6104: Fix location column on racks table --- docs/release-notes/version-2.11.md | 1 + netbox/dcim/tables/racks.py | 10 +++++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/release-notes/version-2.11.md b/docs/release-notes/version-2.11.md index b3f68f8e9..9e8ae26e7 100644 --- a/docs/release-notes/version-2.11.md +++ b/docs/release-notes/version-2.11.md @@ -5,6 +5,7 @@ ### Bug Fixes (from Beta) * [#6100](https://github.com/netbox-community/netbox/issues/6100) - Fix VM interfaces table "add interfaces" link +* [#6104](https://github.com/netbox-community/netbox/issues/6104) - Fix location column on racks table --- diff --git a/netbox/dcim/tables/racks.py b/netbox/dcim/tables/racks.py index b56e3bbc4..b549bad10 100644 --- a/netbox/dcim/tables/racks.py +++ b/netbox/dcim/tables/racks.py @@ -71,7 +71,7 @@ class RackTable(BaseTable): order_by=('_name',), linkify=True ) - group = tables.Column( + location = tables.Column( linkify=True ) site = tables.Column( @@ -88,10 +88,10 @@ class RackTable(BaseTable): class Meta(BaseTable.Meta): model = Rack fields = ( - 'pk', 'name', 'site', 'group', 'status', 'facility_id', 'tenant', 'role', 'serial', 'asset_tag', 'type', + 'pk', 'name', 'site', 'location', 'status', 'facility_id', 'tenant', 'role', 'serial', 'asset_tag', 'type', 'width', 'u_height', ) - default_columns = ('pk', 'name', 'site', 'group', 'status', 'facility_id', 'tenant', 'role', 'u_height') + default_columns = ('pk', 'name', 'site', 'location', 'status', 'facility_id', 'tenant', 'role', 'u_height') class RackDetailTable(RackTable): @@ -113,11 +113,11 @@ class RackDetailTable(RackTable): class Meta(RackTable.Meta): fields = ( - 'pk', 'name', 'site', 'group', 'status', 'facility_id', 'tenant', 'role', 'serial', 'asset_tag', 'type', + 'pk', 'name', 'site', 'location', 'status', 'facility_id', 'tenant', 'role', 'serial', 'asset_tag', 'type', 'width', 'u_height', 'device_count', 'get_utilization', 'get_power_utilization', 'tags', ) default_columns = ( - 'pk', 'name', 'site', 'group', 'status', 'facility_id', 'tenant', 'role', 'u_height', 'device_count', + 'pk', 'name', 'site', 'location', 'status', 'facility_id', 'tenant', 'role', 'u_height', 'device_count', 'get_utilization', 'get_power_utilization', ) From 85d0270af05af0efe9bcbf6db81141e2a47fec7e Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 7 Apr 2021 15:50:24 -0400 Subject: [PATCH 136/182] Fixes #6099: Correct example permission description --- docs/administration/permissions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/administration/permissions.md b/docs/administration/permissions.md index c66c65543..c7c8996dc 100644 --- a/docs/administration/permissions.md +++ b/docs/administration/permissions.md @@ -10,7 +10,7 @@ NetBox v2.9 introduced a new object-based permissions framework, which replace's | ----------- | ----------- | | `{"status": "active"}` | Status is active | | `{"status__in": ["planned", "reserved"]}` | Status is active **OR** reserved | -| `{"status": "active", "role": "testing"}` | Status is active **OR** role is testing | +| `{"status": "active", "role": "testing"}` | Status is active **AND** role is testing | | `{"name__startswith": "Foo"}` | Name starts with "Foo" (case-sensitive) | | `{"name__iendswith": "bar"}` | Name ends with "bar" (case-insensitive) | | `{"vid__gte": 100, "vid__lt": 200}` | VLAN ID is greater than or equal to 100 **AND** less than 200 | From ae3527df164868585941d7b3cde095b24d24862d Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 7 Apr 2021 16:04:32 -0400 Subject: [PATCH 137/182] Fixes #6081: Fix interface connections REST API endpoint --- docs/release-notes/version-2.10.md | 1 + netbox/dcim/api/serializers.py | 2 +- netbox/dcim/api/views.py | 2 ++ 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/release-notes/version-2.10.md b/docs/release-notes/version-2.10.md index 95f8bd8ab..f895e43f7 100644 --- a/docs/release-notes/version-2.10.md +++ b/docs/release-notes/version-2.10.md @@ -13,6 +13,7 @@ * [#5805](https://github.com/netbox-community/netbox/issues/5805) - Fix missing custom field filters for cables, rack reservations * [#6073](https://github.com/netbox-community/netbox/issues/6073) - Permit users to manage their own REST API tokens without needing explicit permission +* [#6081](https://github.com/netbox-community/netbox/issues/6081) - Fix interface connections REST API endpoint --- diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index dc730488c..73059cad3 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -779,7 +779,7 @@ class CablePathSerializer(serializers.ModelSerializer): class InterfaceConnectionSerializer(ValidatedModelSerializer): interface_a = serializers.SerializerMethodField() - interface_b = NestedInterfaceSerializer(source='connected_endpoint') + interface_b = NestedInterfaceSerializer(source='_path.destination') connected_endpoint_reachable = serializers.SerializerMethodField(read_only=True) class Meta: diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index ae39f6ad0..9a090eddc 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -2,6 +2,7 @@ import socket from collections import OrderedDict from django.conf import settings +from django.contrib.contenttypes.models import ContentType from django.db.models import F from django.http import HttpResponseForbidden, HttpResponse from django.shortcuts import get_object_or_404 @@ -580,6 +581,7 @@ class PowerConnectionViewSet(ListModelMixin, GenericViewSet): class InterfaceConnectionViewSet(ListModelMixin, GenericViewSet): queryset = Interface.objects.prefetch_related('device', '_path').filter( # Avoid duplicate connections by only selecting the lower PK in a connected pair + _path__destination_type=ContentType.objects.get_by_natural_key('dcim', 'interface'), _path__destination_id__isnull=False, pk__lt=F('_path__destination_id') ) From 38b09dc6101fd17476deb13979666f251e5bf473 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 7 Apr 2021 16:26:16 -0400 Subject: [PATCH 138/182] Fixes #6105: Hide checkboxes for VMs under cluster VMs view --- docs/release-notes/version-2.11.md | 1 + netbox/virtualization/views.py | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/release-notes/version-2.11.md b/docs/release-notes/version-2.11.md index 9e8ae26e7..b82606010 100644 --- a/docs/release-notes/version-2.11.md +++ b/docs/release-notes/version-2.11.md @@ -6,6 +6,7 @@ * [#6100](https://github.com/netbox-community/netbox/issues/6100) - Fix VM interfaces table "add interfaces" link * [#6104](https://github.com/netbox-community/netbox/issues/6104) - Fix location column on racks table +* [#6105](https://github.com/netbox-community/netbox/issues/6105) - Hide checkboxes for VMs under cluster VMs view --- diff --git a/netbox/virtualization/views.py b/netbox/virtualization/views.py index 4a6ad66b5..6bf9eb6dd 100644 --- a/netbox/virtualization/views.py +++ b/netbox/virtualization/views.py @@ -163,8 +163,6 @@ class ClusterVirtualMachinesView(generic.ObjectView): def get_extra_context(self, request, instance): virtualmachines = VirtualMachine.objects.restrict(request.user, 'view').filter(cluster=instance) virtualmachines_table = tables.VirtualMachineTable(virtualmachines, orderable=False) - if request.user.has_perm('virtualization.change_cluster'): - virtualmachines_table.columns.show('pk') return { 'virtualmachines_table': virtualmachines_table, From 81193eb55054763ede647b0eb27cf61632b5ba3d Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 7 Apr 2021 16:36:09 -0400 Subject: [PATCH 139/182] Fixes #6106: Allow assigning a virtual interface as the parent of an existing interface --- docs/release-notes/version-2.11.md | 1 + netbox/dcim/forms.py | 5 +---- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/docs/release-notes/version-2.11.md b/docs/release-notes/version-2.11.md index b82606010..c474bd6b3 100644 --- a/docs/release-notes/version-2.11.md +++ b/docs/release-notes/version-2.11.md @@ -7,6 +7,7 @@ * [#6100](https://github.com/netbox-community/netbox/issues/6100) - Fix VM interfaces table "add interfaces" link * [#6104](https://github.com/netbox-community/netbox/issues/6104) - Fix location column on racks table * [#6105](https://github.com/netbox-community/netbox/issues/6105) - Hide checkboxes for VMs under cluster VMs view +* [#6106](https://github.com/netbox-community/netbox/issues/6106) - Allow assigning a virtual interface as the parent of an existing interface --- diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 52c2e9ec7..5e322b340 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -3072,10 +3072,7 @@ class InterfaceForm(BootstrapMixin, InterfaceCommonForm, CustomFieldModelForm): parent = DynamicModelChoiceField( queryset=Interface.objects.all(), required=False, - label='Parent interface', - query_params={ - 'kind': 'physical', - } + label='Parent interface' ) lag = DynamicModelChoiceField( queryset=Interface.objects.all(), From 4f7626828a688d1ba25e9e73834568b4e8e943f9 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 7 Apr 2021 16:58:40 -0400 Subject: [PATCH 140/182] Fixes #6107: Fix rack selection field on device form --- docs/release-notes/version-2.11.md | 1 + netbox/dcim/forms.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/release-notes/version-2.11.md b/docs/release-notes/version-2.11.md index c474bd6b3..9de6d2356 100644 --- a/docs/release-notes/version-2.11.md +++ b/docs/release-notes/version-2.11.md @@ -8,6 +8,7 @@ * [#6104](https://github.com/netbox-community/netbox/issues/6104) - Fix location column on racks table * [#6105](https://github.com/netbox-community/netbox/issues/6105) - Hide checkboxes for VMs under cluster VMs view * [#6106](https://github.com/netbox-community/netbox/issues/6106) - Allow assigning a virtual interface as the parent of an existing interface +* [#6107](https://github.com/netbox-community/netbox/issues/6107) - Fix rack selection field on device form --- diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 5e322b340..9437225de 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -2056,7 +2056,7 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): required=False, query_params={ 'site_id': '$site', - 'location_id': 'location', + 'location_id': '$location', } ) position = forms.IntegerField( From 05d8a06cd5f62052d4ed35015906f7ca43a2b0e5 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Thu, 8 Apr 2021 10:08:50 -0400 Subject: [PATCH 141/182] Closes #6109: Add device counts to locations table --- docs/release-notes/version-2.11.md | 4 ++++ netbox/dcim/tables/racks.py | 34 +++--------------------------- netbox/dcim/tables/sites.py | 33 ++++++++++++++++++++++++++++- netbox/dcim/views.py | 10 +++++++-- 4 files changed, 47 insertions(+), 34 deletions(-) diff --git a/docs/release-notes/version-2.11.md b/docs/release-notes/version-2.11.md index 9de6d2356..8183431bc 100644 --- a/docs/release-notes/version-2.11.md +++ b/docs/release-notes/version-2.11.md @@ -2,6 +2,10 @@ ## v2.11.0 (FUTURE) +### Enhancements (from Beta) + +* [#6109](https://github.com/netbox-community/netbox/issues/6109) - Add device counts to locations table + ### Bug Fixes (from Beta) * [#6100](https://github.com/netbox-community/netbox/issues/6100) - Fix VM interfaces table "add interfaces" link diff --git a/netbox/dcim/tables/racks.py b/netbox/dcim/tables/racks.py index b549bad10..3a63eef1e 100644 --- a/netbox/dcim/tables/racks.py +++ b/netbox/dcim/tables/racks.py @@ -1,49 +1,21 @@ import django_tables2 as tables from django_tables2.utils import Accessor -from dcim.models import Rack, Location, RackReservation, RackRole +from dcim.models import Rack, RackReservation, RackRole from tenancy.tables import TenantColumn from utilities.tables import ( - BaseTable, ButtonsColumn, ChoiceFieldColumn, ColorColumn, ColoredLabelColumn, LinkedCountColumn, MPTTColumn, - TagColumn, ToggleColumn, UtilizationColumn, + BaseTable, ButtonsColumn, ChoiceFieldColumn, ColorColumn, ColoredLabelColumn, LinkedCountColumn, TagColumn, + ToggleColumn, UtilizationColumn, ) -from .template_code import LOCATION_ELEVATIONS __all__ = ( 'RackTable', 'RackDetailTable', - 'LocationTable', 'RackReservationTable', 'RackRoleTable', ) -# -# Locations -# - -class LocationTable(BaseTable): - pk = ToggleColumn() - name = MPTTColumn( - linkify=True - ) - site = tables.Column( - linkify=True - ) - rack_count = tables.Column( - verbose_name='Racks' - ) - actions = ButtonsColumn( - model=Location, - prepend_template=LOCATION_ELEVATIONS - ) - - class Meta(BaseTable.Meta): - model = Location - fields = ('pk', 'name', 'site', 'rack_count', 'description', 'slug', 'actions') - default_columns = ('pk', 'name', 'site', 'rack_count', 'description', 'actions') - - # # Rack roles # diff --git a/netbox/dcim/tables/sites.py b/netbox/dcim/tables/sites.py index 288fa5753..b7d46eba5 100644 --- a/netbox/dcim/tables/sites.py +++ b/netbox/dcim/tables/sites.py @@ -1,12 +1,14 @@ import django_tables2 as tables -from dcim.models import Region, Site, SiteGroup +from dcim.models import Location, Region, Site, SiteGroup from tenancy.tables import TenantColumn from utilities.tables import ( BaseTable, ButtonsColumn, ChoiceFieldColumn, LinkedCountColumn, MPTTColumn, TagColumn, ToggleColumn, ) +from .template_code import LOCATION_ELEVATIONS __all__ = ( + 'LocationTable', 'RegionTable', 'SiteTable', 'SiteGroupTable', @@ -86,3 +88,32 @@ class SiteTable(BaseTable): 'contact_email', 'tags', ) default_columns = ('pk', 'name', 'status', 'facility', 'region', 'group', 'tenant', 'asn', 'description') + + +# +# Locations +# + +class LocationTable(BaseTable): + pk = ToggleColumn() + name = MPTTColumn( + linkify=True + ) + site = tables.Column( + linkify=True + ) + rack_count = tables.Column( + verbose_name='Racks' + ) + device_count = tables.Column( + verbose_name='Devices' + ) + actions = ButtonsColumn( + model=Location, + prepend_template=LOCATION_ELEVATIONS + ) + + class Meta(BaseTable.Meta): + model = Location + fields = ('pk', 'name', 'site', 'rack_count', 'device_count', 'description', 'slug', 'actions') + default_columns = ('pk', 'name', 'site', 'rack_count', 'device_count', 'description', 'actions') diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index d892c0823..467382a7b 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -338,12 +338,18 @@ class SiteBulkDeleteView(generic.BulkDeleteView): # -# Rack groups +# Locations # class LocationListView(generic.ObjectListView): queryset = Location.objects.add_related_count( - Location.objects.all(), + Location.objects.add_related_count( + Location.objects.all(), + Device, + 'location', + 'device_count', + cumulative=True + ), Rack, 'location', 'rack_count', From 54d9ca8ed878c05744008f2499682374e165292a Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Thu, 8 Apr 2021 10:10:46 -0400 Subject: [PATCH 142/182] Add racks count to location view --- netbox/templates/dcim/location.html | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/netbox/templates/dcim/location.html b/netbox/templates/dcim/location.html index 0371eeef4..0efb74244 100644 --- a/netbox/templates/dcim/location.html +++ b/netbox/templates/dcim/location.html @@ -40,6 +40,12 @@ {% endif %} + + Racks + + {{ object.racks.count }} + + Devices From d6fcd22752ef510b0fdf602cb48889c6f5001ffb Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Thu, 8 Apr 2021 10:30:13 -0400 Subject: [PATCH 143/182] Fixes #6110: Fix handling of TemplateColumn values for table export --- docs/release-notes/version-2.11.md | 1 + netbox/utilities/tables.py | 7 ++++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/release-notes/version-2.11.md b/docs/release-notes/version-2.11.md index 8183431bc..5038cf299 100644 --- a/docs/release-notes/version-2.11.md +++ b/docs/release-notes/version-2.11.md @@ -13,6 +13,7 @@ * [#6105](https://github.com/netbox-community/netbox/issues/6105) - Hide checkboxes for VMs under cluster VMs view * [#6106](https://github.com/netbox-community/netbox/issues/6106) - Allow assigning a virtual interface as the parent of an existing interface * [#6107](https://github.com/netbox-community/netbox/issues/6107) - Fix rack selection field on device form +* [#6110](https://github.com/netbox-community/netbox/issues/6110) - Fix handling of TemplateColumn values for table export --- diff --git a/netbox/utilities/tables.py b/netbox/utilities/tables.py index 7e9cc9c30..5bb5e9bbf 100644 --- a/netbox/utilities/tables.py +++ b/netbox/utilities/tables.py @@ -15,15 +15,16 @@ from extras.models import CustomField from .paginator import EnhancedPaginator, get_paginate_count -def stripped_value(self, value): +def stripped_value(self, **kwargs): """ Replaces TemplateColumn's value() method to both strip HTML tags and remove any leading/trailing whitespace. """ - return strip_tags(value).strip() + html = super(tables.TemplateColumn, self).value(**kwargs) + return strip_tags(html).strip() if isinstance(html, str) else html # TODO: We're monkey-patching TemplateColumn here to strip leading/trailing whitespace. This will no longer -# be necessary if django-tables2 PR #794 is accepted. (See #5926) +# be necessary under django-tables2 v2.3.5+. (See #5926) tables.TemplateColumn.value = stripped_value From 696b5c80a7555055359808f187993bf3703806ec Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Thu, 8 Apr 2021 13:25:29 -0400 Subject: [PATCH 144/182] Closes #6097: Redirect old slug-based object views --- docs/release-notes/version-2.11.md | 1 + netbox/circuits/urls.py | 2 ++ netbox/dcim/urls.py | 2 ++ netbox/tenancy/urls.py | 2 ++ netbox/utilities/views.py | 13 +++++++++++++ 5 files changed, 20 insertions(+) diff --git a/docs/release-notes/version-2.11.md b/docs/release-notes/version-2.11.md index 5038cf299..356577d09 100644 --- a/docs/release-notes/version-2.11.md +++ b/docs/release-notes/version-2.11.md @@ -4,6 +4,7 @@ ### Enhancements (from Beta) +* [#6097](https://github.com/netbox-community/netbox/issues/6097) - Redirect old slug-based object views * [#6109](https://github.com/netbox-community/netbox/issues/6109) - Add device counts to locations table ### Bug Fixes (from Beta) diff --git a/netbox/circuits/urls.py b/netbox/circuits/urls.py index e634eeeb4..1cea1965e 100644 --- a/netbox/circuits/urls.py +++ b/netbox/circuits/urls.py @@ -2,6 +2,7 @@ from django.urls import path from dcim.views import CableCreateView, PathTraceView from extras.views import ObjectChangeLogView, ObjectJournalView +from utilities.views import SlugRedirectView from . import views from .models import * @@ -15,6 +16,7 @@ urlpatterns = [ path('providers/edit/', views.ProviderBulkEditView.as_view(), name='provider_bulk_edit'), path('providers/delete/', views.ProviderBulkDeleteView.as_view(), name='provider_bulk_delete'), path('providers//', views.ProviderView.as_view(), name='provider'), + path('providers//', SlugRedirectView.as_view(), kwargs={'model': Provider}), path('providers//edit/', views.ProviderEditView.as_view(), name='provider_edit'), path('providers//delete/', views.ProviderDeleteView.as_view(), name='provider_delete'), path('providers//changelog/', ObjectChangeLogView.as_view(), name='provider_changelog', kwargs={'model': Provider}), diff --git a/netbox/dcim/urls.py b/netbox/dcim/urls.py index b23603c97..3c84aae01 100644 --- a/netbox/dcim/urls.py +++ b/netbox/dcim/urls.py @@ -2,6 +2,7 @@ from django.urls import path from extras.views import ImageAttachmentEditView, ObjectChangeLogView, ObjectJournalView from ipam.views import ServiceEditView +from utilities.views import SlugRedirectView from . import views from .models import * @@ -37,6 +38,7 @@ urlpatterns = [ path('sites/edit/', views.SiteBulkEditView.as_view(), name='site_bulk_edit'), path('sites/delete/', views.SiteBulkDeleteView.as_view(), name='site_bulk_delete'), path('sites//', views.SiteView.as_view(), name='site'), + path('sites//', SlugRedirectView.as_view(), kwargs={'model': Site}), path('sites//edit/', views.SiteEditView.as_view(), name='site_edit'), path('sites//delete/', views.SiteDeleteView.as_view(), name='site_delete'), path('sites//changelog/', ObjectChangeLogView.as_view(), name='site_changelog', kwargs={'model': Site}), diff --git a/netbox/tenancy/urls.py b/netbox/tenancy/urls.py index a3db431da..a1f46c7ec 100644 --- a/netbox/tenancy/urls.py +++ b/netbox/tenancy/urls.py @@ -1,6 +1,7 @@ from django.urls import path from extras.views import ObjectChangeLogView, ObjectJournalView +from utilities.views import SlugRedirectView from . import views from .models import Tenant, TenantGroup @@ -25,6 +26,7 @@ urlpatterns = [ path('tenants/edit/', views.TenantBulkEditView.as_view(), name='tenant_bulk_edit'), path('tenants/delete/', views.TenantBulkDeleteView.as_view(), name='tenant_bulk_delete'), path('tenants//', views.TenantView.as_view(), name='tenant'), + path('tenants//', SlugRedirectView.as_view(), kwargs={'model': Tenant}), path('tenants//edit/', views.TenantEditView.as_view(), name='tenant_edit'), path('tenants//delete/', views.TenantDeleteView.as_view(), name='tenant_delete'), path('tenants//changelog/', ObjectChangeLogView.as_view(), name='tenant_changelog', kwargs={'model': Tenant}), diff --git a/netbox/utilities/views.py b/netbox/utilities/views.py index c291a3cf2..a3afcb1c6 100644 --- a/netbox/utilities/views.py +++ b/netbox/utilities/views.py @@ -1,8 +1,10 @@ from django.contrib.auth.mixins import AccessMixin from django.core.exceptions import ImproperlyConfigured +from django.shortcuts import get_object_or_404, redirect from django.urls import reverse from django.urls.exceptions import NoReverseMatch from django.utils.http import is_safe_url +from django.views.generic import View from .permissions import resolve_permission @@ -123,3 +125,14 @@ class GetReturnURLMixin: # If all else fails, return home. Ideally this should never happen. return reverse('home') + + +# +# Views +# + +class SlugRedirectView(View): + + def get(self, request, model, slug): + obj = get_object_or_404(model.objects.restrict(request.user, 'view'), slug=slug) + return redirect(obj.get_absolute_url()) From 03b3f5937f49324db0639c73d5242cd0e9c2a548 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Thu, 8 Apr 2021 13:50:06 -0400 Subject: [PATCH 145/182] Fixes #6108: Do not infer tenant assignment from parent objects for prefixes, IP addresses --- docs/release-notes/version-2.10.md | 1 + netbox/ipam/tables.py | 28 ++++++++-------------------- 2 files changed, 9 insertions(+), 20 deletions(-) diff --git a/docs/release-notes/version-2.10.md b/docs/release-notes/version-2.10.md index f895e43f7..36c9d3276 100644 --- a/docs/release-notes/version-2.10.md +++ b/docs/release-notes/version-2.10.md @@ -14,6 +14,7 @@ * [#5805](https://github.com/netbox-community/netbox/issues/5805) - Fix missing custom field filters for cables, rack reservations * [#6073](https://github.com/netbox-community/netbox/issues/6073) - Permit users to manage their own REST API tokens without needing explicit permission * [#6081](https://github.com/netbox-community/netbox/issues/6081) - Fix interface connections REST API endpoint +* [#6108](https://github.com/netbox-community/netbox/issues/6108) - Do not infer tenant assignment from parent objects for prefixes, IP addresses --- diff --git a/netbox/ipam/tables.py b/netbox/ipam/tables.py index 04a5d130c..b16e180bd 100644 --- a/netbox/ipam/tables.py +++ b/netbox/ipam/tables.py @@ -109,18 +109,6 @@ VLAN_MEMBER_TAGGED = """ {% endif %} """ -TENANT_LINK = """ -{% if record.tenant %} - {{ record.tenant }} -{% elif record.vrf.tenant %} - {{ record.vrf.tenant }}* -{% elif object.tenant %} - {{ object.tenant }} -{% else %} - — -{% endif %} -""" - # # VRFs @@ -210,8 +198,8 @@ class AggregateTable(BaseTable): prefix = tables.LinkColumn( verbose_name='Aggregate' ) - tenant = tables.TemplateColumn( - template_code=TENANT_LINK + tenant = tables.Column( + linkify=True ) date_added = tables.DateColumn( format="Y-m-d", @@ -281,8 +269,8 @@ class PrefixTable(BaseTable): template_code=VRF_LINK, verbose_name='VRF' ) - tenant = tables.TemplateColumn( - template_code=TENANT_LINK + tenant = tables.Column( + linkify=True ) site = tables.Column( linkify=True @@ -349,8 +337,8 @@ class IPAddressTable(BaseTable): default=AVAILABLE_LABEL ) role = ChoiceFieldColumn() - tenant = tables.TemplateColumn( - template_code=TENANT_LINK + tenant = tables.Column( + linkify=True ) assigned_object = tables.Column( linkify=True, @@ -430,8 +418,8 @@ class InterfaceIPAddressTable(BaseTable): verbose_name='VRF' ) status = ChoiceFieldColumn() - tenant = tables.TemplateColumn( - template_code=TENANT_LINK + tenant = tables.Column( + linkify=True ) actions = ButtonsColumn( model=IPAddress From f096c4a5d043033d3fef32ae8bf7f0920a6e7abd Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Thu, 8 Apr 2021 14:18:07 -0400 Subject: [PATCH 146/182] #6081: Tweak queryset filtering --- netbox/dcim/api/views.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index 9a090eddc..3533f0230 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -581,7 +581,8 @@ class PowerConnectionViewSet(ListModelMixin, GenericViewSet): class InterfaceConnectionViewSet(ListModelMixin, GenericViewSet): queryset = Interface.objects.prefetch_related('device', '_path').filter( # Avoid duplicate connections by only selecting the lower PK in a connected pair - _path__destination_type=ContentType.objects.get_by_natural_key('dcim', 'interface'), + _path__destination_type__app_label='dcim', + _path__destination_type__model='interface', _path__destination_id__isnull=False, pk__lt=F('_path__destination_id') ) From e69251b21ae670ed8de857e3a03c20fedda42b70 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Thu, 8 Apr 2021 14:22:45 -0400 Subject: [PATCH 147/182] Fixes #6070: Add missing 'count_ipaddresses' attribute to VMInterface serializer --- docs/release-notes/version-2.10.md | 1 + netbox/dcim/models/device_components.py | 8 ++++---- netbox/virtualization/api/serializers.py | 3 ++- netbox/virtualization/api/views.py | 2 +- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/docs/release-notes/version-2.10.md b/docs/release-notes/version-2.10.md index 36c9d3276..fad8f218b 100644 --- a/docs/release-notes/version-2.10.md +++ b/docs/release-notes/version-2.10.md @@ -12,6 +12,7 @@ ### Bug Fixes * [#5805](https://github.com/netbox-community/netbox/issues/5805) - Fix missing custom field filters for cables, rack reservations +* [#6070](https://github.com/netbox-community/netbox/issues/6070) - Add missing `count_ipaddresses` attribute to VMInterface serializer * [#6073](https://github.com/netbox-community/netbox/issues/6073) - Permit users to manage their own REST API tokens without needing explicit permission * [#6081](https://github.com/netbox-community/netbox/issues/6081) - Fix interface connections REST API endpoint * [#6108](https://github.com/netbox-community/netbox/issues/6108) - Do not infer tenant assignment from parent objects for prefixes, IP addresses diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index 40063234f..1b997ec07 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -478,6 +478,10 @@ class BaseInterface(models.Model): return super().save(*args, **kwargs) + @property + def count_ipaddresses(self): + return self.ip_addresses.count() + @extras_features('export_templates', 'webhooks', 'custom_links') class Interface(CableTermination, PathEndpoint, ComponentModel, BaseInterface): @@ -615,10 +619,6 @@ class Interface(CableTermination, PathEndpoint, ComponentModel, BaseInterface): def is_lag(self): return self.type == InterfaceTypeChoices.TYPE_LAG - @property - def count_ipaddresses(self): - return self.ip_addresses.count() - # # Pass-through ports diff --git a/netbox/virtualization/api/serializers.py b/netbox/virtualization/api/serializers.py index 518b7086c..a6e75e506 100644 --- a/netbox/virtualization/api/serializers.py +++ b/netbox/virtualization/api/serializers.py @@ -109,12 +109,13 @@ class VMInterfaceSerializer(TaggedObjectSerializer, ValidatedModelSerializer): required=False, many=True ) + count_ipaddresses = serializers.IntegerField(read_only=True) class Meta: model = VMInterface fields = [ 'id', 'url', 'virtual_machine', 'name', 'enabled', 'mtu', 'mac_address', 'description', 'mode', - 'untagged_vlan', 'tagged_vlans', 'tags', + 'untagged_vlan', 'tagged_vlans', 'tags', 'count_ipaddresses', ] def validate(self, data): diff --git a/netbox/virtualization/api/views.py b/netbox/virtualization/api/views.py index 586ad5028..1bc40c2de 100644 --- a/netbox/virtualization/api/views.py +++ b/netbox/virtualization/api/views.py @@ -80,7 +80,7 @@ class VirtualMachineViewSet(ConfigContextQuerySetMixin, CustomFieldModelViewSet) class VMInterfaceViewSet(ModelViewSet): queryset = VMInterface.objects.prefetch_related( - 'virtual_machine', 'tags', 'tagged_vlans' + 'virtual_machine', 'tags', 'tagged_vlans', 'ip_addresses', ) serializer_class = serializers.VMInterfaceSerializer filterset_class = filters.VMInterfaceFilterSet From 9e62d1ad8f864802cd627ba5e62a3a6e9d9b9422 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Fri, 9 Apr 2021 09:43:35 -0400 Subject: [PATCH 148/182] Fixes #6130: Improve display of assigned models in custom fields list --- docs/release-notes/version-2.11.md | 1 + netbox/extras/admin.py | 5 ++++- netbox/utilities/forms/fields.py | 4 ++-- netbox/utilities/utils.py | 8 ++++++++ 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/docs/release-notes/version-2.11.md b/docs/release-notes/version-2.11.md index 356577d09..ec53dee25 100644 --- a/docs/release-notes/version-2.11.md +++ b/docs/release-notes/version-2.11.md @@ -15,6 +15,7 @@ * [#6106](https://github.com/netbox-community/netbox/issues/6106) - Allow assigning a virtual interface as the parent of an existing interface * [#6107](https://github.com/netbox-community/netbox/issues/6107) - Fix rack selection field on device form * [#6110](https://github.com/netbox-community/netbox/issues/6110) - Fix handling of TemplateColumn values for table export +* [#6130](https://github.com/netbox-community/netbox/issues/6130) - Improve display of assigned models in custom fields list --- diff --git a/netbox/extras/admin.py b/netbox/extras/admin.py index 90e7541e2..0ceb1cc5b 100644 --- a/netbox/extras/admin.py +++ b/netbox/extras/admin.py @@ -1,8 +1,10 @@ from django import forms from django.contrib import admin from django.contrib.contenttypes.models import ContentType +from django.utils.safestring import mark_safe from utilities.forms import ContentTypeChoiceField, ContentTypeMultipleChoiceField, LaxURLField +from utilities.utils import content_type_name from .models import CustomField, CustomLink, ExportTemplate, JobResult, Webhook from .utils import FeatureQuery @@ -110,7 +112,8 @@ class CustomFieldAdmin(admin.ModelAdmin): ) def models(self, obj): - return ', '.join([ct.name for ct in obj.content_types.all()]) + ct_names = [content_type_name(ct) for ct in obj.content_types.all()] + return mark_safe('
'.join(ct_names)) # diff --git a/netbox/utilities/forms/fields.py b/netbox/utilities/forms/fields.py index bb74ead99..9bc0e3df7 100644 --- a/netbox/utilities/forms/fields.py +++ b/netbox/utilities/forms/fields.py @@ -13,6 +13,7 @@ from django.forms import BoundField from django.urls import reverse from utilities.choices import unpack_grouped_choices +from utilities.utils import content_type_name from utilities.validators import EnhancedURLValidator from . import widgets from .constants import * @@ -124,8 +125,7 @@ class ContentTypeChoiceMixin: def label_from_instance(self, obj): try: - meta = obj.model_class()._meta - return f'{meta.app_config.verbose_name} > {meta.verbose_name}' + return content_type_name(obj) except AttributeError: return super().label_from_instance(obj) diff --git a/netbox/utilities/utils.py b/netbox/utilities/utils.py index f0340f094..e21b9a36d 100644 --- a/netbox/utilities/utils.py +++ b/netbox/utilities/utils.py @@ -296,6 +296,14 @@ def array_to_string(array): return ', '.join('-'.join(map(str, (g[0], g[-1])[:len(g)])) for g in group) +def content_type_name(contenttype): + """ + Return a proper ContentType name. + """ + meta = contenttype.model_class()._meta + return f'{meta.app_config.verbose_name} > {meta.verbose_name}' + + # # Fake request object # From 6efe54aa88cdb759decc2aca76fb4551aac4e2be Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Fri, 9 Apr 2021 09:47:34 -0400 Subject: [PATCH 149/182] Closes #6125: Add locations count to home page --- docs/release-notes/version-2.11.md | 1 + netbox/netbox/views/__init__.py | 3 ++- netbox/templates/home.html | 12 +++++++++++- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/docs/release-notes/version-2.11.md b/docs/release-notes/version-2.11.md index ec53dee25..d2ccb5282 100644 --- a/docs/release-notes/version-2.11.md +++ b/docs/release-notes/version-2.11.md @@ -6,6 +6,7 @@ * [#6097](https://github.com/netbox-community/netbox/issues/6097) - Redirect old slug-based object views * [#6109](https://github.com/netbox-community/netbox/issues/6109) - Add device counts to locations table +* [#6125](https://github.com/netbox-community/netbox/issues/6125) - Add locations count to home page ### Bug Fixes (from Beta) diff --git a/netbox/netbox/views/__init__.py b/netbox/netbox/views/__init__.py index 5406e7206..5cf560ada 100644 --- a/netbox/netbox/views/__init__.py +++ b/netbox/netbox/views/__init__.py @@ -16,7 +16,7 @@ from packaging import version from circuits.models import Circuit, Provider from dcim.models import ( - Cable, ConsolePort, Device, DeviceType, Interface, PowerPanel, PowerFeed, PowerPort, Rack, Site, + Cable, ConsolePort, Device, DeviceType, Interface, Location, PowerPanel, PowerFeed, PowerPort, Rack, Site, ) from extras.choices import JobResultStatusChoices from extras.models import ObjectChange, JobResult @@ -56,6 +56,7 @@ class HomeView(View): # Organization 'site_count': Site.objects.restrict(request.user, 'view').count(), + 'location_count': Location.objects.restrict(request.user, 'view').count(), 'tenant_count': Tenant.objects.restrict(request.user, 'view').count(), # DCIM diff --git a/netbox/templates/home.html b/netbox/templates/home.html index e9180eca3..273a78bc9 100644 --- a/netbox/templates/home.html +++ b/netbox/templates/home.html @@ -31,7 +31,17 @@

Sites

{% endif %} -

Geographic locations

+

Discrete points of presence

+ +
+ {% if perms.dcim.view_location %} + {{ stats.location_count }} +

Locations

+ {% else %} + +

Locations

+ {% endif %} +

Locations within sites

{% if perms.tenancy.view_tenant %} From 7439faad34301eb3dc546aea79d42ea79c4e6fe9 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Fri, 9 Apr 2021 09:56:36 -0400 Subject: [PATCH 150/182] Fixes #6123: Prevent device from being assigned to mismatched site and location --- docs/release-notes/version-2.11.md | 1 + netbox/dcim/models/devices.py | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/docs/release-notes/version-2.11.md b/docs/release-notes/version-2.11.md index d2ccb5282..6a6b7f78a 100644 --- a/docs/release-notes/version-2.11.md +++ b/docs/release-notes/version-2.11.md @@ -16,6 +16,7 @@ * [#6106](https://github.com/netbox-community/netbox/issues/6106) - Allow assigning a virtual interface as the parent of an existing interface * [#6107](https://github.com/netbox-community/netbox/issues/6107) - Fix rack selection field on device form * [#6110](https://github.com/netbox-community/netbox/issues/6110) - Fix handling of TemplateColumn values for table export +* [#6123](https://github.com/netbox-community/netbox/issues/6123) - Prevent device from being assigned to mismatched site and location * [#6130](https://github.com/netbox-community/netbox/issues/6130) - Improve display of assigned models in custom fields list --- diff --git a/netbox/dcim/models/devices.py b/netbox/dcim/models/devices.py index a5efadac5..551fac2d4 100644 --- a/netbox/dcim/models/devices.py +++ b/netbox/dcim/models/devices.py @@ -652,6 +652,10 @@ class Device(PrimaryModel, ConfigContextModel): raise ValidationError({ 'rack': f"Rack {self.rack} does not belong to site {self.site}.", }) + if self.location and self.site != self.location.site: + raise ValidationError({ + 'location': f"Location {self.location} does not belong to site {self.site}.", + }) if self.rack and self.location and self.rack.location != self.location: raise ValidationError({ 'rack': f"Rack {self.rack} does not belong to location {self.location}.", From a3721a94ce4f13500d98eced74b7d5a4b84817d3 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Fri, 9 Apr 2021 10:53:05 -0400 Subject: [PATCH 151/182] Closes #6121: Extend parent interface assignment to VM interfaces --- docs/release-notes/version-2.11.md | 5 ++- .../virtualization/virtualmachine/base.html | 2 +- .../templates/virtualization/vminterface.html | 23 +++++++++-- .../virtualization/vminterface_edit.html | 1 + netbox/virtualization/api/serializers.py | 5 ++- netbox/virtualization/api/views.py | 2 +- netbox/virtualization/filters.py | 5 +++ netbox/virtualization/forms.py | 39 ++++++++++++++++--- .../migrations/0022_vminterface_parent.py | 17 ++++++++ netbox/virtualization/models.py | 16 ++++++++ netbox/virtualization/tables.py | 7 +++- netbox/virtualization/tests/test_filters.py | 15 ++++++- netbox/virtualization/views.py | 9 +++++ 13 files changed, 128 insertions(+), 18 deletions(-) create mode 100644 netbox/virtualization/migrations/0022_vminterface_parent.py diff --git a/docs/release-notes/version-2.11.md b/docs/release-notes/version-2.11.md index 6a6b7f78a..cd75d141b 100644 --- a/docs/release-notes/version-2.11.md +++ b/docs/release-notes/version-2.11.md @@ -6,6 +6,7 @@ * [#6097](https://github.com/netbox-community/netbox/issues/6097) - Redirect old slug-based object views * [#6109](https://github.com/netbox-community/netbox/issues/6109) - Add device counts to locations table +* [#6121](https://github.com/netbox-community/netbox/issues/6121) - Extend parent interface assignment to VM interfaces * [#6125](https://github.com/netbox-community/netbox/issues/6125) - Add locations count to home page ### Bug Fixes (from Beta) @@ -44,7 +45,7 @@ NetBox now supports journaling for all primary objects. The journal is a collect #### Parent Interface Assignments ([#1519](https://github.com/netbox-community/netbox/issues/1519)) -Virtual interfaces can now be assigned to a "parent" physical interface by setting the `parent` field on the interface object. This is helpful for associating subinterfaces with their physical counterpart. For example, you might assign virtual interfaces Gi0/0.100 and Gi0/0.200 as children of the physical interface Gi0/0. +Virtual device and VM interfaces can now be assigned to a "parent" interface by setting the `parent` field on the interface object. This is helpful for associating subinterfaces with their physical counterpart. For example, you might assign virtual interfaces Gi0/0.100 and Gi0/0.200 as children of the physical interface Gi0/0. #### Pre- and Post-Change Snapshots in Webhooks ([#3451](https://github.com/netbox-community/netbox/issues/3451)) @@ -186,3 +187,5 @@ A new provider network model has been introduced to represent the boundary of a * Dropped the `site` foreign key field * virtualization.VirtualMachine * `vcpus` has been changed from an integer to a decimal value +* virtualization.VMInterface + * Added the `parent` field diff --git a/netbox/templates/virtualization/virtualmachine/base.html b/netbox/templates/virtualization/virtualmachine/base.html index 88f7da1de..4d4f894a8 100644 --- a/netbox/templates/virtualization/virtualmachine/base.html +++ b/netbox/templates/virtualization/virtualmachine/base.html @@ -12,7 +12,7 @@ {% block buttons %} {% if perms.virtualization.add_vminterface %} - + Add Interfaces {% endif %} diff --git a/netbox/templates/virtualization/vminterface.html b/netbox/templates/virtualization/vminterface.html index e574e926e..7141dcff1 100644 --- a/netbox/templates/virtualization/vminterface.html +++ b/netbox/templates/virtualization/vminterface.html @@ -39,6 +39,16 @@ {% endif %} + + Parent + + {% if object.parent %} + {{ object.parent }} + {% else %} + None + {% endif %} + + Description {{ object.description|placeholder }} @@ -91,9 +101,14 @@ {% include 'panel_table.html' with table=vlan_table heading="VLANs" %}
-
-
- {% plugin_full_width_page object %} -
+
+
+ {% include 'panel_table.html' with table=child_interfaces_table heading="Child Interfaces" %}
+
+
+
+ {% plugin_full_width_page object %} +
+
{% endblock %} diff --git a/netbox/templates/virtualization/vminterface_edit.html b/netbox/templates/virtualization/vminterface_edit.html index c0ad6e98c..f3ab4f9c2 100644 --- a/netbox/templates/virtualization/vminterface_edit.html +++ b/netbox/templates/virtualization/vminterface_edit.html @@ -17,6 +17,7 @@ {% endif %} {% render_field form.name %} {% render_field form.enabled %} + {% render_field form.parent %} {% render_field form.mac_address %} {% render_field form.mtu %} {% render_field form.description %} diff --git a/netbox/virtualization/api/serializers.py b/netbox/virtualization/api/serializers.py index 0afa8f796..a1428f0cd 100644 --- a/netbox/virtualization/api/serializers.py +++ b/netbox/virtualization/api/serializers.py @@ -106,6 +106,7 @@ class VirtualMachineWithConfigContextSerializer(VirtualMachineSerializer): class VMInterfaceSerializer(PrimaryModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:vminterface-detail') virtual_machine = NestedVirtualMachineSerializer() + parent = NestedVMInterfaceSerializer(required=False, allow_null=True) mode = ChoiceField(choices=InterfaceModeChoices, allow_blank=True, required=False) untagged_vlan = NestedVLANSerializer(required=False, allow_null=True) tagged_vlans = SerializedPKRelatedField( @@ -118,8 +119,8 @@ class VMInterfaceSerializer(PrimaryModelSerializer): class Meta: model = VMInterface fields = [ - 'id', 'url', 'display', 'virtual_machine', 'name', 'enabled', 'mtu', 'mac_address', 'description', 'mode', - 'untagged_vlan', 'tagged_vlans', 'tags', 'custom_fields', 'created', 'last_updated', + 'id', 'url', 'display', 'virtual_machine', 'name', 'enabled', 'parent', 'mtu', 'mac_address', 'description', + 'mode', 'untagged_vlan', 'tagged_vlans', 'tags', 'custom_fields', 'created', 'last_updated', ] def validate(self, data): diff --git a/netbox/virtualization/api/views.py b/netbox/virtualization/api/views.py index ea2b33e4f..5f67a7c74 100644 --- a/netbox/virtualization/api/views.py +++ b/netbox/virtualization/api/views.py @@ -80,7 +80,7 @@ class VirtualMachineViewSet(ConfigContextQuerySetMixin, CustomFieldModelViewSet) class VMInterfaceViewSet(ModelViewSet): queryset = VMInterface.objects.prefetch_related( - 'virtual_machine', 'tags', 'tagged_vlans' + 'virtual_machine', 'parent', 'tags', 'tagged_vlans' ) serializer_class = serializers.VMInterfaceSerializer filterset_class = filters.VMInterfaceFilterSet diff --git a/netbox/virtualization/filters.py b/netbox/virtualization/filters.py index be9b70749..d710bcbe2 100644 --- a/netbox/virtualization/filters.py +++ b/netbox/virtualization/filters.py @@ -264,6 +264,11 @@ class VMInterfaceFilterSet(BaseFilterSet, CustomFieldModelFilterSet, CreatedUpda to_field_name='name', label='Virtual machine', ) + parent_id = django_filters.ModelMultipleChoiceFilter( + field_name='parent', + queryset=VMInterface.objects.all(), + label='Parent interface (ID)', + ) mac_address = MultiValueMACAddressFilter( label='MAC address', ) diff --git a/netbox/virtualization/forms.py b/netbox/virtualization/forms.py index 014b73542..a5c671287 100644 --- a/netbox/virtualization/forms.py +++ b/netbox/virtualization/forms.py @@ -603,6 +603,11 @@ class VirtualMachineFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFil # class VMInterfaceForm(BootstrapMixin, InterfaceCommonForm, CustomFieldModelForm): + parent = DynamicModelChoiceField( + queryset=VMInterface.objects.all(), + required=False, + label='Parent interface' + ) untagged_vlan = DynamicModelChoiceField( queryset=VLAN.objects.all(), required=False, @@ -621,8 +626,8 @@ class VMInterfaceForm(BootstrapMixin, InterfaceCommonForm, CustomFieldModelForm) class Meta: model = VMInterface fields = [ - 'virtual_machine', 'name', 'enabled', 'mac_address', 'mtu', 'description', 'mode', 'tags', 'untagged_vlan', - 'tagged_vlans', + 'virtual_machine', 'name', 'enabled', 'parent', 'mac_address', 'mtu', 'description', 'mode', 'tags', + 'untagged_vlan', 'tagged_vlans', ] widgets = { 'virtual_machine': forms.HiddenInput(), @@ -637,9 +642,12 @@ class VMInterfaceForm(BootstrapMixin, InterfaceCommonForm, CustomFieldModelForm) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + vm_id = self.initial.get('virtual_machine') or self.data.get('virtual_machine') + + # Restrict parent interface assignment by VM + self.fields['parent'].widget.add_query_param('virtualmachine_id', vm_id) # Limit VLAN choices by virtual machine - vm_id = self.initial.get('virtual_machine') or self.data.get('virtual_machine') self.fields['untagged_vlan'].widget.add_query_param('available_on_virtualmachine', vm_id) self.fields['tagged_vlans'].widget.add_query_param('available_on_virtualmachine', vm_id) @@ -655,6 +663,14 @@ class VMInterfaceCreateForm(BootstrapMixin, InterfaceCommonForm): required=False, initial=True ) + parent = DynamicModelChoiceField( + queryset=VMInterface.objects.all(), + required=False, + display_field='display_name', + query_params={ + 'virtualmachine_id': 'virtual_machine', + } + ) mtu = forms.IntegerField( required=False, min_value=INTERFACE_MTU_MIN, @@ -689,9 +705,12 @@ class VMInterfaceCreateForm(BootstrapMixin, InterfaceCommonForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + vm_id = self.initial.get('virtual_machine') or self.data.get('virtual_machine') + + # Restrict parent interface assignment by VM + self.fields['parent'].widget.add_query_param('virtualmachine_id', vm_id) # Limit VLAN choices by virtual machine - vm_id = self.initial.get('virtual_machine') or self.data.get('virtual_machine') self.fields['untagged_vlan'].widget.add_query_param('available_on_virtualmachine', vm_id) self.fields['tagged_vlans'].widget.add_query_param('available_on_virtualmachine', vm_id) @@ -730,6 +749,11 @@ class VMInterfaceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm): disabled=True, widget=forms.HiddenInput() ) + parent = DynamicModelChoiceField( + queryset=VMInterface.objects.all(), + required=False, + display_field='display_name' + ) enabled = forms.NullBooleanField( required=False, widget=BulkEditNullBooleanSelect() @@ -760,14 +784,17 @@ class VMInterfaceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm): class Meta: nullable_fields = [ - 'mtu', 'description', + 'parent', 'mtu', 'description', ] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + vm_id = self.initial.get('virtual_machine') or self.data.get('virtual_machine') + + # Restrict parent interface assignment by VM + self.fields['parent'].widget.add_query_param('virtualmachine_id', vm_id) # Limit VLAN choices by virtual machine - vm_id = self.initial.get('virtual_machine') or self.data.get('virtual_machine') self.fields['untagged_vlan'].widget.add_query_param('available_on_virtualmachine', vm_id) self.fields['tagged_vlans'].widget.add_query_param('available_on_virtualmachine', vm_id) diff --git a/netbox/virtualization/migrations/0022_vminterface_parent.py b/netbox/virtualization/migrations/0022_vminterface_parent.py new file mode 100644 index 000000000..d1249985f --- /dev/null +++ b/netbox/virtualization/migrations/0022_vminterface_parent.py @@ -0,0 +1,17 @@ +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('virtualization', '0021_virtualmachine_vcpus_decimal'), + ] + + operations = [ + migrations.AddField( + model_name='vminterface', + name='parent', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='child_interfaces', to='virtualization.vminterface'), + ), + ] diff --git a/netbox/virtualization/models.py b/netbox/virtualization/models.py index a34d09662..ee8b3e62b 100644 --- a/netbox/virtualization/models.py +++ b/netbox/virtualization/models.py @@ -395,6 +395,14 @@ class VMInterface(PrimaryModel, BaseInterface): max_length=200, blank=True ) + parent = models.ForeignKey( + to='self', + on_delete=models.SET_NULL, + related_name='child_interfaces', + null=True, + blank=True, + verbose_name='Parent interface' + ) untagged_vlan = models.ForeignKey( to='ipam.VLAN', on_delete=models.SET_NULL, @@ -438,6 +446,7 @@ class VMInterface(PrimaryModel, BaseInterface): self.virtual_machine.name, self.name, self.enabled, + self.parent.name if self.parent else None, self.mac_address, self.mtu, self.description, @@ -447,6 +456,13 @@ class VMInterface(PrimaryModel, BaseInterface): def clean(self): super().clean() + # An interface's parent must belong to the same virtual machine + if self.parent and self.parent.virtual_machine != self.virtual_machine: + raise ValidationError({ + 'parent': f"The selected parent interface ({self.parent}) belongs to a different virtual machine " + f"({self.parent.virtual_machine})." + }) + # Validate untagged VLAN if self.untagged_vlan and self.untagged_vlan.site not in [self.virtual_machine.site, None]: raise ValidationError({ diff --git a/netbox/virtualization/tables.py b/netbox/virtualization/tables.py index c30f14165..65bd2b5d1 100644 --- a/netbox/virtualization/tables.py +++ b/netbox/virtualization/tables.py @@ -170,6 +170,9 @@ class VMInterfaceTable(BaseInterfaceTable): name = tables.Column( linkify=True ) + parent = tables.Column( + linkify=True + ) tags = TagColumn( url_name='virtualization:vminterface_list' ) @@ -177,10 +180,10 @@ class VMInterfaceTable(BaseInterfaceTable): class Meta(BaseTable.Meta): model = VMInterface fields = ( - 'pk', 'virtual_machine', 'name', 'enabled', 'mac_address', 'mtu', 'mode', 'description', 'tags', + 'pk', 'virtual_machine', 'name', 'enabled', 'parent', 'mac_address', 'mtu', 'mode', 'description', 'tags', 'ip_addresses', 'untagged_vlan', 'tagged_vlans', ) - default_columns = ('pk', 'virtual_machine', 'name', 'enabled', 'description') + default_columns = ('pk', 'virtual_machine', 'name', 'enabled', 'parent', 'description') class VirtualMachineVMInterfaceTable(VMInterfaceTable): diff --git a/netbox/virtualization/tests/test_filters.py b/netbox/virtualization/tests/test_filters.py index e822d8763..c11423663 100644 --- a/netbox/virtualization/tests/test_filters.py +++ b/netbox/virtualization/tests/test_filters.py @@ -453,12 +453,25 @@ class VMInterfaceTestCase(TestCase): params = {'name': ['Interface 1', 'Interface 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) - def test_assigned_to_interface(self): + def test_enabled(self): params = {'enabled': 'true'} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) params = {'enabled': 'false'} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + def test_parent(self): + # Create child interfaces + parent_interface = VMInterface.objects.first() + child_interfaces = ( + VMInterface(virtual_machine=parent_interface.virtual_machine, name='Child 1', parent=parent_interface), + VMInterface(virtual_machine=parent_interface.virtual_machine, name='Child 2', parent=parent_interface), + VMInterface(virtual_machine=parent_interface.virtual_machine, name='Child 3', parent=parent_interface), + ) + VMInterface.objects.bulk_create(child_interfaces) + + params = {'parent_id': [parent_interface.pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3) + def test_mtu(self): params = {'mtu': [100, 200]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) diff --git a/netbox/virtualization/views.py b/netbox/virtualization/views.py index 6bf9eb6dd..6b316de0e 100644 --- a/netbox/virtualization/views.py +++ b/netbox/virtualization/views.py @@ -421,6 +421,14 @@ class VMInterfaceView(generic.ObjectView): orderable=False ) + # Get child interfaces + child_interfaces = VMInterface.objects.restrict(request.user, 'view').filter(parent=instance) + child_interfaces_tables = tables.VMInterfaceTable( + child_interfaces, + orderable=False + ) + child_interfaces_tables.columns.hide('virtual_machine') + # Get assigned VLANs and annotate whether each is tagged or untagged vlans = [] if instance.untagged_vlan is not None: @@ -437,6 +445,7 @@ class VMInterfaceView(generic.ObjectView): return { 'ipaddress_table': ipaddress_table, + 'child_interfaces_table': child_interfaces_tables, 'vlan_table': vlan_table, } From 4dfba3a2ada99c59b4684086cec0b061a7220305 Mon Sep 17 00:00:00 2001 From: tcaiazza Date: Fri, 9 Apr 2021 14:14:08 -0400 Subject: [PATCH 152/182] Update export-templates.md (#6091) * Update export-templates.md * Update export-templates.md Co-authored-by: Jeremy Stretch --- docs/additional-features/export-templates.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/additional-features/export-templates.md b/docs/additional-features/export-templates.md index 1e0611f06..c9a7eea81 100644 --- a/docs/additional-features/export-templates.md +++ b/docs/additional-features/export-templates.md @@ -18,6 +18,14 @@ Height: {{ rack.u_height }}U To access custom fields of an object within a template, use the `cf` attribute. For example, `{{ obj.cf.color }}` will return the value (if any) for a custom field named `color` on `obj`. +If you need to use the config context data in an export template, you'll should use the function `get_config_context` to get all the config context data. For example: +``` +{% for server in queryset %} +{% set data = server.get_config_context() %} +{{ data.syslog }} +{% endfor %} +``` + A MIME type and file extension can optionally be defined for each export template. The default MIME type is `text/plain`. ## Example From 2cc088c633990b5d81715597e54737fb4a6cd10d Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Fri, 9 Apr 2021 14:42:07 -0400 Subject: [PATCH 153/182] Fixes #6131: Correct handling of boolean fields when cloning objects --- docs/release-notes/version-2.10.md | 1 + netbox/utilities/utils.py | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/release-notes/version-2.10.md b/docs/release-notes/version-2.10.md index fad8f218b..916af2d65 100644 --- a/docs/release-notes/version-2.10.md +++ b/docs/release-notes/version-2.10.md @@ -16,6 +16,7 @@ * [#6073](https://github.com/netbox-community/netbox/issues/6073) - Permit users to manage their own REST API tokens without needing explicit permission * [#6081](https://github.com/netbox-community/netbox/issues/6081) - Fix interface connections REST API endpoint * [#6108](https://github.com/netbox-community/netbox/issues/6108) - Do not infer tenant assignment from parent objects for prefixes, IP addresses +* [#6131](https://github.com/netbox-community/netbox/issues/6131) - Correct handling of boolean fields when cloning objects --- diff --git a/netbox/utilities/utils.py b/netbox/utilities/utils.py index d76b469b2..b7c5564ee 100644 --- a/netbox/utilities/utils.py +++ b/netbox/utilities/utils.py @@ -224,12 +224,12 @@ def prepare_cloned_fields(instance): field = instance._meta.get_field(field_name) field_value = field.value_from_object(instance) - # Swap out False with URL-friendly value + # Pass False as null for boolean fields if field_value is False: - field_value = '' + params.append((field_name, '')) # Omit empty values - if field_value not in (None, ''): + elif field_value not in (None, ''): params.append((field_name, field_value)) # Copy tags From 701ad8a4a966637759680aef3d3b1a70995f70ab Mon Sep 17 00:00:00 2001 From: Marcus Weiner Date: Fri, 9 Apr 2021 20:51:58 +0200 Subject: [PATCH 154/182] Allow skipping TLS cert verification on Redis connection (#6084) * Allow skipping redis tls cert verification * Add config example --- netbox/netbox/configuration.example.py | 6 ++++++ netbox/netbox/settings.py | 26 +++++++++++--------------- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/netbox/netbox/configuration.example.py b/netbox/netbox/configuration.example.py index 0dadb55bc..c40e280dd 100644 --- a/netbox/netbox/configuration.example.py +++ b/netbox/netbox/configuration.example.py @@ -34,6 +34,9 @@ REDIS = { 'PASSWORD': '', 'DATABASE': 0, 'SSL': False, + # Set this to True to skip TLS certificate verification + # This can expose the connection to attacks, be careful + # 'INSECURE_SKIP_TLS_VERIFY': False, }, 'caching': { 'HOST': 'localhost', @@ -44,6 +47,9 @@ REDIS = { 'PASSWORD': '', 'DATABASE': 1, 'SSL': False, + # Set this to True to skip TLS certificate verification + # This can expose the connection to attacks, be careful + # 'INSECURE_SKIP_TLS_VERIFY': False, } } diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index ddc16e101..7fee45850 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -215,6 +215,7 @@ TASKS_REDIS_SENTINEL_TIMEOUT = TASKS_REDIS.get('SENTINEL_TIMEOUT', 10) TASKS_REDIS_PASSWORD = TASKS_REDIS.get('PASSWORD', '') TASKS_REDIS_DATABASE = TASKS_REDIS.get('DATABASE', 0) TASKS_REDIS_SSL = TASKS_REDIS.get('SSL', False) +TASKS_REDIS_SKIP_TLS_VERIFY = TASKS_REDIS.get('INSECURE_SKIP_TLS_VERIFY', False) # Caching if 'caching' not in REDIS: @@ -233,6 +234,7 @@ CACHING_REDIS_SENTINEL_SERVICE = CACHING_REDIS.get('SENTINEL_SERVICE', 'default' CACHING_REDIS_PASSWORD = CACHING_REDIS.get('PASSWORD', '') CACHING_REDIS_DATABASE = CACHING_REDIS.get('DATABASE', 0) CACHING_REDIS_SSL = CACHING_REDIS.get('SSL', False) +CACHING_REDIS_SKIP_TLS_VERIFY = CACHING_REDIS.get('INSECURE_SKIP_TLS_VERIFY', False) # @@ -398,21 +400,14 @@ if CACHING_REDIS_USING_SENTINEL: 'password': CACHING_REDIS_PASSWORD, } else: - if CACHING_REDIS_SSL: - REDIS_CACHE_CON_STRING = 'rediss://' - else: - REDIS_CACHE_CON_STRING = 'redis://' - - if CACHING_REDIS_PASSWORD: - REDIS_CACHE_CON_STRING = '{}:{}@'.format(REDIS_CACHE_CON_STRING, CACHING_REDIS_PASSWORD) - - REDIS_CACHE_CON_STRING = '{}{}:{}/{}'.format( - REDIS_CACHE_CON_STRING, - CACHING_REDIS_HOST, - CACHING_REDIS_PORT, - CACHING_REDIS_DATABASE - ) - CACHEOPS_REDIS = REDIS_CACHE_CON_STRING + CACHEOPS_REDIS = { + 'host': CACHING_REDIS_HOST, + 'port': CACHING_REDIS_PORT, + 'db': CACHING_REDIS_DATABASE, + 'password': CACHING_REDIS_PASSWORD, + 'ssl': CACHING_REDIS_SSL, + 'ssl_cert_reqs': None if CACHING_REDIS_SKIP_TLS_VERIFY else 'required', + } if not CACHE_TIMEOUT: CACHEOPS_ENABLED = False @@ -560,6 +555,7 @@ else: 'DB': TASKS_REDIS_DATABASE, 'PASSWORD': TASKS_REDIS_PASSWORD, 'SSL': TASKS_REDIS_SSL, + 'SSL_CERT_REQS': None if TASKS_REDIS_SKIP_TLS_VERIFY else 'required', 'DEFAULT_TIMEOUT': RQ_DEFAULT_TIMEOUT, } From cc9b750eff5e239c75c1b111b46d7ca867991973 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Fri, 9 Apr 2021 14:58:40 -0400 Subject: [PATCH 155/182] Changelog & docs for #6083 --- docs/configuration/required-settings.md | 1 + docs/release-notes/version-2.10.md | 1 + 2 files changed, 2 insertions(+) diff --git a/docs/configuration/required-settings.md b/docs/configuration/required-settings.md index dba8cdc8c..3158fc73a 100644 --- a/docs/configuration/required-settings.md +++ b/docs/configuration/required-settings.md @@ -66,6 +66,7 @@ Redis is configured using a configuration setting similar to `DATABASE` and thes * `PASSWORD` - Redis password (if set) * `DATABASE` - Numeric database ID * `SSL` - Use SSL connection to Redis +* `INSECURE_SKIP_TLS_VERIFY` - Set to `True` to **disable** TLS certificate verification (not recommended) An example configuration is provided below: diff --git a/docs/release-notes/version-2.10.md b/docs/release-notes/version-2.10.md index 916af2d65..390c318f7 100644 --- a/docs/release-notes/version-2.10.md +++ b/docs/release-notes/version-2.10.md @@ -8,6 +8,7 @@ * [#5756](https://github.com/netbox-community/netbox/issues/5756) - Omit child devices from non-racked devices list under rack view * [#5840](https://github.com/netbox-community/netbox/issues/5840) - Add column to cable termination objects to display cable color * [#6054](https://github.com/netbox-community/netbox/issues/6054) - Display NAPALM-enabled device tabs only when relevant +* [#6083](https://github.com/netbox-community/netbox/issues/6083) - Support disabling TLS certificate validation for Redis ### Bug Fixes From 348fca7e28c78df33996c8097b298881f9b7acc9 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Sun, 11 Apr 2021 12:57:53 -0400 Subject: [PATCH 156/182] Fixes #6117: Handle exception when attempting to assign an MPTT-enabled model as its own parent --- docs/release-notes/version-2.10.md | 1 + netbox/dcim/models/racks.py | 6 ++++++ netbox/dcim/models/sites.py | 10 ++++++++++ netbox/tenancy/models.py | 10 ++++++++++ 4 files changed, 27 insertions(+) diff --git a/docs/release-notes/version-2.10.md b/docs/release-notes/version-2.10.md index 390c318f7..7d99959cc 100644 --- a/docs/release-notes/version-2.10.md +++ b/docs/release-notes/version-2.10.md @@ -17,6 +17,7 @@ * [#6073](https://github.com/netbox-community/netbox/issues/6073) - Permit users to manage their own REST API tokens without needing explicit permission * [#6081](https://github.com/netbox-community/netbox/issues/6081) - Fix interface connections REST API endpoint * [#6108](https://github.com/netbox-community/netbox/issues/6108) - Do not infer tenant assignment from parent objects for prefixes, IP addresses +* [#6117](https://github.com/netbox-community/netbox/issues/6117) - Handle exception when attempting to assign an MPTT-enabled model as its own parent * [#6131](https://github.com/netbox-community/netbox/issues/6131) - Correct handling of boolean fields when cloning objects --- diff --git a/netbox/dcim/models/racks.py b/netbox/dcim/models/racks.py index dfaf7da61..0bd7e5afd 100644 --- a/netbox/dcim/models/racks.py +++ b/netbox/dcim/models/racks.py @@ -111,6 +111,12 @@ class RackGroup(MPTTModel, ChangeLoggedModel): def clean(self): super().clean() + # An MPTT model cannot be its own parent + if self.pk and self.parent_id == self.pk: + raise ValidationError({ + "parent": "Cannot assign self as parent." + }) + # Parent RackGroup (if any) must belong to the same Site if self.parent and self.parent.site != self.site: raise ValidationError(f"Parent rack group ({self.parent}) must belong to the same site ({self.site})") diff --git a/netbox/dcim/models/sites.py b/netbox/dcim/models/sites.py index 923b33124..908750905 100644 --- a/netbox/dcim/models/sites.py +++ b/netbox/dcim/models/sites.py @@ -7,6 +7,7 @@ from timezone_field import TimeZoneField from dcim.choices import * from dcim.constants import * +from django.core.exceptions import ValidationError from dcim.fields import ASNField from extras.models import ChangeLoggedModel, CustomFieldModel, ObjectChange, TaggedItem from extras.utils import extras_features @@ -87,6 +88,15 @@ class Region(MPTTModel, ChangeLoggedModel): object_data=serialize_object(self, exclude=['level', 'lft', 'rght', 'tree_id']) ) + def clean(self): + super().clean() + + # An MPTT model cannot be its own parent + if self.pk and self.parent_id == self.pk: + raise ValidationError({ + "parent": "Cannot assign self as parent." + }) + # # Sites diff --git a/netbox/tenancy/models.py b/netbox/tenancy/models.py index 3ba644c09..dbb8c4835 100644 --- a/netbox/tenancy/models.py +++ b/netbox/tenancy/models.py @@ -1,3 +1,4 @@ +from django.core.exceptions import ValidationError from django.db import models from django.urls import reverse from mptt.models import MPTTModel, TreeForeignKey @@ -74,6 +75,15 @@ class TenantGroup(MPTTModel, ChangeLoggedModel): object_data=serialize_object(self, exclude=['level', 'lft', 'rght', 'tree_id']) ) + def clean(self): + super().clean() + + # An MPTT model cannot be its own parent + if self.pk and self.parent_id == self.pk: + raise ValidationError({ + "parent": "Cannot assign self as parent." + }) + @extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks') class Tenant(ChangeLoggedModel, CustomFieldModel): From 65ed04708418f0edca7b88a0d2d4507c9addc990 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Sun, 11 Apr 2021 13:42:24 -0400 Subject: [PATCH 157/182] Fixes #6124: Location parent filter should return all child locations (not just those directly assigned) --- docs/release-notes/version-2.11.md | 1 + netbox/dcim/filters.py | 13 ++++++++----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/docs/release-notes/version-2.11.md b/docs/release-notes/version-2.11.md index cd75d141b..0fbbb2f9c 100644 --- a/docs/release-notes/version-2.11.md +++ b/docs/release-notes/version-2.11.md @@ -18,6 +18,7 @@ * [#6107](https://github.com/netbox-community/netbox/issues/6107) - Fix rack selection field on device form * [#6110](https://github.com/netbox-community/netbox/issues/6110) - Fix handling of TemplateColumn values for table export * [#6123](https://github.com/netbox-community/netbox/issues/6123) - Prevent device from being assigned to mismatched site and location +* [#6124](https://github.com/netbox-community/netbox/issues/6124) - Location `parent` filter should return all child locations (not just those directly assigned) * [#6130](https://github.com/netbox-community/netbox/issues/6130) - Improve display of assigned models in custom fields list --- diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index d8586eb33..1a7ac266f 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -191,15 +191,18 @@ class LocationFilterSet(BaseFilterSet, NameSlugSearchFilterSet): to_field_name='slug', label='Site (slug)', ) - parent_id = django_filters.ModelMultipleChoiceFilter( + parent_id = TreeNodeMultipleChoiceFilter( queryset=Location.objects.all(), - label='Rack group (ID)', + field_name='parent', + lookup_expr='in', + label='Location (ID)', ) - parent = django_filters.ModelMultipleChoiceFilter( - field_name='parent__slug', + parent = TreeNodeMultipleChoiceFilter( queryset=Location.objects.all(), + field_name='parent', + lookup_expr='in', to_field_name='slug', - label='Rack group (slug)', + label='Location (slug)', ) class Meta: From 0bce1da4e3793f6a8b279c74c02af2148ddf4c0a Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Sun, 11 Apr 2021 13:43:06 -0400 Subject: [PATCH 158/182] Clean up stray references to old RackGroup model --- netbox/dcim/api/views.py | 2 +- netbox/dcim/filters.py | 2 +- netbox/dcim/forms.py | 4 ++-- netbox/dcim/models/power.py | 6 +++--- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index 26313474c..cb46c1eca 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -142,7 +142,7 @@ class SiteViewSet(CustomFieldModelViewSet): # -# Rack groups +# Locations # class LocationViewSet(CustomFieldModelViewSet): diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index 1a7ac266f..4beed04b1 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -1349,7 +1349,7 @@ class PowerPanelFilterSet(BaseFilterSet): queryset=Location.objects.all(), field_name='location', lookup_expr='in', - label='Rack group (ID)', + label='Location (ID)', ) tag = TagFilter() diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 9437225de..feb8c5e81 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -521,9 +521,9 @@ class LocationCSVForm(CustomFieldModelCSVForm): queryset=Location.objects.all(), required=False, to_field_name='name', - help_text='Parent rack group', + help_text='Parent location', error_messages={ - 'invalid_choice': 'Rack group not found.', + 'invalid_choice': 'Location not found.', } ) diff --git a/netbox/dcim/models/power.py b/netbox/dcim/models/power.py index 06d234149..a5e3149f8 100644 --- a/netbox/dcim/models/power.py +++ b/netbox/dcim/models/power.py @@ -66,9 +66,9 @@ class PowerPanel(PrimaryModel): # Location must belong to assigned Site if self.location and self.location.site != self.site: - raise ValidationError("Rack group {} ({}) is in a different site than {}".format( - self.location, self.location.site, self.site - )) + raise ValidationError( + f"Location {self.location} ({self.location.site}) is in a different site than {self.site}" + ) @extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks') From 18f206747cee8754e62ecb3702bc026a5ff8cc55 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 12 Apr 2021 10:46:32 -0400 Subject: [PATCH 159/182] Closes #6088: Improved table configuration form --- docs/release-notes/version-2.11.md | 1 + netbox/netbox/views/generic.py | 2 +- netbox/project-static/js/tableconfig.js | 22 ++++++++++++++++-- .../templatetags/table_config_form.html | 16 +++++++++---- netbox/utilities/forms/forms.py | 14 ++++++++--- netbox/utilities/tables.py | 23 ++++++++++--------- 6 files changed, 56 insertions(+), 22 deletions(-) diff --git a/docs/release-notes/version-2.11.md b/docs/release-notes/version-2.11.md index 0fbbb2f9c..3bf3b2fbc 100644 --- a/docs/release-notes/version-2.11.md +++ b/docs/release-notes/version-2.11.md @@ -4,6 +4,7 @@ ### Enhancements (from Beta) +* [#6088](https://github.com/netbox-community/netbox/issues/6088) - Improved table configuration form * [#6097](https://github.com/netbox-community/netbox/issues/6097) - Redirect old slug-based object views * [#6109](https://github.com/netbox-community/netbox/issues/6109) - Add device counts to locations table * [#6121](https://github.com/netbox-community/netbox/issues/6121) - Extend parent interface assignment to VM interfaces diff --git a/netbox/netbox/views/generic.py b/netbox/netbox/views/generic.py index dadd97b98..8f713fa63 100644 --- a/netbox/netbox/views/generic.py +++ b/netbox/netbox/views/generic.py @@ -182,7 +182,7 @@ class ObjectListView(ObjectPermissionRequiredMixin, View): if request.GET.get('export') == 'table': exclude_columns = {'pk'} exclude_columns.update({ - col for col in table.base_columns if col not in table.visible_columns + name for name, _ in table.available_columns }) exporter = TableExport( export_format=TableExport.CSV, diff --git a/netbox/project-static/js/tableconfig.js b/netbox/project-static/js/tableconfig.js index 8f4692ea4..6851d2e8c 100644 --- a/netbox/project-static/js/tableconfig.js +++ b/netbox/project-static/js/tableconfig.js @@ -1,9 +1,27 @@ $(document).ready(function() { - $('form.userconfigform input.reset').click(function(event) { - // Deselect all columns when the reset button is clicked + + // Select or reset table columns + $('#save_tableconfig').click(function(event) { + $('select[name="columns"] option').attr("selected", "selected"); + }); + $('#reset_tableconfig').click(function(event) { $('select[name="columns"]').val([]); }); + // Swap columns between available and selected lists + $('#add_columns').click(function(e) { + let selected_columns = $('#id_available_columns option:selected'); + $('#id_columns').append($(selected_columns).clone()); + $(selected_columns).remove(); + e.preventDefault(); + }); + $('#remove_columns').click(function(e) { + let selected_columns = $('#id_columns option:selected'); + $('#id_available_columns').append($(selected_columns).clone()); + $(selected_columns).remove(); + e.preventDefault(); + }); + $('form.userconfigform').submit(function(event) { event.preventDefault(); diff --git a/netbox/templates/utilities/templatetags/table_config_form.html b/netbox/templates/utilities/templatetags/table_config_form.html index c92adaee1..65397c18d 100644 --- a/netbox/templates/utilities/templatetags/table_config_form.html +++ b/netbox/templates/utilities/templatetags/table_config_form.html @@ -8,17 +8,23 @@
- {% include 'panel_table.html' with table=powerfeed_table heading='Connected Feeds' %} +
+ {% csrf_token %} +
+ {% render_table powerfeed_table 'inc/table.html' %} + +
+
{% plugin_full_width_page object %}
From b19734004a49b7a0c8653f6d0f134a450ab6f33a Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 12 Apr 2021 15:06:40 -0400 Subject: [PATCH 165/182] Removed the "Additional information" blocks from issue templates (no longer needed) --- .github/ISSUE_TEMPLATE/bug_report.yaml | 5 ----- .github/ISSUE_TEMPLATE/documentation_change.yaml | 5 ----- .github/ISSUE_TEMPLATE/feature_request.yaml | 5 ----- .github/ISSUE_TEMPLATE/housekeeping.yaml | 5 ----- 4 files changed, 20 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index a83e9b34e..a37c5dfb1 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -56,8 +56,3 @@ body: placeholder: "A TypeError exception was raised" validations: required: true - - type: markdown - attributes: - value: | - ### Additional information - You can use the space below to provide any additional information or to attach files. diff --git a/.github/ISSUE_TEMPLATE/documentation_change.yaml b/.github/ISSUE_TEMPLATE/documentation_change.yaml index bff755719..b480e629a 100644 --- a/.github/ISSUE_TEMPLATE/documentation_change.yaml +++ b/.github/ISSUE_TEMPLATE/documentation_change.yaml @@ -33,8 +33,3 @@ body: description: "Describe the proposed changes and why they are necessary" validations: required: true - - type: markdown - attributes: - value: | - ### Additional information - You can use the space below to provide any additional information or to attach files. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml index efa83b376..6282eedde 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yaml +++ b/.github/ISSUE_TEMPLATE/feature_request.yaml @@ -51,8 +51,3 @@ body: description: "List any new dependencies on external libraries or services that this new feature would introduce. For example, does the proposal require the installation of a new Python package? (Not all new features introduce new dependencies.)" - - type: markdown - attributes: - value: | - ### Additional information - You can use the space below to provide any additional information or to attach files. diff --git a/.github/ISSUE_TEMPLATE/housekeeping.yaml b/.github/ISSUE_TEMPLATE/housekeeping.yaml index 0f466aa24..778dca235 100644 --- a/.github/ISSUE_TEMPLATE/housekeeping.yaml +++ b/.github/ISSUE_TEMPLATE/housekeeping.yaml @@ -20,8 +20,3 @@ body: description: "Please provide justification for the proposed change(s)." validations: required: true - - type: markdown - attributes: - value: | - ### Additional information - You can use the space below to provide any additional information or to attach files. From 3d8a3a2204ab7e413933f17f6dfb75838d2279ed Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 12 Apr 2021 15:17:50 -0400 Subject: [PATCH 166/182] Fix link --- docs/rest-api/overview.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/rest-api/overview.md b/docs/rest-api/overview.md index 735e0713b..088286e22 100644 --- a/docs/rest-api/overview.md +++ b/docs/rest-api/overview.md @@ -387,7 +387,7 @@ curl -s -X GET http://netbox/api/ipam/ip-addresses/5618/ | jq '.' ### Creating a New Object -To create a new object, make a `POST` request to the model's _list_ endpoint with JSON data pertaining to the object being created. Note that a REST API token is required for all write operations; see the [authentication documentation](../authentication/index.md) for more information. Also be sure to set the `Content-Type` HTTP header to `application/json`. +To create a new object, make a `POST` request to the model's _list_ endpoint with JSON data pertaining to the object being created. Note that a REST API token is required for all write operations; see the [authentication documentation](authentication.md) for more information. Also be sure to set the `Content-Type` HTTP header to `application/json`. ```no-highlight curl -s -X POST \ From 608bf30bda2fe3e09260e5da6cefd03e8adf7e5e Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 12 Apr 2021 15:52:24 -0400 Subject: [PATCH 167/182] Add cable trace view tests --- netbox/dcim/tests/test_views.py | 92 +++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/netbox/dcim/tests/test_views.py b/netbox/dcim/tests/test_views.py index 8fde267d9..9dffee03c 100644 --- a/netbox/dcim/tests/test_views.py +++ b/netbox/dcim/tests/test_views.py @@ -1246,6 +1246,17 @@ class ConsolePortTestCase(ViewTestCases.DeviceComponentViewTestCase): "Device 1,Console Port 6", ) + def test_trace(self): + consoleport = ConsolePort.objects.first() + consoleserverport = ConsoleServerPort.objects.create( + device=consoleport.device, + name='Console Server Port 1' + ) + Cable(termination_a=consoleport, termination_b=consoleserverport).save() + + response = self.client.get(reverse('dcim:consoleport_trace', kwargs={'pk': consoleport.pk})) + self.assertHttpStatus(response, 200) + class ConsoleServerPortTestCase(ViewTestCases.DeviceComponentViewTestCase): model = ConsoleServerPort @@ -1290,6 +1301,17 @@ class ConsoleServerPortTestCase(ViewTestCases.DeviceComponentViewTestCase): "Device 1,Console Server Port 6", ) + def test_trace(self): + consoleserverport = ConsoleServerPort.objects.first() + consoleport = ConsolePort.objects.create( + device=consoleserverport.device, + name='Console Port 1' + ) + Cable(termination_a=consoleserverport, termination_b=consoleport).save() + + response = self.client.get(reverse('dcim:consoleserverport_trace', kwargs={'pk': consoleserverport.pk})) + self.assertHttpStatus(response, 200) + class PowerPortTestCase(ViewTestCases.DeviceComponentViewTestCase): model = PowerPort @@ -1340,6 +1362,17 @@ class PowerPortTestCase(ViewTestCases.DeviceComponentViewTestCase): "Device 1,Power Port 6", ) + def test_trace(self): + powerport = PowerPort.objects.first() + poweroutlet = PowerOutlet.objects.create( + device=powerport.device, + name='Power Outlet 1' + ) + Cable(termination_a=powerport, termination_b=poweroutlet).save() + + response = self.client.get(reverse('dcim:powerport_trace', kwargs={'pk': powerport.pk})) + self.assertHttpStatus(response, 200) + class PowerOutletTestCase(ViewTestCases.DeviceComponentViewTestCase): model = PowerOutlet @@ -1396,6 +1429,14 @@ class PowerOutletTestCase(ViewTestCases.DeviceComponentViewTestCase): "Device 1,Power Outlet 6", ) + def test_trace(self): + poweroutlet = PowerOutlet.objects.first() + powerport = PowerPort.objects.first() + Cable(termination_a=poweroutlet, termination_b=powerport).save() + + response = self.client.get(reverse('dcim:poweroutlet_trace', kwargs={'pk': poweroutlet.pk})) + self.assertHttpStatus(response, 200) + class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase): model = Interface @@ -1475,6 +1516,13 @@ class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase): "Device 1,Interface 6,1000base-t", ) + def test_trace(self): + interface1, interface2 = Interface.objects.all()[:2] + Cable(termination_a=interface1, termination_b=interface2).save() + + response = self.client.get(reverse('dcim:interface_trace', kwargs={'pk': interface1.pk})) + self.assertHttpStatus(response, 200) + class FrontPortTestCase(ViewTestCases.DeviceComponentViewTestCase): model = FrontPort @@ -1534,6 +1582,17 @@ class FrontPortTestCase(ViewTestCases.DeviceComponentViewTestCase): "Device 1,Front Port 6,8p8c,Rear Port 6,1", ) + def test_trace(self): + frontport = FrontPort.objects.first() + interface = Interface.objects.create( + device=frontport.device, + name='Interface 1' + ) + Cable(termination_a=frontport, termination_b=interface).save() + + response = self.client.get(reverse('dcim:frontport_trace', kwargs={'pk': frontport.pk})) + self.assertHttpStatus(response, 200) + class RearPortTestCase(ViewTestCases.DeviceComponentViewTestCase): model = RearPort @@ -1580,6 +1639,17 @@ class RearPortTestCase(ViewTestCases.DeviceComponentViewTestCase): "Device 1,Rear Port 6,8p8c,1", ) + def test_trace(self): + rearport = RearPort.objects.first() + interface = Interface.objects.create( + device=rearport.device, + name='Interface 1' + ) + Cable(termination_a=rearport, termination_b=interface).save() + + response = self.client.get(reverse('dcim:rearport_trace', kwargs={'pk': rearport.pk})) + self.assertHttpStatus(response, 200) + class DeviceBayTestCase(ViewTestCases.DeviceComponentViewTestCase): model = DeviceBay @@ -1938,3 +2008,25 @@ class PowerFeedTestCase(ViewTestCases.PrimaryObjectViewTestCase): 'max_utilization': 50, 'comments': 'New comments', } + + def test_trace(self): + manufacturer = Manufacturer.objects.create(name='Manufacturer', slug='manufacturer-1') + device_type = DeviceType.objects.create( + manufacturer=manufacturer, model='Device Type 1', slug='device-type-1' + ) + device_role = DeviceRole.objects.create( + name='Device Role', slug='device-role-1' + ) + device = Device.objects.create( + site=Site.objects.first(), device_type=device_type, device_role=device_role + ) + + powerfeed = PowerFeed.objects.first() + powerport = PowerPort.objects.create( + device=device, + name='Power Port 1' + ) + Cable(termination_a=powerfeed, termination_b=powerport).save() + + response = self.client.get(reverse('dcim:powerfeed_trace', kwargs={'pk': powerfeed.pk})) + self.assertHttpStatus(response, 200) From b4b68c0b0052d2617408d1feb482eb14872003dd Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 12 Apr 2021 16:07:03 -0400 Subject: [PATCH 168/182] Move create_test_device() to testing utils --- netbox/dcim/tests/test_views.py | 15 +-------------- netbox/utilities/testing/utils.py | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/netbox/dcim/tests/test_views.py b/netbox/dcim/tests/test_views.py index 9dffee03c..40c0dff34 100644 --- a/netbox/dcim/tests/test_views.py +++ b/netbox/dcim/tests/test_views.py @@ -12,20 +12,7 @@ from dcim.choices import * from dcim.constants import * from dcim.models import * from ipam.models import VLAN -from utilities.testing import ViewTestCases - - -def create_test_device(name): - """ - Convenience method for creating a Device (e.g. for component testing). - """ - site, _ = Site.objects.get_or_create(name='Site 1', slug='site-1') - manufacturer, _ = Manufacturer.objects.get_or_create(name='Manufacturer 1', slug='manufacturer-1') - devicetype, _ = DeviceType.objects.get_or_create(model='Device Type 1', manufacturer=manufacturer) - devicerole, _ = DeviceRole.objects.get_or_create(name='Device Role 1', slug='device-role-1') - device = Device.objects.create(name=name, site=site, device_type=devicetype, device_role=devicerole) - - return device +from utilities.testing import ViewTestCases, create_test_device class RegionTestCase(ViewTestCases.OrganizationalObjectViewTestCase): diff --git a/netbox/utilities/testing/utils.py b/netbox/utilities/testing/utils.py index 9c30002b8..2cf9795b5 100644 --- a/netbox/utilities/testing/utils.py +++ b/netbox/utilities/testing/utils.py @@ -4,6 +4,8 @@ from contextlib import contextmanager from django.contrib.auth.models import Permission, User +from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site + def post_data(data): """ @@ -29,6 +31,19 @@ def post_data(data): return ret +def create_test_device(name): + """ + Convenience method for creating a Device (e.g. for component testing). + """ + site, _ = Site.objects.get_or_create(name='Site 1', slug='site-1') + manufacturer, _ = Manufacturer.objects.get_or_create(name='Manufacturer 1', slug='manufacturer-1') + devicetype, _ = DeviceType.objects.get_or_create(model='Device Type 1', manufacturer=manufacturer) + devicerole, _ = DeviceRole.objects.get_or_create(name='Device Role 1', slug='device-role-1') + device = Device.objects.create(name=name, site=site, device_type=devicetype, device_role=devicerole) + + return device + + def create_test_user(username='testuser', permissions=None): """ Create a User with the given permissions. From a1d32c3a216fa319ecbedf84697f8ba4e6810dc1 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 12 Apr 2021 16:07:52 -0400 Subject: [PATCH 169/182] Add view tests for CircuitTermination --- netbox/circuits/tests/test_views.py | 58 ++++++++++++++++++++++++++++- 1 file changed, 57 insertions(+), 1 deletion(-) diff --git a/netbox/circuits/tests/test_views.py b/netbox/circuits/tests/test_views.py index 20f7b4d83..b4effa11c 100644 --- a/netbox/circuits/tests/test_views.py +++ b/netbox/circuits/tests/test_views.py @@ -1,8 +1,11 @@ import datetime +from django.urls import reverse + from circuits.choices import * from circuits.models import * -from utilities.testing import ViewTestCases +from dcim.models import Cable, Interface, Site +from utilities.testing import ViewTestCases, create_test_device class ProviderTestCase(ViewTestCases.PrimaryObjectViewTestCase): @@ -175,3 +178,56 @@ class ProviderNetworkTestCase(ViewTestCases.PrimaryObjectViewTestCase): 'description': 'New description', 'comments': 'New comments', } + + +class CircuitTerminationTestCase( + ViewTestCases.EditObjectViewTestCase, + ViewTestCases.DeleteObjectViewTestCase, +): + model = CircuitTermination + + @classmethod + def setUpTestData(cls): + + sites = ( + Site(name='Site 1', slug='site-1'), + Site(name='Site 2', slug='site-2'), + Site(name='Site 3', slug='site-3'), + ) + Site.objects.bulk_create(sites) + provider = Provider.objects.create(name='Provider 1', slug='provider-1') + circuittype = CircuitType.objects.create(name='Circuit Type 1', slug='circuit-type-1') + + circuits = ( + Circuit(cid='Circuit 1', provider=provider, type=circuittype), + Circuit(cid='Circuit 2', provider=provider, type=circuittype), + Circuit(cid='Circuit 3', provider=provider, type=circuittype), + ) + Circuit.objects.bulk_create(circuits) + + circuit_terminations = ( + CircuitTermination(circuit=circuits[0], term_side='A', site=sites[0]), + CircuitTermination(circuit=circuits[0], term_side='Z', site=sites[1]), + CircuitTermination(circuit=circuits[1], term_side='A', site=sites[0]), + CircuitTermination(circuit=circuits[1], term_side='Z', site=sites[1]), + ) + CircuitTermination.objects.bulk_create(circuit_terminations) + + cls.form_data = { + 'term_side': 'A', + 'site': sites[2].pk, + 'description': 'New description', + } + + def test_trace(self): + device = create_test_device('Device 1') + + circuittermination = CircuitTermination.objects.first() + interface = Interface.objects.create( + device=device, + name='Interface 1' + ) + Cable(termination_a=circuittermination, termination_b=interface).save() + + response = self.client.get(reverse('circuits:circuittermination_trace', kwargs={'pk': circuittermination.pk})) + self.assertHttpStatus(response, 200) From 9bda2a44aed490b34f5619a422a50118df416da4 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 12 Apr 2021 16:25:52 -0400 Subject: [PATCH 170/182] Fix permissions for cable trace view tests --- netbox/circuits/tests/test_views.py | 2 ++ netbox/dcim/tests/test_views.py | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/netbox/circuits/tests/test_views.py b/netbox/circuits/tests/test_views.py index b4effa11c..62e3e3a22 100644 --- a/netbox/circuits/tests/test_views.py +++ b/netbox/circuits/tests/test_views.py @@ -1,5 +1,6 @@ import datetime +from django.test import override_settings from django.urls import reverse from circuits.choices import * @@ -219,6 +220,7 @@ class CircuitTerminationTestCase( 'description': 'New description', } + @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) def test_trace(self): device = create_test_device('Device 1') diff --git a/netbox/dcim/tests/test_views.py b/netbox/dcim/tests/test_views.py index 40c0dff34..daba2a639 100644 --- a/netbox/dcim/tests/test_views.py +++ b/netbox/dcim/tests/test_views.py @@ -1233,6 +1233,7 @@ class ConsolePortTestCase(ViewTestCases.DeviceComponentViewTestCase): "Device 1,Console Port 6", ) + @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) def test_trace(self): consoleport = ConsolePort.objects.first() consoleserverport = ConsoleServerPort.objects.create( @@ -1288,6 +1289,7 @@ class ConsoleServerPortTestCase(ViewTestCases.DeviceComponentViewTestCase): "Device 1,Console Server Port 6", ) + @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) def test_trace(self): consoleserverport = ConsoleServerPort.objects.first() consoleport = ConsolePort.objects.create( @@ -1349,6 +1351,7 @@ class PowerPortTestCase(ViewTestCases.DeviceComponentViewTestCase): "Device 1,Power Port 6", ) + @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) def test_trace(self): powerport = PowerPort.objects.first() poweroutlet = PowerOutlet.objects.create( @@ -1416,6 +1419,7 @@ class PowerOutletTestCase(ViewTestCases.DeviceComponentViewTestCase): "Device 1,Power Outlet 6", ) + @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) def test_trace(self): poweroutlet = PowerOutlet.objects.first() powerport = PowerPort.objects.first() @@ -1503,6 +1507,7 @@ class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase): "Device 1,Interface 6,1000base-t", ) + @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) def test_trace(self): interface1, interface2 = Interface.objects.all()[:2] Cable(termination_a=interface1, termination_b=interface2).save() @@ -1569,6 +1574,7 @@ class FrontPortTestCase(ViewTestCases.DeviceComponentViewTestCase): "Device 1,Front Port 6,8p8c,Rear Port 6,1", ) + @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) def test_trace(self): frontport = FrontPort.objects.first() interface = Interface.objects.create( @@ -1626,6 +1632,7 @@ class RearPortTestCase(ViewTestCases.DeviceComponentViewTestCase): "Device 1,Rear Port 6,8p8c,1", ) + @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) def test_trace(self): rearport = RearPort.objects.first() interface = Interface.objects.create( @@ -1996,6 +2003,7 @@ class PowerFeedTestCase(ViewTestCases.PrimaryObjectViewTestCase): 'comments': 'New comments', } + @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) def test_trace(self): manufacturer = Manufacturer.objects.create(name='Manufacturer', slug='manufacturer-1') device_type = DeviceType.objects.create( From e5bbf47ab92701b7c31c3b7d6e2115fbffbd9cd8 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 13 Apr 2021 10:14:25 -0400 Subject: [PATCH 171/182] Fixes #5583: Eliminate redundant change records when adding/removing tags --- docs/release-notes/version-2.11.md | 1 + netbox/extras/signals.py | 22 ++++++++++---- netbox/extras/tests/test_changelog.py | 42 ++++++++++++--------------- netbox/utilities/testing/api.py | 32 +++++++++++++++++++- netbox/utilities/testing/views.py | 30 +++++++++++++++++-- 5 files changed, 96 insertions(+), 31 deletions(-) diff --git a/docs/release-notes/version-2.11.md b/docs/release-notes/version-2.11.md index c548d5357..7ab536c67 100644 --- a/docs/release-notes/version-2.11.md +++ b/docs/release-notes/version-2.11.md @@ -13,6 +13,7 @@ ### Bug Fixes (from Beta) +* [#5583](https://github.com/netbox-community/netbox/issues/5583) - Eliminate redundant change records when adding/removing tags * [#6100](https://github.com/netbox-community/netbox/issues/6100) - Fix VM interfaces table "add interfaces" link * [#6104](https://github.com/netbox-community/netbox/issues/6104) - Fix location column on racks table * [#6105](https://github.com/netbox-community/netbox/issues/6105) - Hide checkboxes for VMs under cluster VMs view diff --git a/netbox/extras/signals.py b/netbox/extras/signals.py index 0d6295e5b..63e8f07b7 100644 --- a/netbox/extras/signals.py +++ b/netbox/extras/signals.py @@ -22,23 +22,35 @@ def _handle_changed_object(request, sender, instance, **kwargs): """ Fires when an object is created or updated. """ - # Queue the object for processing once the request completes + m2m_changed = False + + # Determine the type of change being made if kwargs.get('created'): action = ObjectChangeActionChoices.ACTION_CREATE elif 'created' in kwargs: action = ObjectChangeActionChoices.ACTION_UPDATE elif kwargs.get('action') in ['post_add', 'post_remove'] and kwargs['pk_set']: # m2m_changed with objects added or removed + m2m_changed = True action = ObjectChangeActionChoices.ACTION_UPDATE else: return # Record an ObjectChange if applicable if hasattr(instance, 'to_objectchange'): - objectchange = instance.to_objectchange(action) - objectchange.user = request.user - objectchange.request_id = request.id - objectchange.save() + if m2m_changed: + ObjectChange.objects.filter( + changed_object_type=ContentType.objects.get_for_model(instance), + changed_object_id=instance.pk, + request_id=request.id + ).update( + postchange_data=instance.to_objectchange(action).postchange_data + ) + else: + objectchange = instance.to_objectchange(action) + objectchange.user = request.user + objectchange.request_id = request.id + objectchange.save() # Enqueue webhooks enqueue_webhooks(instance, request.user, request.id, action) diff --git a/netbox/extras/tests/test_changelog.py b/netbox/extras/tests/test_changelog.py index 5e44c83d1..91868832c 100644 --- a/netbox/extras/tests/test_changelog.py +++ b/netbox/extras/tests/test_changelog.py @@ -56,19 +56,18 @@ class ChangeLogViewTest(ModelViewTestCase): response = self.client.post(**request) self.assertHttpStatus(response, 302) + # Verify the creation of a new ObjectChange record site = Site.objects.get(name='Site 1') - # First OC is the creation; second is the tags update - oc_list = ObjectChange.objects.filter( + oc = ObjectChange.objects.get( changed_object_type=ContentType.objects.get_for_model(Site), changed_object_id=site.pk - ).order_by('pk') - self.assertEqual(oc_list[0].changed_object, site) - self.assertEqual(oc_list[0].action, ObjectChangeActionChoices.ACTION_CREATE) - self.assertEqual(oc_list[0].prechange_data, None) - self.assertEqual(oc_list[0].postchange_data['custom_fields']['my_field'], form_data['cf_my_field']) - self.assertEqual(oc_list[0].postchange_data['custom_fields']['my_field_select'], form_data['cf_my_field_select']) - self.assertEqual(oc_list[1].action, ObjectChangeActionChoices.ACTION_UPDATE) - self.assertEqual(oc_list[1].postchange_data['tags'], ['Tag 1', 'Tag 2']) + ) + self.assertEqual(oc.changed_object, site) + self.assertEqual(oc.action, ObjectChangeActionChoices.ACTION_CREATE) + self.assertEqual(oc.prechange_data, None) + self.assertEqual(oc.postchange_data['custom_fields']['my_field'], form_data['cf_my_field']) + self.assertEqual(oc.postchange_data['custom_fields']['my_field_select'], form_data['cf_my_field_select']) + self.assertEqual(oc.postchange_data['tags'], ['Tag 1', 'Tag 2']) def test_update_object(self): site = Site(name='Site 1', slug='site-1') @@ -93,8 +92,8 @@ class ChangeLogViewTest(ModelViewTestCase): response = self.client.post(**request) self.assertHttpStatus(response, 302) + # Verify the creation of a new ObjectChange record site.refresh_from_db() - # Get only the most recent OC oc = ObjectChange.objects.filter( changed_object_type=ContentType.objects.get_for_model(Site), changed_object_id=site.pk @@ -259,17 +258,15 @@ class ChangeLogAPITest(APITestCase): self.assertHttpStatus(response, status.HTTP_201_CREATED) site = Site.objects.get(pk=response.data['id']) - # First OC is the creation; second is the tags update - oc_list = ObjectChange.objects.filter( + oc = ObjectChange.objects.get( changed_object_type=ContentType.objects.get_for_model(Site), changed_object_id=site.pk - ).order_by('pk') - self.assertEqual(oc_list[0].changed_object, site) - self.assertEqual(oc_list[0].action, ObjectChangeActionChoices.ACTION_CREATE) - self.assertEqual(oc_list[0].prechange_data, None) - self.assertEqual(oc_list[0].postchange_data['custom_fields'], data['custom_fields']) - self.assertEqual(oc_list[1].action, ObjectChangeActionChoices.ACTION_UPDATE) - self.assertEqual(oc_list[1].postchange_data['tags'], ['Tag 1', 'Tag 2']) + ) + self.assertEqual(oc.changed_object, site) + self.assertEqual(oc.action, ObjectChangeActionChoices.ACTION_CREATE) + self.assertEqual(oc.prechange_data, None) + self.assertEqual(oc.postchange_data['custom_fields'], data['custom_fields']) + self.assertEqual(oc.postchange_data['tags'], ['Tag 1', 'Tag 2']) def test_update_object(self): site = Site(name='Site 1', slug='site-1') @@ -294,11 +291,10 @@ class ChangeLogAPITest(APITestCase): self.assertHttpStatus(response, status.HTTP_200_OK) site = Site.objects.get(pk=response.data['id']) - # Get only the most recent OC - oc = ObjectChange.objects.filter( + oc = ObjectChange.objects.get( changed_object_type=ContentType.objects.get_for_model(Site), changed_object_id=site.pk - ).first() + ) self.assertEqual(oc.changed_object, site) self.assertEqual(oc.action, ObjectChangeActionChoices.ACTION_UPDATE) self.assertEqual(oc.postchange_data['custom_fields'], data['custom_fields']) diff --git a/netbox/utilities/testing/api.py b/netbox/utilities/testing/api.py index f4f4ffefe..132eea2ae 100644 --- a/netbox/utilities/testing/api.py +++ b/netbox/utilities/testing/api.py @@ -6,6 +6,8 @@ from django.test import override_settings from rest_framework import status from rest_framework.test import APIClient +from extras.choices import ObjectChangeActionChoices +from extras.models import ObjectChange from users.models import ObjectPermission, Token from .utils import disable_warnings from .views import ModelTestCase @@ -223,13 +225,23 @@ class APIViewTestCases: response = self.client.post(self._get_list_url(), self.create_data[0], format='json', **self.header) self.assertHttpStatus(response, status.HTTP_201_CREATED) self.assertEqual(self._get_queryset().count(), initial_count + 1) + instance = self._get_queryset().get(pk=response.data['id']) self.assertInstanceEqual( - self._get_queryset().get(pk=response.data['id']), + instance, self.create_data[0], exclude=self.validation_excluded_fields, api=True ) + # Verify ObjectChange creation + if hasattr(self.model, 'to_objectchange'): + objectchanges = ObjectChange.objects.filter( + changed_object_type=ContentType.objects.get_for_model(instance), + changed_object_id=instance.pk + ) + self.assertEqual(len(objectchanges), 1) + self.assertEqual(objectchanges[0].action, ObjectChangeActionChoices.ACTION_CREATE) + def test_bulk_create_objects(self): """ POST a set of objects in a single request. @@ -304,6 +316,15 @@ class APIViewTestCases: api=True ) + # Verify ObjectChange creation + if hasattr(self.model, 'to_objectchange'): + objectchanges = ObjectChange.objects.filter( + changed_object_type=ContentType.objects.get_for_model(instance), + changed_object_id=instance.pk + ) + self.assertEqual(len(objectchanges), 1) + self.assertEqual(objectchanges[0].action, ObjectChangeActionChoices.ACTION_UPDATE) + def test_bulk_update_objects(self): """ PATCH a set of objects in a single request. @@ -367,6 +388,15 @@ class APIViewTestCases: self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT) self.assertFalse(self._get_queryset().filter(pk=instance.pk).exists()) + # Verify ObjectChange creation + if hasattr(self.model, 'to_objectchange'): + objectchanges = ObjectChange.objects.filter( + changed_object_type=ContentType.objects.get_for_model(instance), + changed_object_id=instance.pk + ) + self.assertEqual(len(objectchanges), 1) + self.assertEqual(objectchanges[0].action, ObjectChangeActionChoices.ACTION_DELETE) + def test_bulk_delete_objects(self): """ DELETE a set of objects in a single request. diff --git a/netbox/utilities/testing/views.py b/netbox/utilities/testing/views.py index 703780f95..6b1f4f8a9 100644 --- a/netbox/utilities/testing/views.py +++ b/netbox/utilities/testing/views.py @@ -10,7 +10,8 @@ from django.utils.text import slugify from netaddr import IPNetwork from taggit.managers import TaggableManager -from extras.models import Tag +from extras.choices import ObjectChangeActionChoices +from extras.models import ObjectChange, Tag from users.models import ObjectPermission from utilities.permissions import resolve_permission_ct from .utils import disable_warnings, extract_form_failures, post_data @@ -323,7 +324,16 @@ class ViewTestCases: } self.assertHttpStatus(self.client.post(**request), 302) self.assertEqual(initial_count + 1, self._get_queryset().count()) - self.assertInstanceEqual(self._get_queryset().order_by('pk').last(), self.form_data) + instance = self._get_queryset().order_by('pk').last() + self.assertInstanceEqual(instance, self.form_data) + + # Verify ObjectChange creation + objectchanges = ObjectChange.objects.filter( + changed_object_type=ContentType.objects.get_for_model(instance), + changed_object_id=instance.pk + ) + self.assertEqual(len(objectchanges), 1) + self.assertEqual(objectchanges[0].action, ObjectChangeActionChoices.ACTION_CREATE) @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) def test_create_object_with_constrained_permission(self): @@ -410,6 +420,14 @@ class ViewTestCases: self.assertHttpStatus(self.client.post(**request), 302) self.assertInstanceEqual(self._get_queryset().get(pk=instance.pk), self.form_data) + # Verify ObjectChange creation + objectchanges = ObjectChange.objects.filter( + changed_object_type=ContentType.objects.get_for_model(instance), + changed_object_id=instance.pk + ) + self.assertEqual(len(objectchanges), 1) + self.assertEqual(objectchanges[0].action, ObjectChangeActionChoices.ACTION_UPDATE) + @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) def test_edit_object_with_constrained_permission(self): instance1, instance2 = self._get_queryset().all()[:2] @@ -489,6 +507,14 @@ class ViewTestCases: with self.assertRaises(ObjectDoesNotExist): self._get_queryset().get(pk=instance.pk) + # Verify ObjectChange creation + objectchanges = ObjectChange.objects.filter( + changed_object_type=ContentType.objects.get_for_model(instance), + changed_object_id=instance.pk + ) + self.assertEqual(len(objectchanges), 1) + self.assertEqual(objectchanges[0].action, ObjectChangeActionChoices.ACTION_DELETE) + @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) def test_delete_object_with_constrained_permission(self): instance1, instance2 = self._get_queryset().all()[:2] From a296a9e10948c68faae1040c442a49e00f40d07c Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 13 Apr 2021 10:53:55 -0400 Subject: [PATCH 172/182] Closes #6150: Enable change logging for journal entries --- docs/release-notes/version-2.11.md | 1 + netbox/extras/migrations/0058_journalentry.py | 1 + netbox/extras/models/models.py | 12 ++-- netbox/extras/tables.py | 9 +-- netbox/extras/urls.py | 4 +- netbox/extras/views.py | 4 ++ netbox/templates/extras/journalentry.html | 57 +++++++++++++++++++ 7 files changed, 77 insertions(+), 11 deletions(-) create mode 100644 netbox/templates/extras/journalentry.html diff --git a/docs/release-notes/version-2.11.md b/docs/release-notes/version-2.11.md index 7ab536c67..0afda238a 100644 --- a/docs/release-notes/version-2.11.md +++ b/docs/release-notes/version-2.11.md @@ -10,6 +10,7 @@ * [#6121](https://github.com/netbox-community/netbox/issues/6121) - Extend parent interface assignment to VM interfaces * [#6125](https://github.com/netbox-community/netbox/issues/6125) - Add locations count to home page * [#6146](https://github.com/netbox-community/netbox/issues/6146) - Add bulk disconnect support for power feeds +* [#6150](https://github.com/netbox-community/netbox/issues/6150) - Enable change logging for journal entries ### Bug Fixes (from Beta) diff --git a/netbox/extras/migrations/0058_journalentry.py b/netbox/extras/migrations/0058_journalentry.py index 14be2a50d..22abf965c 100644 --- a/netbox/extras/migrations/0058_journalentry.py +++ b/netbox/extras/migrations/0058_journalentry.py @@ -18,6 +18,7 @@ class Migration(migrations.Migration): ('id', models.BigAutoField(primary_key=True, serialize=False)), ('assigned_object_id', models.PositiveIntegerField()), ('created', models.DateTimeField(auto_now_add=True)), + ('last_updated', models.DateTimeField(auto_now=True, null=True)), ('kind', models.CharField(default='info', max_length=30)), ('comments', models.TextField()), ('assigned_object_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')), diff --git a/netbox/extras/models/models.py b/netbox/extras/models/models.py index 3209a7037..41bc345e2 100644 --- a/netbox/extras/models/models.py +++ b/netbox/extras/models/models.py @@ -7,13 +7,15 @@ from django.contrib.contenttypes.models import ContentType from django.core.validators import ValidationError from django.db import models from django.http import HttpResponse +from django.urls import reverse from django.utils import timezone +from django.utils.formats import date_format, time_format from rest_framework.utils.encoders import JSONEncoder from extras.choices import * from extras.constants import * from extras.utils import extras_features, FeatureQuery, image_upload -from netbox.models import BigIDModel +from netbox.models import BigIDModel, ChangeLoggedModel from utilities.querysets import RestrictedQuerySet from utilities.utils import render_jinja2 @@ -389,7 +391,7 @@ class ImageAttachment(BigIDModel): # Journal entries # -class JournalEntry(BigIDModel): +class JournalEntry(ChangeLoggedModel): """ A historical remark concerning an object; collectively, these form an object's journal. The journal is used to preserve historical context around an object, and complements NetBox's built-in change logging. For example, you @@ -427,8 +429,10 @@ class JournalEntry(BigIDModel): verbose_name_plural = 'journal entries' def __str__(self): - time_created = self.created.replace(microsecond=0) - return f"{time_created} - {self.get_kind_display()}" + return f"{date_format(self.created)} - {time_format(self.created)} ({self.get_kind_display()})" + + def get_absolute_url(self): + return reverse('extras:journalentry', args=[self.pk]) def get_kind_class(self): return JournalEntryKindChoices.CSS_CLASSES.get(self.kind) diff --git a/netbox/extras/tables.py b/netbox/extras/tables.py index a8efd8005..dd7b45c6f 100644 --- a/netbox/extras/tables.py +++ b/netbox/extras/tables.py @@ -102,15 +102,15 @@ class ObjectJournalTable(BaseTable): Used for displaying a set of JournalEntries within the context of a single object. """ created = tables.DateTimeColumn( + linkify=True, format=settings.SHORT_DATETIME_FORMAT ) kind = ChoiceFieldColumn() comments = tables.TemplateColumn( - template_code='{% load helpers %}{{ value|render_markdown }}' + template_code='{% load helpers %}{{ value|render_markdown|truncatewords_html:50 }}' ) actions = ButtonsColumn( - model=JournalEntry, - buttons=('edit', 'delete') + model=JournalEntry ) class Meta(BaseTable.Meta): @@ -128,9 +128,6 @@ class JournalEntryTable(ObjectJournalTable): orderable=False, verbose_name='Object' ) - comments = tables.TemplateColumn( - template_code='{% load helpers %}{{ value|render_markdown|truncatewords_html:50 }}' - ) class Meta(BaseTable.Meta): model = JournalEntry diff --git a/netbox/extras/urls.py b/netbox/extras/urls.py index f38a2ecd3..9ec19d215 100644 --- a/netbox/extras/urls.py +++ b/netbox/extras/urls.py @@ -1,7 +1,7 @@ from django.urls import path from extras import views -from extras.models import ConfigContext, Tag +from extras.models import ConfigContext, JournalEntry, Tag app_name = 'extras' @@ -37,8 +37,10 @@ urlpatterns = [ path('journal-entries/add/', views.JournalEntryEditView.as_view(), name='journalentry_add'), path('journal-entries/edit/', views.JournalEntryBulkEditView.as_view(), name='journalentry_bulk_edit'), path('journal-entries/delete/', views.JournalEntryBulkDeleteView.as_view(), name='journalentry_bulk_delete'), + path('journal-entries//', views.JournalEntryView.as_view(), name='journalentry'), path('journal-entries//edit/', views.JournalEntryEditView.as_view(), name='journalentry_edit'), path('journal-entries//delete/', views.JournalEntryDeleteView.as_view(), name='journalentry_delete'), + path('journal-entries//changelog/', views.ObjectChangeLogView.as_view(), name='journalentry_changelog', kwargs={'model': JournalEntry}), # Change logging path('changelog/', views.ObjectChangeListView.as_view(), name='objectchange_list'), diff --git a/netbox/extras/views.py b/netbox/extras/views.py index 4b62604b9..4cda84d99 100644 --- a/netbox/extras/views.py +++ b/netbox/extras/views.py @@ -306,6 +306,10 @@ class JournalEntryListView(generic.ObjectListView): action_buttons = ('export',) +class JournalEntryView(generic.ObjectView): + queryset = JournalEntry.objects.all() + + class JournalEntryEditView(generic.ObjectEditView): queryset = JournalEntry.objects.all() model_form = forms.JournalEntryForm diff --git a/netbox/templates/extras/journalentry.html b/netbox/templates/extras/journalentry.html new file mode 100644 index 000000000..f64741f36 --- /dev/null +++ b/netbox/templates/extras/journalentry.html @@ -0,0 +1,57 @@ +{% extends 'generic/object.html' %} +{% load helpers %} +{% load static %} + +{% block breadcrumbs %} +
  • Journal Entries
  • +
  • {{ object.assigned_object }}
  • +
  • {{ object }}
  • +{% endblock %} + +{% block content %} +
    +
    +
    +
    + Journal Entry +
    + + + + + + + + + + + + + + + + + +
    Object + {{ object.assigned_object }} +
    Created + {{ object.created }} +
    Created By + {{ object.created_by }} +
    Kind + {{ object.get_kind_display }} +
    +
    +
    +
    +
    +
    + Comments +
    +
    + {{ object.comments|render_markdown }} +
    +
    +
    +
    +{% endblock %} From e5602abee010a16ca465e261be3aee57670e55b5 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 13 Apr 2021 11:30:45 -0400 Subject: [PATCH 173/182] Closes #5848: Filter custom fields by content type in format . --- docs/release-notes/version-2.11.md | 1 + netbox/extras/filters.py | 1 + 2 files changed, 2 insertions(+) diff --git a/docs/release-notes/version-2.11.md b/docs/release-notes/version-2.11.md index 0afda238a..3a58069c6 100644 --- a/docs/release-notes/version-2.11.md +++ b/docs/release-notes/version-2.11.md @@ -4,6 +4,7 @@ ### Enhancements (from Beta) +* [#5848](https://github.com/netbox-community/netbox/issues/5848) - Filter custom fields by content type in format `.` * [#6088](https://github.com/netbox-community/netbox/issues/6088) - Improved table configuration form * [#6097](https://github.com/netbox-community/netbox/issues/6097) - Redirect old slug-based object views * [#6109](https://github.com/netbox-community/netbox/issues/6109) - Add device counts to locations table diff --git a/netbox/extras/filters.py b/netbox/extras/filters.py index 495e03797..4b5c42eeb 100644 --- a/netbox/extras/filters.py +++ b/netbox/extras/filters.py @@ -90,6 +90,7 @@ class CustomFieldModelFilterSet(django_filters.FilterSet): class CustomFieldFilterSet(django_filters.FilterSet): + content_types = ContentTypeFilter() class Meta: model = CustomField From b1d20d322817c60fcd380082088f282a4141acbc Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 13 Apr 2021 11:39:04 -0400 Subject: [PATCH 174/182] Closes #6149: Support image attachments for locations --- docs/release-notes/version-2.11.md | 1 + netbox/dcim/models/sites.py | 3 +++ netbox/dcim/urls.py | 1 + netbox/templates/dcim/location.html | 14 ++++++++++++++ 4 files changed, 19 insertions(+) diff --git a/docs/release-notes/version-2.11.md b/docs/release-notes/version-2.11.md index 3a58069c6..e8fb8500a 100644 --- a/docs/release-notes/version-2.11.md +++ b/docs/release-notes/version-2.11.md @@ -11,6 +11,7 @@ * [#6121](https://github.com/netbox-community/netbox/issues/6121) - Extend parent interface assignment to VM interfaces * [#6125](https://github.com/netbox-community/netbox/issues/6125) - Add locations count to home page * [#6146](https://github.com/netbox-community/netbox/issues/6146) - Add bulk disconnect support for power feeds +* [#6149](https://github.com/netbox-community/netbox/issues/6149) - Support image attachments for locations * [#6150](https://github.com/netbox-community/netbox/issues/6150) - Enable change logging for journal entries ### Bug Fixes (from Beta) diff --git a/netbox/dcim/models/sites.py b/netbox/dcim/models/sites.py index 247735627..225a8e749 100644 --- a/netbox/dcim/models/sites.py +++ b/netbox/dcim/models/sites.py @@ -314,6 +314,9 @@ class Location(NestedGroupModel): max_length=200, blank=True ) + images = GenericRelation( + to='extras.ImageAttachment' + ) csv_headers = ['site', 'parent', 'name', 'slug', 'description'] clone_fields = ['site', 'parent', 'description'] diff --git a/netbox/dcim/urls.py b/netbox/dcim/urls.py index 534a9eec6..11ffd4458 100644 --- a/netbox/dcim/urls.py +++ b/netbox/dcim/urls.py @@ -55,6 +55,7 @@ urlpatterns = [ path('locations//edit/', views.LocationEditView.as_view(), name='location_edit'), path('locations//delete/', views.LocationDeleteView.as_view(), name='location_delete'), path('locations//changelog/', ObjectChangeLogView.as_view(), name='location_changelog', kwargs={'model': Location}), + path('locations//images/add/', ImageAttachmentEditView.as_view(), name='location_add_image', kwargs={'model': Location}), # Rack roles path('rack-roles/', views.RackRoleListView.as_view(), name='rackrole_list'), diff --git a/netbox/templates/dcim/location.html b/netbox/templates/dcim/location.html index 0efb74244..a5eeb4e71 100644 --- a/netbox/templates/dcim/location.html +++ b/netbox/templates/dcim/location.html @@ -58,6 +58,20 @@
    {% include 'inc/custom_fields_panel.html' %} +
    +
    + Images +
    + {% include 'inc/image_attachments.html' with images=object.images.all %} + {% if perms.extras.add_imageattachment %} + + {% endif %} +
    {% plugin_right_page object %}
    From 9cbe3ff5517ee904cb97716d29c4f1686aabeee2 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 13 Apr 2021 11:46:14 -0400 Subject: [PATCH 175/182] Enable close-stale-issue action --- .github/workflows/stale.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 1cd85d867..8fc85ead6 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -1,4 +1,5 @@ -name: 'Close stale issues and PRs' +# close-stale-issues (https://github.com/marketplace/actions/close-stale-issues) +name: 'Close stale issues/PRs' on: schedule: - cron: '0 4 * * *' @@ -9,7 +10,6 @@ jobs: steps: - uses: actions/stale@v3 with: - debug-only: true close-issue-message: > This issue has been automatically closed due to lack of activity. In an effort to reduce noise, please do not comment any further. Note that the @@ -19,7 +19,7 @@ jobs: This PR has been automatically closed due to lack of activity. days-before-stale: 45 days-before-close: 15 - exempt-issue-labels: "status: accepted,status: blocked,status: needs milestone" + exempt-issue-labels: 'status: accepted,status: blocked,status: needs milestone' remove-stale-when-updated: false stale-issue-label: 'pending closure' stale-issue-message: > @@ -27,7 +27,7 @@ jobs: recent activity. It will be closed if no further activity occurs. NetBox is governed by a small group of core maintainers which means not all opened issues may receive direct feedback. Please see our [contributing guide](https://github.com/netbox-community/netbox/blob/develop/CONTRIBUTING.md). - stale-pr-label: "pending closure" + stale-pr-label: 'pending closure' stale-pr-message: > This PR has been automatically marked as stale because it has not had recent activity. It will be closed automatically if no further action is From d54bf5f75eef3b93ab0641a527576eed0a8eb7de Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 13 Apr 2021 11:52:32 -0400 Subject: [PATCH 176/182] Fixes #6144: Fix MAC address field display in VM interfaces search form --- docs/release-notes/version-2.10.md | 8 ++++++++ netbox/virtualization/forms.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/release-notes/version-2.10.md b/docs/release-notes/version-2.10.md index d2ce57484..2ea0b7bbf 100644 --- a/docs/release-notes/version-2.10.md +++ b/docs/release-notes/version-2.10.md @@ -1,5 +1,13 @@ # NetBox v2.10 +## v2.10.10 (FUTURE) + +### Bug Fixes + +* [#6144](https://github.com/netbox-community/netbox/issues/6144) - Fix MAC address field display in VM interfaces search form + +--- + ## v2.10.9 (2021-04-12) ### Enhancements diff --git a/netbox/virtualization/forms.py b/netbox/virtualization/forms.py index 20d0e4ad8..a3e6c4cf4 100644 --- a/netbox/virtualization/forms.py +++ b/netbox/virtualization/forms.py @@ -765,7 +765,7 @@ class VMInterfaceBulkRenameForm(BulkRenameForm): ) -class VMInterfaceFilterForm(forms.Form): +class VMInterfaceFilterForm(BootstrapMixin, forms.Form): model = VMInterface cluster_id = DynamicModelMultipleChoiceField( queryset=Cluster.objects.all(), From c249cd4ffd07d0d05f40f9bb1421ec76c2f5cae6 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 13 Apr 2021 12:05:44 -0400 Subject: [PATCH 177/182] Fixes #6152: Fix custom field filtering for cables, virtual chassis --- docs/release-notes/version-2.10.md | 1 + netbox/dcim/filters.py | 5 ++--- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/release-notes/version-2.10.md b/docs/release-notes/version-2.10.md index 2ea0b7bbf..974eb00b0 100644 --- a/docs/release-notes/version-2.10.md +++ b/docs/release-notes/version-2.10.md @@ -5,6 +5,7 @@ ### Bug Fixes * [#6144](https://github.com/netbox-community/netbox/issues/6144) - Fix MAC address field display in VM interfaces search form +* [#6152](https://github.com/netbox-community/netbox/issues/6152) - Fix custom field filtering for cables, virtual chassis --- diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index 9c8a8a79a..dff552910 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -1,6 +1,5 @@ import django_filters from django.contrib.auth.models import User -from django.db.models import Count from extras.filters import CustomFieldModelFilterSet, LocalConfigContextFilterSet, CreatedUpdatedFilterSet from tenancy.filters import TenancyFilterSet @@ -1011,7 +1010,7 @@ class InventoryItemFilterSet(BaseFilterSet, DeviceComponentFilterSet): return queryset.filter(qs_filter) -class VirtualChassisFilterSet(BaseFilterSet): +class VirtualChassisFilterSet(BaseFilterSet, CustomFieldModelFilterSet): q = django_filters.CharFilter( method='search', label='Search', @@ -1078,7 +1077,7 @@ class VirtualChassisFilterSet(BaseFilterSet): return queryset.filter(qs_filter).distinct() -class CableFilterSet(BaseFilterSet): +class CableFilterSet(BaseFilterSet, CustomFieldModelFilterSet): q = django_filters.CharFilter( method='search', label='Search', From 1fba4b7e32f18e643f6483068062c71a2019c807 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 13 Apr 2021 13:23:25 -0400 Subject: [PATCH 178/182] Fixes #5419: Update parent device/VM when deleting a primary IP --- docs/release-notes/version-2.10.md | 1 + netbox/ipam/apps.py | 3 +++ netbox/ipam/signals.py | 21 +++++++++++++++++++++ 3 files changed, 25 insertions(+) create mode 100644 netbox/ipam/signals.py diff --git a/docs/release-notes/version-2.10.md b/docs/release-notes/version-2.10.md index 974eb00b0..2675bac5f 100644 --- a/docs/release-notes/version-2.10.md +++ b/docs/release-notes/version-2.10.md @@ -4,6 +4,7 @@ ### Bug Fixes +* [#5419](https://github.com/netbox-community/netbox/issues/5419) - Update parent device/VM when deleting a primary IP * [#6144](https://github.com/netbox-community/netbox/issues/6144) - Fix MAC address field display in VM interfaces search form * [#6152](https://github.com/netbox-community/netbox/issues/6152) - Fix custom field filtering for cables, virtual chassis diff --git a/netbox/ipam/apps.py b/netbox/ipam/apps.py index fd4af74b0..413c8c1bc 100644 --- a/netbox/ipam/apps.py +++ b/netbox/ipam/apps.py @@ -4,3 +4,6 @@ from django.apps import AppConfig class IPAMConfig(AppConfig): name = "ipam" verbose_name = "IPAM" + + def ready(self): + import ipam.signals diff --git a/netbox/ipam/signals.py b/netbox/ipam/signals.py new file mode 100644 index 000000000..a8fce8310 --- /dev/null +++ b/netbox/ipam/signals.py @@ -0,0 +1,21 @@ +from django.db.models.signals import pre_delete +from django.dispatch import receiver + +from dcim.models import Device +from virtualization.models import VirtualMachine +from .models import IPAddress + + +@receiver(pre_delete, sender=IPAddress) +def clear_primary_ip(instance, **kwargs): + """ + When an IPAddress is deleted, trigger save() on any Devices/VirtualMachines for which it + was a primary IP. + """ + field_name = f'primary_ip{instance.family}' + device = Device.objects.filter(**{field_name: instance}).first() + if device: + device.save() + virtualmachine = VirtualMachine.objects.filter(**{field_name: instance}).first() + if virtualmachine: + virtualmachine.save() From cc433388f5811823913bcf0e310d0af3f05a710d Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 13 Apr 2021 13:48:22 -0400 Subject: [PATCH 179/182] Fixes #6056: Optimize change log cleanup --- docs/release-notes/version-2.10.md | 1 + netbox/extras/signals.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/release-notes/version-2.10.md b/docs/release-notes/version-2.10.md index 2675bac5f..69db03724 100644 --- a/docs/release-notes/version-2.10.md +++ b/docs/release-notes/version-2.10.md @@ -5,6 +5,7 @@ ### Bug Fixes * [#5419](https://github.com/netbox-community/netbox/issues/5419) - Update parent device/VM when deleting a primary IP +* [#6056](https://github.com/netbox-community/netbox/issues/6056) - Optimize change log cleanup * [#6144](https://github.com/netbox-community/netbox/issues/6144) - Fix MAC address field display in VM interfaces search form * [#6152](https://github.com/netbox-community/netbox/issues/6152) - Fix custom field filtering for cables, virtual chassis diff --git a/netbox/extras/signals.py b/netbox/extras/signals.py index 0d6295e5b..9eeb4ce45 100644 --- a/netbox/extras/signals.py +++ b/netbox/extras/signals.py @@ -4,6 +4,7 @@ from datetime import timedelta from cacheops.signals import cache_invalidated, cache_read from django.conf import settings from django.contrib.contenttypes.models import ContentType +from django.db import DEFAULT_DB_ALIAS from django.db.models.signals import m2m_changed, pre_delete from django.utils import timezone from django_prometheus.models import model_deletes, model_inserts, model_updates @@ -52,7 +53,7 @@ def _handle_changed_object(request, sender, instance, **kwargs): # Housekeeping: 0.1% chance of clearing out expired ObjectChanges if settings.CHANGELOG_RETENTION and random.randint(1, 1000) == 1: cutoff = timezone.now() - timedelta(days=settings.CHANGELOG_RETENTION) - ObjectChange.objects.filter(time__lt=cutoff).delete() + ObjectChange.objects.filter(time__lt=cutoff)._raw_delete(using=DEFAULT_DB_ALIAS) def _handle_deleted_object(request, sender, instance, **kwargs): From 6ad20c53d9714c287d33a51bd9cf0f1be4cbc396 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 13 Apr 2021 15:58:21 -0400 Subject: [PATCH 180/182] Delete unused template --- netbox/templates/ipam/inc/vlangroup_header.html | 14 -------------- 1 file changed, 14 deletions(-) delete mode 100644 netbox/templates/ipam/inc/vlangroup_header.html diff --git a/netbox/templates/ipam/inc/vlangroup_header.html b/netbox/templates/ipam/inc/vlangroup_header.html deleted file mode 100644 index 2507a749f..000000000 --- a/netbox/templates/ipam/inc/vlangroup_header.html +++ /dev/null @@ -1,14 +0,0 @@ -
    - {% if perms.ipam.add_vlan and first_available_vlan %} - - Add a VLAN - - {% endif %} - {% if perms.ipam.change_vlangroup %} - - - Edit this VLAN Group - - {% endif %} -
    -

    {{ object }}

    From 46e144f647d3234c66828d024dcdf66f0d403d37 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 13 Apr 2021 16:03:07 -0400 Subject: [PATCH 181/182] Clean up object header --- netbox/project-static/css/base.css | 3 +++ netbox/templates/extras/report.html | 2 +- netbox/templates/extras/script.html | 2 +- netbox/templates/extras/script_result.html | 2 +- netbox/templates/generic/object.html | 9 +++++++-- netbox/templates/inc/created_updated.html | 3 --- netbox/templates/users/userkey.html | 4 +++- 7 files changed, 16 insertions(+), 9 deletions(-) delete mode 100644 netbox/templates/inc/created_updated.html diff --git a/netbox/project-static/css/base.css b/netbox/project-static/css/base.css index 2efa3978f..75348a5c9 100644 --- a/netbox/project-static/css/base.css +++ b/netbox/project-static/css/base.css @@ -362,6 +362,9 @@ table.report th a { border-radius: .25em; vertical-align: middle; } +h1.title { + margin-top: 0; +} .text-nowrap { white-space: nowrap; } diff --git a/netbox/templates/extras/report.html b/netbox/templates/extras/report.html index 76a34c060..f2c5edf23 100644 --- a/netbox/templates/extras/report.html +++ b/netbox/templates/extras/report.html @@ -27,7 +27,7 @@ {% endif %} -

    {{ report.name }}

    +

    {{ report.name }}

    {% if report.description %}

    {{ report.description }}

    {% endif %} diff --git a/netbox/templates/extras/script.html b/netbox/templates/extras/script.html index 3f0839512..7a99d245d 100644 --- a/netbox/templates/extras/script.html +++ b/netbox/templates/extras/script.html @@ -15,7 +15,7 @@ -

    {{ script }}

    +

    {{ script }}

    {{ script.Meta.description|render_markdown }}

    - {% include 'inc/created_updated.html' %} +

    + Created {{ object.created }} · Updated {{ object.last_updated|timesince }} ago +

    {% if not object.is_active %}