From c89193d3317a0f0655634fdba53bf30789c967da Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 28 Jul 2023 08:11:28 -0400 Subject: [PATCH 01/24] Closes #13080: Differentiate more clearly between old and new version placeholders in upgrade guide --- docs/installation/upgrading.md | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/docs/installation/upgrading.md b/docs/installation/upgrading.md index 27401c3cf..95304cd98 100644 --- a/docs/installation/upgrading.md +++ b/docs/installation/upgrading.md @@ -48,36 +48,40 @@ Download the [latest stable release](https://github.com/netbox-community/netbox/ Download and extract the latest version: ```no-highlight -wget https://github.com/netbox-community/netbox/archive/vX.Y.Z.tar.gz -sudo tar -xzf vX.Y.Z.tar.gz -C /opt -sudo ln -sfn /opt/netbox-X.Y.Z/ /opt/netbox +# Set $NEWVER to the NetBox version being installed +NEWVER=3.5.0 +wget https://github.com/netbox-community/netbox/archive/v$NEWVER.tar.gz +sudo tar -xzf v$NEWVER.tar.gz -C /opt +sudo ln -sfn /opt/netbox-$NEWVER/ /opt/netbox ``` Copy `local_requirements.txt`, `configuration.py`, and `ldap_config.py` (if present) from the current installation to the new version: ```no-highlight -sudo cp /opt/netbox-X.Y.Z/local_requirements.txt /opt/netbox/ -sudo cp /opt/netbox-X.Y.Z/netbox/netbox/configuration.py /opt/netbox/netbox/netbox/ -sudo cp /opt/netbox-X.Y.Z/netbox/netbox/ldap_config.py /opt/netbox/netbox/netbox/ +# Set $OLDVER to the NetBox version currently installed +NEWVER=3.4.9 +sudo cp /opt/netbox-$OLDVER/local_requirements.txt /opt/netbox/ +sudo cp /opt/netbox-$OLDVER/netbox/netbox/configuration.py /opt/netbox/netbox/netbox/ +sudo cp /opt/netbox-$OLDVER/netbox/netbox/ldap_config.py /opt/netbox/netbox/netbox/ ``` Be sure to replicate your uploaded media as well. (The exact action necessary will depend on where you choose to store your media, but in general moving or copying the media directory will suffice.) ```no-highlight -sudo cp -pr /opt/netbox-X.Y.Z/netbox/media/ /opt/netbox/netbox/ +sudo cp -pr /opt/netbox-$OLDVER/netbox/media/ /opt/netbox/netbox/ ``` Also make sure to copy or link any custom scripts and reports that you've made. Note that if these are stored outside the project root, you will not need to copy them. (Check the `SCRIPTS_ROOT` and `REPORTS_ROOT` parameters in the configuration file above if you're unsure.) ```no-highlight -sudo cp -r /opt/netbox-X.Y.Z/netbox/scripts /opt/netbox/netbox/ -sudo cp -r /opt/netbox-X.Y.Z/netbox/reports /opt/netbox/netbox/ +sudo cp -r /opt/netbox-$OLDVER/netbox/scripts /opt/netbox/netbox/ +sudo cp -r /opt/netbox-$OLDVER/netbox/reports /opt/netbox/netbox/ ``` If you followed the original installation guide to set up gunicorn, be sure to copy its configuration as well: ```no-highlight -sudo cp /opt/netbox-X.Y.Z/gunicorn.py /opt/netbox/ +sudo cp /opt/netbox-$OLDVER/gunicorn.py /opt/netbox/ ``` ### Option B: Clone the Git Repository From 7158360dfa296e1c3192bc3b9172ba1ae50db5f9 Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Tue, 18 Jul 2023 14:58:04 +0530 Subject: [PATCH 02/24] moves non-racked devices to tab #11803 --- netbox/dcim/views.py | 28 +++++++--- netbox/templates/dcim/rack.html | 1 - .../dcim/rack/non_racked_devices.html | 51 +++++++++++++++++++ 3 files changed, 71 insertions(+), 9 deletions(-) create mode 100644 netbox/templates/dcim/rack/non_racked_devices.html diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 008db382a..5b93e5f0b 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -681,13 +681,6 @@ class RackView(generic.ObjectView): (PowerFeed.objects.restrict(request.user).filter(rack=instance), 'rack_id'), ) - # Get 0U devices located within the rack - nonracked_devices = Device.objects.filter( - rack=instance, - position__isnull=True, - parent_bay__isnull=True - ).prefetch_related('device_type__manufacturer', 'parent_bay', 'device_role') - peer_racks = Rack.objects.restrict(request.user, 'view').filter(site=instance.site) if instance.location: @@ -704,7 +697,6 @@ class RackView(generic.ObjectView): return { 'related_models': related_models, - 'nonracked_devices': nonracked_devices, 'next_rack': next_rack, 'prev_rack': prev_rack, 'svg_extra': svg_extra, @@ -731,6 +723,26 @@ class RackRackReservationsView(generic.ObjectChildrenView): return parent.reservations.restrict(request.user, 'view') +@register_model_view(Rack, 'nonracked_devices', 'nonracked-devices') +class RackNonRackedView(generic.ObjectChildrenView): + queryset = Rack.objects.all() + child_model = Device + table = tables.DeviceTable + filterset = filtersets.DeviceFilterSet + template_name = 'dcim/rack/non_racked_devices.html' + tab = ViewTab( + label=_('Non-Racked Devices'), + badge=lambda obj: obj.devices.filter(rack=obj, position__isnull=True, parent_bay__isnull=True).count(), + weight=500, + permission='dcim.view_device', + ) + + def get_children(self, request, parent): + return parent.devices.restrict(request.user, 'view').filter( + rack=parent, position__isnull=True, parent_bay__isnull=True + ) + + @register_model_view(Rack, 'edit') class RackEditView(generic.ObjectEditView): queryset = Rack.objects.all() diff --git a/netbox/templates/dcim/rack.html b/netbox/templates/dcim/rack.html index 52b5d4bfe..e513b178d 100644 --- a/netbox/templates/dcim/rack.html +++ b/netbox/templates/dcim/rack.html @@ -190,7 +190,6 @@ {% include 'inc/panels/related_objects.html' %} - {% include 'dcim/inc/nonracked_devices.html' %} {% plugin_right_page object %} diff --git a/netbox/templates/dcim/rack/non_racked_devices.html b/netbox/templates/dcim/rack/non_racked_devices.html new file mode 100644 index 000000000..700c66369 --- /dev/null +++ b/netbox/templates/dcim/rack/non_racked_devices.html @@ -0,0 +1,51 @@ +{% extends 'dcim/rack/base.html' %} +{% load helpers %} + +{% block extra_controls %} + {% if perms.dcim.add_device %} + + {% endif %} +{% endblock %} + +{% block content %} + {% include 'inc/table_controls_htmx.html' with table_modal="DeviceTable_config" %} + +
+ {% csrf_token %} + +
+
+ {% include 'htmx/table.html' %} +
+
+ +
+
+ {% if 'bulk_edit' in actions %} + + {% endif %} + {% if 'bulk_delete' in actions %} + + {% endif %} +
+
+
+{% endblock content %} + +{% block modals %} + {{ block.super }} + {% table_config_form table %} +{% endblock modals %} From 2aa51d0d94322bd2c38c7eafd0f1913e708b5d15 Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Fri, 28 Jul 2023 18:53:22 +0530 Subject: [PATCH 03/24] Adds contact assignment bulk import (#13109) * adds contact assignment bulk import #11307 * Remove unsupported tags field added by NetBoxModelImportForm --------- Co-authored-by: Jeremy Stretch --- netbox/netbox/navigation/menu.py | 2 +- netbox/tenancy/forms/bulk_import.py | 28 +++++++++++++++++++++++++++- netbox/tenancy/urls.py | 1 + netbox/tenancy/views.py | 5 +++++ 4 files changed, 34 insertions(+), 2 deletions(-) diff --git a/netbox/netbox/navigation/menu.py b/netbox/netbox/navigation/menu.py index e009f62f1..1379beba5 100644 --- a/netbox/netbox/navigation/menu.py +++ b/netbox/netbox/navigation/menu.py @@ -46,7 +46,7 @@ ORGANIZATION_MENU = Menu( get_model_item('tenancy', 'contact', _('Contacts')), get_model_item('tenancy', 'contactgroup', _('Contact Groups')), get_model_item('tenancy', 'contactrole', _('Contact Roles')), - get_model_item('tenancy', 'contactassignment', _('Contact Assignments'), actions=[]), + get_model_item('tenancy', 'contactassignment', _('Contact Assignments'), actions=['import']), ), ), ), diff --git a/netbox/tenancy/forms/bulk_import.py b/netbox/tenancy/forms/bulk_import.py index f9b8accd9..0aec0e28f 100644 --- a/netbox/tenancy/forms/bulk_import.py +++ b/netbox/tenancy/forms/bulk_import.py @@ -1,9 +1,11 @@ +from django.contrib.contenttypes.models import ContentType from django.utils.translation import gettext as _ from netbox.forms import NetBoxModelImportForm from tenancy.models import * -from utilities.forms.fields import CSVModelChoiceField, SlugField +from utilities.forms.fields import CSVContentTypeField, CSVModelChoiceField, SlugField __all__ = ( + 'ContactAssignmentImportForm', 'ContactImportForm', 'ContactGroupImportForm', 'ContactRoleImportForm', @@ -81,3 +83,27 @@ class ContactImportForm(NetBoxModelImportForm): class Meta: model = Contact fields = ('name', 'title', 'phone', 'email', 'address', 'link', 'group', 'description', 'comments', 'tags') + + +class ContactAssignmentImportForm(NetBoxModelImportForm): + content_type = CSVContentTypeField( + queryset=ContentType.objects.all(), + help_text=_("One or more assigned object types") + ) + contact = CSVModelChoiceField( + queryset=Contact.objects.all(), + to_field_name='name', + help_text=_('Assigned contact') + ) + role = CSVModelChoiceField( + queryset=ContactRole.objects.all(), + to_field_name='name', + help_text=_('Assigned role') + ) + + # Remove the tags field added by NetBoxModelImportForm (unsupported by ContactAssignment) + tags = None + + class Meta: + model = ContactAssignment + fields = ('content_type', 'object_id', 'contact', 'priority', 'role') diff --git a/netbox/tenancy/urls.py b/netbox/tenancy/urls.py index 87491ea0e..ad9908c62 100644 --- a/netbox/tenancy/urls.py +++ b/netbox/tenancy/urls.py @@ -49,6 +49,7 @@ urlpatterns = [ # Contact assignments path('contact-assignments/', views.ContactAssignmentListView.as_view(), name='contactassignment_list'), path('contact-assignments/add/', views.ContactAssignmentEditView.as_view(), name='contactassignment_add'), + path('contact-assignments/import/', views.ContactAssignmentBulkImportView.as_view(), name='contactassignment_import'), path('contact-assignments/edit/', views.ContactAssignmentBulkEditView.as_view(), name='contactassignment_bulk_edit'), path('contact-assignments/delete/', views.ContactAssignmentBulkDeleteView.as_view(), name='contactassignment_bulk_delete'), path('contact-assignments//', include(get_model_urls('tenancy', 'contactassignment'))), diff --git a/netbox/tenancy/views.py b/netbox/tenancy/views.py index bbe901bde..23020e794 100644 --- a/netbox/tenancy/views.py +++ b/netbox/tenancy/views.py @@ -420,6 +420,11 @@ class ContactAssignmentBulkEditView(generic.BulkEditView): form = forms.ContactAssignmentBulkEditForm +class ContactAssignmentBulkImportView(generic.BulkImportView): + queryset = ContactAssignment.objects.all() + model_form = forms.ContactAssignmentImportForm + + class ContactAssignmentBulkDeleteView(generic.BulkDeleteView): queryset = ContactAssignment.objects.all() filterset = filtersets.ContactAssignmentFilterSet From 9d0457fe1aa4f0c081d9bc7be0842155a65f1cfc Mon Sep 17 00:00:00 2001 From: Bruno Blanes Date: Fri, 28 Jul 2023 10:26:46 -0300 Subject: [PATCH 04/24] Add Brazilian power outlet standard to choices.py (#13012) * Add Brazilian power outlet standard to choices.py * Eliminate possible name conflict * Rename group and add IEC 60906-1 plug type * Update choices.py Add Brazilian power port standard --- netbox/dcim/choices.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/netbox/dcim/choices.py b/netbox/dcim/choices.py index 1112b1344..e850a8c51 100644 --- a/netbox/dcim/choices.py +++ b/netbox/dcim/choices.py @@ -318,6 +318,10 @@ class PowerPortTypeChoices(ChoiceSet): TYPE_IEC_3PNE4H = 'iec-60309-3p-n-e-4h' TYPE_IEC_3PNE6H = 'iec-60309-3p-n-e-6h' TYPE_IEC_3PNE9H = 'iec-60309-3p-n-e-9h' + # IEC 60906-1 + TYPE_IEC_60906_1 = 'iec-60906-1' + TYPE_NBR_14136_10A = 'nbr-14136-10a' + TYPE_NBR_14136_20A = 'nbr-14136-20a' # NEMA non-locking TYPE_NEMA_115P = 'nema-1-15p' TYPE_NEMA_515P = 'nema-5-15p' @@ -429,6 +433,11 @@ class PowerPortTypeChoices(ChoiceSet): (TYPE_IEC_3PNE6H, '3P+N+E 6H'), (TYPE_IEC_3PNE9H, '3P+N+E 9H'), )), + ('IEC 60906-1', ( + (TYPE_IEC_60906_1, 'IEC 60906-1'), + (TYPE_NBR_14136_10A, '2P+T 10A (NBR 14136)'), + (TYPE_NBR_14136_20A, '2P+T 20A (NBR 14136)'), + )), ('NEMA (Non-locking)', ( (TYPE_NEMA_115P, 'NEMA 1-15P'), (TYPE_NEMA_515P, 'NEMA 5-15P'), @@ -553,6 +562,10 @@ class PowerOutletTypeChoices(ChoiceSet): TYPE_IEC_3PNE4H = 'iec-60309-3p-n-e-4h' TYPE_IEC_3PNE6H = 'iec-60309-3p-n-e-6h' TYPE_IEC_3PNE9H = 'iec-60309-3p-n-e-9h' + # IEC 60906-1 + TYPE_IEC_60906_1 = 'iec-60906-1' + TYPE_NBR_14136_10A = 'nbr-14136-10a' + TYPE_NBR_14136_20A = 'nbr-14136-20a' # NEMA non-locking TYPE_NEMA_115R = 'nema-1-15r' TYPE_NEMA_515R = 'nema-5-15r' @@ -657,6 +670,11 @@ class PowerOutletTypeChoices(ChoiceSet): (TYPE_IEC_3PNE6H, '3P+N+E 6H'), (TYPE_IEC_3PNE9H, '3P+N+E 9H'), )), + ('IEC 60906-1', ( + (TYPE_IEC_60906_1, 'IEC 60906-1'), + (TYPE_NBR_14136_10A, '2P+T 10A (NBR 14136)'), + (TYPE_NBR_14136_20A, '2P+T 20A (NBR 14136)'), + )), ('NEMA (Non-locking)', ( (TYPE_NEMA_115R, 'NEMA 1-15R'), (TYPE_NEMA_515R, 'NEMA 5-15R'), From 90146941b5b1301258babfa91de96267badf4657 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 28 Jul 2023 09:49:09 -0400 Subject: [PATCH 05/24] Fixes #13285: Cast default u_height value to a decimal for validation --- netbox/dcim/models/devices.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/dcim/models/devices.py b/netbox/dcim/models/devices.py index 4cf330ffd..fbc92e1fe 100644 --- a/netbox/dcim/models/devices.py +++ b/netbox/dcim/models/devices.py @@ -232,7 +232,7 @@ class DeviceType(PrimaryModel, WeightMixin): super().clean() # U height must be divisible by 0.5 - if self.u_height % decimal.Decimal(0.5): + if decimal.Decimal(self.u_height) % decimal.Decimal(0.5): raise ValidationError({ 'u_height': "U height must be in increments of 0.5 rack units." }) From d9dc6cec3a07bf555d8613a77f7af1ce1954db6f Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 28 Jul 2023 10:02:42 -0400 Subject: [PATCH 06/24] Changelog for #11803, #13009, #13234, #13285 --- docs/release-notes/version-3.5.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/release-notes/version-3.5.md b/docs/release-notes/version-3.5.md index 62e8741d6..ddbc47154 100644 --- a/docs/release-notes/version-3.5.md +++ b/docs/release-notes/version-3.5.md @@ -4,8 +4,11 @@ ### Enhancements +* [#11803](https://github.com/netbox-community/netbox/issues/11803) - Move non-rack devices list to a separate tab under the rack view * [#12625](https://github.com/netbox-community/netbox/issues/12625) - Mask sensitive parameters when viewing a configured data source +* [#13009](https://github.com/netbox-community/netbox/issues/13009) - Add IEC 10609-1 and NBR 14136 power port & outlet types * [#13097](https://github.com/netbox-community/netbox/issues/13097) - Implement a faster initial poll for report & script results +* [#13234](https://github.com/netbox-community/netbox/issues/13234) - Add 100GBASE-X-DSFP and 100GBASE-X-SFPDD interface types ### Bug Fixes @@ -13,6 +16,7 @@ * [#13167](https://github.com/netbox-community/netbox/issues/13167) - Fix missing script results when fetched via REST API * [#13233](https://github.com/netbox-community/netbox/issues/13233) - Remove extraneous VLAN group field from bulk edit form for interfaces * [#13237](https://github.com/netbox-community/netbox/issues/13237) - Permit unauthenticated access to content types REST API endpoint when `LOGIN_REQUIRED` is false +* [#13285](https://github.com/netbox-community/netbox/issues/13285) - Fix exception when importing device type missing rack unit height value --- From 4f984c0831de10994888063a5836a090c5c6201e Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 28 Jul 2023 10:11:16 -0400 Subject: [PATCH 07/24] Release v3.5.7 --- .github/ISSUE_TEMPLATE/bug_report.yaml | 2 +- .github/ISSUE_TEMPLATE/feature_request.yaml | 2 +- docs/release-notes/version-3.5.md | 2 +- netbox/netbox/settings.py | 2 +- requirements.txt | 12 ++++++------ 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index 8cb548de2..42a716ae7 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -14,7 +14,7 @@ body: attributes: label: NetBox version description: What version of NetBox are you currently running? - placeholder: v3.5.6 + placeholder: v3.5.7 validations: required: true - type: dropdown diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml index df931c77b..b04fda1b6 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yaml +++ b/.github/ISSUE_TEMPLATE/feature_request.yaml @@ -14,7 +14,7 @@ body: attributes: label: NetBox version description: What version of NetBox are you currently running? - placeholder: v3.5.6 + placeholder: v3.5.7 validations: required: true - type: dropdown diff --git a/docs/release-notes/version-3.5.md b/docs/release-notes/version-3.5.md index ddbc47154..5fc6961fc 100644 --- a/docs/release-notes/version-3.5.md +++ b/docs/release-notes/version-3.5.md @@ -1,6 +1,6 @@ # NetBox v3.5 -## v3.5.7 (FUTURE) +## v3.5.7 (2023-07-28) ### Enhancements diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 7d2da2996..aec4f76f6 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -25,7 +25,7 @@ from netbox.constants import RQ_QUEUE_DEFAULT, RQ_QUEUE_HIGH, RQ_QUEUE_LOW # Environment setup # -VERSION = '3.5.7-dev' +VERSION = '3.5.7' # Hostname HOSTNAME = platform.node() diff --git a/requirements.txt b/requirements.txt index 3750e724b..f1235fa2c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ bleach==6.0.0 -boto3==1.28.1 +boto3==1.28.14 Django==4.1.10 django-cors-headers==4.2.0 django-debug-toolbar==4.1.0 @@ -15,21 +15,21 @@ django-tables2==2.6.0 django-taggit==4.0.0 django-timezone-field==5.1 djangorestframework==3.14.0 -drf-spectacular==0.26.3 +drf-spectacular==0.26.4 drf-spectacular-sidecar==2023.7.1 dulwich==0.21.5 feedparser==6.0.10 graphene-django==3.0.0 -gunicorn==20.1.0 +gunicorn==21.2.0 Jinja2==3.1.2 Markdown==3.3.7 -mkdocs-material==9.1.18 +mkdocs-material==9.1.21 mkdocstrings[python-legacy]==0.22.0 netaddr==0.8.0 Pillow==10.0.0 psycopg2-binary==2.9.6 -PyYAML==6.0 -sentry-sdk==1.28.0 +PyYAML==6.0.1 +sentry-sdk==1.28.1 social-auth-app-django==5.2.0 social-auth-core[openidconnect]==4.4.2 svgwrite==1.4.3 From 006c353d46d9aced93e39ac9efaa4a71aaa54e06 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 28 Jul 2023 10:31:54 -0400 Subject: [PATCH 08/24] PRVB --- docs/release-notes/version-3.5.md | 4 ++++ netbox/netbox/settings.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/release-notes/version-3.5.md b/docs/release-notes/version-3.5.md index 5fc6961fc..66c2cabce 100644 --- a/docs/release-notes/version-3.5.md +++ b/docs/release-notes/version-3.5.md @@ -1,5 +1,9 @@ # NetBox v3.5 +## v3.5.8 (FUTURE) + +--- + ## v3.5.7 (2023-07-28) ### Enhancements diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index aec4f76f6..58256d079 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -25,7 +25,7 @@ from netbox.constants import RQ_QUEUE_DEFAULT, RQ_QUEUE_HIGH, RQ_QUEUE_LOW # Environment setup # -VERSION = '3.5.7' +VERSION = '3.5.8-dev' # Hostname HOSTNAME = platform.node() From 0b10131564dc16138b9b7c7cd869d705771c229e Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Sun, 30 Jul 2023 13:34:08 -0400 Subject: [PATCH 09/24] Satisfy PEP8 E721 linter complaints --- netbox/dcim/graphql/gfk_mixins.py | 64 ++++++++++++------------ netbox/ipam/graphql/gfk_mixins.py | 30 +++++------ netbox/ipam/utils.py | 2 +- netbox/utilities/forms/bulk_import.py | 4 +- netbox/utilities/templatetags/helpers.py | 2 +- 5 files changed, 51 insertions(+), 51 deletions(-) diff --git a/netbox/dcim/graphql/gfk_mixins.py b/netbox/dcim/graphql/gfk_mixins.py index f3b8b696b..c97aa4c2b 100644 --- a/netbox/dcim/graphql/gfk_mixins.py +++ b/netbox/dcim/graphql/gfk_mixins.py @@ -53,23 +53,23 @@ class LinkPeerType(graphene.Union): @classmethod def resolve_type(cls, instance, info): - if type(instance) == CircuitTermination: + if type(instance) is CircuitTermination: return CircuitTerminationType - if type(instance) == ConsolePortType: + if type(instance) is ConsolePortType: return ConsolePortType - if type(instance) == ConsoleServerPort: + if type(instance) is ConsoleServerPort: return ConsoleServerPortType - if type(instance) == FrontPort: + if type(instance) is FrontPort: return FrontPortType - if type(instance) == Interface: + if type(instance) is Interface: return InterfaceType - if type(instance) == PowerFeed: + if type(instance) is PowerFeed: return PowerFeedType - if type(instance) == PowerOutlet: + if type(instance) is PowerOutlet: return PowerOutletType - if type(instance) == PowerPort: + if type(instance) is PowerPort: return PowerPortType - if type(instance) == RearPort: + if type(instance) is RearPort: return RearPortType @@ -89,23 +89,23 @@ class CableTerminationTerminationType(graphene.Union): @classmethod def resolve_type(cls, instance, info): - if type(instance) == CircuitTermination: + if type(instance) is CircuitTermination: return CircuitTerminationType - if type(instance) == ConsolePortType: + if type(instance) is ConsolePortType: return ConsolePortType - if type(instance) == ConsoleServerPort: + if type(instance) is ConsoleServerPort: return ConsoleServerPortType - if type(instance) == FrontPort: + if type(instance) is FrontPort: return FrontPortType - if type(instance) == Interface: + if type(instance) is Interface: return InterfaceType - if type(instance) == PowerFeed: + if type(instance) is PowerFeed: return PowerFeedType - if type(instance) == PowerOutlet: + if type(instance) is PowerOutlet: return PowerOutletType - if type(instance) == PowerPort: + if type(instance) is PowerPort: return PowerPortType - if type(instance) == RearPort: + if type(instance) is RearPort: return RearPortType @@ -123,19 +123,19 @@ class InventoryItemTemplateComponentType(graphene.Union): @classmethod def resolve_type(cls, instance, info): - if type(instance) == ConsolePortTemplate: + if type(instance) is ConsolePortTemplate: return ConsolePortTemplateType - if type(instance) == ConsoleServerPortTemplate: + if type(instance) is ConsoleServerPortTemplate: return ConsoleServerPortTemplateType - if type(instance) == FrontPortTemplate: + if type(instance) is FrontPortTemplate: return FrontPortTemplateType - if type(instance) == InterfaceTemplate: + if type(instance) is InterfaceTemplate: return InterfaceTemplateType - if type(instance) == PowerOutletTemplate: + if type(instance) is PowerOutletTemplate: return PowerOutletTemplateType - if type(instance) == PowerPortTemplate: + if type(instance) is PowerPortTemplate: return PowerPortTemplateType - if type(instance) == RearPortTemplate: + if type(instance) is RearPortTemplate: return RearPortTemplateType @@ -153,17 +153,17 @@ class InventoryItemComponentType(graphene.Union): @classmethod def resolve_type(cls, instance, info): - if type(instance) == ConsolePort: + if type(instance) is ConsolePort: return ConsolePortType - if type(instance) == ConsoleServerPort: + if type(instance) is ConsoleServerPort: return ConsoleServerPortType - if type(instance) == FrontPort: + if type(instance) is FrontPort: return FrontPortType - if type(instance) == Interface: + if type(instance) is Interface: return InterfaceType - if type(instance) == PowerOutlet: + if type(instance) is PowerOutlet: return PowerOutletType - if type(instance) == PowerPort: + if type(instance) is PowerPort: return PowerPortType - if type(instance) == RearPort: + if type(instance) is RearPort: return RearPortType diff --git a/netbox/ipam/graphql/gfk_mixins.py b/netbox/ipam/graphql/gfk_mixins.py index 31742c4a4..01c79690a 100644 --- a/netbox/ipam/graphql/gfk_mixins.py +++ b/netbox/ipam/graphql/gfk_mixins.py @@ -24,11 +24,11 @@ class IPAddressAssignmentType(graphene.Union): @classmethod def resolve_type(cls, instance, info): - if type(instance) == Interface: + if type(instance) is Interface: return InterfaceType - if type(instance) == FHRPGroup: + if type(instance) is FHRPGroup: return FHRPGroupType - if type(instance) == VMInterface: + if type(instance) is VMInterface: return VMInterfaceType @@ -42,11 +42,11 @@ class L2VPNAssignmentType(graphene.Union): @classmethod def resolve_type(cls, instance, info): - if type(instance) == Interface: + if type(instance) is Interface: return InterfaceType - if type(instance) == VLAN: + if type(instance) is VLAN: return VLANType - if type(instance) == VMInterface: + if type(instance) is VMInterface: return VMInterfaceType @@ -59,9 +59,9 @@ class FHRPGroupInterfaceType(graphene.Union): @classmethod def resolve_type(cls, instance, info): - if type(instance) == Interface: + if type(instance) is Interface: return InterfaceType - if type(instance) == VMInterface: + if type(instance) is VMInterface: return VMInterfaceType @@ -79,17 +79,17 @@ class VLANGroupScopeType(graphene.Union): @classmethod def resolve_type(cls, instance, info): - if type(instance) == Cluster: + if type(instance) is Cluster: return ClusterType - if type(instance) == ClusterGroup: + if type(instance) is ClusterGroup: return ClusterGroupType - if type(instance) == Location: + if type(instance) is Location: return LocationType - if type(instance) == Rack: + if type(instance) is Rack: return RackType - if type(instance) == Region: + if type(instance) is Region: return RegionType - if type(instance) == Site: + if type(instance) is Site: return SiteType - if type(instance) == SiteGroup: + if type(instance) is SiteGroup: return SiteGroupType diff --git a/netbox/ipam/utils.py b/netbox/ipam/utils.py index 93a40e5a0..262fd8d46 100644 --- a/netbox/ipam/utils.py +++ b/netbox/ipam/utils.py @@ -121,7 +121,7 @@ def add_available_vlans(vlans, vlan_group=None): }) vlans = list(vlans) + new_vlans - vlans.sort(key=lambda v: v.vid if type(v) == VLAN else v['vid']) + vlans.sort(key=lambda v: v.vid if type(v) is VLAN else v['vid']) return vlans diff --git a/netbox/utilities/forms/bulk_import.py b/netbox/utilities/forms/bulk_import.py index b7f432e63..6bdfd5662 100644 --- a/netbox/utilities/forms/bulk_import.py +++ b/netbox/utilities/forms/bulk_import.py @@ -123,9 +123,9 @@ class BulkImportForm(BootstrapMixin, SyncedDataMixin, forms.Form): records = [] try: for data in yaml.load_all(data, Loader=yaml.SafeLoader): - if type(data) == list: + if type(data) is list: records.extend(data) - elif type(data) == dict: + elif type(data) is dict: records.append(data) else: raise forms.ValidationError({ diff --git a/netbox/utilities/templatetags/helpers.py b/netbox/utilities/templatetags/helpers.py index 09a083112..aaee9679c 100644 --- a/netbox/utilities/templatetags/helpers.py +++ b/netbox/utilities/templatetags/helpers.py @@ -114,7 +114,7 @@ def annotated_date(date_value): if not date_value: return '' - if type(date_value) == datetime.date: + if type(date_value) is datetime.date: long_ts = date(date_value, 'DATE_FORMAT') short_ts = date(date_value, 'SHORT_DATE_FORMAT') else: From a4c9cbc6ddf23e65558aa15780885189bb4ae34d Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 2 Aug 2023 08:55:38 -0400 Subject: [PATCH 10/24] Remove hard-coded test runner --- netbox/netbox/settings.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 58256d079..2744ba701 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -461,8 +461,6 @@ LOGIN_REDIRECT_URL = f'/{BASE_PATH}' DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' -TEST_RUNNER = "django_rich.test.RichRunner" - # Exclude potentially sensitive models from wildcard view exemption. These may still be exempted # by specifying the model individually in the EXEMPT_VIEW_PERMISSIONS configuration parameter. EXEMPT_EXCLUDE_MODELS = ( From a68831d3a1a438881e9ba254918d6b391a5303af Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Wed, 2 Aug 2023 17:25:54 +0530 Subject: [PATCH 11/24] fixes provider_network_id for related circuits #13343 --- netbox/circuits/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/circuits/views.py b/netbox/circuits/views.py index f1cfdd1d5..64dd82682 100644 --- a/netbox/circuits/views.py +++ b/netbox/circuits/views.py @@ -163,7 +163,7 @@ class ProviderNetworkView(generic.ObjectView): related_models = ( ( Circuit.objects.restrict(request.user, 'view').filter(terminations__provider_network=instance), - 'providernetwork_id', + 'provider_network_id', ), ) From ab916a18191849d9e91c8344eef18d4a55e85555 Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Tue, 1 Aug 2023 20:35:16 +0530 Subject: [PATCH 12/24] fixes dummy payload URL for webhook test --- netbox/extras/tests/test_webhooks.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/netbox/extras/tests/test_webhooks.py b/netbox/extras/tests/test_webhooks.py index 19264dabb..ef7637765 100644 --- a/netbox/extras/tests/test_webhooks.py +++ b/netbox/extras/tests/test_webhooks.py @@ -31,8 +31,8 @@ class WebhookTest(APITestCase): def setUpTestData(cls): site_ct = ContentType.objects.get_for_model(Site) - DUMMY_URL = "http://localhost/" - DUMMY_SECRET = "LOOKATMEIMASECRETSTRING" + DUMMY_URL = 'http://localhost:9000/' + DUMMY_SECRET = 'LOOKATMEIMASECRETSTRING' webhooks = Webhook.objects.bulk_create(( Webhook(name='Webhook 1', type_create=True, payload_url=DUMMY_URL, secret=DUMMY_SECRET, additional_headers='X-Foo: Bar'), @@ -259,7 +259,7 @@ class WebhookTest(APITestCase): name='Conditional Webhook', type_create=True, type_update=True, - payload_url='http://localhost/', + payload_url='http://localhost:9000/', conditions={ 'and': [ { From 57860f26b778311e66115fe94cf6b95337f54593 Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Thu, 3 Aug 2023 01:15:09 +0530 Subject: [PATCH 13/24] Adds assigned bool for IP address API (#13301) * adds assigned bool for ip address API #13151 * Add filterset test --------- Co-authored-by: Jeremy Stretch --- netbox/ipam/filtersets.py | 16 ++++++++++++++++ netbox/ipam/tests/test_filtersets.py | 6 ++++++ 2 files changed, 22 insertions(+) diff --git a/netbox/ipam/filtersets.py b/netbox/ipam/filtersets.py index d011472d9..9b57cb273 100644 --- a/netbox/ipam/filtersets.py +++ b/netbox/ipam/filtersets.py @@ -591,6 +591,10 @@ class IPAddressFilterSet(NetBoxModelFilterSet, TenancyFilterSet): method='_assigned_to_interface', label=_('Is assigned to an interface'), ) + assigned = django_filters.BooleanFilter( + method='_assigned', + label=_('Is assigned'), + ) status = django_filters.MultipleChoiceFilter( choices=IPAddressStatusChoices, null_value=None @@ -706,6 +710,18 @@ class IPAddressFilterSet(NetBoxModelFilterSet, TenancyFilterSet): assigned_object_id__isnull=False ) + def _assigned(self, queryset, name, value): + if value: + return queryset.exclude( + assigned_object_type__isnull=True, + assigned_object_id__isnull=True + ) + else: + return queryset.filter( + assigned_object_type__isnull=True, + assigned_object_id__isnull=True + ) + class FHRPGroupFilterSet(NetBoxModelFilterSet): protocol = django_filters.MultipleChoiceFilter( diff --git a/netbox/ipam/tests/test_filtersets.py b/netbox/ipam/tests/test_filtersets.py index 3d9a66567..0ae7544ab 100644 --- a/netbox/ipam/tests/test_filtersets.py +++ b/netbox/ipam/tests/test_filtersets.py @@ -992,6 +992,12 @@ class IPAddressTestCase(TestCase, ChangeLoggedFilterSetTests): params = {'fhrpgroup_id': [fhrp_groups[0].pk, fhrp_groups[1].pk]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_assigned(self): + params = {'assigned': 'true'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 8) + params = {'assigned': 'false'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + def test_assigned_to_interface(self): params = {'assigned_to_interface': 'true'} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6) From a807cca29e094a2aa1872411a63f2a0bac98acb8 Mon Sep 17 00:00:00 2001 From: Matej Vadnjal Date: Wed, 2 Aug 2023 22:08:14 +0200 Subject: [PATCH 14/24] Fixes #13033: add formatted speed column to Interfaces (#13275) * Fixes #13033: add formatted speed column to Interfaces * use TemplateColumn instead of own class --- netbox/dcim/tables/devices.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/netbox/dcim/tables/devices.py b/netbox/dcim/tables/devices.py index db2655d27..42b34e999 100644 --- a/netbox/dcim/tables/devices.py +++ b/netbox/dcim/tables/devices.py @@ -545,6 +545,11 @@ class InterfaceTable(ModularDeviceComponentTable, BaseInterfaceTable, PathEndpoi } ) mgmt_only = columns.BooleanColumn() + speed_formatted = columns.TemplateColumn( + template_code='{% load helpers %}{{ value|humanize_speed }}', + accessor=Accessor('speed'), + verbose_name='Speed' + ) wireless_link = tables.Column( linkify=True ) @@ -568,7 +573,7 @@ class InterfaceTable(ModularDeviceComponentTable, BaseInterfaceTable, PathEndpoi model = models.Interface fields = ( 'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'enabled', 'type', 'mgmt_only', 'mtu', - 'speed', 'duplex', 'mode', 'mac_address', 'wwn', 'poe_mode', 'poe_type', 'rf_role', 'rf_channel', + 'speed', 'speed_formatted', 'duplex', 'mode', 'mac_address', 'wwn', 'poe_mode', 'poe_type', 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'description', 'mark_connected', 'cable', 'cable_color', 'wireless_link', 'wireless_lans', 'link_peer', 'connection', 'tags', 'vdcs', 'vrf', 'l2vpn', 'ip_addresses', 'fhrp_groups', 'untagged_vlan', 'tagged_vlans', 'created', 'last_updated', From 9cc295827b262693d43eb6f03dc0a3722fd3a88e Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 3 Aug 2023 14:53:58 -0400 Subject: [PATCH 15/24] Fixes #13369: Fix job termination status for failed reports --- netbox/extras/reports.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/netbox/extras/reports.py b/netbox/extras/reports.py index 8f3af2a09..6af81a9d9 100644 --- a/netbox/extras/reports.py +++ b/netbox/extras/reports.py @@ -214,20 +214,18 @@ class Report(object): self.active_test = method_name test_method = getattr(self, method_name) test_method() + job.data = self._results if self.failed: self.logger.warning("Report failed") - job.status = JobStatusChoices.STATUS_FAILED + job.terminate(status=JobStatusChoices.STATUS_FAILED) else: self.logger.info("Report completed successfully") - job.status = JobStatusChoices.STATUS_COMPLETED + job.terminate() except Exception as e: stacktrace = traceback.format_exc() self.log_failure(None, f"An exception occurred: {type(e).__name__}: {e}
{stacktrace}
") logger.error(f"Exception raised during report execution: {e}") job.terminate(status=JobStatusChoices.STATUS_ERRORED) - finally: - job.data = self._results - job.terminate() # Perform any post-run tasks self.post_run() From 93a862cded3b15d293c0680f58c45c2da6d2e397 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 4 Aug 2023 08:55:43 -0400 Subject: [PATCH 16/24] Add stadium analogy and behavior anti-patterns --- CONTRIBUTING.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6b71fb515..301fac079 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,12 +14,25 @@

-Some general tips for engaging here on GitHub: +## :information_source: Welcome to the Stadium! + +In her book [Working in Public](https://www.amazon.com/Working-Public-Making-Maintenance-Software/dp/0578675862), Nadia Eghbal defines four production models for open source projects, categorized by contributor and user growth: federations, clubs, toys, and stadiums. The NetBox project fits her definition of a stadium very well: + +> Stadiums are projects with low contributor growth and high user growth. While they may receive casual contributions, their regular contributor base does not grow proportionately to their users. As a result, they tend to be powered by one or a few developers. + +The bulk of NetBox's development is carried out by a handful of core maintainers, with occasional contributions from collaborators in the community. We find the stadium analogy very useful in conveying the roles and obligations of both contributors and users. + +If you're a contributor, actively working on the center stage, you have an obligation to produce quality content that will benefit the project as a whole. Conversely, if you're in the audience consuming the work being produced, you have the option of making requests and suggestions, but must also recognize that contributors are under no obligation to act on them. + +NetBox users are welcome to participate in either role, on stage or in the crowd. We ask only that you acknowledge the role you've chosen and respect the roles of others. + +### General Tips for Working on GitHub * Register for a free [GitHub account](https://github.com/signup) if you haven't already. * You can use [GitHub Markdown](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax) for formatting text and adding images. * To help mitigate notification spam, please avoid "bumping" issues with no activity. (To vote an issue up or down, use a :thumbsup: or :thumbsdown: reaction.) * Please avoid pinging members with `@` unless they've previously expressed interest or involvement with that particular issue. +* Familiarize yourself with [this list of discussion anti-patterns](https://github.com/bradfitz/issue-tracker-behaviors) and make every effort to avoid them. ## :bug: Reporting Bugs From 7f22c6bf12e5e4e18691fe65973f6a33dab8d066 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 4 Aug 2023 10:12:15 -0400 Subject: [PATCH 17/24] Include notes re: demo data and netbox-docker --- docs/development/release-checklist.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/development/release-checklist.md b/docs/development/release-checklist.md index efb0f44b9..000948ee7 100644 --- a/docs/development/release-checklist.md +++ b/docs/development/release-checklist.md @@ -43,10 +43,22 @@ Follow these instructions to perform a new installation of NetBox in a temporary Submit a pull request to merge the `feature` branch into the `develop` branch in preparation for its release. Once it has been merged, continue with the section for patch releases below. +### Rebuild Demo Data (After Release) + +After the release of a new minor version, generate a new demo data snapshot compatible with the new release. See the [`netbox-demo-data`](https://github.com/netbox-community/netbox-demo-data) repository for instructions. + --- ## Patch Releases +### Notify netbox-docker Project of Any Relevant Changes + +Notify the [`netbox-docker`](https://github.com/netbox-community/netbox-docker) maintainers (in **#netbox-docker**) of any changes that may be relevant to their build process, including: + +* Significant changes to `upgrade.sh` +* Increases in minimum versions for service dependencies (PostgreSQL, Redis, etc.) +* Any changes to the reference installation + ### Update Requirements Before each release, update each of NetBox's Python dependencies to its most recent stable version. These are defined in `requirements.txt`, which is updated from `base_requirements.txt` using `pip`. To do this: From 43ce453938c9acdcd1bfdeebda906800e37ac86d Mon Sep 17 00:00:00 2001 From: Henrik Strand Date: Fri, 4 Aug 2023 17:32:52 +0200 Subject: [PATCH 18/24] Adding interface TYPE_400GE_CFP2/400gbase-x-cfp2 (#13338) * Added 400G CFP2 to InterfaceTypeChoices * Added new type to choises --- netbox/dcim/choices.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/netbox/dcim/choices.py b/netbox/dcim/choices.py index e850a8c51..21bd3ed7e 100644 --- a/netbox/dcim/choices.py +++ b/netbox/dcim/choices.py @@ -834,6 +834,7 @@ class InterfaceTypeChoices(ChoiceSet): TYPE_200GE_CFP2 = '200gbase-x-cfp2' TYPE_200GE_QSFP56 = '200gbase-x-qsfp56' TYPE_200GE_QSFP_DD = '200gbase-x-qsfpdd' + TYPE_400GE_CFP2 = '400gbase-x-cfp2' TYPE_400GE_QSFP_DD = '400gbase-x-qsfpdd' TYPE_400GE_OSFP = '400gbase-x-osfp' TYPE_400GE_CDFP = '400gbase-x-cdfp' @@ -976,6 +977,7 @@ class InterfaceTypeChoices(ChoiceSet): (TYPE_100GE_CFP, 'CFP (100GE)'), (TYPE_100GE_CFP2, 'CFP2 (100GE)'), (TYPE_200GE_CFP2, 'CFP2 (200GE)'), + (TYPE_400GE_CFP2, 'CFP2 (400GE)'), (TYPE_100GE_CFP4, 'CFP4 (100GE)'), (TYPE_100GE_CXP, 'CXP (100GE)'), (TYPE_100GE_CPAK, 'Cisco CPAK (100GE)'), From 01bb09db674d99316cc8347c26cb430268e9e8fb Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Tue, 1 Aug 2023 18:55:42 +0530 Subject: [PATCH 19/24] adds delete for SyncedDataMixin when related AutoSyncRecord is available #12750 --- netbox/netbox/models/features.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/netbox/netbox/models/features.py b/netbox/netbox/models/features.py index 8bacba534..1e55ec2a3 100644 --- a/netbox/netbox/models/features.py +++ b/netbox/netbox/models/features.py @@ -442,6 +442,19 @@ class SyncedDataMixin(models.Model): return ret + def delete(self, *args, **kwargs): + from core.models import AutoSyncRecord + + # Delete AutoSyncRecord + content_type = ContentType.objects.get_for_model(self) + AutoSyncRecord.objects.filter( + datafile=self.data_file, + object_type=content_type, + object_id=self.pk + ).delete() + + return super().delete(*args, **kwargs) + def resolve_data_file(self): """ Determine the designated DataFile object identified by its parent DataSource and its path. Returns None if From 88562d7dcfb642532d24134abf26c7bea4dc7180 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 4 Aug 2023 13:36:33 -0400 Subject: [PATCH 20/24] Changelog for #12750, #12889, #13033, #13151, #13343, #13369 --- docs/release-notes/version-3.5.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/release-notes/version-3.5.md b/docs/release-notes/version-3.5.md index 66c2cabce..4347d9837 100644 --- a/docs/release-notes/version-3.5.md +++ b/docs/release-notes/version-3.5.md @@ -2,6 +2,18 @@ ## v3.5.8 (FUTURE) +### Enhancements + +* [#12889](https://github.com/netbox-community/netbox/issues/12889) - Add 400GE CFP2 interface type +* [#13033](https://github.com/netbox-community/netbox/issues/13033) - Add human-friendly speed column to interfaces table +* [#13151](https://github.com/netbox-community/netbox/issues/13151) - Add "assigned" filter for IP addresses + +### Bug Fixes + +* [#12750](https://github.com/netbox-community/netbox/issues/12750) - Automatically delete an AutoSyncRecord when its object is deleted +* [#13343](https://github.com/netbox-community/netbox/issues/13343) - Fix filtering of circuits under provider network view +* [#13369](https://github.com/netbox-community/netbox/issues/13369) - Fix job termination status for failed reports + --- ## v3.5.7 (2023-07-28) From 0dd319d0c817ad71edf6542c4ffdac366e862822 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 4 Aug 2023 14:05:41 -0400 Subject: [PATCH 21/24] Closes #11675: Add support for specifying import/export route targets during VRF bulk import --- netbox/ipam/forms/bulk_import.py | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/netbox/ipam/forms/bulk_import.py b/netbox/ipam/forms/bulk_import.py index 683d40f49..3bce26249 100644 --- a/netbox/ipam/forms/bulk_import.py +++ b/netbox/ipam/forms/bulk_import.py @@ -1,7 +1,6 @@ from django import forms from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ValidationError -from django.db.models import Q from django.utils.translation import gettext as _ from dcim.models import Device, Interface, Site @@ -10,7 +9,9 @@ from ipam.constants import * from ipam.models import * from netbox.forms import NetBoxModelImportForm from tenancy.models import Tenant -from utilities.forms.fields import CSVChoiceField, CSVContentTypeField, CSVModelChoiceField, SlugField +from utilities.forms.fields import ( + CSVChoiceField, CSVContentTypeField, CSVModelChoiceField, CSVModelMultipleChoiceField, SlugField +) from virtualization.models import VirtualMachine, VMInterface __all__ = ( @@ -41,10 +42,25 @@ class VRFImportForm(NetBoxModelImportForm): to_field_name='name', help_text=_('Assigned tenant') ) + import_targets = CSVModelMultipleChoiceField( + queryset=RouteTarget.objects.all(), + required=False, + to_field_name='name', + help_text=_('Import route targets') + ) + export_targets = CSVModelMultipleChoiceField( + queryset=RouteTarget.objects.all(), + required=False, + to_field_name='name', + help_text=_('Export route targets') + ) class Meta: model = VRF - fields = ('name', 'rd', 'tenant', 'enforce_unique', 'description', 'comments', 'tags') + fields = ( + 'name', 'rd', 'tenant', 'enforce_unique', 'description', 'import_targets', 'export_targets', 'comments', + 'tags', + ) class RouteTargetImportForm(NetBoxModelImportForm): From 2236b86c35c1f6bfe34aa426cf24eaf0be404237 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 4 Aug 2023 14:37:40 -0400 Subject: [PATCH 22/24] Closes #11922: Populate assigned VDCs when adding a child interface --- netbox/dcim/forms/model_forms.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/netbox/dcim/forms/model_forms.py b/netbox/dcim/forms/model_forms.py index 219216045..3c02e6e4e 100644 --- a/netbox/dcim/forms/model_forms.py +++ b/netbox/dcim/forms/model_forms.py @@ -1042,6 +1042,9 @@ class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm): queryset=VirtualDeviceContext.objects.all(), required=False, label='Virtual Device Contexts', + initial_params={ + 'interfaces': '$parent', + }, query_params={ 'device_id': '$device', } From f9648d854416590d98eb7b5d7cf6be18bd75b8cf Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 7 Aug 2023 10:48:41 -0400 Subject: [PATCH 23/24] Closes #13400: Add 'name' property to BaseTable class --- netbox/netbox/tables/tables.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/netbox/netbox/tables/tables.py b/netbox/netbox/tables/tables.py index 20eab822d..975311e4a 100644 --- a/netbox/netbox/tables/tables.py +++ b/netbox/netbox/tables/tables.py @@ -54,7 +54,7 @@ class BaseTable(tables.Table): # 3. Meta.fields selected_columns = None if user is not None and not isinstance(user, AnonymousUser): - selected_columns = user.config.get(f"tables.{self.__class__.__name__}.columns") + selected_columns = user.config.get(f"tables.{self.name}.columns") if not selected_columns: selected_columns = getattr(self.Meta, 'default_columns', self.Meta.fields) @@ -113,6 +113,10 @@ class BaseTable(tables.Table): columns.append((name, column.verbose_name)) return columns + @property + def name(self): + return self.__class__.__name__ + @property def available_columns(self): return self._get_columns(visible=False) @@ -138,17 +142,16 @@ class BaseTable(tables.Table): """ # Save ordering preference if request.user.is_authenticated: - table_name = self.__class__.__name__ if self.prefixed_order_by_field in request.GET: if request.GET[self.prefixed_order_by_field]: # If an ordering has been specified as a query parameter, save it as the # user's preferred ordering for this table. ordering = request.GET.getlist(self.prefixed_order_by_field) - request.user.config.set(f'tables.{table_name}.ordering', ordering, commit=True) + request.user.config.set(f'tables.{self.name}.ordering', ordering, commit=True) else: # If the ordering has been set to none (empty), clear any existing preference. - request.user.config.clear(f'tables.{table_name}.ordering', commit=True) - elif ordering := request.user.config.get(f'tables.{table_name}.ordering'): + request.user.config.clear(f'tables.{self.name}.ordering', commit=True) + elif ordering := request.user.config.get(f'tables.{self.name}.ordering'): # If no ordering has been specified, set the preferred ordering (if any). self.order_by = ordering From f5a1f83f9fa9d98c945d21eb0f7ccb8cd37fbf59 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 7 Aug 2023 15:29:20 -0400 Subject: [PATCH 24/24] Closes #13368: Report installed plugins during server error (#13387) * Introduce get_installed_plugins() utility * Extend 500 error template to list installed plugins * Move get_plugin_config() to extras.plugins.utils --- netbox/extras/plugins/__init__.py | 21 ---------------- netbox/extras/plugins/utils.py | 37 +++++++++++++++++++++++++++++ netbox/extras/tests/test_plugins.py | 3 ++- netbox/netbox/api/views.py | 11 ++------- netbox/netbox/views/errors.py | 3 +++ netbox/templates/500.html | 5 +++- 6 files changed, 48 insertions(+), 32 deletions(-) create mode 100644 netbox/extras/plugins/utils.py diff --git a/netbox/extras/plugins/__init__.py b/netbox/extras/plugins/__init__.py index 83c7a7bb0..8736a3197 100644 --- a/netbox/extras/plugins/__init__.py +++ b/netbox/extras/plugins/__init__.py @@ -2,7 +2,6 @@ import collections from importlib import import_module from django.apps import AppConfig -from django.conf import settings from django.core.exceptions import ImproperlyConfigured from django.utils.module_loading import import_string from packaging import version @@ -146,23 +145,3 @@ class PluginConfig(AppConfig): for setting, value in cls.default_settings.items(): if setting not in user_config: user_config[setting] = value - - -# -# Utilities -# - -def get_plugin_config(plugin_name, parameter, default=None): - """ - Return the value of the specified plugin configuration parameter. - - Args: - plugin_name: The name of the plugin - parameter: The name of the configuration parameter - default: The value to return if the parameter is not defined (default: None) - """ - try: - plugin_config = settings.PLUGINS_CONFIG[plugin_name] - return plugin_config.get(parameter, default) - except KeyError: - raise ImproperlyConfigured(f"Plugin {plugin_name} is not registered.") diff --git a/netbox/extras/plugins/utils.py b/netbox/extras/plugins/utils.py new file mode 100644 index 000000000..c260f156d --- /dev/null +++ b/netbox/extras/plugins/utils.py @@ -0,0 +1,37 @@ +from django.apps import apps +from django.conf import settings +from django.core.exceptions import ImproperlyConfigured + +__all__ = ( + 'get_installed_plugins', + 'get_plugin_config', +) + + +def get_installed_plugins(): + """ + Return a dictionary mapping the names of installed plugins to their versions. + """ + plugins = {} + for plugin_name in settings.PLUGINS: + plugin_name = plugin_name.rsplit('.', 1)[-1] + plugin_config = apps.get_app_config(plugin_name) + plugins[plugin_name] = getattr(plugin_config, 'version', None) + + return dict(sorted(plugins.items())) + + +def get_plugin_config(plugin_name, parameter, default=None): + """ + Return the value of the specified plugin configuration parameter. + + Args: + plugin_name: The name of the plugin + parameter: The name of the configuration parameter + default: The value to return if the parameter is not defined (default: None) + """ + try: + plugin_config = settings.PLUGINS_CONFIG[plugin_name] + return plugin_config.get(parameter, default) + except KeyError: + raise ImproperlyConfigured(f"Plugin {plugin_name} is not registered.") diff --git a/netbox/extras/tests/test_plugins.py b/netbox/extras/tests/test_plugins.py index cb7629ad2..42dde43fd 100644 --- a/netbox/extras/tests/test_plugins.py +++ b/netbox/extras/tests/test_plugins.py @@ -5,8 +5,9 @@ from django.core.exceptions import ImproperlyConfigured from django.test import Client, TestCase, override_settings from django.urls import reverse -from extras.plugins import PluginMenu, get_plugin_config +from extras.plugins import PluginMenu from extras.tests.dummy_plugin import config as dummy_config +from extras.plugins.utils import get_plugin_config from netbox.graphql.schema import Query from netbox.registry import registry diff --git a/netbox/netbox/api/views.py b/netbox/netbox/api/views.py index 5c55697ff..97f690762 100644 --- a/netbox/netbox/api/views.py +++ b/netbox/netbox/api/views.py @@ -11,6 +11,7 @@ from rest_framework.reverse import reverse from rest_framework.views import APIView from rq.worker import Worker +from extras.plugins.utils import get_installed_plugins from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired @@ -61,19 +62,11 @@ class StatusView(APIView): installed_apps[app_config.name] = version installed_apps = {k: v for k, v in sorted(installed_apps.items())} - # Gather installed plugins - plugins = {} - for plugin_name in settings.PLUGINS: - plugin_name = plugin_name.rsplit('.', 1)[-1] - plugin_config = apps.get_app_config(plugin_name) - plugins[plugin_name] = getattr(plugin_config, 'version', None) - plugins = {k: v for k, v in sorted(plugins.items())} - return Response({ 'django-version': DJANGO_VERSION, 'installed-apps': installed_apps, 'netbox-version': settings.VERSION, - 'plugins': plugins, + 'plugins': get_installed_plugins(), 'python-version': platform.python_version(), 'rq-workers-running': Worker.count(get_connection('default')), }) diff --git a/netbox/netbox/views/errors.py b/netbox/netbox/views/errors.py index c74c67cef..a81d45cb5 100644 --- a/netbox/netbox/views/errors.py +++ b/netbox/netbox/views/errors.py @@ -11,6 +11,8 @@ from django.views.defaults import ERROR_500_TEMPLATE_NAME, page_not_found from django.views.generic import View from sentry_sdk import capture_message +from extras.plugins.utils import get_installed_plugins + __all__ = ( 'handler_404', 'handler_500', @@ -53,4 +55,5 @@ def handler_500(request, template_name=ERROR_500_TEMPLATE_NAME): 'exception': str(type_), 'netbox_version': settings.VERSION, 'python_version': platform.python_version(), + 'plugins': get_installed_plugins(), })) diff --git a/netbox/templates/500.html b/netbox/templates/500.html index 6cface941..0257e7c43 100644 --- a/netbox/templates/500.html +++ b/netbox/templates/500.html @@ -30,7 +30,10 @@ {{ error }} Python version: {{ python_version }} -NetBox version: {{ netbox_version }} +NetBox version: {{ netbox_version }} +Plugins: {% for plugin, version in plugins.items %} + {{ plugin }}: {{ version }}{% empty %}None installed{% endfor %} +

If further assistance is required, please post to the NetBox discussion forum on GitHub.