From 830cf4b31f6036c4ca17422d9bace348f034c41e Mon Sep 17 00:00:00 2001 From: kkthxbye-code Date: Thu, 28 Oct 2021 21:29:08 +0200 Subject: [PATCH 01/29] Fix #7399 - LDAP using excessive CPU when AUTH_LDAP_FIND_GROUP_PERMS is enabled --- netbox/netbox/authentication.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/netbox/authentication.py b/netbox/netbox/authentication.py index 653fad3b0..a67ec451d 100644 --- a/netbox/netbox/authentication.py +++ b/netbox/netbox/authentication.py @@ -34,7 +34,7 @@ class ObjectPermissionMixin(): object_permissions = ObjectPermission.objects.filter( self.get_permission_filter(user_obj), enabled=True - ).prefetch_related('object_types') + ).order_by('id').distinct('id').prefetch_related('object_types') # Create a dictionary mapping permissions to their constraints perms = defaultdict(list) From ae6ed97a80f55e87cd75dc0e942069758a1cd38d Mon Sep 17 00:00:00 2001 From: kkthxbye-code Date: Thu, 11 Nov 2021 11:53:31 +0100 Subject: [PATCH 02/29] Clear sys.modules cache when reloading scripts --- netbox/extras/scripts.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/netbox/extras/scripts.py b/netbox/extras/scripts.py index 9c46278ae..b128f7461 100644 --- a/netbox/extras/scripts.py +++ b/netbox/extras/scripts.py @@ -3,6 +3,7 @@ import json import logging import os import pkgutil +import sys import traceback from collections import OrderedDict @@ -477,6 +478,10 @@ def get_scripts(use_names=False): # Iterate through all modules within the reports path. These are the user-created files in which reports are # defined. for importer, module_name, _ in pkgutil.iter_modules([settings.SCRIPTS_ROOT]): + # Remove cached module to ensure consistency with filesystem + if module_name in sys.modules: + del sys.modules[module_name] + module = importer.find_module(module_name).load_module(module_name) if use_names and hasattr(module, 'name'): module_name = module.name From 8aa73c59009a1936f40123dd46696d3776a6048c Mon Sep 17 00:00:00 2001 From: Flo <52775027+FloEisen@users.noreply.github.com> Date: Fri, 12 Nov 2021 16:05:42 +0100 Subject: [PATCH 03/29] Add IEEE 802.15.1 Interface Type --- netbox/dcim/choices.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/netbox/dcim/choices.py b/netbox/dcim/choices.py index 5a732fa8d..d3cdbdf8f 100644 --- a/netbox/dcim/choices.py +++ b/netbox/dcim/choices.py @@ -739,6 +739,7 @@ class InterfaceTypeChoices(ChoiceSet): TYPE_80211AC = 'ieee802.11ac' TYPE_80211AD = 'ieee802.11ad' TYPE_80211AX = 'ieee802.11ax' + TYPE_802151 = 'ieee802.15.1' # Cellular TYPE_GSM = 'gsm' @@ -850,6 +851,7 @@ class InterfaceTypeChoices(ChoiceSet): (TYPE_80211AC, 'IEEE 802.11ac'), (TYPE_80211AD, 'IEEE 802.11ad'), (TYPE_80211AX, 'IEEE 802.11ax'), + (TYPE_802151, 'IEEE 802.15.1 (Bluetooth)'), ) ), ( From 17e01644f5ff6d55a18f5f308058cd3258b7f6c0 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Fri, 12 Nov 2021 13:32:52 -0500 Subject: [PATCH 04/29] Fixes #7813: Fix handling of errors during export template rendering --- docs/release-notes/version-3.0.md | 4 ++ netbox/netbox/views/generic.py | 66 ++++++++++++++++++------------- 2 files changed, 42 insertions(+), 28 deletions(-) diff --git a/docs/release-notes/version-3.0.md b/docs/release-notes/version-3.0.md index 4e7ead79c..e6217f967 100644 --- a/docs/release-notes/version-3.0.md +++ b/docs/release-notes/version-3.0.md @@ -2,6 +2,10 @@ ## v3.0.11 (FUTURE) +### Bug Fixes + +* [#7813](https://github.com/netbox-community/netbox/issues/7813) - Fix handling of errors during export template rendering + --- ## v3.0.10 (2021-11-12) diff --git a/netbox/netbox/views/generic.py b/netbox/netbox/views/generic.py index 44e83f5ec..1c2ff9917 100644 --- a/netbox/netbox/views/generic.py +++ b/netbox/netbox/views/generic.py @@ -93,6 +93,13 @@ class ObjectListView(ObjectPermissionRequiredMixin, View): def get_required_permission(self): return get_permission_for_model(self.queryset.model, 'view') + def get_table(self, request, permissions): + table = self.table(self.queryset, user=request.user) + if 'pk' in table.base_columns and (permissions['change'] or permissions['delete']): + table.columns.show('pk') + + return table + def export_yaml(self): """ Export the queryset of objects as concatenated YAML documents. @@ -123,8 +130,20 @@ class ObjectListView(ObjectPermissionRequiredMixin, View): filename=f'netbox_{self.queryset.model._meta.verbose_name_plural}.csv' ) - def get(self, request): + def export_template(self, template, request): + """ + Render an ExportTemplate using the current queryset. + :param template: ExportTemplate instance + :param request: The current request + """ + try: + return template.render_to_response(self.queryset) + except Exception as e: + messages.error(request, f"There was an error rendering the selected export template ({template.name}): {e}") + return redirect(request.path) + + def get(self, request): model = self.queryset.model content_type = ContentType.objects.get_for_model(model) @@ -137,42 +156,33 @@ class ObjectListView(ObjectPermissionRequiredMixin, View): perm_name = get_permission_for_model(model, action) permissions[action] = request.user.has_perm(perm_name) - # Export template/YAML rendering - if 'export' in request.GET and request.GET['export'] != 'table': + if 'export' in request.GET: - # An export template has been specified - if request.GET['export']: - et = get_object_or_404(ExportTemplate, content_type=content_type, name=request.GET['export']) - try: - return et.render_to_response(self.queryset) - except Exception as e: - messages.error( - request, - "There was an error rendering the selected export template ({}): {}".format( - et.name, e - ) - ) + # Export the current table view + if request.GET['export'] == 'table': + table = self.get_table(request, permissions) + columns = [name for name, _ in table.selected_columns] + return self.export_table(table, columns) - # Check for YAML export support + # Render an ExportTemplate + elif request.GET['export']: + template = get_object_or_404(ExportTemplate, content_type=content_type, name=request.GET['export']) + return self.export_template(template, request) + + # Check for YAML export support on the model elif hasattr(model, 'to_yaml'): response = HttpResponse(self.export_yaml(), content_type='text/yaml') filename = 'netbox_{}.yaml'.format(self.queryset.model._meta.verbose_name_plural) response['Content-Disposition'] = 'attachment; filename="{}"'.format(filename) return response - # Construct the objects table - table = self.table(self.queryset, user=request.user) - if 'pk' in table.base_columns and (permissions['change'] or permissions['delete']): - table.columns.show('pk') + # Fall back to default table/YAML export + else: + table = self.get_table(request, permissions) + return self.export_table(table) - # Handle table-based exports (current view or static CSV-based) - if request.GET.get('export') == 'table': - columns = [name for name, _ in table.selected_columns] - return self.export_table(table, columns) - elif 'export' in request.GET: - return self.export_table(table) - - # Paginate the objects table + # Render the objects table + table = self.get_table(request, permissions) paginate_table(table, request) context = { From bb99c3e6f9247d85f9f81e43e9d5505c6ddee1ee Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Fri, 12 Nov 2021 13:46:06 -0500 Subject: [PATCH 05/29] Changelog for #7803, #7810 --- docs/release-notes/version-3.0.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/release-notes/version-3.0.md b/docs/release-notes/version-3.0.md index e6217f967..de2ae3f74 100644 --- a/docs/release-notes/version-3.0.md +++ b/docs/release-notes/version-3.0.md @@ -2,6 +2,11 @@ ## v3.0.11 (FUTURE) +### Enhancements + +* [#7803](https://github.com/netbox-community/netbox/issues/7803) - Improve live reloading of custom scripts +* [#7810](https://github.com/netbox-community/netbox/issues/7810) - Add IEEE 802.15.1 interface type + ### Bug Fixes * [#7813](https://github.com/netbox-community/netbox/issues/7813) - Fix handling of errors during export template rendering From 1fed564c477e220e2b5acc19e30becf850e74e03 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Fri, 12 Nov 2021 14:44:14 -0500 Subject: [PATCH 06/29] Clean up script & report lists --- netbox/templates/extras/report_list.html | 190 +++++++++++------------ netbox/templates/extras/script_list.html | 130 ++++++++-------- 2 files changed, 149 insertions(+), 171 deletions(-) diff --git a/netbox/templates/extras/report_list.html b/netbox/templates/extras/report_list.html index 498e56b8d..99d6da730 100644 --- a/netbox/templates/extras/report_list.html +++ b/netbox/templates/extras/report_list.html @@ -3,108 +3,94 @@ {% block title %}Reports{% endblock %} -{% block content %} -
-
- {% if reports %} - {% for module, module_reports in reports %} -
-
{{ module|bettertitle }}
-
- - - - - - - - - - - - {% for report in module_reports %} - - - - - - - - {% for method, stats in report.result.data.items %} - - - - - {% endfor %} - {% endfor %} - -
NameStatusDescriptionLast Run
- {{ report.name }} - - {% include 'extras/inc/job_label.html' with result=report.result %} - {{ report.description|render_markdown|placeholder }} - {% if report.result %} - {{ report.result.created|annotated_date }} - {% else %} - Never - {% endif %} - - {% if perms.extras.run_report %} -
-
- {% csrf_token %} - -
-
- {% endif %} -
- {{ method }} - - {{ stats.success }} - {{ stats.info }} - {{ stats.warning }} - {{ stats.failure }} -
+{% block tabs %} + +{% endblock tabs %} + +{% block content-wrapper %} +
+ {% if reports %} + {% for module, module_reports in reports %} +
+
+ + {{ module|bettertitle }} +
+
+ + + + + + + + + + + + {% for report in module_reports %} + + + + + + + + {% for method, stats in report.result.data.items %} + + + + + {% endfor %} {% endfor %} - {% else %} - - {% endif %} + +
NameStatusDescriptionLast Run
+ {{ report.name }} + + {% include 'extras/inc/job_label.html' with result=report.result %} + {{ report.description|render_markdown|placeholder }} + {% if report.result %} + {{ report.result.created|annotated_date }} + {% else %} + Never + {% endif %} + + {% if perms.extras.run_report %} +
+
+ {% csrf_token %} + +
- + {% endif %} +
+ {{ method }} + + {{ stats.success }} + {{ stats.info }} + {{ stats.warning }} + {{ stats.failure }} +
+
-
- {% if reports %} -
-
- {% for module, module_reports in reports %} -
{{ module|bettertitle }}
- - {% endfor %} -
-
- {% endif %} -
-
-{% endblock %} + {% endfor %} + {% else %} + + {% endif %} +
+{% endblock content-wrapper %} diff --git a/netbox/templates/extras/script_list.html b/netbox/templates/extras/script_list.html index 1cc35d36c..ccbdca705 100644 --- a/netbox/templates/extras/script_list.html +++ b/netbox/templates/extras/script_list.html @@ -3,74 +3,66 @@ {% block title %}Scripts{% endblock %} -{% block content %} -
-
- {% if scripts %} - {% for module, module_scripts in scripts.items %} -

{{ module|bettertitle }}

- - - - - - - - - - - {% for class_name, script in module_scripts.items %} - - - - - {% if script.result %} - - {% else %} - - {% endif %} - - {% endfor %} - -
NameStatusDescriptionLast Run
- {{ script }} - - {% include 'extras/inc/job_label.html' with result=script.result %} - {{ script.Meta.description|render_markdown }} - {{ script.result.created|annotated_date }} - Never
+{% block tabs %} + +{% endblock tabs %} + +{% block content-wrapper %} +
+ {% if scripts %} + {% for module, module_scripts in scripts.items %} +
+
+ + {{ module|bettertitle }} +
+
+ + + + + + + + + + + {% for class_name, script in module_scripts.items %} + + + + + {% if script.result %} + + {% else %} + + {% endif %} + {% endfor %} - {% else %} -
-

No Scripts Found

- Scripts should be saved to {{ settings.SCRIPTS_ROOT }}. -
- This path can be changed by setting SCRIPTS_ROOT in NetBox's configuration. -
- {% endif %} + +
NameStatusDescriptionLast Run
+ {{ script }} + + {% include 'extras/inc/job_label.html' with result=script.result %} + + {{ script.Meta.description|render_markdown|placeholder }} + + {{ script.result.created|annotated_date }} + Never
+
-
- {% if scripts %} -
-
- {% for module, module_scripts in scripts.items %} -
{{ module|bettertitle }}
-
- -
- {% endfor %} -
-
- {% endif %} -
-
-{% endblock %} + {% endfor %} + {% else %} +
+

No Scripts Found

+ Scripts should be saved to {{ settings.SCRIPTS_ROOT }}. +
+ This path can be changed by setting SCRIPTS_ROOT in NetBox's configuration. +
+ {% endif %} +
+{% endblock content-wrapper %} From f49d7008a0d7074e0211a753eeed81fd81b1a949 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Fri, 12 Nov 2021 15:05:33 -0500 Subject: [PATCH 07/29] Add q filters for connection lists --- netbox/dcim/filtersets.py | 13 +++++++++++++ netbox/dcim/forms/filtersets.py | 15 +++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py index df7f415e2..ba7ede783 100644 --- a/netbox/dcim/filtersets.py +++ b/netbox/dcim/filtersets.py @@ -1394,6 +1394,10 @@ class PowerFeedFilterSet(PrimaryModelFilterSet, CableTerminationFilterSet, PathE # class ConnectionFilterSet(BaseFilterSet): + q = django_filters.CharFilter( + method='search', + label='Search', + ) site_id = MultiValueNumberFilter( method='filter_connections', field_name='device__site_id' @@ -1416,6 +1420,15 @@ class ConnectionFilterSet(BaseFilterSet): return queryset return queryset.filter(**{f'{name}__in': value}) + def search(self, queryset, name, value): + if not value.strip(): + return queryset + qs_filter = ( + Q(device__name__icontains=value) | + Q(cable__label__icontains=value) + ) + return queryset.filter(qs_filter) + class ConsoleConnectionFilterSet(ConnectionFilterSet): diff --git a/netbox/dcim/forms/filtersets.py b/netbox/dcim/forms/filtersets.py index 4ef53c469..93299a17e 100644 --- a/netbox/dcim/forms/filtersets.py +++ b/netbox/dcim/forms/filtersets.py @@ -1068,6 +1068,11 @@ class InventoryItemFilterForm(DeviceComponentFilterForm): # class ConsoleConnectionFilterForm(BootstrapMixin, forms.Form): + q = forms.CharField( + required=False, + widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), + label=_('Search') + ) region_id = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), required=False, @@ -1095,6 +1100,11 @@ class ConsoleConnectionFilterForm(BootstrapMixin, forms.Form): class PowerConnectionFilterForm(BootstrapMixin, forms.Form): + q = forms.CharField( + required=False, + widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), + label=_('Search') + ) region_id = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), required=False, @@ -1122,6 +1132,11 @@ class PowerConnectionFilterForm(BootstrapMixin, forms.Form): class InterfaceConnectionFilterForm(BootstrapMixin, forms.Form): + q = forms.CharField( + required=False, + widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), + label=_('Search') + ) region_id = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), required=False, From 50f283cf28ae607731b314646707991d11014ae9 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Fri, 12 Nov 2021 15:17:44 -0500 Subject: [PATCH 08/29] Add q filter for extras models --- netbox/extras/filtersets.py | 60 +++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/netbox/extras/filtersets.py b/netbox/extras/filtersets.py index af8d904f4..1ed25cdac 100644 --- a/netbox/extras/filtersets.py +++ b/netbox/extras/filtersets.py @@ -35,6 +35,10 @@ EXACT_FILTER_TYPES = ( class WebhookFilterSet(BaseFilterSet): + q = django_filters.CharFilter( + method='search', + label='Search', + ) content_types = ContentTypeFilter() http_method = django_filters.MultipleChoiceFilter( choices=WebhookHttpMethodChoices @@ -47,30 +51,81 @@ class WebhookFilterSet(BaseFilterSet): 'http_method', 'http_content_type', 'secret', 'ssl_verification', 'ca_file_path', ] + def search(self, queryset, name, value): + if not value.strip(): + return queryset + return queryset.filter( + Q(name__icontains=value) | + Q(payload_url__icontains=value) + ) + class CustomFieldFilterSet(BaseFilterSet): + q = django_filters.CharFilter( + method='search', + label='Search', + ) content_types = ContentTypeFilter() class Meta: model = CustomField fields = ['id', 'content_types', 'name', 'required', 'filter_logic', 'weight'] + def search(self, queryset, name, value): + if not value.strip(): + return queryset + return queryset.filter( + Q(name__icontains=value) | + Q(label__icontains=value) | + Q(description__icontains=value) + ) + class CustomLinkFilterSet(BaseFilterSet): + q = django_filters.CharFilter( + method='search', + label='Search', + ) class Meta: model = CustomLink fields = ['id', 'content_type', 'name', 'link_text', 'link_url', 'weight', 'group_name', 'new_window'] + def search(self, queryset, name, value): + if not value.strip(): + return queryset + return queryset.filter( + Q(name__icontains=value) | + Q(link_text__icontains=value) | + Q(link_url__icontains=value) | + Q(group_name__icontains=value) + ) + class ExportTemplateFilterSet(BaseFilterSet): + q = django_filters.CharFilter( + method='search', + label='Search', + ) class Meta: model = ExportTemplate fields = ['id', 'content_type', 'name'] + def search(self, queryset, name, value): + if not value.strip(): + return queryset + return queryset.filter( + Q(name__icontains=value) | + Q(description__icontains=value) + ) + class ImageAttachmentFilterSet(BaseFilterSet): + q = django_filters.CharFilter( + method='search', + label='Search', + ) created = django_filters.DateTimeFilter() content_type = ContentTypeFilter() @@ -78,6 +133,11 @@ class ImageAttachmentFilterSet(BaseFilterSet): model = ImageAttachment fields = ['id', 'content_type_id', 'object_id', 'name'] + def search(self, queryset, name, value): + if not value.strip(): + return queryset + return queryset.filter(name__icontains=value) + class JournalEntryFilterSet(ChangeLoggedModelFilterSet): q = django_filters.CharFilter( From 467fa5a84757bb011515356ca498f7c9037dff55 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Fri, 12 Nov 2021 15:30:16 -0500 Subject: [PATCH 09/29] Add q filters for Token and ObjectPermission filter sets --- netbox/users/filtersets.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/netbox/users/filtersets.py b/netbox/users/filtersets.py index 80643c1e5..ad296ea25 100644 --- a/netbox/users/filtersets.py +++ b/netbox/users/filtersets.py @@ -99,8 +99,20 @@ class TokenFilterSet(BaseFilterSet): model = Token fields = ['id', 'key', 'write_enabled'] + def search(self, queryset, name, value): + if not value.strip(): + return queryset + return queryset.filter( + Q(user__username__icontains=value) | + Q(description__icontains=value) + ) + class ObjectPermissionFilterSet(BaseFilterSet): + q = django_filters.CharFilter( + method='search', + label='Search', + ) user_id = django_filters.ModelMultipleChoiceFilter( field_name='users', queryset=User.objects.all(), @@ -127,3 +139,11 @@ class ObjectPermissionFilterSet(BaseFilterSet): class Meta: model = ObjectPermission fields = ['id', 'name', 'enabled', 'object_types'] + + def search(self, queryset, name, value): + if not value.strip(): + return queryset + return queryset.filter( + Q(name__icontains=value) | + Q(description__icontains=value) + ) From f3beabba69c369fb4f53ac832150ce87c83e25c5 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Fri, 12 Nov 2021 15:33:49 -0500 Subject: [PATCH 10/29] Changelog for #2101 --- docs/release-notes/version-3.0.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes/version-3.0.md b/docs/release-notes/version-3.0.md index de2ae3f74..bb9a18c75 100644 --- a/docs/release-notes/version-3.0.md +++ b/docs/release-notes/version-3.0.md @@ -4,6 +4,7 @@ ### Enhancements +* [#2101](https://github.com/netbox-community/netbox/issues/2101) - Add missing `q` filters for necessary models * [#7803](https://github.com/netbox-community/netbox/issues/7803) - Improve live reloading of custom scripts * [#7810](https://github.com/netbox-community/netbox/issues/7810) - Add IEEE 802.15.1 interface type From fa8a8abc984faa62073fd06996c2dce436751bd2 Mon Sep 17 00:00:00 2001 From: Rhys Barrie Date: Sat, 13 Nov 2021 21:30:38 -0500 Subject: [PATCH 11/29] netbox-community/netbox#7424: Add virtual_chassis and virtual_chassis_id filter to device components --- netbox/dcim/filtersets.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py index ba7ede783..f7cf011ce 100644 --- a/netbox/dcim/filtersets.py +++ b/netbox/dcim/filtersets.py @@ -861,6 +861,17 @@ class DeviceComponentFilterSet(django_filters.FilterSet): to_field_name='name', label='Device (name)', ) + virtual_chassis_id = django_filters.ModelMultipleChoiceFilter( + field_name='device__virtual_chassis', + queryset=VirtualChassis.objects.all(), + label='Virtual Chassis (ID)' + ) + virtual_chassis = django_filters.ModelMultipleChoiceFilter( + field_name='device__virtual_chassis__name', + queryset=VirtualChassis.objects.all(), + to_field_name='name', + label='Virtual Chassis', + ) tag = TagFilter() def search(self, queryset, name, value): From 6b21c8453fc68cb6bb999f69fd52342759aed22c Mon Sep 17 00:00:00 2001 From: Rhys Barrie Date: Sat, 13 Nov 2021 21:33:52 -0500 Subject: [PATCH 12/29] netbox-community/netbox#7424: Add virtual_chassis field to device component filter form --- netbox/dcim/api/nested_serializers.py | 2 +- netbox/dcim/forms/filtersets.py | 24 +++++++++++++++--------- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/netbox/dcim/api/nested_serializers.py b/netbox/dcim/api/nested_serializers.py index 67ae9b046..1fdde78d7 100644 --- a/netbox/dcim/api/nested_serializers.py +++ b/netbox/dcim/api/nested_serializers.py @@ -340,7 +340,7 @@ class NestedVirtualChassisSerializer(WritableNestedSerializer): class Meta: model = models.VirtualChassis - fields = ['id', 'name', 'url', 'master', 'member_count'] + fields = ['id', 'url', 'display', 'name', 'master', 'member_count'] # diff --git a/netbox/dcim/forms/filtersets.py b/netbox/dcim/forms/filtersets.py index 93299a17e..6d76c4003 100644 --- a/netbox/dcim/forms/filtersets.py +++ b/netbox/dcim/forms/filtersets.py @@ -92,6 +92,12 @@ class DeviceComponentFilterForm(BootstrapMixin, CustomFieldModelFilterForm): label=_('Location'), fetch_trigger='open' ) + virtual_chassis_id = DynamicModelMultipleChoiceField( + queryset=VirtualChassis.objects.all(), + required=False, + label=_('Virtual Chassis'), + fetch_trigger='open' + ) device_id = DynamicModelMultipleChoiceField( queryset=Device.objects.all(), required=False, @@ -888,7 +894,7 @@ class ConsolePortFilterForm(DeviceComponentFilterForm): field_groups = [ ['q', 'tag'], ['name', 'label', 'type', 'speed'], - ['region_id', 'site_group_id', 'site_id', 'location_id', 'device_id'], + ['region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id'], ] type = forms.MultipleChoiceField( choices=ConsolePortTypeChoices, @@ -908,7 +914,7 @@ class ConsoleServerPortFilterForm(DeviceComponentFilterForm): field_groups = [ ['q', 'tag'], ['name', 'label', 'type', 'speed'], - ['region_id', 'site_group_id', 'site_id', 'location_id', 'device_id'], + ['region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id'], ] type = forms.MultipleChoiceField( choices=ConsolePortTypeChoices, @@ -928,7 +934,7 @@ class PowerPortFilterForm(DeviceComponentFilterForm): field_groups = [ ['q', 'tag'], ['name', 'label', 'type'], - ['region_id', 'site_group_id', 'site_id', 'location_id', 'device_id'], + ['region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id'], ] type = forms.MultipleChoiceField( choices=PowerPortTypeChoices, @@ -943,7 +949,7 @@ class PowerOutletFilterForm(DeviceComponentFilterForm): field_groups = [ ['q', 'tag'], ['name', 'label', 'type'], - ['region_id', 'site_group_id', 'site_id', 'location_id', 'device_id'], + ['region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id'], ] type = forms.MultipleChoiceField( choices=PowerOutletTypeChoices, @@ -958,7 +964,7 @@ class InterfaceFilterForm(DeviceComponentFilterForm): field_groups = [ ['q', 'tag'], ['name', 'label', 'kind', 'type', 'enabled', 'mgmt_only', 'mac_address'], - ['region_id', 'site_group_id', 'site_id', 'location_id', 'device_id'], + ['region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id'], ] kind = forms.MultipleChoiceField( choices=InterfaceKindChoices, @@ -993,7 +999,7 @@ class FrontPortFilterForm(DeviceComponentFilterForm): field_groups = [ ['q', 'tag'], ['name', 'label', 'type', 'color'], - ['region_id', 'site_group_id', 'site_id', 'location_id', 'device_id'], + ['region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id'], ] model = FrontPort type = forms.MultipleChoiceField( @@ -1012,7 +1018,7 @@ class RearPortFilterForm(DeviceComponentFilterForm): field_groups = [ ['q', 'tag'], ['name', 'label', 'type', 'color'], - ['region_id', 'site_group_id', 'site_id', 'location_id', 'device_id'], + ['region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id'], ] type = forms.MultipleChoiceField( choices=PortTypeChoices, @@ -1030,7 +1036,7 @@ class DeviceBayFilterForm(DeviceComponentFilterForm): field_groups = [ ['q', 'tag'], ['name', 'label'], - ['region_id', 'site_group_id', 'site_id', 'location_id', 'device_id'], + ['region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id'], ] tag = TagFilterField(model) @@ -1040,7 +1046,7 @@ class InventoryItemFilterForm(DeviceComponentFilterForm): field_groups = [ ['q', 'tag'], ['name', 'label', 'manufacturer_id', 'serial', 'asset_tag', 'discovered'], - ['region_id', 'site_group_id', 'site_id', 'location_id', 'device_id'], + ['region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id'], ] manufacturer_id = DynamicModelMultipleChoiceField( queryset=Manufacturer.objects.all(), From f77f7ca0ec7887ad1f075654f87658908d83346b Mon Sep 17 00:00:00 2001 From: Rhys Barrie Date: Sat, 13 Nov 2021 21:35:13 -0500 Subject: [PATCH 13/29] netbox-community/netbox#7424:make device component device field filter from selected virtual chassis --- netbox/dcim/forms/filtersets.py | 1 + 1 file changed, 1 insertion(+) diff --git a/netbox/dcim/forms/filtersets.py b/netbox/dcim/forms/filtersets.py index 6d76c4003..70a20d8a5 100644 --- a/netbox/dcim/forms/filtersets.py +++ b/netbox/dcim/forms/filtersets.py @@ -104,6 +104,7 @@ class DeviceComponentFilterForm(BootstrapMixin, CustomFieldModelFilterForm): query_params={ 'site_id': '$site_id', 'location_id': '$location_id', + 'virtual_chassis_id': '$virtual_chassis_id' }, label=_('Device'), fetch_trigger='open' From a8c958ece2038be0ac849312663785f4e4791d51 Mon Sep 17 00:00:00 2001 From: Rhys Barrie Date: Sat, 13 Nov 2021 22:01:15 -0500 Subject: [PATCH 14/29] netbox-community/netbox#7424: fix test failure from adding virtual chassis filter field --- netbox/dcim/tests/test_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/dcim/tests/test_api.py b/netbox/dcim/tests/test_api.py index e5977b760..3fc48beed 100644 --- a/netbox/dcim/tests/test_api.py +++ b/netbox/dcim/tests/test_api.py @@ -1524,7 +1524,7 @@ class ConnectedDeviceTest(APITestCase): class VirtualChassisTest(APIViewTestCases.APIViewTestCase): model = VirtualChassis - brief_fields = ['id', 'master', 'member_count', 'name', 'url'] + brief_fields = ['display', 'id', 'master', 'member_count', 'name', 'url'] @classmethod def setUpTestData(cls): From 68b544c676fba9542d09cd7b5c71e0315c885b80 Mon Sep 17 00:00:00 2001 From: Rhys Barrie Date: Sat, 13 Nov 2021 22:16:18 -0500 Subject: [PATCH 15/29] netbox-community/netbox#7424: add filterset test for virtual_chassis_id --- netbox/dcim/tests/test_filtersets.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/netbox/dcim/tests/test_filtersets.py b/netbox/dcim/tests/test_filtersets.py index fb94bde08..2b5da8576 100644 --- a/netbox/dcim/tests/test_filtersets.py +++ b/netbox/dcim/tests/test_filtersets.py @@ -2048,6 +2048,11 @@ class InterfaceTestCase(TestCase, ChangeLoggedFilterSetTests): ) Device.objects.bulk_create(devices) + # VirtualChassis assignment for filtering + virtual_chassis = VirtualChassis.objects.create(master=devices[0]) + Device.objects.filter(pk=devices[0].pk).update(virtual_chassis=virtual_chassis, vc_position=1, vc_priority=1) + Device.objects.filter(pk=devices[1].pk).update(virtual_chassis=virtual_chassis, vc_position=2, vc_priority=2) + interfaces = ( Interface(device=devices[0], name='Interface 1', label='A', type=InterfaceTypeChoices.TYPE_1GE_SFP, enabled=True, mgmt_only=True, mtu=100, mode=InterfaceModeChoices.MODE_ACCESS, mac_address='00-00-00-00-00-01', description='First'), Interface(device=devices[1], name='Interface 2', label='B', type=InterfaceTypeChoices.TYPE_1GE_GBIC, enabled=True, mgmt_only=True, mtu=200, mode=InterfaceModeChoices.MODE_TAGGED, mac_address='00-00-00-00-00-02', description='Second'), @@ -2157,6 +2162,10 @@ class InterfaceTestCase(TestCase, ChangeLoggedFilterSetTests): params = {'location': [locations[0].slug, locations[1].slug]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_virtual_chassis_id(self): + params = {'virtual_chassis_id': [VirtualChassis.objects.first().pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_device(self): devices = Device.objects.all()[:2] params = {'device_id': [devices[0].pk, devices[1].pk]} From a5a480133ff0c2c9764f2b72d10d0c50434fac08 Mon Sep 17 00:00:00 2001 From: Rhys Barrie Date: Sat, 13 Nov 2021 23:08:46 -0500 Subject: [PATCH 16/29] netbox-community/netbox#7229: Fix context of VLAN table in VLAN Group view --- netbox/templates/ipam/vlangroup.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/netbox/templates/ipam/vlangroup.html b/netbox/templates/ipam/vlangroup.html index a46bef3b0..daa2c8e8c 100644 --- a/netbox/templates/ipam/vlangroup.html +++ b/netbox/templates/ipam/vlangroup.html @@ -1,6 +1,7 @@ {% extends 'generic/object.html' %} {% load helpers %} {% load plugins %} +{% load render_table from django_tables2 %} {% block breadcrumbs %} {{ block.super }} @@ -68,7 +69,7 @@ VLANs
- {% include 'inc/table.html' with table=vlans_table %} + {% render_table vlans_table 'inc/table.html' %}
{% if perms.ipam.add_vlan %}