From eca624b13d4bd04b3b0600993f919c1a60f32722 Mon Sep 17 00:00:00 2001 From: Arthur Hanson Date: Mon, 23 Jan 2023 05:48:14 -0800 Subject: [PATCH 001/144] 11487 remove set null from read-only custom fields bulk edit (#11552) * 11487 remove set null from read-only custom fields bulk edit * 11487 removes unreleased sentry-sdk --- netbox/netbox/forms/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/netbox/forms/base.py b/netbox/netbox/forms/base.py index d69445e78..b4ad39b5e 100644 --- a/netbox/netbox/forms/base.py +++ b/netbox/netbox/forms/base.py @@ -131,7 +131,7 @@ class NetBoxModelBulkEditForm(BootstrapMixin, CustomFieldsMixin, forms.Form): def _extend_nullable_fields(self): nullable_custom_fields = [ - name for name, customfield in self.custom_fields.items() if not customfield.required + name for name, customfield in self.custom_fields.items() if (not customfield.required and customfield.ui_visibility == CustomFieldVisibilityChoices.VISIBILITY_READ_WRITE) ] self.nullable_fields = (*self.nullable_fields, *nullable_custom_fields) From 0f9a3039637dc4e02b8a6895004660cb3f4f517a Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 23 Jan 2023 10:21:11 -0500 Subject: [PATCH 002/144] Changelog for #11487 --- docs/release-notes/version-3.4.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes/version-3.4.md b/docs/release-notes/version-3.4.md index 363a629ad..db677c496 100644 --- a/docs/release-notes/version-3.4.md +++ b/docs/release-notes/version-3.4.md @@ -4,6 +4,7 @@ ### Bug Fixes +* [#11487](https://github.com/netbox-community/netbox/issues/11487) - Remove "set null" option from non-writable custom fields during bulk edit * [#11555](https://github.com/netbox-community/netbox/issues/11555) - Avoid inadvertent interpretation of search query as regular expression under global search (previously [#11516](https://github.com/netbox-community/netbox/issues/11516)) --- From 6a793087b44a36f754ff90306d301162a5fa5177 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 23 Jan 2023 10:23:49 -0500 Subject: [PATCH 003/144] Reference GitHub advisory reporting --- SECURITY.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SECURITY.md b/SECURITY.md index b389dd2b3..c434b6110 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -24,7 +24,7 @@ If you believe you've uncovered a security vulnerability and wish to report it c Please note that we **DO NOT** accept reports generated by automated tooling which merely suggest that a file or file(s) _may_ be vulnerable under certain conditions, as these are most often innocuous. -If you believe that you've found a vulnerability which meets all of these conditions, please email a brief description of the suspected bug and instructions for reproduction to **security@netbox.dev**. For any security concerns regarding NetBox deployed via Docker, please see the [netbox-docker](https://github.com/netbox-community/netbox-docker) project. +If you believe that you've found a vulnerability which meets all of these conditions, please [submit a draft security advisory](https://github.com/netbox-community/netbox/security/advisories/new) on GitHub, or email a brief description of the suspected bug and instructions for reproduction to **security@netbox.dev**. For any security concerns regarding NetBox deployed via Docker, please see the [netbox-docker](https://github.com/netbox-community/netbox-docker) project. ### Bug Bounties From 39087d10eb1c779b2c37d8dae33eb5dbc9dd943b Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 23 Jan 2023 10:44:42 -0500 Subject: [PATCH 004/144] Add NetBox Labs as a sponsor --- README.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index f44ce725f..e14f31b56 100644 --- a/README.md +++ b/README.md @@ -67,15 +67,17 @@ complete list of requirements, see `requirements.txt`. The code is available

Thank you to our sponsors!

+ [![NetBox Labs](https://raw.githubusercontent.com/wiki/netbox-community/netbox/images/sponsors/netbox_labs.png)](https://netboxlabs.com) +            [![DigitalOcean](https://raw.githubusercontent.com/wiki/netbox-community/netbox/images/sponsors/digitalocean.png)](https://try.digitalocean.com/developer-cloud)            - [![Equinix Metal](https://raw.githubusercontent.com/wiki/netbox-community/netbox/images/sponsors/equinix.png)](https://metal.equinix.com/) -            - [![NS1](https://raw.githubusercontent.com/wiki/netbox-community/netbox/images/sponsors/ns1.png)](https://ns1.com/) + [![NS1](https://raw.githubusercontent.com/wiki/netbox-community/netbox/images/sponsors/ns1.png)](https://ns1.com)
- [![Sentry](https://raw.githubusercontent.com/wiki/netbox-community/netbox/images/sponsors/sentry.png)](https://sentry.io/) + [![Sentry](https://raw.githubusercontent.com/wiki/netbox-community/netbox/images/sponsors/sentry.png)](https://sentry.io)            - [![Stellar Technologies](https://raw.githubusercontent.com/wiki/netbox-community/netbox/images/sponsors/stellar.png)](https://stellar.tech/) + [![Equinix Metal](https://raw.githubusercontent.com/wiki/netbox-community/netbox/images/sponsors/equinix.png)](https://metal.equinix.com) +            + [![Stellar Technologies](https://raw.githubusercontent.com/wiki/netbox-community/netbox/images/sponsors/stellar.png)](https://stellar.tech)
From b79a2976f7f288617505043a4216ca01487da3d7 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 24 Jan 2023 14:40:09 -0500 Subject: [PATCH 005/144] Closes #10888, #10889: Add supplementary notes to installation docs --- docs/installation/3-netbox.md | 5 ++++- docs/installation/4-gunicorn.md | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/installation/3-netbox.md b/docs/installation/3-netbox.md index 68a582e7f..26a2bf917 100644 --- a/docs/installation/3-netbox.md +++ b/docs/installation/3-netbox.md @@ -272,7 +272,10 @@ See the [housekeeping documentation](../administration/housekeeping.md) for furt ## Test the Application -At this point, we should be able to run NetBox's development server for testing. We can check by starting a development instance: +At this point, we should be able to run NetBox's development server for testing. We can check by starting a development instance locally. + +!!! tip + Check that the Python virtual environment is still active before attempting to run the server. ```no-highlight python3 manage.py runserver 0.0.0.0:8000 --insecure diff --git a/docs/installation/4-gunicorn.md b/docs/installation/4-gunicorn.md index 21d1f1211..1183a9123 100644 --- a/docs/installation/4-gunicorn.md +++ b/docs/installation/4-gunicorn.md @@ -14,7 +14,10 @@ While the provided configuration should suffice for most initial installations, ## systemd Setup -We'll use systemd to control both gunicorn and NetBox's background worker process. First, copy `contrib/netbox.service` and `contrib/netbox-rq.service` to the `/etc/systemd/system/` directory and reload the systemd daemon: +We'll use systemd to control both gunicorn and NetBox's background worker process. First, copy `contrib/netbox.service` and `contrib/netbox-rq.service` to the `/etc/systemd/system/` directory and reload the systemd daemon. + +!!! warning "Check user & group assignment" + The stock service configuration files packaged with NetBox assume that the service will run with the `netbox` user and group names. If these differ on your installation, be sure to update the service files accordingly. ```no-highlight sudo cp -v /opt/netbox/contrib/*.service /etc/systemd/system/ From d5ccda355fc7be6ee8e87fce0ca1c951a3e2f973 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 24 Jan 2023 15:44:02 -0500 Subject: [PATCH 006/144] Fixes #11562: Correct ordering of virtual chassis interfaces with duplicate names --- docs/release-notes/version-3.4.md | 1 + netbox/dcim/tables/devices.py | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/release-notes/version-3.4.md b/docs/release-notes/version-3.4.md index db677c496..b7ce5380b 100644 --- a/docs/release-notes/version-3.4.md +++ b/docs/release-notes/version-3.4.md @@ -6,6 +6,7 @@ * [#11487](https://github.com/netbox-community/netbox/issues/11487) - Remove "set null" option from non-writable custom fields during bulk edit * [#11555](https://github.com/netbox-community/netbox/issues/11555) - Avoid inadvertent interpretation of search query as regular expression under global search (previously [#11516](https://github.com/netbox-community/netbox/issues/11516)) +* [#11562](https://github.com/netbox-community/netbox/issues/11562) - Correct ordering of virtual chassis interfaces with duplicate names --- diff --git a/netbox/dcim/tables/devices.py b/netbox/dcim/tables/devices.py index 7a2ea50ba..730309156 100644 --- a/netbox/dcim/tables/devices.py +++ b/netbox/dcim/tables/devices.py @@ -580,7 +580,6 @@ class DeviceInterfaceTable(InterfaceTable): 'wireless_lans', 'link_peer', 'connection', 'tags', 'vdcs', 'vrf', 'l2vpn', 'ip_addresses', 'fhrp_groups', 'untagged_vlan', 'tagged_vlans', 'actions', ) - order_by = ('name',) default_columns = ( 'pk', 'name', 'label', 'enabled', 'type', 'parent', 'lag', 'mtu', 'mode', 'description', 'ip_addresses', 'cable', 'connection', From b8de9c0875cedb1a6decc48c61bee1b183e4af95 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 25 Jan 2023 09:55:45 -0500 Subject: [PATCH 007/144] Fixes #11528: Permit import of devices using uploaded file --- netbox/dcim/forms/bulk_import.py | 77 ++++++++----------- netbox/dcim/urls.py | 1 - netbox/dcim/views.py | 17 ++-- netbox/templates/dcim/device_import.html | 5 -- .../templates/dcim/device_import_child.html | 5 -- .../dcim/inc/device_import_header.html | 8 -- 6 files changed, 37 insertions(+), 76 deletions(-) delete mode 100644 netbox/templates/dcim/device_import.html delete mode 100644 netbox/templates/dcim/device_import_child.html delete mode 100644 netbox/templates/dcim/inc/device_import_header.html diff --git a/netbox/dcim/forms/bulk_import.py b/netbox/dcim/forms/bulk_import.py index bdbaf9f18..3f016899e 100644 --- a/netbox/dcim/forms/bulk_import.py +++ b/netbox/dcim/forms/bulk_import.py @@ -18,7 +18,6 @@ from .common import ModuleCommonForm __all__ = ( 'CableImportForm', - 'ChildDeviceImportForm', 'ConsolePortImportForm', 'ConsoleServerPortImportForm', 'DeviceBayImportForm', @@ -413,6 +412,18 @@ class DeviceImportForm(BaseDeviceImportForm): required=False, help_text=_('Mounted rack face') ) + parent = CSVModelChoiceField( + queryset=Device.objects.all(), + to_field_name='name', + required=False, + help_text=_('Parent device (for child devices)') + ) + device_bay = CSVModelChoiceField( + queryset=DeviceBay.objects.all(), + to_field_name='name', + required=False, + help_text=_('Device bay in which this device is installed (for child devices)') + ) airflow = CSVChoiceField( choices=DeviceAirflowChoices, required=False, @@ -422,8 +433,8 @@ class DeviceImportForm(BaseDeviceImportForm): class Meta(BaseDeviceImportForm.Meta): fields = [ 'name', 'device_role', 'tenant', 'manufacturer', 'device_type', 'platform', 'serial', 'asset_tag', 'status', - 'site', 'location', 'rack', 'position', 'face', 'airflow', 'virtual_chassis', 'vc_position', 'vc_priority', - 'cluster', 'description', 'comments', 'tags', + 'site', 'location', 'rack', 'position', 'face', 'parent', 'device_bay', 'airflow', 'virtual_chassis', + 'vc_position', 'vc_priority', 'cluster', 'description', 'comments', 'tags', ] def __init__(self, data=None, *args, **kwargs): @@ -434,6 +445,7 @@ class DeviceImportForm(BaseDeviceImportForm): # Limit location queryset by assigned site params = {f"site__{self.fields['site'].to_field_name}": data.get('site')} self.fields['location'].queryset = self.fields['location'].queryset.filter(**params) + self.fields['parent'].queryset = self.fields['parent'].queryset.filter(**params) # Limit rack queryset by assigned site and group params = { @@ -442,6 +454,23 @@ class DeviceImportForm(BaseDeviceImportForm): } self.fields['rack'].queryset = self.fields['rack'].queryset.filter(**params) + # Limit device bay queryset by parent device + if parent := data.get('parent'): + params = {f"device__{self.fields['parent'].to_field_name}": parent} + self.fields['device_bay'].queryset = self.fields['device_bay'].queryset.filter(**params) + + def clean(self): + super().clean() + + # Inherit site and rack from parent device + if parent := self.cleaned_data.get('parent'): + self.instance.site = parent.site + self.instance.rack = parent.rack + + # Set parent_bay reverse relationship + if device_bay := self.cleaned_data.get('device_bay'): + self.instance.parent_bay = device_bay + class ModuleImportForm(ModuleCommonForm, NetBoxModelImportForm): device = CSVModelChoiceField( @@ -495,48 +524,6 @@ class ModuleImportForm(ModuleCommonForm, NetBoxModelImportForm): return self.cleaned_data['replicate_components'] -class ChildDeviceImportForm(BaseDeviceImportForm): - parent = CSVModelChoiceField( - queryset=Device.objects.all(), - to_field_name='name', - help_text=_('Parent device') - ) - device_bay = CSVModelChoiceField( - queryset=DeviceBay.objects.all(), - to_field_name='name', - help_text=_('Device bay in which this device is installed') - ) - - class Meta(BaseDeviceImportForm.Meta): - fields = [ - 'name', 'device_role', 'tenant', 'manufacturer', 'device_type', 'platform', 'serial', 'asset_tag', 'status', - 'parent', 'device_bay', 'virtual_chassis', 'vc_position', 'vc_priority', 'cluster', 'comments', 'tags' - ] - - def __init__(self, data=None, *args, **kwargs): - super().__init__(data, *args, **kwargs) - - if data: - - # Limit device bay queryset by parent device - params = {f"device__{self.fields['parent'].to_field_name}": data.get('parent')} - self.fields['device_bay'].queryset = self.fields['device_bay'].queryset.filter(**params) - - def clean(self): - super().clean() - - # Set parent_bay reverse relationship - device_bay = self.cleaned_data.get('device_bay') - if device_bay: - self.instance.parent_bay = device_bay - - # Inherit site and rack from parent device - parent = self.cleaned_data.get('parent') - if parent: - self.instance.site = parent.site - self.instance.rack = parent.rack - - # # Device components # diff --git a/netbox/dcim/urls.py b/netbox/dcim/urls.py index 6772f96ad..c71a0aff1 100644 --- a/netbox/dcim/urls.py +++ b/netbox/dcim/urls.py @@ -177,7 +177,6 @@ urlpatterns = [ path('devices/', views.DeviceListView.as_view(), name='device_list'), path('devices/add/', views.DeviceEditView.as_view(), name='device_add'), path('devices/import/', views.DeviceBulkImportView.as_view(), name='device_import'), - path('devices/import/child-devices/', views.ChildDeviceBulkImportView.as_view(), name='device_import_child'), path('devices/edit/', views.DeviceBulkEditView.as_view(), name='device_bulk_edit'), path('devices/rename/', views.DeviceBulkRenameView.as_view(), name='device_bulk_rename'), path('devices/delete/', views.DeviceBulkDeleteView.as_view(), name='device_bulk_delete'), diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 46d12937b..0643ac739 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -2090,22 +2090,15 @@ class DeviceBulkImportView(generic.BulkImportView): queryset = Device.objects.all() model_form = forms.DeviceImportForm table = tables.DeviceImportTable - template_name = 'dcim/device_import.html' - - -class ChildDeviceBulkImportView(generic.BulkImportView): - queryset = Device.objects.all() - model_form = forms.ChildDeviceImportForm - table = tables.DeviceImportTable - template_name = 'dcim/device_import_child.html' def save_object(self, object_form, request): obj = object_form.save() - # Save the reverse relation to the parent device bay - device_bay = obj.parent_bay - device_bay.installed_device = obj - device_bay.save() + # For child devices, save the reverse relation to the parent device bay + if getattr(obj, 'parent_bay', None): + device_bay = obj.parent_bay + device_bay.installed_device = obj + device_bay.save() return obj diff --git a/netbox/templates/dcim/device_import.html b/netbox/templates/dcim/device_import.html deleted file mode 100644 index b30de60c2..000000000 --- a/netbox/templates/dcim/device_import.html +++ /dev/null @@ -1,5 +0,0 @@ -{% extends 'generic/bulk_import.html' %} - -{% block tabs %} - {% include 'dcim/inc/device_import_header.html' %} -{% endblock %} diff --git a/netbox/templates/dcim/device_import_child.html b/netbox/templates/dcim/device_import_child.html deleted file mode 100644 index d0dc72b61..000000000 --- a/netbox/templates/dcim/device_import_child.html +++ /dev/null @@ -1,5 +0,0 @@ -{% extends 'generic/bulk_import.html' %} - -{% block tabs %} - {% include 'dcim/inc/device_import_header.html' with active_tab='child_import' %} -{% endblock %} diff --git a/netbox/templates/dcim/inc/device_import_header.html b/netbox/templates/dcim/inc/device_import_header.html deleted file mode 100644 index 97e849c2a..000000000 --- a/netbox/templates/dcim/inc/device_import_header.html +++ /dev/null @@ -1,8 +0,0 @@ - From 6f74c5ec03305b7f597a839d35b2ee0c0e8f75ad Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 25 Jan 2023 10:09:37 -0500 Subject: [PATCH 008/144] Fixes #11528: Show edit/delete buttons in user tokens table --- docs/release-notes/version-3.4.md | 2 ++ netbox/users/tables.py | 10 +++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/docs/release-notes/version-3.4.md b/docs/release-notes/version-3.4.md index b7ce5380b..b7ca6b27c 100644 --- a/docs/release-notes/version-3.4.md +++ b/docs/release-notes/version-3.4.md @@ -5,6 +5,8 @@ ### Bug Fixes * [#11487](https://github.com/netbox-community/netbox/issues/11487) - Remove "set null" option from non-writable custom fields during bulk edit +* [#11491](https://github.com/netbox-community/netbox/issues/11491) - Show edit/delete buttons in user tokens table +* [#11528](https://github.com/netbox-community/netbox/issues/11528) - Permit import of devices using uploaded file * [#11555](https://github.com/netbox-community/netbox/issues/11555) - Avoid inadvertent interpretation of search query as regular expression under global search (previously [#11516](https://github.com/netbox-community/netbox/issues/11516)) * [#11562](https://github.com/netbox-community/netbox/issues/11562) - Correct ordering of virtual chassis interfaces with duplicate names diff --git a/netbox/users/tables.py b/netbox/users/tables.py index 8fbe9e8b3..0f1484887 100644 --- a/netbox/users/tables.py +++ b/netbox/users/tables.py @@ -19,6 +19,14 @@ COPY_BUTTON = """ """ +class TokenActionsColumn(columns.ActionsColumn): + # Subclass ActionsColumn to disregard permissions for edit & delete buttons + actions = { + 'edit': columns.ActionsItem('Edit', 'pencil', None, 'warning'), + 'delete': columns.ActionsItem('Delete', 'trash-can-outline', None, 'danger'), + } + + class TokenTable(NetBoxTable): key = columns.TemplateColumn( template_code=TOKEN @@ -32,7 +40,7 @@ class TokenTable(NetBoxTable): allowed_ips = columns.TemplateColumn( template_code=ALLOWED_IPS ) - actions = columns.ActionsColumn( + actions = TokenActionsColumn( actions=('edit', 'delete'), extra_buttons=COPY_BUTTON ) From 55b1549895c31185d2525eee150918b5c063685c Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 25 Jan 2023 10:15:11 -0500 Subject: [PATCH 009/144] Closes #10762: Permit selection custom fields to have only one choice --- docs/release-notes/version-3.4.md | 4 ++++ netbox/extras/models/customfields.py | 9 ++++++--- netbox/extras/tests/test_api.py | 1 + 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/docs/release-notes/version-3.4.md b/docs/release-notes/version-3.4.md index b7ca6b27c..5134c9972 100644 --- a/docs/release-notes/version-3.4.md +++ b/docs/release-notes/version-3.4.md @@ -2,6 +2,10 @@ ## v3.4.4 (FUTURE) +### Enhancements + +* [#10762](https://github.com/netbox-community/netbox/issues/10762) - Permit selection custom fields to have only one choice + ### Bug Fixes * [#11487](https://github.com/netbox-community/netbox/issues/11487) - Remove "set null" option from non-writable custom fields during bulk edit diff --git a/netbox/extras/models/customfields.py b/netbox/extras/models/customfields.py index 14b033bcd..4842c0654 100644 --- a/netbox/extras/models/customfields.py +++ b/netbox/extras/models/customfields.py @@ -273,10 +273,13 @@ class CustomField(CloningMixin, ExportTemplatesMixin, WebhooksMixin, ChangeLogge 'choices': "Choices may be set only for custom selection fields." }) - # A selection field must have at least two choices defined - if self.type == CustomFieldTypeChoices.TYPE_SELECT and self.choices and len(self.choices) < 2: + # Selection fields must have at least one choice defined + if self.type in ( + CustomFieldTypeChoices.TYPE_SELECT, + CustomFieldTypeChoices.TYPE_MULTISELECT + ) and not self.choices: raise ValidationError({ - 'choices': "Selection fields must specify at least two choices." + 'choices': "Selection fields must specify at least one choice." }) # A selection field's default (if any) must be present in its available choices diff --git a/netbox/extras/tests/test_api.py b/netbox/extras/tests/test_api.py index 29e725507..81a607eec 100644 --- a/netbox/extras/tests/test_api.py +++ b/netbox/extras/tests/test_api.py @@ -101,6 +101,7 @@ class CustomFieldTest(APIViewTestCases.APIViewTestCase): 'content_types': ['dcim.site'], 'name': 'cf6', 'type': 'select', + 'choices': ['A', 'B', 'C'] }, ] bulk_update_data = { From 9cb75e98340473bed2b0bf40e86e1a5d1ab22553 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 25 Jan 2023 21:25:25 -0500 Subject: [PATCH 010/144] Closes #11585: Add IP address filters for services --- docs/release-notes/version-3.4.md | 1 + netbox/ipam/filtersets.py | 12 ++++++++++++ netbox/ipam/tests/test_filtersets.py | 23 +++++++++++++++++++++++ 3 files changed, 36 insertions(+) diff --git a/docs/release-notes/version-3.4.md b/docs/release-notes/version-3.4.md index 5134c9972..4fedddab2 100644 --- a/docs/release-notes/version-3.4.md +++ b/docs/release-notes/version-3.4.md @@ -5,6 +5,7 @@ ### Enhancements * [#10762](https://github.com/netbox-community/netbox/issues/10762) - Permit selection custom fields to have only one choice +* [#11585](https://github.com/netbox-community/netbox/issues/11585) - Add IP address filters for services ### Bug Fixes diff --git a/netbox/ipam/filtersets.py b/netbox/ipam/filtersets.py index c30064ff1..d069eed27 100644 --- a/netbox/ipam/filtersets.py +++ b/netbox/ipam/filtersets.py @@ -923,6 +923,18 @@ class ServiceFilterSet(NetBoxModelFilterSet): to_field_name='name', label=_('Virtual machine (name)'), ) + ipaddress_id = django_filters.ModelMultipleChoiceFilter( + field_name='ipaddresses', + queryset=IPAddress.objects.all(), + label=_('IP address (ID)'), + ) + ipaddress = django_filters.ModelMultipleChoiceFilter( + field_name='ipaddresses__address', + queryset=IPAddress.objects.all(), + to_field_name='address', + label=_('IP address'), + ) + port = NumericArrayFilter( field_name='ports', lookup_expr='contains' diff --git a/netbox/ipam/tests/test_filtersets.py b/netbox/ipam/tests/test_filtersets.py index a2b06080a..711009a7e 100644 --- a/netbox/ipam/tests/test_filtersets.py +++ b/netbox/ipam/tests/test_filtersets.py @@ -1420,6 +1420,19 @@ class ServiceTestCase(TestCase, ChangeLoggedFilterSetTests): ) Device.objects.bulk_create(devices) + interface = Interface.objects.create( + device=devices[0], + name='eth0', + type=InterfaceTypeChoices.TYPE_VIRTUAL + ) + interface_ct = ContentType.objects.get_for_model(Interface).pk + ip_addresses = ( + IPAddress(address='192.0.2.1/24', assigned_object_type_id=interface_ct, assigned_object_id=interface.pk), + IPAddress(address='192.0.2.2/24', assigned_object_type_id=interface_ct, assigned_object_id=interface.pk), + IPAddress(address='192.0.2.3/24', assigned_object_type_id=interface_ct, assigned_object_id=interface.pk), + ) + IPAddress.objects.bulk_create(ip_addresses) + clustertype = ClusterType.objects.create(name='Cluster Type 1', slug='cluster-type-1') cluster = Cluster.objects.create(type=clustertype, name='Cluster 1') @@ -1439,6 +1452,9 @@ class ServiceTestCase(TestCase, ChangeLoggedFilterSetTests): Service(virtual_machine=virtual_machines[2], name='Service 6', protocol=ServiceProtocolChoices.PROTOCOL_UDP, ports=[2003]), ) Service.objects.bulk_create(services) + services[0].ipaddresses.add(ip_addresses[0]) + services[1].ipaddresses.add(ip_addresses[1]) + services[2].ipaddresses.add(ip_addresses[2]) def test_name(self): params = {'name': ['Service 1', 'Service 2']} @@ -1470,6 +1486,13 @@ class ServiceTestCase(TestCase, ChangeLoggedFilterSetTests): params = {'virtual_machine': [vms[0].name, vms[1].name]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_ipaddress(self): + ips = IPAddress.objects.all()[:2] + params = {'ipaddress_id': [ips[0].pk, ips[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'ipaddress': [str(ips[0].address), str(ips[1].address)]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + class L2VPNTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = L2VPN.objects.all() From 22a9df82e6d4e3a3085982538bfb82c5a49b9d6b Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Thu, 26 Jan 2023 08:46:25 -0500 Subject: [PATCH 011/144] Closes #11554: Add module types count to manufacturers list --- docs/release-notes/version-3.4.md | 1 + netbox/dcim/tables/devicetypes.py | 20 +++++++++++++++----- netbox/dcim/views.py | 1 + 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/docs/release-notes/version-3.4.md b/docs/release-notes/version-3.4.md index 4fedddab2..d4f238b3c 100644 --- a/docs/release-notes/version-3.4.md +++ b/docs/release-notes/version-3.4.md @@ -5,6 +5,7 @@ ### Enhancements * [#10762](https://github.com/netbox-community/netbox/issues/10762) - Permit selection custom fields to have only one choice +* [#11554](https://github.com/netbox-community/netbox/issues/11554) - Add module types count to manufacturers list * [#11585](https://github.com/netbox-community/netbox/issues/11585) - Add IP address filters for services ### Bug Fixes diff --git a/netbox/dcim/tables/devicetypes.py b/netbox/dcim/tables/devicetypes.py index 42d9c7879..c452c3efb 100644 --- a/netbox/dcim/tables/devicetypes.py +++ b/netbox/dcim/tables/devicetypes.py @@ -34,10 +34,19 @@ class ManufacturerTable(ContactsColumnMixin, NetBoxTable): url_params={'manufacturer_id': 'pk'}, verbose_name='Device Types' ) - inventoryitem_count = tables.Column( + moduletype_count = columns.LinkedCountColumn( + viewname='dcim:moduletype_list', + url_params={'manufacturer_id': 'pk'}, + verbose_name='Module Types' + ) + inventoryitem_count = columns.LinkedCountColumn( + viewname='dcim:inventoryitem_list', + url_params={'manufacturer_id': 'pk'}, verbose_name='Inventory Items' ) - platform_count = tables.Column( + platform_count = columns.LinkedCountColumn( + viewname='dcim:platform_list', + url_params={'manufacturer_id': 'pk'}, verbose_name='Platforms' ) slug = tables.Column() @@ -48,11 +57,12 @@ class ManufacturerTable(ContactsColumnMixin, NetBoxTable): class Meta(NetBoxTable.Meta): model = models.Manufacturer fields = ( - 'pk', 'id', 'name', 'devicetype_count', 'inventoryitem_count', 'platform_count', 'description', 'slug', - 'tags', 'contacts', 'actions', 'created', 'last_updated', + 'pk', 'id', 'name', 'devicetype_count', 'moduletype_count', 'inventoryitem_count', 'platform_count', + 'description', 'slug', 'tags', 'contacts', 'actions', 'created', 'last_updated', ) default_columns = ( - 'pk', 'name', 'devicetype_count', 'inventoryitem_count', 'platform_count', 'description', 'slug', + 'pk', 'name', 'devicetype_count', 'moduletype_count', 'inventoryitem_count', 'platform_count', + 'description', 'slug', ) diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 0643ac739..80b369b6d 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -842,6 +842,7 @@ class RackReservationBulkDeleteView(generic.BulkDeleteView): class ManufacturerListView(generic.ObjectListView): queryset = Manufacturer.objects.annotate( devicetype_count=count_related(DeviceType, 'manufacturer'), + moduletype_count=count_related(ModuleType, 'manufacturer'), inventoryitem_count=count_related(InventoryItem, 'manufacturer'), platform_count=count_related(Platform, 'manufacturer') ) From ccc108a2179606426b4e1004abc741ab20b3d9bd Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Thu, 26 Jan 2023 10:53:59 -0500 Subject: [PATCH 012/144] Closes #11598: Add buttons to easily switch between rack list and elevations views --- docs/release-notes/version-3.4.md | 1 + netbox/dcim/views.py | 1 + .../templates/dcim/rack_elevation_list.html | 51 ++++++++++--------- netbox/templates/dcim/rack_list.html | 9 ++++ netbox/templates/generic/object_list.html | 7 ++- 5 files changed, 41 insertions(+), 28 deletions(-) create mode 100644 netbox/templates/dcim/rack_list.html diff --git a/docs/release-notes/version-3.4.md b/docs/release-notes/version-3.4.md index d4f238b3c..4c9992f20 100644 --- a/docs/release-notes/version-3.4.md +++ b/docs/release-notes/version-3.4.md @@ -7,6 +7,7 @@ * [#10762](https://github.com/netbox-community/netbox/issues/10762) - Permit selection custom fields to have only one choice * [#11554](https://github.com/netbox-community/netbox/issues/11554) - Add module types count to manufacturers list * [#11585](https://github.com/netbox-community/netbox/issues/11585) - Add IP address filters for services +* [#11598](https://github.com/netbox-community/netbox/issues/11598) - Add buttons to easily switch between rack list and elevations views ### Bug Fixes diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 80b369b6d..9b49e799c 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -642,6 +642,7 @@ class RackListView(generic.ObjectListView): filterset = filtersets.RackFilterSet filterset_form = forms.RackFilterForm table = tables.RackTable + template_name = 'dcim/rack_list.html' class RackElevationListView(generic.ObjectListView): diff --git a/netbox/templates/dcim/rack_elevation_list.html b/netbox/templates/dcim/rack_elevation_list.html index c9d9a248a..bd02c9f74 100644 --- a/netbox/templates/dcim/rack_elevation_list.html +++ b/netbox/templates/dcim/rack_elevation_list.html @@ -5,31 +5,34 @@ {% block title %}Rack Elevations{% endblock %} {% block controls %} -
-
-
- -
-
- Front - Rear -
- -
+
+
+ + View List + +
+ +
+
+ Front + Rear +
+
+
{% endblock %} {% block content-wrapper %} diff --git a/netbox/templates/dcim/rack_list.html b/netbox/templates/dcim/rack_list.html new file mode 100644 index 000000000..897625af6 --- /dev/null +++ b/netbox/templates/dcim/rack_list.html @@ -0,0 +1,9 @@ +{% extends 'generic/object_list.html' %} +{% load helpers %} +{% load static %} + +{% block extra_controls %} + + View Elevations + +{% endblock %} \ No newline at end of file diff --git a/netbox/templates/generic/object_list.html b/netbox/templates/generic/object_list.html index 8b3e317c0..e269e9da6 100644 --- a/netbox/templates/generic/object_list.html +++ b/netbox/templates/generic/object_list.html @@ -26,16 +26,15 @@ Context:
{% plugin_list_buttons model %} - {% block extra_controls %}{% endblock %} {% if 'add' in actions %} - {% add_button model %} + {% add_button model %} {% endif %} {% if 'import' in actions %} - {% import_button model %} + {% import_button model %} {% endif %} {% if 'export' in actions %} - {% export_button model %} + {% export_button model %} {% endif %}
From fbc9fea0a5b0943da6f10df3551f071ce555a6ed Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 27 Jan 2023 16:44:10 -0500 Subject: [PATCH 013/144] Fixes #11267: Avoid catching ImportError exceptions when loading plugins (#11566) * Avoid catching ImportErrors when loading plugin URLs * Avoid catching ImportErrors when loading plugin resources --- netbox/extras/plugins/__init__.py | 70 ++++++++++++++++--------------- netbox/extras/plugins/urls.py | 13 +++--- 2 files changed, 42 insertions(+), 41 deletions(-) diff --git a/netbox/extras/plugins/__init__.py b/netbox/extras/plugins/__init__.py index 7694a1fbe..0b2123a4e 100644 --- a/netbox/extras/plugins/__init__.py +++ b/netbox/extras/plugins/__init__.py @@ -1,4 +1,5 @@ import collections +from importlib.util import find_spec from django.apps import AppConfig from django.conf import settings @@ -21,6 +22,15 @@ registry['plugins'] = { 'template_extensions': collections.defaultdict(list), } +DEFAULT_RESOURCE_PATHS = { + 'search_indexes': 'search.indexes', + 'graphql_schema': 'graphql.schema', + 'menu': 'navigation.menu', + 'menu_items': 'navigation.menu_items', + 'template_extensions': 'template_content.template_extensions', + 'user_preferences': 'preferences.preferences', +} + # # Plugin AppConfig class @@ -58,58 +68,50 @@ class PluginConfig(AppConfig): # Django apps to append to INSTALLED_APPS when plugin requires them. django_apps = [] - # Default integration paths. Plugin authors can override these to customize the paths to - # integrated components. - search_indexes = 'search.indexes' - graphql_schema = 'graphql.schema' - menu = 'navigation.menu' - menu_items = 'navigation.menu_items' - template_extensions = 'template_content.template_extensions' - user_preferences = 'preferences.preferences' + # Optional plugin resources + search_indexes = None + graphql_schema = None + menu = None + menu_items = None + template_extensions = None + user_preferences = None + + def _load_resource(self, name): + # Import from the configured path, if defined. + if getattr(self, name): + return import_string(f"{self.__module__}.{self.name}") + # Fall back to the resource's default path. Return None if the module has not been provided. + default_path = DEFAULT_RESOURCE_PATHS[name] + default_module = f'{self.__module__}.{default_path}'.rsplit('.', 1)[0] + if find_spec(default_module): + setattr(self, name, default_path) + return import_string(f"{self.__module__}.{default_path}") def ready(self): plugin_name = self.name.rsplit('.', 1)[-1] # Register search extensions (if defined) - try: - search_indexes = import_string(f"{self.__module__}.{self.search_indexes}") - for idx in search_indexes: - register_search(idx) - except ImportError: - pass + search_indexes = self._load_resource('search_indexes') or [] + for idx in search_indexes: + register_search(idx) # Register template content (if defined) - try: - template_extensions = import_string(f"{self.__module__}.{self.template_extensions}") + if template_extensions := self._load_resource('template_extensions'): register_template_extensions(template_extensions) - except ImportError: - pass # Register navigation menu and/or menu items (if defined) - try: - menu = import_string(f"{self.__module__}.{self.menu}") + if menu := self._load_resource('menu'): register_menu(menu) - except ImportError: - pass - try: - menu_items = import_string(f"{self.__module__}.{self.menu_items}") + if menu_items := self._load_resource('menu_items'): register_menu_items(self.verbose_name, menu_items) - except ImportError: - pass # Register GraphQL schema (if defined) - try: - graphql_schema = import_string(f"{self.__module__}.{self.graphql_schema}") + if graphql_schema := self._load_resource('graphql_schema'): register_graphql_schema(graphql_schema) - except ImportError: - pass # Register user preferences (if defined) - try: - user_preferences = import_string(f"{self.__module__}.{self.user_preferences}") + if user_preferences := self._load_resource('user_preferences'): register_user_preferences(plugin_name, user_preferences) - except ImportError: - pass @classmethod def validate(cls, user_config, netbox_version): diff --git a/netbox/extras/plugins/urls.py b/netbox/extras/plugins/urls.py index b4360dc9e..2f237f56a 100644 --- a/netbox/extras/plugins/urls.py +++ b/netbox/extras/plugins/urls.py @@ -1,9 +1,11 @@ +from importlib import import_module + from django.apps import apps from django.conf import settings from django.conf.urls import include from django.contrib.admin.views.decorators import staff_member_required from django.urls import path -from django.utils.module_loading import import_string +from django.utils.module_loading import import_string, module_has_submodule from . import views @@ -19,24 +21,21 @@ plugin_admin_patterns = [ # Register base/API URL patterns for each plugin for plugin_path in settings.PLUGINS: + plugin = import_module(plugin_path) plugin_name = plugin_path.split('.')[-1] app = apps.get_app_config(plugin_name) base_url = getattr(app, 'base_url') or app.label # Check if the plugin specifies any base URLs - try: + if module_has_submodule(plugin, 'urls'): urlpatterns = import_string(f"{plugin_path}.urls.urlpatterns") plugin_patterns.append( path(f"{base_url}/", include((urlpatterns, app.label))) ) - except ImportError: - pass # Check if the plugin specifies any API URLs - try: + if module_has_submodule(plugin, 'api.urls'): urlpatterns = import_string(f"{plugin_path}.api.urls.urlpatterns") plugin_api_patterns.append( path(f"{base_url}/", include((urlpatterns, f"{app.label}-api"))) ) - except ImportError: - pass From 0da518e83de6500bb25572fb9c2618c198915d7f Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Fri, 27 Jan 2023 16:45:20 -0500 Subject: [PATCH 014/144] Changelog for #11267 --- docs/release-notes/version-3.4.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes/version-3.4.md b/docs/release-notes/version-3.4.md index 4c9992f20..612e5bb74 100644 --- a/docs/release-notes/version-3.4.md +++ b/docs/release-notes/version-3.4.md @@ -11,6 +11,7 @@ ### Bug Fixes +* [#11267](https://github.com/netbox-community/netbox/issues/11267) - Avoid catching ImportErrors when loading plugin resources * [#11487](https://github.com/netbox-community/netbox/issues/11487) - Remove "set null" option from non-writable custom fields during bulk edit * [#11491](https://github.com/netbox-community/netbox/issues/11491) - Show edit/delete buttons in user tokens table * [#11528](https://github.com/netbox-community/netbox/issues/11528) - Permit import of devices using uploaded file From 892fd95b5f3813b9d3365ebe100c842e88480a41 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Fri, 27 Jan 2023 16:46:49 -0500 Subject: [PATCH 015/144] Update NetBox Cloud link --- docs/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.md b/docs/index.md index d61465443..6a53403d6 100644 --- a/docs/index.md +++ b/docs/index.md @@ -52,4 +52,4 @@ NetBox is built on the enormously popular [Django](http://www.djangoproject.com/ * Try out our [public demo](https://demo.netbox.dev/) if you want to jump right in * The [installation guide](./installation/index.md) will help you get your own deployment up and running * Or try the community [Docker image](https://github.com/netbox-community/netbox-docker) for a low-touch approach -* [NetBox Cloud](https://www.getnetbox.io/) is a hosted solution offered by NS1 +* [NetBox Cloud](https://netboxlabs.com/netbox-cloud) is a managed solution offered by [NetBox Labs](https://netboxlabs.com/) From e7ad6eeb7482c3b5cd7f9b64738da7a52e9d5ebe Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Fri, 27 Jan 2023 19:56:12 -0500 Subject: [PATCH 016/144] Fixes #11613: Correct plugin import logic fix from #11267 --- netbox/extras/plugins/__init__.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/netbox/extras/plugins/__init__.py b/netbox/extras/plugins/__init__.py index 0b2123a4e..b56113ca1 100644 --- a/netbox/extras/plugins/__init__.py +++ b/netbox/extras/plugins/__init__.py @@ -1,5 +1,5 @@ import collections -from importlib.util import find_spec +from importlib import import_module from django.apps import AppConfig from django.conf import settings @@ -80,12 +80,15 @@ class PluginConfig(AppConfig): # Import from the configured path, if defined. if getattr(self, name): return import_string(f"{self.__module__}.{self.name}") + # Fall back to the resource's default path. Return None if the module has not been provided. - default_path = DEFAULT_RESOURCE_PATHS[name] - default_module = f'{self.__module__}.{default_path}'.rsplit('.', 1)[0] - if find_spec(default_module): - setattr(self, name, default_path) - return import_string(f"{self.__module__}.{default_path}") + default_path = f'{self.__module__}.{DEFAULT_RESOURCE_PATHS[name]}' + default_module, resource_name = default_path.rsplit('.', 1) + try: + module = import_module(default_module) + return getattr(module, resource_name, None) + except ModuleNotFoundError: + pass def ready(self): plugin_name = self.name.rsplit('.', 1)[-1] From 46ede62f3fbb9938f5fe19f03cb1df991db69fee Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 30 Jan 2023 10:25:20 -0500 Subject: [PATCH 017/144] Fix rendering of example code --- docs/plugins/development/navigation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/plugins/development/navigation.md b/docs/plugins/development/navigation.md index 63402c747..5f4a8a0dc 100644 --- a/docs/plugins/development/navigation.md +++ b/docs/plugins/development/navigation.md @@ -51,7 +51,7 @@ menu_items = (item1, item2, item3) Each menu item represents a link and (optionally) a set of buttons comprising one entry in NetBox's navigation menu. Menu items are defined as PluginMenuItem instances. An example is shown below. -```python filename="navigation.py" +```python title="navigation.py" from extras.plugins import PluginMenuButton, PluginMenuItem from utilities.choices import ButtonColorChoices From 10e27cfa00031c1875e3fd0c81776e6e3a44ad88 Mon Sep 17 00:00:00 2001 From: Arthur Date: Tue, 31 Jan 2023 09:56:09 -0800 Subject: [PATCH 018/144] 11620 fix interface poe type filter --- netbox/dcim/forms/filtersets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/dcim/forms/filtersets.py b/netbox/dcim/forms/filtersets.py index c00e83672..4dd2f73eb 100644 --- a/netbox/dcim/forms/filtersets.py +++ b/netbox/dcim/forms/filtersets.py @@ -1170,7 +1170,7 @@ class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm): label='PoE mode' ) poe_type = MultipleChoiceField( - choices=InterfacePoEModeChoices, + choices=InterfacePoETypeChoices, required=False, label='PoE type' ) From a137cd6cbefa4654cc8f69c367b3258d3ccdadaa Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 1 Feb 2023 10:33:45 -0500 Subject: [PATCH 019/144] Fixes #11635: Pre-populate assigned VRF when following "first available IP" link from prefix view --- docs/release-notes/version-3.4.md | 2 ++ netbox/templates/ipam/prefix.html | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/release-notes/version-3.4.md b/docs/release-notes/version-3.4.md index 612e5bb74..3c92bc61d 100644 --- a/docs/release-notes/version-3.4.md +++ b/docs/release-notes/version-3.4.md @@ -17,6 +17,8 @@ * [#11528](https://github.com/netbox-community/netbox/issues/11528) - Permit import of devices using uploaded file * [#11555](https://github.com/netbox-community/netbox/issues/11555) - Avoid inadvertent interpretation of search query as regular expression under global search (previously [#11516](https://github.com/netbox-community/netbox/issues/11516)) * [#11562](https://github.com/netbox-community/netbox/issues/11562) - Correct ordering of virtual chassis interfaces with duplicate names +* [#11620](https://github.com/netbox-community/netbox/issues/11620) - Correct available filter choices for interface PoE type +* [#11635](https://github.com/netbox-community/netbox/issues/11635) - Pre-populate assigned VRF when following "first available IP" link from prefix view --- diff --git a/netbox/templates/ipam/prefix.html b/netbox/templates/ipam/prefix.html index a0baf3325..6d986aed5 100644 --- a/netbox/templates/ipam/prefix.html +++ b/netbox/templates/ipam/prefix.html @@ -133,7 +133,7 @@ {% with first_available_ip=object.get_first_available_ip %} {% if first_available_ip %} {% if perms.ipam.add_ipaddress %} - {{ first_available_ip }} + {{ first_available_ip }} {% else %} {{ first_available_ip }} {% endif %} From fb2771370cdf7f1d17db8e0f3985785333372d79 Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Thu, 2 Feb 2023 06:33:57 -0800 Subject: [PATCH 020/144] handled scripts error when only interval is used --- netbox/extras/forms/scripts.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/netbox/extras/forms/scripts.py b/netbox/extras/forms/scripts.py index 79dc8c869..8216c5413 100644 --- a/netbox/extras/forms/scripts.py +++ b/netbox/extras/forms/scripts.py @@ -45,12 +45,16 @@ class ScriptForm(BootstrapMixin, forms.Form): self.fields['_interval'] = interval self.fields['_commit'] = commit - def clean__schedule_at(self): + def clean(self): scheduled_time = self.cleaned_data['_schedule_at'] - if scheduled_time and scheduled_time < timezone.now(): + if scheduled_time and scheduled_time < local_now(): raise forms.ValidationError(_('Scheduled time must be in the future.')) - return scheduled_time + # When interval is used without schedule at, raise an exception + if self.cleaned_data['_interval'] and not scheduled_time: + raise forms.ValidationError(_('Scheduled time must be set when recurs is used.')) + + return self.cleaned_data @property def requires_input(self): From 98a2f3e4979fa473f5d508dae5f50913177a88c9 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Thu, 2 Feb 2023 14:18:32 -0500 Subject: [PATCH 021/144] Refresh the README --- README.md | 119 ++++++++++++++++++------------------------------------ 1 file changed, 39 insertions(+), 80 deletions(-) diff --git a/README.md b/README.md index e14f31b56..053aa8461 100644 --- a/README.md +++ b/README.md @@ -1,71 +1,59 @@
NetBox logo + + The premiere source of truth powering network automation
+![Master branch build status](https://github.com/netbox-community/netbox/workflows/CI/badge.svg?branch=master) + NetBox is the leading solution for modeling and documenting modern networks. By combining the traditional disciplines of IP address management (IPAM) and datacenter infrastructure management (DCIM) with powerful APIs and extensions, NetBox provides the ideal "source of truth" to power network automation. -Available as open source software under the Apache 2.0 license, NetBox is -employed by thousands of organizations around the world. +Available as open source software under the Apache 2.0 license, NetBox serves +as the cornerstone for network automation in thousands of organizations. -![Master branch build status](https://github.com/netbox-community/netbox/workflows/CI/badge.svg?branch=master) - -[![Timeline graph](https://images.repography.com/29023055/netbox-community/netbox/recent-activity/31db894eee74b8a5475e3af307a81b6c_timeline.svg)](https://github.com/netbox-community/netbox/commits) -[![Issue status graph](https://images.repography.com/29023055/netbox-community/netbox/recent-activity/31db894eee74b8a5475e3af307a81b6c_issues.svg)](https://github.com/netbox-community/netbox/issues) -[![Pull request status graph](https://images.repography.com/29023055/netbox-community/netbox/recent-activity/31db894eee74b8a5475e3af307a81b6c_prs.svg)](https://github.com/netbox-community/netbox/pulls) -[![Top contributors](https://images.repography.com/29023055/netbox-community/netbox/recent-activity/31db894eee74b8a5475e3af307a81b6c_users.svg)](https://github.com/netbox-community/netbox/graphs/contributors) -
Stats via [Repography](https://repography.com) - -## About NetBox +* **Physical infrasucture:** Accurately model the physical world, from global regions down to individual racks of gear. Then connect everything - network, console, and power! +* **Modern IPAM:** All the standard IPAM functionality you expect, plus VRF import/export tracking, VLAN management, and overlay support. +* **Data circuits:** Confidently manage the delivery of crtical circuits from various service providers, modeled seamlessly alongside your own infrastructure. +* **Power tracking:** Map the distribution of power from upstream sources to individual feeds and outlets. +* **Organization:** Manage tenant and contact assignments natively. +* **Powerful search:** Easily find anything you need using a single global search function. +* **Comprehensive logging:** Leverage both automatic change logging and user-submitted journal entries to track your network's growth over time. +* **Endless customization:** Custom fields, custom links, tags, export templates, custom validation, reports, scripts, and more! +* **Flexible permissions:** An advanced permissions systems enables very flexible delegation of permissions. +* **Integrations:** Easily connect NetBox to your other tooling via its REST & GraphQL APIs. +* **Plugins:** Not finding what you need in the core application? Try one of many community plugins - or build your own! ![Screenshot of NetBox UI](docs/media/screenshots/netbox-ui.png "NetBox UI") -Myriad infrastructure components can be modeled in NetBox, including: +## Getting Started -* Hierarchical regions, site groups, sites, and locations -* Racks, devices, and device components -* Cables and wireless connections -* Power distribution -* Data circuits and providers -* Virtual machines and clusters -* IP prefixes, ranges, and addresses -* VRFs and route targets -* L2VPN and overlays -* FHRP groups (VRRP, HSRP, etc.) -* AS numbers -* VLANs and scoped VLAN groups -* Organizational tenants and contacts +* Just want to explore? Check out [our public demo](https://demo.netbox.dev/) right now! +* The [official documentation](https://docs.netbox.dev) offers a comprehensive introduction. +* Choose your deployment: [self-hosted](https://github.com/netbox-community/netbox), [Docker](https://github.com/netbox-community/netbox-docker), or [NetBox Cloud](https://netboxlabs.com/netbox-cloud/). +* Check out [our wiki](https://github.com/netbox-community/netbox/wiki/Community-Contributions) for even more projects to get the most out of NetBox! -In addition to its extensive built-in models and functionality, NetBox can be -customized and extended through the use of: +## Get Involved -* Custom fields -* Custom links -* Configuration contexts -* Custom model validation rules -* Reports -* Custom scripts -* Export templates -* Conditional webhooks -* Plugins -* Single sign-on (SSO) authentication -* NAPALM integration -* Detailed change logging +* Follow [@NetBoxOfficial](https://twitter.com/NetBoxOfficial) on Twitter! +* Join the conversation on [the discussion forum](https://github.com/netbox-community/netbox/discussions) and [Slack](https://netdev.chat/)! +* Already a power user? You can [suggest a feature](https://github.com/netbox-community/netbox/issues/new?assignees=&labels=type%3A+feature&template=feature_request.yaml) or [report a bug](https://github.com/netbox-community/netbox/issues/new?assignees=&labels=type%3A+bug&template=bug_report.yaml) on GitHub. +* Contributions from the community are encouraged and appreciated! Check out our [contributing guide](CONTRIBUTING.md) to get started. -NetBox also features a complete REST API as well as a GraphQL API for easily -integrating with other tools and systems. - -The complete documentation for NetBox can be found at [docs.netbox.dev](https://docs.netbox.dev/). -A public demo instance is available at [demo.netbox.dev](https://demo.netbox.dev). - -NetBox runs as a web application atop the [Django](https://www.djangoproject.com/) -Python framework with a [PostgreSQL](https://www.postgresql.org/) database. For a -complete list of requirements, see `requirements.txt`. The code is available -[on GitHub](https://github.com/netbox-community/netbox). +## Project Stats + +
+ Timeline graph + Issues graph + Pull requests graph + Top contributors +
Stats via Repography +
+ +## Sponsors
-

Thank you to our sponsors!

[![NetBox Labs](https://raw.githubusercontent.com/wiki/netbox-community/netbox/images/sponsors/netbox_labs.png)](https://netboxlabs.com)            @@ -76,34 +64,10 @@ complete list of requirements, see `requirements.txt`. The code is available [![Sentry](https://raw.githubusercontent.com/wiki/netbox-community/netbox/images/sponsors/sentry.png)](https://sentry.io)            [![Equinix Metal](https://raw.githubusercontent.com/wiki/netbox-community/netbox/images/sponsors/equinix.png)](https://metal.equinix.com) -            - [![Stellar Technologies](https://raw.githubusercontent.com/wiki/netbox-community/netbox/images/sponsors/stellar.png)](https://stellar.tech)
-### Discussion - -* [GitHub Discussions](https://github.com/netbox-community/netbox/discussions) - Discussion forum hosted by GitHub; ideal for Q&A and other structured discussions -* [Slack](https://netdev.chat/) - Real-time chat hosted by the NetDev Community; best for unstructured discussion or just hanging out - -### Installation - -Please see [the documentation](https://docs.netbox.dev/) for -instructions on installing NetBox. To upgrade NetBox, please download the -[latest release](https://github.com/netbox-community/netbox/releases) and -run `upgrade.sh`. - -### Providing Feedback - -The best platform for general feedback, assistance, and other discussion is our -[GitHub discussions](https://github.com/netbox-community/netbox/discussions). -To report a bug or request a specific feature, please open a GitHub issue using -the [appropriate template](https://github.com/netbox-community/netbox/issues/new/choose). - -If you are interested in contributing to the development of NetBox, please read -our [contributing guide](CONTRIBUTING.md) prior to beginning any work. - -### Screenshots +## Screenshots ![Screenshot of main page (dark mode)](docs/media/screenshots/home-dark.png "Main page (dark mode)") @@ -112,8 +76,3 @@ our [contributing guide](CONTRIBUTING.md) prior to beginning any work. ![Screenshot of prefixes hierarchy](docs/media/screenshots/prefixes-list.png "Prefixes hierarchy") ![Screenshot of cable trace](docs/media/screenshots/cable-trace.png "Cable tracing") - -### Related projects - -Please see [our wiki](https://github.com/netbox-community/netbox/wiki/Community-Contributions) -for a list of relevant community projects. From 95b2acb6031b94bb88bca846f5352ce307def44f Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Thu, 2 Feb 2023 14:59:16 -0500 Subject: [PATCH 022/144] Fixes #11650: Display error message when attempting to create device component with duplicate name --- docs/release-notes/version-3.4.md | 2 ++ netbox/netbox/views/generic/object_views.py | 3 +++ 2 files changed, 5 insertions(+) diff --git a/docs/release-notes/version-3.4.md b/docs/release-notes/version-3.4.md index 3c92bc61d..130b14544 100644 --- a/docs/release-notes/version-3.4.md +++ b/docs/release-notes/version-3.4.md @@ -17,8 +17,10 @@ * [#11528](https://github.com/netbox-community/netbox/issues/11528) - Permit import of devices using uploaded file * [#11555](https://github.com/netbox-community/netbox/issues/11555) - Avoid inadvertent interpretation of search query as regular expression under global search (previously [#11516](https://github.com/netbox-community/netbox/issues/11516)) * [#11562](https://github.com/netbox-community/netbox/issues/11562) - Correct ordering of virtual chassis interfaces with duplicate names +* [#11574](https://github.com/netbox-community/netbox/issues/11574) - Fix exception when attempting to schedule reports/scripts * [#11620](https://github.com/netbox-community/netbox/issues/11620) - Correct available filter choices for interface PoE type * [#11635](https://github.com/netbox-community/netbox/issues/11635) - Pre-populate assigned VRF when following "first available IP" link from prefix view +* [#11650](https://github.com/netbox-community/netbox/issues/11650) - Display error message when attempting to create device component with duplicate name --- diff --git a/netbox/netbox/views/generic/object_views.py b/netbox/netbox/views/generic/object_views.py index 795f4ad56..d855490d1 100644 --- a/netbox/netbox/views/generic/object_views.py +++ b/netbox/netbox/views/generic/object_views.py @@ -453,6 +453,9 @@ class ComponentCreateView(GetReturnURLMixin, BaseObjectView): if component_form.is_valid(): new_components.append(component_form) + else: + form.errors.update(component_form.errors) + break if not form.errors and not component_form.errors: try: From 699edd049c5b7426e01e1471b7f36d560ed182ce Mon Sep 17 00:00:00 2001 From: Maximilian Wilhelm Date: Thu, 2 Feb 2023 21:22:55 +0100 Subject: [PATCH 023/144] Closes #11152: Add support to abort custom script gracefully (#11621) Signed-off-by: Maximilian Wilhelm --- docs/customization/custom-scripts.md | 13 +++++++++++++ netbox/extras/scripts.py | 10 +++++++++- netbox/utilities/exceptions.py | 7 +++++++ 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/docs/customization/custom-scripts.md b/docs/customization/custom-scripts.md index 456bcf472..af1e9b5b6 100644 --- a/docs/customization/custom-scripts.md +++ b/docs/customization/custom-scripts.md @@ -142,6 +142,19 @@ obj.full_clean() obj.save() ``` +## Error handling + +Sometimes things go wrong and a script will run into an `Exception`. If that happens and an uncaught exception is raised by the custom script, the execution is aborted and a full stack trace is reported. + +Although this is helpful for debugging, in some situations it might be required to cleanly abort the execution of a custom script (e.g. because of invalid input data) and thereby make sure no changes are performed on the database. In this case the script can throw an `AbortScript` exception, which will prevent the stack trace from being reported, but still terminating the script's execution and reporting a given error message. + +```python +from utilities.exceptions import AbortScript + +if some_error: + raise AbortScript("Some meaningful error message") +``` + ## Variable Reference ### Default Options diff --git a/netbox/extras/scripts.py b/netbox/extras/scripts.py index 998d727a4..77c96de56 100644 --- a/netbox/extras/scripts.py +++ b/netbox/extras/scripts.py @@ -21,7 +21,7 @@ from extras.models import JobResult from extras.signals import clear_webhooks from ipam.formfields import IPAddressFormField, IPNetworkFormField from ipam.validators import MaxPrefixLengthValidator, MinPrefixLengthValidator, prefix_validator -from utilities.exceptions import AbortTransaction +from utilities.exceptions import AbortScript, AbortTransaction from utilities.forms import add_blank_choice, DynamicModelChoiceField, DynamicModelMultipleChoiceField from .context_managers import change_logging from .forms import ScriptForm @@ -470,6 +470,14 @@ def run_script(data, request, commit=True, *args, **kwargs): except AbortTransaction: script.log_info("Database changes have been reverted automatically.") clear_webhooks.send(request) + except AbortScript as e: + script.log_failure( + f"Script aborted with error: {e}" + ) + script.log_info("Database changes have been reverted due to error.") + logger.error(f"Script aborted with error: {e}") + job_result.set_status(JobResultStatusChoices.STATUS_ERRORED) + clear_webhooks.send(request) except Exception as e: stacktrace = traceback.format_exc() script.log_failure( diff --git a/netbox/utilities/exceptions.py b/netbox/utilities/exceptions.py index 657e90745..d7418d0cb 100644 --- a/netbox/utilities/exceptions.py +++ b/netbox/utilities/exceptions.py @@ -24,6 +24,13 @@ class AbortRequest(Exception): self.message = message +class AbortScript(Exception): + """ + Raised to cleanly abort a script. + """ + pass + + class PermissionsViolation(Exception): """ Raised when an operation was prevented because it would violate the From 37d0135cab92339652239775da4c94c67fe914e1 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Thu, 2 Feb 2023 15:24:54 -0500 Subject: [PATCH 024/144] Release v3.4.4 --- .github/ISSUE_TEMPLATE/bug_report.yaml | 2 +- .github/ISSUE_TEMPLATE/feature_request.yaml | 2 +- base_requirements.txt | 2 +- docs/release-notes/version-3.4.md | 3 ++- netbox/netbox/settings.py | 2 +- requirements.txt | 6 +++--- 6 files changed, 9 insertions(+), 8 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index 80810f2ba..9ed740fff 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.4.3 + placeholder: v3.4.4 validations: required: true - type: dropdown diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml index 975fc025a..8e4ab54a5 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.4.3 + placeholder: v3.4.4 validations: required: true - type: dropdown diff --git a/base_requirements.txt b/base_requirements.txt index 3e4811ece..7292c676b 100644 --- a/base_requirements.txt +++ b/base_requirements.txt @@ -1,6 +1,6 @@ # HTML sanitizer # https://github.com/mozilla/bleach -bleach +bleach<6.0 # The Python web framework on which NetBox is built # https://github.com/django/django diff --git a/docs/release-notes/version-3.4.md b/docs/release-notes/version-3.4.md index 130b14544..1581ce681 100644 --- a/docs/release-notes/version-3.4.md +++ b/docs/release-notes/version-3.4.md @@ -1,10 +1,11 @@ # NetBox v3.4 -## v3.4.4 (FUTURE) +## v3.4.4 (2023-02-02) ### Enhancements * [#10762](https://github.com/netbox-community/netbox/issues/10762) - Permit selection custom fields to have only one choice +* [#11152](https://github.com/netbox-community/netbox/issues/11152) - Introduce AbortScript exception to elegantly abort scripts * [#11554](https://github.com/netbox-community/netbox/issues/11554) - Add module types count to manufacturers list * [#11585](https://github.com/netbox-community/netbox/issues/11585) - Add IP address filters for services * [#11598](https://github.com/netbox-community/netbox/issues/11598) - Add buttons to easily switch between rack list and elevations views diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 4d74307a0..8517efca1 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -24,7 +24,7 @@ from netbox.constants import RQ_QUEUE_DEFAULT, RQ_QUEUE_HIGH, RQ_QUEUE_LOW # Environment setup # -VERSION = '3.4.4-dev' +VERSION = '3.4.4' # Hostname HOSTNAME = platform.node() diff --git a/requirements.txt b/requirements.txt index 3ab7faace..3cb2529a8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ bleach==5.0.1 -Django==4.1.5 +Django==4.1.6 django-cors-headers==3.13.0 django-debug-toolbar==3.8.1 django-filter==22.1 @@ -19,13 +19,13 @@ graphene-django==3.0.0 gunicorn==20.1.0 Jinja2==3.1.2 Markdown==3.3.7 -mkdocs-material==9.0.6 +mkdocs-material==9.0.10 mkdocstrings[python-legacy]==0.20.0 netaddr==0.8.0 Pillow==9.4.0 psycopg2-binary==2.9.5 PyYAML==6.0 -sentry-sdk==1.13.0 +sentry-sdk==1.14.0 social-auth-app-django==5.0.0 social-auth-core[openidconnect]==4.3.0 svgwrite==1.4.3 From 7ebfa4c1d1890fafb6393d97e88aa76229368f67 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Thu, 2 Feb 2023 15:41:24 -0500 Subject: [PATCH 025/144] PRVB --- docs/release-notes/version-3.4.md | 4 ++++ netbox/netbox/settings.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/release-notes/version-3.4.md b/docs/release-notes/version-3.4.md index 1581ce681..15b84436d 100644 --- a/docs/release-notes/version-3.4.md +++ b/docs/release-notes/version-3.4.md @@ -1,5 +1,9 @@ # NetBox v3.4 +## v3.4.5 (FUTURE) + +--- + ## v3.4.4 (2023-02-02) ### Enhancements diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 8517efca1..cda6ee643 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -24,7 +24,7 @@ from netbox.constants import RQ_QUEUE_DEFAULT, RQ_QUEUE_HIGH, RQ_QUEUE_LOW # Environment setup # -VERSION = '3.4.4' +VERSION = '3.4.5-dev' # Hostname HOSTNAME = platform.node() From 5e1bb20f3208d34321fa218f901f71aaa6f439d5 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 7 Feb 2023 16:49:07 -0500 Subject: [PATCH 026/144] Display login message as success --- netbox/users/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/users/views.py b/netbox/users/views.py index 832a4e592..a82620914 100644 --- a/netbox/users/views.py +++ b/netbox/users/views.py @@ -96,7 +96,7 @@ class LoginView(View): # Authenticate user auth_login(request, form.get_user()) logger.info(f"User {request.user} successfully authenticated") - messages.info(request, f"Logged in as {request.user}.") + messages.success(request, f"Logged in as {request.user}.") # Ensure the user has a UserConfig defined. (This should normally be handled by # create_userconfig() on user creation.) From edbd597bf2687ef31d1fbaa394b2080bd57f95a9 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 7 Feb 2023 16:52:54 -0500 Subject: [PATCH 027/144] Update housekeeping command docs --- docs/administration/housekeeping.md | 1 + netbox/extras/management/commands/housekeeping.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/administration/housekeeping.md b/docs/administration/housekeeping.md index da1a5443b..fcc3aa04e 100644 --- a/docs/administration/housekeeping.md +++ b/docs/administration/housekeeping.md @@ -5,6 +5,7 @@ NetBox includes a `housekeeping` management command that should be run nightly. * Clearing expired authentication sessions from the database * Deleting changelog records older than the configured [retention time](../configuration/miscellaneous.md#changelog_retention) * Deleting job result records older than the configured [retention time](../configuration/miscellaneous.md#jobresult_retention) +* Check for new NetBox releases (if [`RELEASE_CHECK_URL`](../configuration/miscellaneous.md#release_check_url) is set) This command can be invoked directly, or by using the shell script provided at `/opt/netbox/contrib/netbox-housekeeping.sh`. This script can be linked from your cron scheduler's daily jobs directory (e.g. `/etc/cron.daily`) or referenced directly within the cron configuration file. diff --git a/netbox/extras/management/commands/housekeeping.py b/netbox/extras/management/commands/housekeeping.py index 42690568d..172e26bf2 100644 --- a/netbox/extras/management/commands/housekeeping.py +++ b/netbox/extras/management/commands/housekeeping.py @@ -37,7 +37,7 @@ class Command(BaseCommand): f"clearing sessions; skipping." ) - # Delete expired ObjectRecords + # Delete expired ObjectChanges if options['verbosity']: self.stdout.write("[*] Checking for expired changelog records") if config.CHANGELOG_RETENTION: From 3f28d6aef3b40726ee6c9fade941c31272271d70 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 7 Feb 2023 16:55:50 -0500 Subject: [PATCH 028/144] Add step for creating search index --- docs/development/adding-models.md | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/docs/development/adding-models.md b/docs/development/adding-models.md index aef11d666..7de897a97 100644 --- a/docs/development/adding-models.md +++ b/docs/development/adding-models.md @@ -54,15 +54,19 @@ Each model should have a corresponding FilterSet class defined. This is used to Create a table class for the model in `tables.py` by subclassing `utilities.tables.BaseTable`. Under the table's `Meta` class, be sure to list both the fields and default columns. -## 9. Create the object template +## 9. Create a SearchIndex subclass + +If this model will be included in global search results, create a subclass of `netbox.search.SearchIndex` for it and specify the fields to be indexed. + +## 10. Create the object template Create the HTML template for the object view. (The other views each typically employ a generic template.) This template should extend `generic/object.html`. -## 10. Add the model to the navigation menu +## 11. Add the model to the navigation menu Add the relevant navigation menu items in `netbox/netbox/navigation/menu.py`. -## 11. REST API components +## 12. REST API components Create the following for each model: @@ -71,13 +75,13 @@ Create the following for each model: * API view in `api/views.py` * Endpoint route in `api/urls.py` -## 12. GraphQL API components +## 13. GraphQL API components Create a Graphene object type for the model in `graphql/types.py` by subclassing the appropriate class from `netbox.graphql.types`. Also extend the schema class defined in `graphql/schema.py` with the individual object and object list fields per the established convention. -## 13. Add tests +## 14. Add tests Add tests for the following: @@ -85,7 +89,7 @@ Add tests for the following: * API views * Filter sets -## 14. Documentation +## 15. Documentation Create a new documentation page for the model in `docs/models//.md`. Include this file under the "features" documentation where appropriate. From 56c7a238a4905d3220b63d8b33b07ca52df7efd1 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 7 Feb 2023 17:24:26 -0500 Subject: [PATCH 029/144] Fixes #11683: Fix CSV header attribute detection when auto-detecting import format --- docs/release-notes/version-3.4.md | 4 ++++ netbox/netbox/views/generic/bulk_views.py | 4 ++-- netbox/utilities/forms/forms.py | 2 ++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/release-notes/version-3.4.md b/docs/release-notes/version-3.4.md index 15b84436d..b23251a70 100644 --- a/docs/release-notes/version-3.4.md +++ b/docs/release-notes/version-3.4.md @@ -2,6 +2,10 @@ ## v3.4.5 (FUTURE) +### Bug Fixes + +* [#11683](https://github.com/netbox-community/netbox/issues/11683) - Fix CSV header attribute detection when auto-detecting import format + --- ## v3.4.4 (2023-02-02) diff --git a/netbox/netbox/views/generic/bulk_views.py b/netbox/netbox/views/generic/bulk_views.py index ab3e8f100..6060475d8 100644 --- a/netbox/netbox/views/generic/bulk_views.py +++ b/netbox/netbox/views/generic/bulk_views.py @@ -384,8 +384,8 @@ class BulkImportView(GetReturnURLMixin, BaseMultiObjectView): 'data': record, 'instance': instance, } - if form.cleaned_data['format'] == ImportFormatChoices.CSV: - model_form_kwargs['headers'] = form._csv_headers + if hasattr(form, '_csv_headers'): + model_form_kwargs['headers'] = form._csv_headers # Add CSV headers model_form = self.model_form(**model_form_kwargs) # When updating, omit all form fields other than those specified in the record. (No diff --git a/netbox/utilities/forms/forms.py b/netbox/utilities/forms/forms.py index 99d03f2a6..9884ffac5 100644 --- a/netbox/utilities/forms/forms.py +++ b/netbox/utilities/forms/forms.py @@ -197,6 +197,8 @@ class ImportForm(BootstrapMixin, forms.Form): self.cleaned_data['data'] = self._clean_json(data) elif format == ImportFormatChoices.YAML: self.cleaned_data['data'] = self._clean_yaml(data) + else: + raise forms.ValidationError(f"Unknown data format: {format}") def _detect_format(self, data): """ From 91705aa9fdfb4b730aaad3b438f65b853ff00107 Mon Sep 17 00:00:00 2001 From: kkthxbye <400797+kkthxbye-code@users.noreply.github.com> Date: Wed, 8 Feb 2023 20:36:20 +0100 Subject: [PATCH 030/144] Fixes #11032 - Replication fields broken in custom validation (#11698) * Fixes #11032 - Replication fields broken in custom validation * Use getattr instead of hasattr to make sure custom validation is triggered as normal --------- Co-authored-by: kkthxbye-code <> --- netbox/netbox/models/features.py | 4 ++++ netbox/netbox/views/generic/object_views.py | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/netbox/netbox/models/features.py b/netbox/netbox/models/features.py index 8e5af0ab5..f041d016d 100644 --- a/netbox/netbox/models/features.py +++ b/netbox/netbox/models/features.py @@ -257,6 +257,10 @@ class CustomValidationMixin(models.Model): def clean(self): super().clean() + # If the instance is a base for replications, skip custom validation + if getattr(self, '_replicated_base', False): + return + # Send the post_clean signal post_clean.send(sender=self.__class__, instance=self) diff --git a/netbox/netbox/views/generic/object_views.py b/netbox/netbox/views/generic/object_views.py index d855490d1..475cca9d3 100644 --- a/netbox/netbox/views/generic/object_views.py +++ b/netbox/netbox/views/generic/object_views.py @@ -436,6 +436,10 @@ class ComponentCreateView(GetReturnURLMixin, BaseObjectView): form = self.initialize_form(request) instance = self.alter_object(self.queryset.model(), request) + # Note that the form instance is a replicated field base + # This is needed to avoid running custom validators multiple times + form.instance._replicated_base = hasattr(self.form, "replication_fields") + if form.is_valid(): new_components = [] data = deepcopy(request.POST) From 3c970c331ceb7fef0f78d05c6e500c177ab6d037 Mon Sep 17 00:00:00 2001 From: kkthxbye-code Date: Wed, 8 Feb 2023 09:33:06 +0100 Subject: [PATCH 031/144] Fixes #11582: Fix missing VC form errors ### Fixes: #11582 Not sure if this is the correct fix or not. The reason that the custom field errors were not shown is that messages.html only shows non_field_errors if the form passed to the context is named form. This is probably an issue in more places, but not sure how to make it generic. A change to messages.html would also need to support formsets. Any input appreciated @jeremystretch or @arthanson --- netbox/templates/dcim/virtualchassis_add_member.html | 2 ++ netbox/templates/dcim/virtualchassis_edit.html | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/netbox/templates/dcim/virtualchassis_add_member.html b/netbox/templates/dcim/virtualchassis_add_member.html index 17ffd64d9..bc2ba2f55 100644 --- a/netbox/templates/dcim/virtualchassis_add_member.html +++ b/netbox/templates/dcim/virtualchassis_add_member.html @@ -5,6 +5,8 @@ {% block content %}
+ {% render_errors membership_form %} + {% csrf_token %}
Add New Member
diff --git a/netbox/templates/dcim/virtualchassis_edit.html b/netbox/templates/dcim/virtualchassis_edit.html index f98a9fe64..433837cf5 100644 --- a/netbox/templates/dcim/virtualchassis_edit.html +++ b/netbox/templates/dcim/virtualchassis_edit.html @@ -8,6 +8,10 @@
+ {% for form in formset %} + {% render_errors form %} + {% endfor %} + {% csrf_token %} {{ pk_form.pk }} {{ formset.management_form }} From f9237285fdfa1d412e2ff8de8a4ee7475d634c74 Mon Sep 17 00:00:00 2001 From: kkthxbye-code Date: Wed, 8 Feb 2023 10:09:07 +0100 Subject: [PATCH 032/144] Fixes #11601 - Add partial lookup to IPRangeFilterSet --- netbox/ipam/filtersets.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/netbox/ipam/filtersets.py b/netbox/ipam/filtersets.py index d069eed27..c312b02ff 100644 --- a/netbox/ipam/filtersets.py +++ b/netbox/ipam/filtersets.py @@ -441,9 +441,9 @@ class IPRangeFilterSet(TenancyFilterSet, NetBoxModelFilterSet): def search(self, queryset, name, value): if not value.strip(): return queryset - qs_filter = Q(description__icontains=value) + qs_filter = Q(description__icontains=value) | Q(start_address__contains=value) | Q(end_address__contains=value) try: - ipaddress = str(netaddr.IPNetwork(value.strip()).cidr) + ipaddress = str(netaddr.IPNetwork(value.strip())) qs_filter |= Q(start_address=ipaddress) qs_filter |= Q(end_address=ipaddress) except (AddrFormatError, ValueError): From b5da383a179be36312c4def9fc6de29bde0a3df0 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 8 Feb 2023 14:56:14 -0500 Subject: [PATCH 033/144] Changelog for #11032, #11582, #11601 --- docs/release-notes/version-3.4.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/release-notes/version-3.4.md b/docs/release-notes/version-3.4.md index b23251a70..b8ebe4a33 100644 --- a/docs/release-notes/version-3.4.md +++ b/docs/release-notes/version-3.4.md @@ -4,6 +4,9 @@ ### Bug Fixes +* [#11032](https://github.com/netbox-community/netbox/issues/11032) - Fix false custom validation errors during component creation +* [#11582](https://github.com/netbox-community/netbox/issues/11582) - Ensure form validation errors are displayed when adding virtual chassis members +* [#11601](https://github.com/netbox-community/netbox/issues/11601) - Fix partial matching of start/end addresses for IP range search * [#11683](https://github.com/netbox-community/netbox/issues/11683) - Fix CSV header attribute detection when auto-detecting import format --- From df499ea8aca4bbdd5589bba4a3b0265f148ca8b9 Mon Sep 17 00:00:00 2001 From: kkthxbye <400797+kkthxbye-code@users.noreply.github.com> Date: Mon, 13 Feb 2023 23:44:35 +0100 Subject: [PATCH 034/144] Fixes #11459 - Allow using null in conditions (#11722) * Fixes #11459 - Allow using null in conditions - Update docs to reflect this - Change docs example from primary_ip to primary_ip4 as computed properties are not serialized when queuing webhooks * Update netbox/extras/conditions.py --------- Co-authored-by: Simon Toft Co-authored-by: Jeremy Stretch --- docs/reference/conditions.md | 6 +++--- netbox/extras/conditions.py | 3 ++- netbox/extras/tests/test_conditions.py | 10 ++++++++++ 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/docs/reference/conditions.md b/docs/reference/conditions.md index fb8b66139..514006b01 100644 --- a/docs/reference/conditions.md +++ b/docs/reference/conditions.md @@ -97,7 +97,7 @@ Multiple conditions can be combined into nested sets using AND or OR logic. This ### Examples -`status` is "active" and `primary_ip` is defined _or_ the "exempt" tag is applied. +`status` is "active" and `primary_ip4` is defined _or_ the "exempt" tag is applied. ```json { @@ -109,8 +109,8 @@ Multiple conditions can be combined into nested sets using AND or OR logic. This "value": "active" }, { - "attr": "primary_ip", - "value": "", + "attr": "primary_ip4", + "value": null, "negate": true } ] diff --git a/netbox/extras/conditions.py b/netbox/extras/conditions.py index 965488c3a..c6744e524 100644 --- a/netbox/extras/conditions.py +++ b/netbox/extras/conditions.py @@ -44,7 +44,8 @@ class Condition: bool: (EQ, CONTAINS), int: (EQ, GT, GTE, LT, LTE, CONTAINS), float: (EQ, GT, GTE, LT, LTE, CONTAINS), - list: (EQ, IN, CONTAINS) + list: (EQ, IN, CONTAINS), + type(None): (EQ,) } def __init__(self, attr, value, op=EQ, negate=False): diff --git a/netbox/extras/tests/test_conditions.py b/netbox/extras/tests/test_conditions.py index 8e02eb75d..e7275482a 100644 --- a/netbox/extras/tests/test_conditions.py +++ b/netbox/extras/tests/test_conditions.py @@ -126,6 +126,16 @@ class ConditionSetTest(TestCase): with self.assertRaises(ValueError): ConditionSet({'foo': []}) + def test_null_value(self): + cs = ConditionSet({ + 'and': [ + {'attr': 'a', 'value': None, 'op': 'eq', 'negate': True}, + ] + }) + self.assertFalse(cs.eval({'a': None})) + self.assertTrue(cs.eval({'a': "string"})) + self.assertTrue(cs.eval({'a': {"key": "value"}})) + def test_and_single_depth(self): cs = ConditionSet({ 'and': [ From d748851027783998df542dd3ab6e8f55284d4fca Mon Sep 17 00:00:00 2001 From: kkthxbye <400797+kkthxbye-code@users.noreply.github.com> Date: Mon, 13 Feb 2023 23:49:08 +0100 Subject: [PATCH 035/144] Fixes #11711 - Use CSVModelMultipleChoiceField when importing custom multiple object fields (#11712) * Fixes #11711 - Use CSVModelMultipleChoiceField when importing custom multiple object fields * Fix pep8 --------- Co-authored-by: kkthxbye-code <> --- netbox/extras/models/customfields.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/netbox/extras/models/customfields.py b/netbox/extras/models/customfields.py index 4842c0654..c7a19e4b8 100644 --- a/netbox/extras/models/customfields.py +++ b/netbox/extras/models/customfields.py @@ -21,7 +21,7 @@ from netbox.models.features import CloningMixin, ExportTemplatesMixin, WebhooksM from netbox.search import FieldTypes from utilities import filters from utilities.forms import ( - CSVChoiceField, CSVMultipleChoiceField, DatePicker, DynamicModelChoiceField, DynamicModelMultipleChoiceField, + CSVChoiceField, CSVModelMultipleChoiceField, CSVMultipleChoiceField, DatePicker, DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField, LaxURLField, StaticSelectMultiple, StaticSelect, add_blank_choice, ) from utilities.querysets import RestrictedQuerySet @@ -422,10 +422,12 @@ class CustomField(CloningMixin, ExportTemplatesMixin, WebhooksMixin, ChangeLogge # Multiple objects elif self.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT: model = self.object_type.model_class() - field = DynamicModelMultipleChoiceField( + field_class = CSVModelMultipleChoiceField if for_csv_import else DynamicModelMultipleChoiceField + + field = field_class( queryset=model.objects.all(), required=required, - initial=initial + initial=initial, ) # Text From 9f91b89467d5eae9ab63e9256c6f760b7341eee5 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 13 Feb 2023 17:53:01 -0500 Subject: [PATCH 036/144] #11711: Use CSVModelChoiceField for custom object fields during CSV import --- netbox/extras/models/customfields.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/netbox/extras/models/customfields.py b/netbox/extras/models/customfields.py index c7a19e4b8..fa16b8501 100644 --- a/netbox/extras/models/customfields.py +++ b/netbox/extras/models/customfields.py @@ -20,10 +20,12 @@ from netbox.models import ChangeLoggedModel from netbox.models.features import CloningMixin, ExportTemplatesMixin, WebhooksMixin from netbox.search import FieldTypes from utilities import filters -from utilities.forms import ( - CSVChoiceField, CSVModelMultipleChoiceField, CSVMultipleChoiceField, DatePicker, DynamicModelChoiceField, DynamicModelMultipleChoiceField, - JSONField, LaxURLField, StaticSelectMultiple, StaticSelect, add_blank_choice, +from utilities.forms.fields import ( + CSVChoiceField, CSVModelChoiceField, CSVModelMultipleChoiceField, CSVMultipleChoiceField, DynamicModelChoiceField, + DynamicModelMultipleChoiceField, JSONField, LaxURLField, ) +from utilities.forms.widgets import DatePicker, StaticSelectMultiple, StaticSelect +from utilities.forms.utils import add_blank_choice from utilities.querysets import RestrictedQuerySet from utilities.validators import validate_regex @@ -413,7 +415,8 @@ class CustomField(CloningMixin, ExportTemplatesMixin, WebhooksMixin, ChangeLogge # Object elif self.type == CustomFieldTypeChoices.TYPE_OBJECT: model = self.object_type.model_class() - field = DynamicModelChoiceField( + field_class = CSVModelChoiceField if for_csv_import else DynamicModelChoiceField + field = field_class( queryset=model.objects.all(), required=required, initial=initial @@ -423,7 +426,6 @@ class CustomField(CloningMixin, ExportTemplatesMixin, WebhooksMixin, ChangeLogge elif self.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT: model = self.object_type.model_class() field_class = CSVModelMultipleChoiceField if for_csv_import else DynamicModelMultipleChoiceField - field = field_class( queryset=model.objects.all(), required=required, From 3150c1f8b3c38f11dc182d22071da3b9bd62b15e Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 13 Feb 2023 17:58:41 -0500 Subject: [PATCH 037/144] Changelog for #11459, #11711 --- docs/release-notes/version-3.4.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/release-notes/version-3.4.md b/docs/release-notes/version-3.4.md index b8ebe4a33..58c3aea49 100644 --- a/docs/release-notes/version-3.4.md +++ b/docs/release-notes/version-3.4.md @@ -5,9 +5,11 @@ ### Bug Fixes * [#11032](https://github.com/netbox-community/netbox/issues/11032) - Fix false custom validation errors during component creation +* [#11459](https://github.com/netbox-community/netbox/issues/11459) - Enable evaluating null values in custom validation rules * [#11582](https://github.com/netbox-community/netbox/issues/11582) - Ensure form validation errors are displayed when adding virtual chassis members * [#11601](https://github.com/netbox-community/netbox/issues/11601) - Fix partial matching of start/end addresses for IP range search * [#11683](https://github.com/netbox-community/netbox/issues/11683) - Fix CSV header attribute detection when auto-detecting import format +* [#11711](https://github.com/netbox-community/netbox/issues/11711) - Fix CSV import for multiple-object custom fields --- From c78022a74cbce0d3e05e41a3d22b74675f7c645b Mon Sep 17 00:00:00 2001 From: kkthxbye-code Date: Thu, 16 Feb 2023 12:33:08 +0100 Subject: [PATCH 038/144] Change the way we invalidate the module cache to support reloading code from subpackages --- netbox/extras/scripts.py | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/netbox/extras/scripts.py b/netbox/extras/scripts.py index 77c96de56..313058d57 100644 --- a/netbox/extras/scripts.py +++ b/netbox/extras/scripts.py @@ -524,27 +524,39 @@ def get_scripts(use_names=False): defined name in place of the actual module name. """ scripts = {} - # Iterate through all modules within the scripts path. These are the user-created files in which reports are + + # Get all modules within the scripts path. These are the user-created files in which scripts are # defined. - for importer, module_name, _ in pkgutil.iter_modules([settings.SCRIPTS_ROOT]): - # Use a lock as removing and loading modules is not thread safe - with lock: - # Remove cached module to ensure consistency with filesystem - if module_name in sys.modules: + modules = list(pkgutil.iter_modules([settings.SCRIPTS_ROOT])) + modules_bases = set([name.split(".")[0] for _, name, _ in modules]) + + # Deleting from sys.modules needs to done behind a lock to prevent race conditions where a module is + # removed from sys.modules while another thread is importing + with lock: + for module_name in list(sys.modules.keys()): + # Everything sharing a base module path with a module in the script folder is removed. + # We also remove all modules with a base module called "scripts". This allows modifying imported + # non-script modules without having to reload the RQ worker. + module_base = module_name.split(".")[0] + if module_base == "scripts" or module_base in modules_bases: del sys.modules[module_name] - module = importer.find_module(module_name).load_module(module_name) + for importer, module_name, _ in modules: + module = importer.find_module(module_name).load_module(module_name) if use_names and hasattr(module, 'name'): module_name = module.name + module_scripts = {} script_order = getattr(module, "script_order", ()) ordered_scripts = [cls for cls in script_order if is_script(cls)] unordered_scripts = [cls for _, cls in inspect.getmembers(module, is_script) if cls not in script_order] + for cls in [*ordered_scripts, *unordered_scripts]: # For scripts in submodules use the full import path w/o the root module as the name script_name = cls.full_name.split(".", maxsplit=1)[1] module_scripts[script_name] = cls + if module_scripts: scripts[module_name] = module_scripts From 959404980464c7b6f9b80a17f9d938aa9cdcb968 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?aron=20bergur=20j=C3=B3hannsson?= Date: Tue, 14 Feb 2023 23:58:41 +0000 Subject: [PATCH 039/144] Fixes #11473 graphql invalid tag filter returns all devices/interfaces --- netbox/netbox/graphql/fields.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/netbox/netbox/graphql/fields.py b/netbox/netbox/graphql/fields.py index 5b8e6cc5b..1e215c947 100644 --- a/netbox/netbox/graphql/fields.py +++ b/netbox/netbox/graphql/fields.py @@ -60,6 +60,8 @@ class ObjectListField(DjangoListField): filterset_class = django_object_type._meta.filterset_class if filterset_class: filterset = filterset_class(data=args, queryset=queryset, request=info.context) + if not filterset.is_valid(): + return [] return filterset.qs return queryset From eee1a0e10a9f54dcacaa541ee978f35893991eec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?aron=20bergur=20j=C3=B3hannsson?= Date: Thu, 16 Feb 2023 09:08:30 +0000 Subject: [PATCH 040/144] change empty list to qs.none() --- netbox/netbox/graphql/fields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/netbox/graphql/fields.py b/netbox/netbox/graphql/fields.py index 1e215c947..7c359e82e 100644 --- a/netbox/netbox/graphql/fields.py +++ b/netbox/netbox/graphql/fields.py @@ -61,7 +61,7 @@ class ObjectListField(DjangoListField): if filterset_class: filterset = filterset_class(data=args, queryset=queryset, request=info.context) if not filterset.is_valid(): - return [] + return queryset.none() return filterset.qs return queryset From 2db181ea4989d6d55a28f0600afdeac29b25d796 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aron=20Bergur=20J=C3=B3hannsson?= <71449504+aronbj20@users.noreply.github.com> Date: Thu, 16 Feb 2023 16:26:22 +0000 Subject: [PATCH 041/144] Closes #11592: Expose FILE_UPLOAD_MAX_MEMORY_SIZE as a setting (#11742) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Closes #11592: Expose FILE_UPLOAD_MAX_MEMOMORY_SIZE as a setting * change configuration settings to alphabetic order * Small example and documentation --------- Co-authored-by: aron bergur jóhannsson --- docs/configuration/miscellaneous.md | 8 ++++++++ netbox/netbox/configuration_example.py | 4 ++++ netbox/netbox/settings.py | 1 + 3 files changed, 13 insertions(+) diff --git a/docs/configuration/miscellaneous.md b/docs/configuration/miscellaneous.md index 4eb090554..eac5d0a2f 100644 --- a/docs/configuration/miscellaneous.md +++ b/docs/configuration/miscellaneous.md @@ -69,6 +69,14 @@ By default, NetBox will permit users to create duplicate prefixes and IP address --- +## FILE_UPLOAD_MAX_MEMORY_SIZE + +Default: 2621440 (i.e. 2.5 MB). + +The maximum size (in bytes) that an upload will be before it gets streamed to the file system. Changing this setting can be useful for example to be able to upload files bigger than 2.5MB to custom scripts for processing. + +--- + ## GRAPHQL_ENABLED !!! tip "Dynamic Configuration Parameter" diff --git a/netbox/netbox/configuration_example.py b/netbox/netbox/configuration_example.py index 14fcde022..7158308af 100644 --- a/netbox/netbox/configuration_example.py +++ b/netbox/netbox/configuration_example.py @@ -217,6 +217,10 @@ RQ_DEFAULT_TIMEOUT = 300 # this setting is derived from the installed location. # SCRIPTS_ROOT = '/opt/netbox/netbox/scripts' +# The maximum size (in bytes) that an upload will be before it gets streamed to the file system. +# Useful to be able to upload files bigger than 2.5Mbyte to custom scripts for processing. +# FILE_UPLOAD_MAX_MEMORY_SIZE = 2621440 + # The name to use for the csrf token cookie. CSRF_COOKIE_NAME = 'csrftoken' diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index cda6ee643..f6ce7ff33 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -91,6 +91,7 @@ DOCS_ROOT = getattr(configuration, 'DOCS_ROOT', os.path.join(os.path.dirname(BAS EMAIL = getattr(configuration, 'EMAIL', {}) EXEMPT_VIEW_PERMISSIONS = getattr(configuration, 'EXEMPT_VIEW_PERMISSIONS', []) FIELD_CHOICES = getattr(configuration, 'FIELD_CHOICES', {}) +FILE_UPLOAD_MAX_MEMORY_SIZE = getattr(configuration, 'FILE_UPLOAD_MAX_MEMORY_SIZE', 2621440) HTTP_PROXIES = getattr(configuration, 'HTTP_PROXIES', None) INTERNAL_IPS = getattr(configuration, 'INTERNAL_IPS', ('127.0.0.1', '::1')) JINJA2_FILTERS = getattr(configuration, 'JINJA2_FILTERS', {}) From 3a4fee4e6e6ecc40141d9b6dec6c59ed1a1da96d Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Thu, 16 Feb 2023 20:15:48 -0500 Subject: [PATCH 042/144] Changelog for #11226, #11335, #11473, #11592 --- docs/release-notes/version-3.4.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/release-notes/version-3.4.md b/docs/release-notes/version-3.4.md index 58c3aea49..9eb0f0cec 100644 --- a/docs/release-notes/version-3.4.md +++ b/docs/release-notes/version-3.4.md @@ -2,10 +2,17 @@ ## v3.4.5 (FUTURE) +### Enhancements + +* [#11592](https://github.com/netbox-community/netbox/issues/11592) - Introduce `FILE_UPLOAD_MAX_MEMORY_SIZE` configuration parameter + ### Bug Fixes * [#11032](https://github.com/netbox-community/netbox/issues/11032) - Fix false custom validation errors during component creation +* [#11226](https://github.com/netbox-community/netbox/issues/11226) - Ensure scripts and reports within submodules are automatically reloaded +* [#11335](https://github.com/netbox-community/netbox/issues/11335) - Avoid exception when rendering change log after uninstalling a plugin * [#11459](https://github.com/netbox-community/netbox/issues/11459) - Enable evaluating null values in custom validation rules +* [#11473](https://github.com/netbox-community/netbox/issues/11473) - GraphQL requests specifying an invalid filter should return an empty queryset * [#11582](https://github.com/netbox-community/netbox/issues/11582) - Ensure form validation errors are displayed when adding virtual chassis members * [#11601](https://github.com/netbox-community/netbox/issues/11601) - Fix partial matching of start/end addresses for IP range search * [#11683](https://github.com/netbox-community/netbox/issues/11683) - Fix CSV header attribute detection when auto-detecting import format From c36e7a1d0b3372844d1cfd1ad8c6482a5fc154fb Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Fri, 17 Feb 2023 10:11:39 -0500 Subject: [PATCH 043/144] Update introduction doc --- docs/introduction.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/introduction.md b/docs/introduction.md index fe82e68aa..bfa0900cb 100644 --- a/docs/introduction.md +++ b/docs/introduction.md @@ -4,7 +4,7 @@ NetBox was originally developed by its lead maintainer, [Jeremy Stretch](https://github.com/jeremystretch), while he was working as a network engineer at [DigitalOcean](https://www.digitalocean.com/) in 2015 as part of an effort to automate their network provisioning. Recognizing the new tool's potential, DigitalOcean agreed to release it as an open source project in June 2016. -Since then, thousands of organizations around the world have embraced NetBox as their central network source of truth to empower both network operators and automation. +Since then, thousands of organizations around the world have embraced NetBox as their central network source of truth to empower both network operators and automation. Today, the open source project is stewarded by [NetBox Labs](https://netboxlabs.com/) and a team of volunteer maintainers. Beyond the core product, myriad [plugins](https://netbox.dev/plugins/) have been developed by the NetBox community to enhance and expand its feature set. ## Key Features @@ -17,6 +17,7 @@ NetBox was built specifically to serve the needs of network engineers and operat * AS number (ASN) management * Rack elevations with SVG rendering * Device modeling using pre-defined types +* Virtual chassis and device contexts * Network, power, and console cabling with SVG traces * Power distribution modeling * Data circuit and provider tracking @@ -29,12 +30,13 @@ NetBox was built specifically to serve the needs of network engineers and operat * Tenant ownership assignment * Device & VM configuration contexts for advanced configuration rendering * Custom fields for data model extension -* Support for custom validation rules +* Custom validation rules * Custom reports & scripts executable directly within the UI * Extensive plugin framework for adding custom functionality * Single sign-on (SSO) authentication * Robust object-based permissions * Detailed, automatic change logging +* Global search engine * NAPALM integration ## What NetBox Is Not From c031951f4b06ce63f6fb55bf06374f391cc6f91c Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Fri, 17 Feb 2023 16:50:10 -0500 Subject: [PATCH 044/144] Closes #11110: Add start_address and end_address filters for IP ranges --- docs/release-notes/version-3.4.md | 1 + netbox/ipam/filtersets.py | 14 ++++++++++++++ netbox/ipam/tests/test_filtersets.py | 8 ++++++++ 3 files changed, 23 insertions(+) diff --git a/docs/release-notes/version-3.4.md b/docs/release-notes/version-3.4.md index 9eb0f0cec..edce381c0 100644 --- a/docs/release-notes/version-3.4.md +++ b/docs/release-notes/version-3.4.md @@ -4,6 +4,7 @@ ### Enhancements +* [#11110](https://github.com/netbox-community/netbox/issues/11110) - Add `start_address` and `end_address` filters for IP ranges * [#11592](https://github.com/netbox-community/netbox/issues/11592) - Introduce `FILE_UPLOAD_MAX_MEMORY_SIZE` configuration parameter ### Bug Fixes diff --git a/netbox/ipam/filtersets.py b/netbox/ipam/filtersets.py index c312b02ff..2e9f56bbc 100644 --- a/netbox/ipam/filtersets.py +++ b/netbox/ipam/filtersets.py @@ -405,6 +405,14 @@ class IPRangeFilterSet(TenancyFilterSet, NetBoxModelFilterSet): field_name='start_address', lookup_expr='family' ) + start_address = MultiValueCharFilter( + method='filter_address', + label=_('Address'), + ) + end_address = MultiValueCharFilter( + method='filter_address', + label=_('Address'), + ) contains = django_filters.CharFilter( method='search_contains', label=_('Ranges which contain this prefix or IP'), @@ -461,6 +469,12 @@ class IPRangeFilterSet(TenancyFilterSet, NetBoxModelFilterSet): except (AddrFormatError, ValueError): return queryset.none() + def filter_address(self, queryset, name, value): + try: + return queryset.filter(**{f'{name}__net_in': value}) + except ValidationError: + return queryset.none() + class IPAddressFilterSet(NetBoxModelFilterSet, TenancyFilterSet): family = django_filters.NumberFilter( diff --git a/netbox/ipam/tests/test_filtersets.py b/netbox/ipam/tests/test_filtersets.py index 711009a7e..13b3ae163 100644 --- a/netbox/ipam/tests/test_filtersets.py +++ b/netbox/ipam/tests/test_filtersets.py @@ -680,6 +680,14 @@ class IPRangeTestCase(TestCase, ChangeLoggedFilterSetTests): params = {'family': '6'} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + def test_start_address(self): + params = {'start_address': ['10.0.1.100', '10.0.2.100']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_end_address(self): + params = {'end_address': ['10.0.1.199', '10.0.2.199']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_contains(self): params = {'contains': '10.0.1.150/24'} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) From 126f9ba05f5690dcebc00a2c9de2d4f310f00b6a Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Fri, 17 Feb 2023 16:57:52 -0500 Subject: [PATCH 045/144] Raise stale timers from 60/30 to 90/30 --- .github/workflows/stale.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index ab259af2a..3b37aae56 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -24,7 +24,7 @@ jobs: necessary. close-pr-message: > This PR has been automatically closed due to lack of activity. - days-before-stale: 60 + days-before-stale: 90 days-before-close: 30 exempt-issue-labels: 'status: accepted,status: blocked,status: needs milestone' operations-per-run: 100 From afc752b4ce5d87702ffd1c9a6f600bcf82db8680 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Fri, 17 Feb 2023 21:31:19 -0500 Subject: [PATCH 046/144] Fixes #11723: Circuit terminations should link to their associated circuits (rather than site or provider network) --- docs/release-notes/version-3.4.md | 1 + netbox/circuits/models/circuits.py | 6 ++---- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/docs/release-notes/version-3.4.md b/docs/release-notes/version-3.4.md index edce381c0..636755ac4 100644 --- a/docs/release-notes/version-3.4.md +++ b/docs/release-notes/version-3.4.md @@ -18,6 +18,7 @@ * [#11601](https://github.com/netbox-community/netbox/issues/11601) - Fix partial matching of start/end addresses for IP range search * [#11683](https://github.com/netbox-community/netbox/issues/11683) - Fix CSV header attribute detection when auto-detecting import format * [#11711](https://github.com/netbox-community/netbox/issues/11711) - Fix CSV import for multiple-object custom fields +* [#11723](https://github.com/netbox-community/netbox/issues/11723) - Circuit terminations should link to their associated circuits (rather than site or provider network) --- diff --git a/netbox/circuits/models/circuits.py b/netbox/circuits/models/circuits.py index 8ef5761fd..eba7f4de0 100644 --- a/netbox/circuits/models/circuits.py +++ b/netbox/circuits/models/circuits.py @@ -196,12 +196,10 @@ class CircuitTermination( ) def __str__(self): - return f'Termination {self.term_side}: {self.site or self.provider_network}' + return f'{self.circuit}: Termination {self.term_side}' def get_absolute_url(self): - if self.site: - return self.site.get_absolute_url() - return self.provider_network.get_absolute_url() + return self.circuit.get_absolute_url() def clean(self): super().clean() From 315371bf7c5f51c49609dd8b393b0f348a36e10e Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Sun, 19 Feb 2023 16:17:57 -0500 Subject: [PATCH 047/144] Fixes #11786: List only applicable object types in form widget when filtering custom fields --- docs/release-notes/version-3.4.md | 1 + netbox/extras/forms/filtersets.py | 18 ++++++------------ 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/docs/release-notes/version-3.4.md b/docs/release-notes/version-3.4.md index 636755ac4..731400a92 100644 --- a/docs/release-notes/version-3.4.md +++ b/docs/release-notes/version-3.4.md @@ -19,6 +19,7 @@ * [#11683](https://github.com/netbox-community/netbox/issues/11683) - Fix CSV header attribute detection when auto-detecting import format * [#11711](https://github.com/netbox-community/netbox/issues/11711) - Fix CSV import for multiple-object custom fields * [#11723](https://github.com/netbox-community/netbox/issues/11723) - Circuit terminations should link to their associated circuits (rather than site or provider network) +* [#11786](https://github.com/netbox-community/netbox/issues/11786) - List only applicable object types in form widget when filtering custom fields --- diff --git a/netbox/extras/forms/filtersets.py b/netbox/extras/forms/filtersets.py index e6a9089bc..22c7364db 100644 --- a/netbox/extras/forms/filtersets.py +++ b/netbox/extras/forms/filtersets.py @@ -38,8 +38,7 @@ class CustomFieldFilterForm(SavedFiltersMixin, FilterForm): ('Attributes', ('type', 'content_type_id', 'group_name', 'weight', 'required', 'ui_visibility')), ) content_type_id = ContentTypeMultipleChoiceField( - queryset=ContentType.objects.all(), - limit_choices_to=FeatureQuery('custom_fields'), + queryset=ContentType.objects.filter(FeatureQuery('custom_fields').get_query()), required=False, label=_('Object type') ) @@ -79,8 +78,7 @@ class JobResultFilterForm(SavedFiltersMixin, FilterForm): ) obj_type = ContentTypeChoiceField( label=_('Object Type'), - queryset=ContentType.objects.all(), - limit_choices_to=FeatureQuery('job_results'), # TODO: This doesn't actually work + queryset=ContentType.objects.filter(FeatureQuery('job_results').get_query()), required=False, ) status = MultipleChoiceField( @@ -135,8 +133,7 @@ class CustomLinkFilterForm(SavedFiltersMixin, FilterForm): ('Attributes', ('content_types', 'enabled', 'new_window', 'weight')), ) content_types = ContentTypeMultipleChoiceField( - queryset=ContentType.objects.all(), - limit_choices_to=FeatureQuery('custom_links'), + queryset=ContentType.objects.filter(FeatureQuery('custom_links').get_query()), required=False ) enabled = forms.NullBooleanField( @@ -162,8 +159,7 @@ class ExportTemplateFilterForm(SavedFiltersMixin, FilterForm): ('Attributes', ('content_types', 'mime_type', 'file_extension', 'as_attachment')), ) content_types = ContentTypeMultipleChoiceField( - queryset=ContentType.objects.all(), - limit_choices_to=FeatureQuery('export_templates'), + queryset=ContentType.objects.filter(FeatureQuery('export_templates').get_query()), required=False ) mime_type = forms.CharField( @@ -187,8 +183,7 @@ class SavedFilterFilterForm(SavedFiltersMixin, FilterForm): ('Attributes', ('content_types', 'enabled', 'shared', 'weight')), ) content_types = ContentTypeMultipleChoiceField( - queryset=ContentType.objects.all(), - limit_choices_to=FeatureQuery('export_templates'), + queryset=ContentType.objects.filter(FeatureQuery('export_templates').get_query()), required=False ) enabled = forms.NullBooleanField( @@ -215,8 +210,7 @@ class WebhookFilterForm(SavedFiltersMixin, FilterForm): ('Events', ('type_create', 'type_update', 'type_delete')), ) content_type_id = ContentTypeMultipleChoiceField( - queryset=ContentType.objects.all(), - limit_choices_to=FeatureQuery('webhooks'), + queryset=ContentType.objects.filter(FeatureQuery('webhooks').get_query()), required=False, label=_('Object type') ) From ce166b12ce1144fc110a974d053902c4c7be323c Mon Sep 17 00:00:00 2001 From: kkthxbye-code Date: Mon, 6 Feb 2023 14:00:34 +0100 Subject: [PATCH 048/144] Proof of concept for showing containing prefixes when searching for ip-addresses. --- netbox/extras/lookups.py | 15 ++++++++++++++- netbox/netbox/search/__init__.py | 7 +++++++ netbox/netbox/search/backends.py | 23 +++++++++++++++-------- 3 files changed, 36 insertions(+), 9 deletions(-) diff --git a/netbox/extras/lookups.py b/netbox/extras/lookups.py index 7197efcfc..4cdda52b4 100644 --- a/netbox/extras/lookups.py +++ b/netbox/extras/lookups.py @@ -1,4 +1,4 @@ -from django.db.models import CharField, Lookup +from django.db.models import CharField, TextField, Lookup class Empty(Lookup): @@ -14,4 +14,17 @@ class Empty(Lookup): return 'CAST(LENGTH(%s) AS BOOLEAN) != %s' % (lhs, rhs), params +class NetContainsOrEquals(Lookup): + """ + This lookup has the same functionality as the one from the ipam app except lhs is cast to inet + """ + lookup_name = 'net_contains_or_equals' + + def as_sql(self, qn, connection): + lhs, lhs_params = self.process_lhs(qn, connection) + rhs, rhs_params = self.process_rhs(qn, connection) + params = lhs_params + rhs_params + return 'CAST(%s as inet) >>= %s' % (lhs, rhs), params + CharField.register_lookup(Empty) +TextField.register_lookup(NetContainsOrEquals) diff --git a/netbox/netbox/search/__init__.py b/netbox/netbox/search/__init__.py index 1eec8e097..f4cc07c4d 100644 --- a/netbox/netbox/search/__init__.py +++ b/netbox/netbox/search/__init__.py @@ -2,6 +2,7 @@ from collections import namedtuple from django.db import models +from ipam.fields import IPAddressField, IPNetworkField from netbox.registry import registry ObjectFieldValue = namedtuple('ObjectFieldValue', ('name', 'type', 'weight', 'value')) @@ -11,6 +12,8 @@ class FieldTypes: FLOAT = 'float' INTEGER = 'int' STRING = 'str' + INET = 'inet' + CIDR = 'cidr' class LookupTypes: @@ -43,6 +46,10 @@ class SearchIndex: field_cls = instance._meta.get_field(field_name).__class__ if issubclass(field_cls, (models.FloatField, models.DecimalField)): return FieldTypes.FLOAT + if issubclass(field_cls, IPAddressField): + return FieldTypes.INET + if issubclass(field_cls, (IPNetworkField)): + return FieldTypes.CIDR if issubclass(field_cls, models.IntegerField): return FieldTypes.INTEGER return FieldTypes.STRING diff --git a/netbox/netbox/search/backends.py b/netbox/netbox/search/backends.py index d659a7abb..6bbfdd7d1 100644 --- a/netbox/netbox/search/backends.py +++ b/netbox/netbox/search/backends.py @@ -3,10 +3,12 @@ from collections import defaultdict from django.conf import settings from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ImproperlyConfigured -from django.db.models import F, Window +from django.db.models import F, Window, Q from django.db.models.functions import window from django.db.models.signals import post_delete, post_save from django.utils.module_loading import import_string +import netaddr +from netaddr.core import AddrFormatError from extras.models import CachedValue, CustomField from netbox.registry import registry @@ -95,18 +97,23 @@ class CachedValueSearchBackend(SearchBackend): def search(self, value, user=None, object_types=None, lookup=DEFAULT_LOOKUP_TYPE): - # Define the search parameters - params = { - f'value__{lookup}': value - } + query_filter = Q(**{f'value__{lookup}': value}) + if lookup in (LookupTypes.STARTSWITH, LookupTypes.ENDSWITH): # Partial string matches are valid only on string values - params['type'] = FieldTypes.STRING + query_filter &= Q(type=FieldTypes.STRING) if object_types: - params['object_type__in'] = object_types + query_filter &= Q(object_typeo__in=object_types) + + if lookup == LookupTypes.PARTIAL: + try: + address = str(netaddr.IPNetwork(value.strip()).cidr) + query_filter |= Q(type=FieldTypes.CIDR) & Q(value__net_contains_or_equals=address) + except (AddrFormatError, ValueError): + pass # Construct the base queryset to retrieve matching results - queryset = CachedValue.objects.filter(**params).annotate( + queryset = CachedValue.objects.filter(query_filter).annotate( # Annotate the rank of each result for its object according to its weight row_number=Window( expression=window.RowNumber(), From a61e7e7c04bde1a7642a264d46430c1dbc06f4e5 Mon Sep 17 00:00:00 2001 From: kkthxbye-code Date: Mon, 6 Feb 2023 21:25:24 +0100 Subject: [PATCH 049/144] Fix typo in search query --- netbox/netbox/search/backends.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/netbox/search/backends.py b/netbox/netbox/search/backends.py index 6bbfdd7d1..2e550a879 100644 --- a/netbox/netbox/search/backends.py +++ b/netbox/netbox/search/backends.py @@ -103,7 +103,7 @@ class CachedValueSearchBackend(SearchBackend): # Partial string matches are valid only on string values query_filter &= Q(type=FieldTypes.STRING) if object_types: - query_filter &= Q(object_typeo__in=object_types) + query_filter &= Q(object_type__in=object_types) if lookup == LookupTypes.PARTIAL: try: From eed1b8f4126d5cca6d7aa5a03e8d2e2882fce7c3 Mon Sep 17 00:00:00 2001 From: kkthxbye-code Date: Tue, 7 Feb 2023 09:40:25 +0100 Subject: [PATCH 050/144] Create CachedValueField to contain search specific lookups --- netbox/extras/fields.py | 7 +++++++ netbox/extras/lookups.py | 4 ++-- .../0085_change_cachedvalue_value_type.py | 19 +++++++++++++++++++ netbox/extras/models/search.py | 3 ++- 4 files changed, 30 insertions(+), 3 deletions(-) create mode 100644 netbox/extras/fields.py create mode 100644 netbox/extras/migrations/0085_change_cachedvalue_value_type.py diff --git a/netbox/extras/fields.py b/netbox/extras/fields.py new file mode 100644 index 000000000..ffd66801d --- /dev/null +++ b/netbox/extras/fields.py @@ -0,0 +1,7 @@ +from django.db.models import TextField + +class CachedValueField(TextField): + """ + Currently a dummy field to prevent custom lookups being applied globally to TextField. + """ + pass \ No newline at end of file diff --git a/netbox/extras/lookups.py b/netbox/extras/lookups.py index 4cdda52b4..a52ef2e8d 100644 --- a/netbox/extras/lookups.py +++ b/netbox/extras/lookups.py @@ -1,5 +1,5 @@ from django.db.models import CharField, TextField, Lookup - +from .fields import CachedValueField class Empty(Lookup): """ @@ -27,4 +27,4 @@ class NetContainsOrEquals(Lookup): return 'CAST(%s as inet) >>= %s' % (lhs, rhs), params CharField.register_lookup(Empty) -TextField.register_lookup(NetContainsOrEquals) +CachedValueField.register_lookup(NetContainsOrEquals) diff --git a/netbox/extras/migrations/0085_change_cachedvalue_value_type.py b/netbox/extras/migrations/0085_change_cachedvalue_value_type.py new file mode 100644 index 000000000..e6512753e --- /dev/null +++ b/netbox/extras/migrations/0085_change_cachedvalue_value_type.py @@ -0,0 +1,19 @@ +# Generated by Django 4.1.6 on 2023-02-07 08:21 + +from django.db import migrations +import extras.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('extras', '0084_staging'), + ] + + operations = [ + migrations.AlterField( + model_name='cachedvalue', + name='value', + field=extras.fields.CachedValueField(), + ), + ] diff --git a/netbox/extras/models/search.py b/netbox/extras/models/search.py index 7c5860e00..6d088abb0 100644 --- a/netbox/extras/models/search.py +++ b/netbox/extras/models/search.py @@ -4,6 +4,7 @@ from django.contrib.contenttypes.models import ContentType from django.db import models from utilities.fields import RestrictedGenericForeignKey +from ..fields import CachedValueField __all__ = ( 'CachedValue', @@ -36,7 +37,7 @@ class CachedValue(models.Model): type = models.CharField( max_length=30 ) - value = models.TextField() + value = CachedValueField() weight = models.PositiveSmallIntegerField( default=1000 ) From 18ea7d1e13c71679c0f03d1c79fcd6ac78c8779c Mon Sep 17 00:00:00 2001 From: kkthxbye-code Date: Tue, 7 Feb 2023 13:24:57 +0100 Subject: [PATCH 051/144] pep8 fixes --- netbox/extras/fields.py | 3 ++- netbox/extras/lookups.py | 2 ++ netbox/netbox/search/backends.py | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/netbox/extras/fields.py b/netbox/extras/fields.py index ffd66801d..6cd44432f 100644 --- a/netbox/extras/fields.py +++ b/netbox/extras/fields.py @@ -1,7 +1,8 @@ from django.db.models import TextField + class CachedValueField(TextField): """ Currently a dummy field to prevent custom lookups being applied globally to TextField. """ - pass \ No newline at end of file + pass diff --git a/netbox/extras/lookups.py b/netbox/extras/lookups.py index a52ef2e8d..d4ed2b6a4 100644 --- a/netbox/extras/lookups.py +++ b/netbox/extras/lookups.py @@ -1,6 +1,7 @@ from django.db.models import CharField, TextField, Lookup from .fields import CachedValueField + class Empty(Lookup): """ Filter on whether a string is empty. @@ -26,5 +27,6 @@ class NetContainsOrEquals(Lookup): params = lhs_params + rhs_params return 'CAST(%s as inet) >>= %s' % (lhs, rhs), params + CharField.register_lookup(Empty) CachedValueField.register_lookup(NetContainsOrEquals) diff --git a/netbox/netbox/search/backends.py b/netbox/netbox/search/backends.py index 2e550a879..53a4fe683 100644 --- a/netbox/netbox/search/backends.py +++ b/netbox/netbox/search/backends.py @@ -108,7 +108,7 @@ class CachedValueSearchBackend(SearchBackend): if lookup == LookupTypes.PARTIAL: try: address = str(netaddr.IPNetwork(value.strip()).cidr) - query_filter |= Q(type=FieldTypes.CIDR) & Q(value__net_contains_or_equals=address) + query_filter |= Q(type=FieldTypes.CIDR) & Q(value__net_contains_or_equals=address) except (AddrFormatError, ValueError): pass From fc7cb106c1889a0a2a27b8bbdb6ad49118a10eb4 Mon Sep 17 00:00:00 2001 From: kkthxbye-code Date: Sat, 18 Feb 2023 19:29:57 +0100 Subject: [PATCH 052/144] Address feedback --- netbox/extras/lookups.py | 2 +- netbox/netbox/search/__init__.py | 2 +- netbox/netbox/search/backends.py | 5 +++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/netbox/extras/lookups.py b/netbox/extras/lookups.py index d4ed2b6a4..77fe2301e 100644 --- a/netbox/extras/lookups.py +++ b/netbox/extras/lookups.py @@ -25,7 +25,7 @@ class NetContainsOrEquals(Lookup): lhs, lhs_params = self.process_lhs(qn, connection) rhs, rhs_params = self.process_rhs(qn, connection) params = lhs_params + rhs_params - return 'CAST(%s as inet) >>= %s' % (lhs, rhs), params + return 'CAST(%s AS INET) >>= %s' % (lhs, rhs), params CharField.register_lookup(Empty) diff --git a/netbox/netbox/search/__init__.py b/netbox/netbox/search/__init__.py index f4cc07c4d..6d53e9a97 100644 --- a/netbox/netbox/search/__init__.py +++ b/netbox/netbox/search/__init__.py @@ -48,7 +48,7 @@ class SearchIndex: return FieldTypes.FLOAT if issubclass(field_cls, IPAddressField): return FieldTypes.INET - if issubclass(field_cls, (IPNetworkField)): + if issubclass(field_cls, IPNetworkField): return FieldTypes.CIDR if issubclass(field_cls, models.IntegerField): return FieldTypes.INTEGER diff --git a/netbox/netbox/search/backends.py b/netbox/netbox/search/backends.py index 53a4fe683..10e164c09 100644 --- a/netbox/netbox/search/backends.py +++ b/netbox/netbox/search/backends.py @@ -99,11 +99,12 @@ class CachedValueSearchBackend(SearchBackend): query_filter = Q(**{f'value__{lookup}': value}) + if object_types: + query_filter &= Q(object_type__in=object_types) + if lookup in (LookupTypes.STARTSWITH, LookupTypes.ENDSWITH): # Partial string matches are valid only on string values query_filter &= Q(type=FieldTypes.STRING) - if object_types: - query_filter &= Q(object_type__in=object_types) if lookup == LookupTypes.PARTIAL: try: From 25278becef6c6d6d3c8556931444a333c033e179 Mon Sep 17 00:00:00 2001 From: kkthxbye-code Date: Sat, 18 Feb 2023 19:30:29 +0100 Subject: [PATCH 053/144] Change Prefix and Aggregate search index weights to better order search results. --- netbox/ipam/search.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/netbox/ipam/search.py b/netbox/ipam/search.py index fd6db6a63..ad4403321 100644 --- a/netbox/ipam/search.py +++ b/netbox/ipam/search.py @@ -6,7 +6,7 @@ from netbox.search import SearchIndex, register_search class AggregateIndex(SearchIndex): model = models.Aggregate fields = ( - ('prefix', 100), + ('prefix', 120), ('description', 500), ('date_added', 2000), ('comments', 5000), @@ -70,7 +70,7 @@ class L2VPNIndex(SearchIndex): class PrefixIndex(SearchIndex): model = models.Prefix fields = ( - ('prefix', 100), + ('prefix', 110), ('description', 500), ('comments', 5000), ) From 9efc4689cca381ec699048ef87ed669b3faabb5b Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Sun, 19 Feb 2023 18:52:26 -0500 Subject: [PATCH 054/144] Changelog for #11685 --- docs/release-notes/version-3.4.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes/version-3.4.md b/docs/release-notes/version-3.4.md index 731400a92..485b85719 100644 --- a/docs/release-notes/version-3.4.md +++ b/docs/release-notes/version-3.4.md @@ -6,6 +6,7 @@ * [#11110](https://github.com/netbox-community/netbox/issues/11110) - Add `start_address` and `end_address` filters for IP ranges * [#11592](https://github.com/netbox-community/netbox/issues/11592) - Introduce `FILE_UPLOAD_MAX_MEMORY_SIZE` configuration parameter +* [#11685](https://github.com/netbox-community/netbox/issues/11685) - Match on containing prefixes and aggregates when querying for IP addresses using global search ### Bug Fixes From e635e3e9595e15dd197bebe0a9d885afcb3e4921 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Sun, 19 Feb 2023 18:53:46 -0500 Subject: [PATCH 055/144] Fixes #11658: Remove reindex command call from search migration --- netbox/extras/migrations/0083_search.py | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/netbox/extras/migrations/0083_search.py b/netbox/extras/migrations/0083_search.py index 0c53de638..349066918 100644 --- a/netbox/extras/migrations/0083_search.py +++ b/netbox/extras/migrations/0083_search.py @@ -1,27 +1,10 @@ -import sys import uuid import django.db.models.deletion import django.db.models.lookups -from django.core import management from django.db import migrations, models -def reindex(apps, schema_editor): - # Build the search index (except during tests) - if 'test' not in sys.argv: - management.call_command( - 'reindex', - 'circuits', - 'dcim', - 'extras', - 'ipam', - 'tenancy', - 'virtualization', - 'wireless', - ) - - class Migration(migrations.Migration): dependencies = [ @@ -57,8 +40,4 @@ class Migration(migrations.Migration): 'ordering': ('weight', 'object_type', 'object_id'), }, ), - migrations.RunPython( - code=reindex, - reverse_code=migrations.RunPython.noop - ), ] From cd09501d4d0765f03b7c1dc243e88905386559f0 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Sun, 19 Feb 2023 20:08:57 -0500 Subject: [PATCH 056/144] #11685: Omit no-op migration --- netbox/extras/migrations/0083_search.py | 3 ++- .../0085_change_cachedvalue_value_type.py | 19 ------------------- 2 files changed, 2 insertions(+), 20 deletions(-) delete mode 100644 netbox/extras/migrations/0085_change_cachedvalue_value_type.py diff --git a/netbox/extras/migrations/0083_search.py b/netbox/extras/migrations/0083_search.py index 349066918..4c7ae1084 100644 --- a/netbox/extras/migrations/0083_search.py +++ b/netbox/extras/migrations/0083_search.py @@ -3,6 +3,7 @@ import uuid import django.db.models.deletion import django.db.models.lookups from django.db import migrations, models +import extras.fields class Migration(migrations.Migration): @@ -32,7 +33,7 @@ class Migration(migrations.Migration): ('object_id', models.PositiveBigIntegerField()), ('field', models.CharField(max_length=200)), ('type', models.CharField(max_length=30)), - ('value', models.TextField()), + ('value', extras.fields.CachedValueField()), ('weight', models.PositiveSmallIntegerField(default=1000)), ('object_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='contenttypes.contenttype')), ], diff --git a/netbox/extras/migrations/0085_change_cachedvalue_value_type.py b/netbox/extras/migrations/0085_change_cachedvalue_value_type.py deleted file mode 100644 index e6512753e..000000000 --- a/netbox/extras/migrations/0085_change_cachedvalue_value_type.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 4.1.6 on 2023-02-07 08:21 - -from django.db import migrations -import extras.fields - - -class Migration(migrations.Migration): - - dependencies = [ - ('extras', '0084_staging'), - ] - - operations = [ - migrations.AlterField( - model_name='cachedvalue', - name='value', - field=extras.fields.CachedValueField(), - ), - ] From 0855ff8b420b80356e24791c7264af18c84d0d50 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Fri, 17 Feb 2023 20:30:35 -0500 Subject: [PATCH 057/144] Skip clearing cache when handling new objects --- netbox/netbox/search/backends.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/netbox/netbox/search/backends.py b/netbox/netbox/search/backends.py index 10e164c09..14b5a987c 100644 --- a/netbox/netbox/search/backends.py +++ b/netbox/netbox/search/backends.py @@ -54,11 +54,11 @@ class SearchBackend: """ raise NotImplementedError - def caching_handler(self, sender, instance, **kwargs): + def caching_handler(self, sender, instance, created, **kwargs): """ Receiver for the post_save signal, responsible for caching object creation/changes. """ - self.cache(instance) + self.cache(instance, remove_existing=not created) def removal_handler(self, sender, instance, **kwargs): """ From 3e946c78d03d0166401364b76de925063a0b320c Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 20 Feb 2023 09:02:58 -0500 Subject: [PATCH 058/144] #11685: Clear cached search records for relevant IPAM objects --- .../migrations/0064_clear_search_cache.py | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 netbox/ipam/migrations/0064_clear_search_cache.py diff --git a/netbox/ipam/migrations/0064_clear_search_cache.py b/netbox/ipam/migrations/0064_clear_search_cache.py new file mode 100644 index 000000000..92d2b2cd7 --- /dev/null +++ b/netbox/ipam/migrations/0064_clear_search_cache.py @@ -0,0 +1,28 @@ +from django.db import migrations + + +def clear_cache(apps, schema_editor): + """ + Clear existing CachedValues referencing IPAddressFields or IPNetworkFields. (#11658 + introduced new cache record types for these.) + """ + ContentType = apps.get_model('contenttypes', 'ContentType') + CachedValue = apps.get_model('extras', 'CachedValue') + + for model_name in ('Aggregate', 'IPAddress', 'IPRange', 'Prefix'): + content_type = ContentType.objects.get(app_label='ipam', model=model_name.lower()) + CachedValue.objects.filter(object_type=content_type).delete() + + +class Migration(migrations.Migration): + + dependencies = [ + ('ipam', '0063_standardize_description_comments'), + ] + + operations = [ + migrations.RunPython( + code=clear_cache, + reverse_code=migrations.RunPython.noop + ), + ] From 5a4d8a71075a4529be8d5ef22d76d69861240f45 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 20 Feb 2023 09:39:33 -0500 Subject: [PATCH 059/144] Closes #11787: Rebuild any missing search cache entires after upgrade --- docs/release-notes/version-3.4.md | 2 ++ netbox/extras/management/commands/reindex.py | 31 ++++++++++++++------ netbox/netbox/search/backends.py | 14 ++++++++- upgrade.sh | 5 ++++ 4 files changed, 42 insertions(+), 10 deletions(-) diff --git a/docs/release-notes/version-3.4.md b/docs/release-notes/version-3.4.md index 485b85719..a006eb7bf 100644 --- a/docs/release-notes/version-3.4.md +++ b/docs/release-notes/version-3.4.md @@ -7,6 +7,7 @@ * [#11110](https://github.com/netbox-community/netbox/issues/11110) - Add `start_address` and `end_address` filters for IP ranges * [#11592](https://github.com/netbox-community/netbox/issues/11592) - Introduce `FILE_UPLOAD_MAX_MEMORY_SIZE` configuration parameter * [#11685](https://github.com/netbox-community/netbox/issues/11685) - Match on containing prefixes and aggregates when querying for IP addresses using global search +* [#11787](https://github.com/netbox-community/netbox/issues/11787) - Upgrade script will automatically rebuild missing search cache ### Bug Fixes @@ -20,6 +21,7 @@ * [#11683](https://github.com/netbox-community/netbox/issues/11683) - Fix CSV header attribute detection when auto-detecting import format * [#11711](https://github.com/netbox-community/netbox/issues/11711) - Fix CSV import for multiple-object custom fields * [#11723](https://github.com/netbox-community/netbox/issues/11723) - Circuit terminations should link to their associated circuits (rather than site or provider network) +* [#11775](https://github.com/netbox-community/netbox/issues/11775) - Skip checking for old search cache records when creating a new object * [#11786](https://github.com/netbox-community/netbox/issues/11786) - List only applicable object types in form widget when filtering custom fields --- diff --git a/netbox/extras/management/commands/reindex.py b/netbox/extras/management/commands/reindex.py index b601a1ac1..9a29c54f5 100644 --- a/netbox/extras/management/commands/reindex.py +++ b/netbox/extras/management/commands/reindex.py @@ -15,6 +15,11 @@ class Command(BaseCommand): nargs='*', help='One or more apps or models to reindex', ) + parser.add_argument( + '--lazy', + action='store_true', + help="For each model, reindex objects only if no cache entries already exist" + ) def _get_indexers(self, *model_names): indexers = {} @@ -60,14 +65,15 @@ class Command(BaseCommand): raise CommandError("No indexers found!") self.stdout.write(f'Reindexing {len(indexers)} models.') - # Clear all cached values for the specified models - self.stdout.write('Clearing cached values... ', ending='') - self.stdout.flush() - content_types = [ - ContentType.objects.get_for_model(model) for model in indexers.keys() - ] - deleted_count = search_backend.clear(content_types) - self.stdout.write(f'{deleted_count} entries deleted.') + # Clear all cached values for the specified models (if not being lazy) + if not kwargs['lazy']: + self.stdout.write('Clearing cached values... ', ending='') + self.stdout.flush() + content_types = [ + ContentType.objects.get_for_model(model) for model in indexers.keys() + ] + deleted_count = search_backend.clear(content_types) + self.stdout.write(f'{deleted_count} entries deleted.') # Index models self.stdout.write('Indexing models') @@ -76,11 +82,18 @@ class Command(BaseCommand): model_name = model._meta.model_name self.stdout.write(f' {app_label}.{model_name}... ', ending='') self.stdout.flush() + + if kwargs['lazy']: + content_type = ContentType.objects.get_for_model(model) + if cached_count := search_backend.count(object_types=[content_type]): + self.stdout.write(f'Skipping (found {cached_count} existing).') + continue + i = search_backend.cache(model.objects.iterator(), remove_existing=False) if i: self.stdout.write(f'{i} entries cached.') else: - self.stdout.write(f'None found.') + self.stdout.write(f'No objects found.') msg = f'Completed.' if total_count := search_backend.size: diff --git a/netbox/netbox/search/backends.py b/netbox/netbox/search/backends.py index 14b5a987c..f428842f5 100644 --- a/netbox/netbox/search/backends.py +++ b/netbox/netbox/search/backends.py @@ -80,7 +80,13 @@ class SearchBackend: def clear(self, object_types=None): """ - Delete *all* cached data. + Delete *all* cached data (optionally filtered by object type). + """ + raise NotImplementedError + + def count(self, object_types=None): + """ + Return a count of all cache entries (optionally filtered by object type). """ raise NotImplementedError @@ -218,6 +224,12 @@ class CachedValueSearchBackend(SearchBackend): # Call _raw_delete() on the queryset to avoid first loading instances into memory return qs._raw_delete(using=qs.db) + def count(self, object_types=None): + qs = CachedValue.objects.all() + if object_types: + qs = qs.filter(object_type__in=object_types) + return qs.count() + @property def size(self): return CachedValue.objects.count() diff --git a/upgrade.sh b/upgrade.sh index 161d65e32..cac046a9f 100755 --- a/upgrade.sh +++ b/upgrade.sh @@ -103,6 +103,11 @@ COMMAND="python3 netbox/manage.py remove_stale_contenttypes --no-input" echo "Removing stale content types ($COMMAND)..." eval $COMMAND || exit 1 +# Rebuild the search cache (lazily) +COMMAND="python3 netbox/manage.py reindex --lazy" +echo "Rebuilding search cache ($COMMAND)..." +eval $COMMAND || exit 1 + # Delete any expired user sessions COMMAND="python3 netbox/manage.py clearsessions" echo "Removing expired user sessions ($COMMAND)..." From 972ba7bfdc16b42f68b3172ad605a3e113e4ea79 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 20 Feb 2023 10:27:30 -0500 Subject: [PATCH 060/144] #11685: Fix migration --- netbox/ipam/migrations/0064_clear_search_cache.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/netbox/ipam/migrations/0064_clear_search_cache.py b/netbox/ipam/migrations/0064_clear_search_cache.py index 92d2b2cd7..856fe99e1 100644 --- a/netbox/ipam/migrations/0064_clear_search_cache.py +++ b/netbox/ipam/migrations/0064_clear_search_cache.py @@ -10,8 +10,11 @@ def clear_cache(apps, schema_editor): CachedValue = apps.get_model('extras', 'CachedValue') for model_name in ('Aggregate', 'IPAddress', 'IPRange', 'Prefix'): - content_type = ContentType.objects.get(app_label='ipam', model=model_name.lower()) - CachedValue.objects.filter(object_type=content_type).delete() + try: + content_type = ContentType.objects.get(app_label='ipam', model=model_name.lower()) + CachedValue.objects.filter(object_type=content_type).delete() + except ContentType.DoesNotExist: + pass class Migration(migrations.Migration): From 3586cf79d4cebcff81ab46fc471b32930ce2d0f1 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 21 Feb 2023 08:42:39 -0500 Subject: [PATCH 061/144] Arrange parameters alphabetically --- netbox/netbox/configuration_example.py | 34 ++++++++++++-------------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/netbox/netbox/configuration_example.py b/netbox/netbox/configuration_example.py index 7158308af..92f6133a3 100644 --- a/netbox/netbox/configuration_example.py +++ b/netbox/netbox/configuration_example.py @@ -107,6 +107,9 @@ CORS_ORIGIN_REGEX_WHITELIST = [ # r'^(https?://)?(\w+\.)?example\.com$', ] +# The name to use for the CSRF token cookie. +CSRF_COOKIE_NAME = 'csrftoken' + # Set to True to enable server debugging. WARNING: Debugging introduces a substantial performance penalty and may reveal # sensitive information about your installation. Only enable debugging while performing testing. Never enable debugging # on a production system. @@ -127,6 +130,9 @@ EMAIL = { 'FROM_EMAIL': '', } +# Localization +ENABLE_LOCALIZATION = False + # Exempt certain models from the enforcement of view permissions. Models listed here will be viewable by all users and # by anonymous users. List models in the form `.`. Add '*' to this list to exempt all models. EXEMPT_VIEW_PERMISSIONS = [ @@ -168,16 +174,6 @@ LOGOUT_REDIRECT_URL = 'home' # the default value of this setting is derived from the installed location. # MEDIA_ROOT = '/opt/netbox/netbox/media' -# By default uploaded media is stored on the local filesystem. Using Django-storages is also supported. Provide the -# class path of the storage driver in STORAGE_BACKEND and any configuration options in STORAGE_CONFIG. For example: -# STORAGE_BACKEND = 'storages.backends.s3boto3.S3Boto3Storage' -# STORAGE_CONFIG = { -# 'AWS_ACCESS_KEY_ID': 'Key ID', -# 'AWS_SECRET_ACCESS_KEY': 'Secret', -# 'AWS_STORAGE_BUCKET_NAME': 'netbox', -# 'AWS_S3_REGION_NAME': 'eu-west-1', -# } - # Expose Prometheus monitoring metrics at the HTTP endpoint '/metrics' METRICS_ENABLED = False @@ -217,13 +213,6 @@ RQ_DEFAULT_TIMEOUT = 300 # this setting is derived from the installed location. # SCRIPTS_ROOT = '/opt/netbox/netbox/scripts' -# The maximum size (in bytes) that an upload will be before it gets streamed to the file system. -# Useful to be able to upload files bigger than 2.5Mbyte to custom scripts for processing. -# FILE_UPLOAD_MAX_MEMORY_SIZE = 2621440 - -# The name to use for the csrf token cookie. -CSRF_COOKIE_NAME = 'csrftoken' - # The name to use for the session cookie. SESSION_COOKIE_NAME = 'sessionid' @@ -232,8 +221,15 @@ SESSION_COOKIE_NAME = 'sessionid' # database access.) Note that the user as which NetBox runs must have read and write permissions to this path. SESSION_FILE_PATH = None -# Localization -ENABLE_LOCALIZATION = False +# By default, uploaded media is stored on the local filesystem. Using Django-storages is also supported. Provide the +# class path of the storage driver in STORAGE_BACKEND and any configuration options in STORAGE_CONFIG. For example: +# STORAGE_BACKEND = 'storages.backends.s3boto3.S3Boto3Storage' +# STORAGE_CONFIG = { +# 'AWS_ACCESS_KEY_ID': 'Key ID', +# 'AWS_SECRET_ACCESS_KEY': 'Secret', +# 'AWS_STORAGE_BUCKET_NAME': 'netbox', +# 'AWS_S3_REGION_NAME': 'eu-west-1', +# } # Time zone (default: UTC) TIME_ZONE = 'UTC' From c280ca35d69f1c968b8d454db04ae23e20ef338e Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 21 Feb 2023 08:45:52 -0500 Subject: [PATCH 062/144] Release v3.4.5 --- .github/ISSUE_TEMPLATE/bug_report.yaml | 2 +- .github/ISSUE_TEMPLATE/feature_request.yaml | 2 +- docs/configuration/miscellaneous.md | 6 +++--- docs/release-notes/version-3.4.md | 3 +-- netbox/netbox/settings.py | 2 +- requirements.txt | 12 ++++++------ 6 files changed, 13 insertions(+), 14 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index 9ed740fff..99c060e8a 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.4.4 + placeholder: v3.4.5 validations: required: true - type: dropdown diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml index 8e4ab54a5..1338be9c1 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.4.4 + placeholder: v3.4.5 validations: required: true - type: dropdown diff --git a/docs/configuration/miscellaneous.md b/docs/configuration/miscellaneous.md index eac5d0a2f..8550564d8 100644 --- a/docs/configuration/miscellaneous.md +++ b/docs/configuration/miscellaneous.md @@ -69,11 +69,11 @@ By default, NetBox will permit users to create duplicate prefixes and IP address --- -## FILE_UPLOAD_MAX_MEMORY_SIZE +## `FILE_UPLOAD_MAX_MEMORY_SIZE` -Default: 2621440 (i.e. 2.5 MB). +Default: `2621440` (2.5 MB). -The maximum size (in bytes) that an upload will be before it gets streamed to the file system. Changing this setting can be useful for example to be able to upload files bigger than 2.5MB to custom scripts for processing. +The maximum amount (in bytes) of uploaded data that will be held in memory before being written to the filesystem. Changing this setting can be useful for example to be able to upload files bigger than 2.5MB to custom scripts for processing. --- diff --git a/docs/release-notes/version-3.4.md b/docs/release-notes/version-3.4.md index a006eb7bf..11df3d47a 100644 --- a/docs/release-notes/version-3.4.md +++ b/docs/release-notes/version-3.4.md @@ -1,6 +1,6 @@ # NetBox v3.4 -## v3.4.5 (FUTURE) +## v3.4.5 (2023-02-21) ### Enhancements @@ -13,7 +13,6 @@ * [#11032](https://github.com/netbox-community/netbox/issues/11032) - Fix false custom validation errors during component creation * [#11226](https://github.com/netbox-community/netbox/issues/11226) - Ensure scripts and reports within submodules are automatically reloaded -* [#11335](https://github.com/netbox-community/netbox/issues/11335) - Avoid exception when rendering change log after uninstalling a plugin * [#11459](https://github.com/netbox-community/netbox/issues/11459) - Enable evaluating null values in custom validation rules * [#11473](https://github.com/netbox-community/netbox/issues/11473) - GraphQL requests specifying an invalid filter should return an empty queryset * [#11582](https://github.com/netbox-community/netbox/issues/11582) - Ensure form validation errors are displayed when adding virtual chassis members diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index f6ce7ff33..2d56a025a 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -24,7 +24,7 @@ from netbox.constants import RQ_QUEUE_DEFAULT, RQ_QUEUE_HIGH, RQ_QUEUE_LOW # Environment setup # -VERSION = '3.4.5-dev' +VERSION = '3.4.5' # Hostname HOSTNAME = platform.node() diff --git a/requirements.txt b/requirements.txt index 3cb2529a8..8bbb80d1e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ bleach==5.0.1 -Django==4.1.6 +Django==4.1.7 django-cors-headers==3.13.0 django-debug-toolbar==3.8.1 django-filter==22.1 @@ -9,23 +9,23 @@ django-pglocks==1.0.4 django-prometheus==2.2.0 django-redis==5.2.0 django-rich==1.4.0 -django-rq==2.6.0 -django-tables2==2.5.1 +django-rq==2.7.0 +django-tables2==2.5.2 django-taggit==3.1.0 django-timezone-field==5.0 djangorestframework==3.14.0 -drf-yasg[validation]==1.21.4 +drf-yasg[validation]==1.21.5 graphene-django==3.0.0 gunicorn==20.1.0 Jinja2==3.1.2 Markdown==3.3.7 -mkdocs-material==9.0.10 +mkdocs-material==9.0.13 mkdocstrings[python-legacy]==0.20.0 netaddr==0.8.0 Pillow==9.4.0 psycopg2-binary==2.9.5 PyYAML==6.0 -sentry-sdk==1.14.0 +sentry-sdk==1.15.0 social-auth-app-django==5.0.0 social-auth-core[openidconnect]==4.3.0 svgwrite==1.4.3 From 561f1eadfc2386bd266b941d019d830c37c53cf3 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 21 Feb 2023 09:03:19 -0500 Subject: [PATCH 063/144] PRVB --- docs/release-notes/version-3.4.md | 4 ++++ netbox/netbox/settings.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/release-notes/version-3.4.md b/docs/release-notes/version-3.4.md index 11df3d47a..cb8ba1e6e 100644 --- a/docs/release-notes/version-3.4.md +++ b/docs/release-notes/version-3.4.md @@ -1,5 +1,9 @@ # NetBox v3.4 +## v3.4.6 (FUTURE) + +--- + ## v3.4.5 (2023-02-21) ### Enhancements diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 2d56a025a..967766763 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -24,7 +24,7 @@ from netbox.constants import RQ_QUEUE_DEFAULT, RQ_QUEUE_HIGH, RQ_QUEUE_LOW # Environment setup # -VERSION = '3.4.5' +VERSION = '3.4.6-dev' # Hostname HOSTNAME = platform.node() From ed77c0383093345112af94c27c9fa1f848d6dc37 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 27 Feb 2023 08:26:32 -0500 Subject: [PATCH 064/144] Fixes #11796: When importing devices, restrict rack by location only if the location field is specified --- docs/release-notes/version-3.4.md | 4 ++++ netbox/dcim/forms/bulk_import.py | 7 +++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/docs/release-notes/version-3.4.md b/docs/release-notes/version-3.4.md index cb8ba1e6e..e1d719dc6 100644 --- a/docs/release-notes/version-3.4.md +++ b/docs/release-notes/version-3.4.md @@ -2,6 +2,10 @@ ## v3.4.6 (FUTURE) +### Bug Fixes + +* [#11796](https://github.com/netbox-community/netbox/issues/11796) - When importing devices, restrict rack by location only if the location field is specified + --- ## v3.4.5 (2023-02-21) diff --git a/netbox/dcim/forms/bulk_import.py b/netbox/dcim/forms/bulk_import.py index 3f016899e..da658d732 100644 --- a/netbox/dcim/forms/bulk_import.py +++ b/netbox/dcim/forms/bulk_import.py @@ -447,11 +447,14 @@ class DeviceImportForm(BaseDeviceImportForm): self.fields['location'].queryset = self.fields['location'].queryset.filter(**params) self.fields['parent'].queryset = self.fields['parent'].queryset.filter(**params) - # Limit rack queryset by assigned site and group + # Limit rack queryset by assigned site and location params = { f"site__{self.fields['site'].to_field_name}": data.get('site'), - f"location__{self.fields['location'].to_field_name}": data.get('location'), } + if 'location' in data: + params.update({ + f"location__{self.fields['location'].to_field_name}": data.get('location'), + }) self.fields['rack'].queryset = self.fields['rack'].queryset.filter(**params) # Limit device bay queryset by parent device From cfa6b28ceb8667bc60f2924969f56be1d828f136 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 27 Feb 2023 09:22:48 -0500 Subject: [PATCH 065/144] Closes #11807: Restore default page size when navigating between views --- docs/release-notes/version-3.4.md | 4 ++++ netbox/netbox/preferences.py | 2 +- netbox/utilities/paginator.py | 2 -- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/release-notes/version-3.4.md b/docs/release-notes/version-3.4.md index e1d719dc6..484f5bab7 100644 --- a/docs/release-notes/version-3.4.md +++ b/docs/release-notes/version-3.4.md @@ -2,6 +2,10 @@ ## v3.4.6 (FUTURE) +### Enhancements + +* [#11807](https://github.com/netbox-community/netbox/issues/11807) - Restore default page size when navigating between views + ### Bug Fixes * [#11796](https://github.com/netbox-community/netbox/issues/11796) - When importing devices, restrict rack by location only if the location field is specified diff --git a/netbox/netbox/preferences.py b/netbox/netbox/preferences.py index c88b56072..5ef216259 100644 --- a/netbox/netbox/preferences.py +++ b/netbox/netbox/preferences.py @@ -24,7 +24,7 @@ PREFERENCES = { 'pagination.per_page': UserPreference( label=_('Page length'), choices=get_page_lengths(), - description=_('The number of objects to display per page'), + description=_('The default number of objects to display per page'), coerce=lambda x: int(x) ), 'pagination.placement': UserPreference( diff --git a/netbox/utilities/paginator.py b/netbox/utilities/paginator.py index 1f07aa42a..db6326a9c 100644 --- a/netbox/utilities/paginator.py +++ b/netbox/utilities/paginator.py @@ -76,8 +76,6 @@ def get_paginate_count(request): if 'per_page' in request.GET: try: per_page = int(request.GET.get('per_page')) - if request.user.is_authenticated: - request.user.config.set('pagination.per_page', per_page, commit=True) return _max_allowed(per_page) except ValueError: pass From 0744ff2fa014ee4d23f89d588f35506c93a1ab60 Mon Sep 17 00:00:00 2001 From: Rafael Ganascim <67799240+rganascim@users.noreply.github.com> Date: Mon, 27 Feb 2023 13:42:30 -0300 Subject: [PATCH 066/144] Fixes #11758 - replace unsafe chars in menu label (#11831) * Fixes #11758 - replace unsafe chars in menu label * Fixes #11758 - replace unsafe chars in menu label --- netbox/extras/plugins/navigation.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/netbox/extras/plugins/navigation.py b/netbox/extras/plugins/navigation.py index e667965b8..288a78512 100644 --- a/netbox/extras/plugins/navigation.py +++ b/netbox/extras/plugins/navigation.py @@ -1,5 +1,6 @@ from netbox.navigation import MenuGroup from utilities.choices import ButtonColorChoices +from django.utils.text import slugify __all__ = ( 'PluginMenu', @@ -21,7 +22,7 @@ class PluginMenu: @property def name(self): - return self.label.replace(' ', '_') + return slugify(self.label) class PluginMenuItem: From 67499cbf069bff4edf85103969b6349555fbe20b Mon Sep 17 00:00:00 2001 From: Sebastian Himmler Date: Fri, 24 Feb 2023 06:36:39 +0100 Subject: [PATCH 067/144] add conntected_enpoints property to graphql --- netbox/dcim/graphql/mixins.py | 8 ++++++++ netbox/dcim/graphql/types.py | 14 +++++++------- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/netbox/dcim/graphql/mixins.py b/netbox/dcim/graphql/mixins.py index 133d6259f..f8e626fe8 100644 --- a/netbox/dcim/graphql/mixins.py +++ b/netbox/dcim/graphql/mixins.py @@ -10,3 +10,11 @@ class CabledObjectMixin: def resolve_link_peers(self, info): return self.link_peers + + +class PathEndpointMixin: + connected_endpoints = graphene.List('dcim.graphql.gfk_mixins.LinkPeerType') + + def resolve_connected_endpoints(self, info): + # Handle empty values + return self.connected_endpoints or None diff --git a/netbox/dcim/graphql/types.py b/netbox/dcim/graphql/types.py index 41f0092f9..3c6c0a885 100644 --- a/netbox/dcim/graphql/types.py +++ b/netbox/dcim/graphql/types.py @@ -7,7 +7,7 @@ from extras.graphql.mixins import ( from ipam.graphql.mixins import IPAddressesMixin, VLANGroupsMixin from netbox.graphql.scalars import BigInt from netbox.graphql.types import BaseObjectType, OrganizationalObjectType, NetBoxObjectType -from .mixins import CabledObjectMixin +from .mixins import CabledObjectMixin, PathEndpointMixin __all__ = ( 'CableType', @@ -117,7 +117,7 @@ class CableTerminationType(NetBoxObjectType): filterset_class = filtersets.CableTerminationFilterSet -class ConsolePortType(ComponentObjectType, CabledObjectMixin): +class ConsolePortType(ComponentObjectType, CabledObjectMixin, PathEndpointMixin): class Meta: model = models.ConsolePort @@ -139,7 +139,7 @@ class ConsolePortTemplateType(ComponentTemplateObjectType): return self.type or None -class ConsoleServerPortType(ComponentObjectType, CabledObjectMixin): +class ConsoleServerPortType(ComponentObjectType, CabledObjectMixin, PathEndpointMixin): class Meta: model = models.ConsoleServerPort @@ -241,7 +241,7 @@ class FrontPortTemplateType(ComponentTemplateObjectType): filterset_class = filtersets.FrontPortTemplateFilterSet -class InterfaceType(IPAddressesMixin, ComponentObjectType, CabledObjectMixin): +class InterfaceType(IPAddressesMixin, ComponentObjectType, CabledObjectMixin, PathEndpointMixin): class Meta: model = models.Interface @@ -354,7 +354,7 @@ class PlatformType(OrganizationalObjectType): filterset_class = filtersets.PlatformFilterSet -class PowerFeedType(NetBoxObjectType, CabledObjectMixin): +class PowerFeedType(NetBoxObjectType, CabledObjectMixin, PathEndpointMixin): class Meta: model = models.PowerFeed @@ -362,7 +362,7 @@ class PowerFeedType(NetBoxObjectType, CabledObjectMixin): filterset_class = filtersets.PowerFeedFilterSet -class PowerOutletType(ComponentObjectType, CabledObjectMixin): +class PowerOutletType(ComponentObjectType, CabledObjectMixin, PathEndpointMixin): class Meta: model = models.PowerOutlet @@ -398,7 +398,7 @@ class PowerPanelType(NetBoxObjectType, ContactsMixin): filterset_class = filtersets.PowerPanelFilterSet -class PowerPortType(ComponentObjectType, CabledObjectMixin): +class PowerPortType(ComponentObjectType, CabledObjectMixin, PathEndpointMixin): class Meta: model = models.PowerPort From 559a318584d0224cd014f5f6d3d3b34d3298af5e Mon Sep 17 00:00:00 2001 From: Simon Toft Date: Fri, 10 Feb 2023 12:19:44 +0100 Subject: [PATCH 068/144] Fixes #11565 - Populate custom field defaults when creating FHRP groups with VIP --- netbox/ipam/forms/model_forms.py | 1 + netbox/netbox/models/features.py | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/netbox/ipam/forms/model_forms.py b/netbox/ipam/forms/model_forms.py index 430a4b2f8..a34222479 100644 --- a/netbox/ipam/forms/model_forms.py +++ b/netbox/ipam/forms/model_forms.py @@ -578,6 +578,7 @@ class FHRPGroupForm(NetBoxModelForm): role=FHRP_PROTOCOL_ROLE_MAPPINGS.get(self.cleaned_data['protocol'], IPAddressRoleChoices.ROLE_VIP), assigned_object=instance ) + ipaddress.populate_custom_field_defaults() ipaddress.save() # Check that the new IPAddress conforms with any assigned object-level permissions diff --git a/netbox/netbox/models/features.py b/netbox/netbox/models/features.py index f041d016d..62482a26f 100644 --- a/netbox/netbox/models/features.py +++ b/netbox/netbox/models/features.py @@ -216,6 +216,13 @@ class CustomFieldsMixin(models.Model): return dict(groups) + def populate_custom_field_defaults(self): + """ + Apply the default value for each custom field + """ + for cf in self.custom_fields: + self.custom_field_data[cf.name] = cf.default + def clean(self): super().clean() from extras.models import CustomField From 6e7d2f53aab3144c53e774a1b8f36b0347649056 Mon Sep 17 00:00:00 2001 From: Marc <56593749+MarcMocker@users.noreply.github.com> Date: Sun, 26 Feb 2023 21:50:15 +0100 Subject: [PATCH 069/144] Change Interpreter in shebang to python3 --- netbox/generate_secret_key.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/generate_secret_key.py b/netbox/generate_secret_key.py index c3de29cee..21efd0a6d 100755 --- a/netbox/generate_secret_key.py +++ b/netbox/generate_secret_key.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # This script will generate a random 50-character string suitable for use as a SECRET_KEY. import secrets From 88d5119c5956508d5198988aee2a5e979d806140 Mon Sep 17 00:00:00 2001 From: Pieter Lambrecht Date: Thu, 23 Feb 2023 21:37:25 +0100 Subject: [PATCH 070/144] Search device by primary IP address --- netbox/dcim/filtersets.py | 4 +++- netbox/dcim/search.py | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py index c10ef44c3..1ea56b3ef 100644 --- a/netbox/dcim/filtersets.py +++ b/netbox/dcim/filtersets.py @@ -981,7 +981,9 @@ class DeviceFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilter Q(serial__icontains=value.strip()) | Q(inventoryitems__serial__icontains=value.strip()) | Q(asset_tag__icontains=value.strip()) | - Q(comments__icontains=value) + Q(comments__icontains=value) | + Q(primary_ip4__address__startswith=value) | + Q(primary_ip6__address__startswith=value) ).distinct() def _has_primary_ip(self, queryset, name, value): diff --git a/netbox/dcim/search.py b/netbox/dcim/search.py index bae4f030f..7cb2c6205 100644 --- a/netbox/dcim/search.py +++ b/netbox/dcim/search.py @@ -41,6 +41,8 @@ class DeviceIndex(SearchIndex): ('asset_tag', 50), ('serial', 60), ('name', 100), + ('primary_ip4', 110), + ('primary_ip6', 110), ('description', 500), ('comments', 5000), ) From 3418b7adf6cf0e57d2dfd39bd05f2271267c7fc8 Mon Sep 17 00:00:00 2001 From: Pieter Lambrecht Date: Fri, 24 Feb 2023 11:51:57 +0100 Subject: [PATCH 071/144] remove DeviceIndex search for ipaddresses --- netbox/dcim/search.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/netbox/dcim/search.py b/netbox/dcim/search.py index 7cb2c6205..bae4f030f 100644 --- a/netbox/dcim/search.py +++ b/netbox/dcim/search.py @@ -41,8 +41,6 @@ class DeviceIndex(SearchIndex): ('asset_tag', 50), ('serial', 60), ('name', 100), - ('primary_ip4', 110), - ('primary_ip6', 110), ('description', 500), ('comments', 5000), ) From 6ea30798bff10df933bd6ae20250c818d76a1ee6 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 27 Feb 2023 14:41:34 -0500 Subject: [PATCH 072/144] #10058: Enable primary IP search for virtual machines too --- netbox/virtualization/filtersets.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/netbox/virtualization/filtersets.py b/netbox/virtualization/filtersets.py index 4463e902a..8f656811a 100644 --- a/netbox/virtualization/filtersets.py +++ b/netbox/virtualization/filtersets.py @@ -238,7 +238,9 @@ class VirtualMachineFilterSet( return queryset return queryset.filter( Q(name__icontains=value) | - Q(comments__icontains=value) + Q(comments__icontains=value) | + Q(primary_ip4__address__startswith=value) | + Q(primary_ip6__address__startswith=value) ) def _has_primary_ip(self, queryset, name, value): From a554164d1d59b36129628f1125a6b7498ab5194c Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 27 Feb 2023 14:46:03 -0500 Subject: [PATCH 073/144] Changelog for #10058, #11565, #11758, #11817 --- docs/release-notes/version-3.4.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/release-notes/version-3.4.md b/docs/release-notes/version-3.4.md index 484f5bab7..5e4b96158 100644 --- a/docs/release-notes/version-3.4.md +++ b/docs/release-notes/version-3.4.md @@ -4,10 +4,14 @@ ### Enhancements +* [#10058](https://github.com/netbox-community/netbox/issues/10058) - Enable searching for devices/VMs by primary IP address * [#11807](https://github.com/netbox-community/netbox/issues/11807) - Restore default page size when navigating between views +* [#11817](https://github.com/netbox-community/netbox/issues/11817) - Add `connected_endpoints` field to GraphQL API for cabled objects ### Bug Fixes +* [#11565](https://github.com/netbox-community/netbox/issues/11565) - Apply custom field defaults to IP address created during FHRP group creation +* [#11758](https://github.com/netbox-community/netbox/issues/11758) - Support non-URL-safe characters in plugin menu titles * [#11796](https://github.com/netbox-community/netbox/issues/11796) - When importing devices, restrict rack by location only if the location field is specified --- From eed6990b398e2d2e4e260eef21c558f667b439ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?aron=20bergur=20j=C3=B3hannsson?= Date: Thu, 23 Feb 2023 19:18:26 +0000 Subject: [PATCH 074/144] Closes #11011: Hide virtual interfaces --- netbox/dcim/tables/devices.py | 1 + .../src/tables/interfaceTable.ts | 126 ++++++++---------- .../device/inc/interface_table_controls.html | 1 + 3 files changed, 59 insertions(+), 69 deletions(-) diff --git a/netbox/dcim/tables/devices.py b/netbox/dcim/tables/devices.py index 730309156..686382a8c 100644 --- a/netbox/dcim/tables/devices.py +++ b/netbox/dcim/tables/devices.py @@ -588,6 +588,7 @@ class DeviceInterfaceTable(InterfaceTable): 'class': get_interface_row_class, 'data-name': lambda record: record.name, 'data-enabled': get_interface_state_attribute, + 'data-type': lambda record: record.type, } diff --git a/netbox/project-static/src/tables/interfaceTable.ts b/netbox/project-static/src/tables/interfaceTable.ts index d2b20f322..b5d82a322 100644 --- a/netbox/project-static/src/tables/interfaceTable.ts +++ b/netbox/project-static/src/tables/interfaceTable.ts @@ -1,6 +1,5 @@ import { getElements, replaceAll, findFirstAdjacent } from '../util'; -type InterfaceState = 'enabled' | 'disabled'; type ShowHide = 'show' | 'hide'; function isShowHide(value: unknown): value is ShowHide { @@ -27,54 +26,23 @@ class ButtonState { * Underlying Button DOM Element */ public button: HTMLButtonElement; - /** - * Table rows with `data-enabled` set to `"enabled"` - */ - private enabledRows: NodeListOf; - /** - * Table rows with `data-enabled` set to `"disabled"` - */ - private disabledRows: NodeListOf; - constructor(button: HTMLButtonElement, table: HTMLTableElement) { + /** + * Table rows provided in constructor + */ + private rows: NodeListOf; + + constructor(button: HTMLButtonElement, rows: NodeListOf) { this.button = button; - this.enabledRows = table.querySelectorAll('tr[data-enabled="enabled"]'); - this.disabledRows = table.querySelectorAll('tr[data-enabled="disabled"]'); + this.rows = rows; } /** - * This button's controlled type. For example, a button with the class `toggle-disabled` has - * directive 'disabled' because it controls the visibility of rows with - * `data-enabled="disabled"`. Likewise, `toggle-enabled` controls rows with - * `data-enabled="enabled"`. + * Remove visibility of button state rows. */ - private get directive(): InterfaceState { - if (this.button.classList.contains('toggle-disabled')) { - return 'disabled'; - } else if (this.button.classList.contains('toggle-enabled')) { - return 'enabled'; - } - // If this class has been instantiated but doesn't contain these classes, it's probably because - // the classes are missing in the HTML template. - console.warn(this.button); - throw new Error('Toggle button does not contain expected class'); - } - - /** - * Toggle visibility of rows with `data-enabled="enabled"`. - */ - private toggleEnabledRows(): void { - for (const row of this.enabledRows) { - row.classList.toggle('d-none'); - } - } - - /** - * Toggle visibility of rows with `data-enabled="disabled"`. - */ - private toggleDisabledRows(): void { - for (const row of this.disabledRows) { - row.classList.toggle('d-none'); + private hideRows(): void { + for (const row of this.rows) { + row.classList.add('d-none'); } } @@ -111,17 +79,6 @@ class ButtonState { } } - /** - * Toggle visibility for the rows this element controls. - */ - private toggleRows(): void { - if (this.directive === 'enabled') { - this.toggleEnabledRows(); - } else if (this.directive === 'disabled') { - this.toggleDisabledRows(); - } - } - /** * Toggle the DOM element's `data-state` attribute. */ @@ -139,17 +96,20 @@ class ButtonState { private toggle(): void { this.toggleState(); this.toggleButton(); - this.toggleRows(); } /** - * When the button is clicked, toggle all controlled elements. + * When the button is clicked, toggle all controlled elements and hide rows based on + * buttonstate. */ public handleClick(event: Event): void { const button = event.currentTarget as HTMLButtonElement; if (button.isEqualNode(this.button)) { this.toggle(); } + if (this.buttonState === 'hide') { + this.hideRows(); + } } } @@ -174,14 +134,25 @@ class TableState { // @ts-expect-error null handling is performed in the constructor private disabledButton: ButtonState; + /** + * Instance of ButtonState for the 'show/hide virtual rows' button. + */ + // @ts-expect-error null handling is performed in the constructor + private virtualButton: ButtonState; + /** * Underlying DOM Table Caption Element. */ private caption: Nullable = null; + /** + * All table rows in table + */ + private rows: NodeListOf; + constructor(table: HTMLTableElement) { this.table = table; - + this.rows = this.table.querySelectorAll('tr'); try { const toggleEnabledButton = findFirstAdjacent( this.table, @@ -191,6 +162,10 @@ class TableState { this.table, 'button.toggle-disabled', ); + const toggleVirtualButton = findFirstAdjacent( + this.table, + 'button.toggle-virtual', + ); const caption = this.table.querySelector('caption'); this.caption = caption; @@ -203,13 +178,28 @@ class TableState { throw new TableStateError("Table is missing a 'toggle-disabled' button.", table); } + if (toggleVirtualButton === null) { + throw new TableStateError("Table is missing a 'toggle-virtual' button.", table); + } + // Attach event listeners to the buttons elements. toggleEnabledButton.addEventListener('click', event => this.handleClick(event, this)); toggleDisabledButton.addEventListener('click', event => this.handleClick(event, this)); + toggleVirtualButton.addEventListener('click', event => this.handleClick(event, this)); // Instantiate ButtonState for each button for state management. - this.enabledButton = new ButtonState(toggleEnabledButton, this.table); - this.disabledButton = new ButtonState(toggleDisabledButton, this.table); + this.enabledButton = new ButtonState( + toggleEnabledButton, + table.querySelectorAll('tr[data-enabled="enabled"]'), + ); + this.disabledButton = new ButtonState( + toggleDisabledButton, + table.querySelectorAll('tr[data-enabled="disabled"]'), + ); + this.virtualButton = new ButtonState( + toggleVirtualButton, + table.querySelectorAll('tr[data-type="virtual"]'), + ); } catch (err) { if (err instanceof TableStateError) { // This class is useless for tables that don't have toggle buttons. @@ -261,22 +251,20 @@ class TableState { } /** - * When toggle buttons are clicked, pass the event to the relevant button's handler and update - * this instance's state. + * When toggle buttons are clicked, reapply visability all rows and + * pass the event to all button handlers * * @param event onClick event for toggle buttons. * @param instance Instance of TableState (`this` cannot be used since that's context-specific). */ public handleClick(event: Event, instance: TableState): void { - const button = event.currentTarget as HTMLButtonElement; - const enabled = button.isEqualNode(instance.enabledButton.button); - const disabled = button.isEqualNode(instance.disabledButton.button); - - if (enabled) { - instance.enabledButton.handleClick(event); - } else if (disabled) { - instance.disabledButton.handleClick(event); + for (const row of this.rows) { + row.classList.remove('d-none'); } + + instance.enabledButton.handleClick(event); + instance.disabledButton.handleClick(event); + instance.virtualButton.handleClick(event); instance.toggleCaption(); } } diff --git a/netbox/templates/dcim/device/inc/interface_table_controls.html b/netbox/templates/dcim/device/inc/interface_table_controls.html index 14e552439..2b082cfe6 100644 --- a/netbox/templates/dcim/device/inc/interface_table_controls.html +++ b/netbox/templates/dcim/device/inc/interface_table_controls.html @@ -7,5 +7,6 @@ {% endblock extra_table_controls %} From 1f0db6d2fa7c49cb9a1bd29768b61cbbb2197894 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?aron=20bergur=20j=C3=B3hannsson?= Date: Thu, 23 Feb 2023 19:28:49 +0000 Subject: [PATCH 075/144] include static assets --- netbox/project-static/dist/netbox.js | Bin 380899 -> 380690 bytes netbox/project-static/dist/netbox.js.map | Bin 353676 -> 353576 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/netbox/project-static/dist/netbox.js b/netbox/project-static/dist/netbox.js index 19cdae0bd318381cbc0701ff151e7f87ad3ae9ee..4792a3a6d57ca794da12f642a88983f07cd0111d 100644 GIT binary patch delta 15568 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 W;vgsn1G|j$Y+(}AoWtB2{Qm$M3~|{2 delta 15934 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 diff --git a/netbox/project-static/dist/netbox.js.map b/netbox/project-static/dist/netbox.js.map index d0563b9fc670f99db1fe7ed2c7fb22555367e360..16817c42b601e9e7fca785053cfe61d4ecf55785 100644 GIT binary patch delta 623 zcmah`&r1SP5N6c}oum#@QfDC=D2kL&f_aZ^Tb6ZqmG#Hb#8$CFibPKq?6GUSf1zWC z!vCTEi4OIDG;deUAm}o@H}lQ+eKYg$o_Tu9+=W`02$Nyzg>Bl}KyKOWF<0+$Xd*Vj zK-}jl-U<-t4t3~rv;+hkjj~*GlbK@+(IJFg1|PA<2_}Xd9K<@MJn=r!mc%k;(h!sJ-#7I=X} zkNhEwIob$&QKUCz$T#K%xv>f}`>_Vu^T@t9i@z3T$#Wz|6)95Up*GC7>Byrk14-AmP?oivRGlYr0AZ=QiW|#j9q={G)FIUr=tOgyif3CLFN&Ac~n=IhVq>96LyvR2m1M!E1^EGD&G zb1e}cX*8Bp00l+B9ckZVNecaGK-&_rqCqf?HK3^n^e6)|+Tli9iV_k>R$aC(v(y1iO=j7;!@wU{dV+Q3daH#^S;=# NSU*~u{mDi9!e20p%OL;& From 4f12eccde6618c313fb75f538af2627daab59329 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?aron=20bergur=20j=C3=B3hannsson?= Date: Fri, 24 Feb 2023 08:14:33 +0000 Subject: [PATCH 076/144] Update toggle caption for vif --- netbox/project-static/dist/netbox.js | Bin 380690 -> 380966 bytes netbox/project-static/dist/netbox.js.map | Bin 353576 -> 353776 bytes .../src/tables/interfaceTable.ts | 17 ++++++++++++----- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/netbox/project-static/dist/netbox.js b/netbox/project-static/dist/netbox.js index 4792a3a6d57ca794da12f642a88983f07cd0111d..ed6e127477872c2c111e2c18a555bdc54769df5f 100644 GIT binary patch delta 246 zcmbOF7M3ln6SfNI6y@2LWMmfWm1P!{lqTj(_uR@VJ$=GfRt^?5 zHN~RoyxUl1rf=TLDuu*-v6WQ~&b5TFT(_|*iRdV}WELkT<)o%4s40X&4K&=wD$1;; z1~EzwDyra_SCU$kmYAGctYnp!2ec{`NI=x6yXK*pfuS0qSQ@Gro7LNkwy`dd008+J BP|5%R delta 73 zcmZ3sKz!0X@rD-07N!>F7M3ln6SlHj=jExXDW*q3KN9SRFxx^z`Iy atg;}M@bnqmSox-_Y-5$!K6yLqWC;MVwi#vs diff --git a/netbox/project-static/dist/netbox.js.map b/netbox/project-static/dist/netbox.js.map index 16817c42b601e9e7fca785053cfe61d4ecf55785..dd1ec47c90c838edcfb2385e1f8e8a65c560d54a 100644 GIT binary patch delta 310 zcmZ2+O7z2N(S{br7N!>F7M3ln{lToxc`iEc)8m3!#r#~$Tyz{A9X)kC9UW73oE;sV zL9BEq2-DrsInPQiIJ-Rbsnt;v*Dy>3RTIvinS{~{}h%F7M3ln{lOBhRW3S?j*gx>o{o;GI?j%c&N}Xn&N)ug&jzzf zvjUkFPSbw`vnqg?RZi12LReLWfJ{Rt9dAcRZ=Fa-$7G#S=jpFQSkE$<25#RK%4#M6 E0NtS=X#fBK diff --git a/netbox/project-static/src/tables/interfaceTable.ts b/netbox/project-static/src/tables/interfaceTable.ts index b5d82a322..56a0ae754 100644 --- a/netbox/project-static/src/tables/interfaceTable.ts +++ b/netbox/project-static/src/tables/interfaceTable.ts @@ -236,15 +236,22 @@ class TableState { private toggleCaption(): void { const showEnabled = this.enabledButton.buttonState === 'show'; const showDisabled = this.disabledButton.buttonState === 'show'; + const showVirtual = this.virtualButton.buttonState === 'show'; - if (showEnabled && !showDisabled) { + if (showEnabled && !showDisabled && !showVirtual) { this.captionText = 'Showing Enabled Interfaces'; - } else if (showEnabled && showDisabled) { + } else if (showEnabled && showDisabled && !showVirtual) { this.captionText = 'Showing Enabled & Disabled Interfaces'; - } else if (!showEnabled && showDisabled) { + } else if (!showEnabled && showDisabled && !showVirtual) { this.captionText = 'Showing Disabled Interfaces'; - } else if (!showEnabled && !showDisabled) { - this.captionText = 'Hiding Enabled & Disabled Interfaces'; + } else if (!showEnabled && !showDisabled && !showVirtual) { + this.captionText = 'Hiding Enabled, Disabled & Virtual Interfaces'; + } else if (!showEnabled && !showDisabled && showVirtual) { + this.captionText = 'Showing Virtual Interfaces'; + } else if (showEnabled && !showDisabled && showVirtual) { + this.captionText = 'Showing Enabled & Virtual Interfaces'; + } else if (showEnabled && showDisabled && showVirtual) { + this.captionText = 'Showing Enabled, Disabled & Virtual Interfaces'; } else { this.captionText = ''; } From ed5fd140ebec002295d0bc11657ed2d1006bd9d0 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 27 Feb 2023 15:28:17 -0500 Subject: [PATCH 077/144] Optimize shallow_compare_dict() --- netbox/utilities/utils.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/netbox/utilities/utils.py b/netbox/utilities/utils.py index 23c2666df..aec0d896c 100644 --- a/netbox/utilities/utils.py +++ b/netbox/utilities/utils.py @@ -359,18 +359,18 @@ def prepare_cloned_fields(instance): return QueryDict(urlencode(params), mutable=True) -def shallow_compare_dict(source_dict, destination_dict, exclude=None): +def shallow_compare_dict(source_dict, destination_dict, exclude=tuple()): """ Return a new dictionary of the different keys. The values of `destination_dict` are returned. Only the equality of the first layer of keys/values is checked. `exclude` is a list or tuple of keys to be ignored. """ difference = {} - for key in destination_dict: - if source_dict.get(key) != destination_dict[key]: - if isinstance(exclude, (list, tuple)) and key in exclude: - continue - difference[key] = destination_dict[key] + for key, value in destination_dict.items(): + if key in exclude: + continue + if source_dict.get(key) != value: + difference[key] = value return difference From c9e5a4c996fe165d1a20c1434a99b491af91964c Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 27 Feb 2023 15:35:04 -0500 Subject: [PATCH 078/144] Changelog for #11011 --- docs/release-notes/version-3.4.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes/version-3.4.md b/docs/release-notes/version-3.4.md index 5e4b96158..3049853ea 100644 --- a/docs/release-notes/version-3.4.md +++ b/docs/release-notes/version-3.4.md @@ -5,6 +5,7 @@ ### Enhancements * [#10058](https://github.com/netbox-community/netbox/issues/10058) - Enable searching for devices/VMs by primary IP address +* [#11011](https://github.com/netbox-community/netbox/issues/11011) - Add ability to toggle visibility of virtual interfaces under device view * [#11807](https://github.com/netbox-community/netbox/issues/11807) - Restore default page size when navigating between views * [#11817](https://github.com/netbox-community/netbox/issues/11817) - Add `connected_endpoints` field to GraphQL API for cabled objects From 189668fbfb51b5cf071e16662e53bff18c7aae64 Mon Sep 17 00:00:00 2001 From: "Daniel W. Anner" Date: Wed, 1 Mar 2023 18:20:37 +0000 Subject: [PATCH 079/144] Implemented PoE choice for IEEE 802.3az --- netbox/dcim/choices.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/netbox/dcim/choices.py b/netbox/dcim/choices.py index f1485b67f..8c1888cc2 100644 --- a/netbox/dcim/choices.py +++ b/netbox/dcim/choices.py @@ -1135,6 +1135,7 @@ class InterfacePoETypeChoices(ChoiceSet): TYPE_1_8023AF = 'type1-ieee802.3af' TYPE_2_8023AT = 'type2-ieee802.3at' + TYPE_2_8023AZ = 'type2-ieee802.3az' TYPE_3_8023BT = 'type3-ieee802.3bt' TYPE_4_8023BT = 'type4-ieee802.3bt' @@ -1149,6 +1150,7 @@ class InterfacePoETypeChoices(ChoiceSet): ( (TYPE_1_8023AF, '802.3af (Type 1)'), (TYPE_2_8023AT, '802.3at (Type 2)'), + (TYPE_2_8023AZ, '802.3az (Type 2)'), (TYPE_3_8023BT, '802.3bt (Type 3)'), (TYPE_4_8023BT, '802.3bt (Type 4)'), ) From 6640fc9eb73731bc2a1731e80ada853198f13db5 Mon Sep 17 00:00:00 2001 From: rmanyari Date: Wed, 1 Mar 2023 14:49:40 -0700 Subject: [PATCH 080/144] Fixes #11470: Validation and user friendly message on invalid address query param (#11858) * Fixes #11470: Validation and user friendly message on invalid address query param * Update invalid input handling to return empty set instead of raising exception --- netbox/ipam/filtersets.py | 27 +++++++++++++++++++++++++++ netbox/ipam/tests/test_filtersets.py | 21 +++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/netbox/ipam/filtersets.py b/netbox/ipam/filtersets.py index 2e9f56bbc..dbda8811f 100644 --- a/netbox/ipam/filtersets.py +++ b/netbox/ipam/filtersets.py @@ -16,6 +16,7 @@ from virtualization.models import VirtualMachine, VMInterface from .choices import * from .models import * +from rest_framework import serializers __all__ = ( 'AggregateFilterSet', @@ -599,7 +600,33 @@ class IPAddressFilterSet(NetBoxModelFilterSet, TenancyFilterSet): return queryset.none() return queryset.filter(q) + def parse_inet_addresses(self, value): + ''' + Parse networks or IP addresses and cast to a format + acceptable by the Postgres inet type. + + Skips invalid values. + ''' + parsed = [] + for addr in value: + if netaddr.valid_ipv4(addr) or netaddr.valid_ipv6(addr): + parsed.append(addr) + continue + try: + network = netaddr.IPNetwork(addr) + parsed.append(str(network)) + except (AddrFormatError, ValueError): + continue + return parsed + def filter_address(self, queryset, name, value): + # Let's first parse the addresses passed + # as argument. If they are all invalid, + # we return an empty queryset + value = self.parse_inet_addresses(value) + if (len(value) == 0): + return queryset.none() + try: return queryset.filter(address__net_in=value) except ValidationError: diff --git a/netbox/ipam/tests/test_filtersets.py b/netbox/ipam/tests/test_filtersets.py index 13b3ae163..c53522d7a 100644 --- a/netbox/ipam/tests/test_filtersets.py +++ b/netbox/ipam/tests/test_filtersets.py @@ -10,6 +10,7 @@ from ipam.models import * from utilities.testing import ChangeLoggedFilterSetTests, create_test_device, create_test_virtualmachine from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface from tenancy.models import Tenant, TenantGroup +from rest_framework import serializers class ASNTestCase(TestCase, ChangeLoggedFilterSetTests): @@ -851,6 +852,26 @@ class IPAddressTestCase(TestCase, ChangeLoggedFilterSetTests): params = {'address': ['2001:db8::1/64', '2001:db8::1/65']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + # Check for valid edge cases. Note that Postgres inet type + # only accepts netmasks in the int form, so the filterset + # casts netmasks in the xxx.xxx.xxx.xxx format. + params = {'address': ['24']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 0) + params = {'address': ['10.0.0.1/255.255.255.0']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + params = {'address': ['10.0.0.1/255.255.255.0', '10.0.0.1/25']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + # Check for invalid input. + params = {'address': ['/24']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 0) + params = {'address': ['10.0.0.1/255.255.999.0']} # Invalid netmask + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 0) + + # Check for partially invalid input. + params = {'address': ['10.0.0.1', '/24', '10.0.0.10/24']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_mask_length(self): params = {'mask_length': '24'} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5) From e270cb20ba40552947003f1c6779ef8c0559cf4f Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 1 Mar 2023 17:34:57 -0500 Subject: [PATCH 081/144] Changelog for #11470, #11871 --- docs/release-notes/version-3.4.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/release-notes/version-3.4.md b/docs/release-notes/version-3.4.md index 3049853ea..b708947d6 100644 --- a/docs/release-notes/version-3.4.md +++ b/docs/release-notes/version-3.4.md @@ -8,9 +8,11 @@ * [#11011](https://github.com/netbox-community/netbox/issues/11011) - Add ability to toggle visibility of virtual interfaces under device view * [#11807](https://github.com/netbox-community/netbox/issues/11807) - Restore default page size when navigating between views * [#11817](https://github.com/netbox-community/netbox/issues/11817) - Add `connected_endpoints` field to GraphQL API for cabled objects +* [#11871](https://github.com/netbox-community/netbox/issues/11871) - Add IEEE 802.3az PoE type for interfaces ### Bug Fixes +* [#11470](https://github.com/netbox-community/netbox/issues/11470) - Avoid raising exception when filtering IPs by an invalid address * [#11565](https://github.com/netbox-community/netbox/issues/11565) - Apply custom field defaults to IP address created during FHRP group creation * [#11758](https://github.com/netbox-community/netbox/issues/11758) - Support non-URL-safe characters in plugin menu titles * [#11796](https://github.com/netbox-community/netbox/issues/11796) - When importing devices, restrict rack by location only if the location field is specified From 07b39fe44ad47148d786e1c17df8bc51ce930c27 Mon Sep 17 00:00:00 2001 From: Ximalas Date: Thu, 2 Mar 2023 14:59:08 +0100 Subject: [PATCH 082/144] Update choices.py: Adding Cisco StackWise-1T (#11886) Cisco Catalyst 9300X Series adds Cisco StackWise-1T. https://www.cisco.com/c/en/us/products/collateral/switches/catalyst-9300-series-switches/nb-06-cat9300-ser-data-sheet-cte-en.html --- netbox/dcim/choices.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/netbox/dcim/choices.py b/netbox/dcim/choices.py index 8c1888cc2..c495c42ec 100644 --- a/netbox/dcim/choices.py +++ b/netbox/dcim/choices.py @@ -902,6 +902,7 @@ class InterfaceTypeChoices(ChoiceSet): TYPE_STACKWISE160 = 'cisco-stackwise-160' TYPE_STACKWISE320 = 'cisco-stackwise-320' TYPE_STACKWISE480 = 'cisco-stackwise-480' + TYPE_STACKWISE1T = 'cisco-stackwise-1t' TYPE_JUNIPER_VCP = 'juniper-vcp' TYPE_SUMMITSTACK = 'extreme-summitstack' TYPE_SUMMITSTACK128 = 'extreme-summitstack-128' @@ -1078,6 +1079,7 @@ class InterfaceTypeChoices(ChoiceSet): (TYPE_STACKWISE160, 'Cisco StackWise-160'), (TYPE_STACKWISE320, 'Cisco StackWise-320'), (TYPE_STACKWISE480, 'Cisco StackWise-480'), + (TYPE_STACKWISE1T, 'Cisco StackWise-1T'), (TYPE_JUNIPER_VCP, 'Juniper VCP'), (TYPE_SUMMITSTACK, 'Extreme SummitStack'), (TYPE_SUMMITSTACK128, 'Extreme SummitStack-128'), From d29a4a60f9e5d40698d48be452f4ba19ba8c8812 Mon Sep 17 00:00:00 2001 From: jose_d Date: Fri, 3 Mar 2023 17:22:38 +0100 Subject: [PATCH 083/144] README.md: typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 053aa8461..22e53f0da 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ as the cornerstone for network automation in thousands of organizations. * **Physical infrasucture:** Accurately model the physical world, from global regions down to individual racks of gear. Then connect everything - network, console, and power! * **Modern IPAM:** All the standard IPAM functionality you expect, plus VRF import/export tracking, VLAN management, and overlay support. -* **Data circuits:** Confidently manage the delivery of crtical circuits from various service providers, modeled seamlessly alongside your own infrastructure. +* **Data circuits:** Confidently manage the delivery of critical circuits from various service providers, modeled seamlessly alongside your own infrastructure. * **Power tracking:** Map the distribution of power from upstream sources to individual feeds and outlets. * **Organization:** Manage tenant and contact assignments natively. * **Powerful search:** Easily find anything you need using a single global search function. From ee5b707e688f62e3b08242fe2ea39e26b42d1534 Mon Sep 17 00:00:00 2001 From: Charly Forot <97433029+charlyforot@users.noreply.github.com> Date: Mon, 6 Mar 2023 16:46:33 +0100 Subject: [PATCH 084/144] README.md: typo infrasucture -> infrastructure --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 22e53f0da..e3c9611c0 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ NetBox provides the ideal "source of truth" to power network automation. Available as open source software under the Apache 2.0 license, NetBox serves as the cornerstone for network automation in thousands of organizations. -* **Physical infrasucture:** Accurately model the physical world, from global regions down to individual racks of gear. Then connect everything - network, console, and power! +* **Physical infrastructure:** Accurately model the physical world, from global regions down to individual racks of gear. Then connect everything - network, console, and power! * **Modern IPAM:** All the standard IPAM functionality you expect, plus VRF import/export tracking, VLAN management, and overlay support. * **Data circuits:** Confidently manage the delivery of critical circuits from various service providers, modeled seamlessly alongside your own infrastructure. * **Power tracking:** Map the distribution of power from upstream sources to individual feeds and outlets. From d48a8770de78abf7f46004b3330f1e9f14f83755 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 7 Mar 2023 09:34:25 -0500 Subject: [PATCH 085/144] Fixes #11903: Fix escaping of return URL values for action buttons in tables --- docs/release-notes/version-3.4.md | 2 ++ netbox/netbox/tables/columns.py | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/release-notes/version-3.4.md b/docs/release-notes/version-3.4.md index b708947d6..171e5813d 100644 --- a/docs/release-notes/version-3.4.md +++ b/docs/release-notes/version-3.4.md @@ -8,6 +8,7 @@ * [#11011](https://github.com/netbox-community/netbox/issues/11011) - Add ability to toggle visibility of virtual interfaces under device view * [#11807](https://github.com/netbox-community/netbox/issues/11807) - Restore default page size when navigating between views * [#11817](https://github.com/netbox-community/netbox/issues/11817) - Add `connected_endpoints` field to GraphQL API for cabled objects +* [#11862](https://github.com/netbox-community/netbox/issues/11862) - Add Cisco StackWise 1T interface type * [#11871](https://github.com/netbox-community/netbox/issues/11871) - Add IEEE 802.3az PoE type for interfaces ### Bug Fixes @@ -16,6 +17,7 @@ * [#11565](https://github.com/netbox-community/netbox/issues/11565) - Apply custom field defaults to IP address created during FHRP group creation * [#11758](https://github.com/netbox-community/netbox/issues/11758) - Support non-URL-safe characters in plugin menu titles * [#11796](https://github.com/netbox-community/netbox/issues/11796) - When importing devices, restrict rack by location only if the location field is specified +* [#11903](https://github.com/netbox-community/netbox/issues/11903) - Fix escaping of return URL values for action buttons in tables --- diff --git a/netbox/netbox/tables/columns.py b/netbox/netbox/tables/columns.py index 519f6021e..66ee787a8 100644 --- a/netbox/netbox/tables/columns.py +++ b/netbox/netbox/tables/columns.py @@ -1,5 +1,6 @@ from dataclasses import dataclass from typing import Optional +from urllib.parse import quote import django_tables2 as tables from django.conf import settings @@ -8,7 +9,6 @@ from django.db.models import DateField, DateTimeField from django.template import Context, Template from django.urls import reverse from django.utils.dateparse import parse_date -from django.utils.encoding import escape_uri_path from django.utils.html import escape from django.utils.formats import date_format from django.utils.safestring import mark_safe @@ -235,7 +235,7 @@ class ActionsColumn(tables.Column): model = table.Meta.model request = getattr(table, 'context', {}).get('request') - url_appendix = f'?return_url={escape_uri_path(request.get_full_path())}' if request else '' + url_appendix = f'?return_url={quote(request.get_full_path())}' if request else '' html = '' # Compile actions menu From 33286aad391de51a460189649c5187ddbd8ed4ce Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Fri, 3 Mar 2023 00:41:38 +0530 Subject: [PATCH 086/144] added the missing filterset --- netbox/dcim/filtersets.py | 1 + 1 file changed, 1 insertion(+) diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py index 1ea56b3ef..493ccbbea 100644 --- a/netbox/dcim/filtersets.py +++ b/netbox/dcim/filtersets.py @@ -1727,6 +1727,7 @@ class CableFilterSet(TenancyFilterSet, NetBoxModelFilterSet): class CableTerminationFilterSet(BaseFilterSet): + termination_type = ContentTypeFilter() class Meta: model = CableTermination From fa60f9d2a882df13cc8e8b47a4f227455897f815 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aron=20Bergur=20J=C3=B3hannsson?= Date: Thu, 9 Mar 2023 13:21:13 +0000 Subject: [PATCH 087/144] Closes #11294: Markdown Preview (#11894) * MarkdownWidget * Change border and color of active markdown tab * Fix template name typo * Add render markdown endpoint * Static assets for markdown widget * widget style fix and unique ids based on name * Replace SmallTextArea with SmallMarkdownWidget * Clear innerHTML before swapping * render markdown directly in template * change render markdown view path * remove small markdown widget * Simplify rendering logic * Use a form to clean input Markdown data --------- Co-authored-by: Jeremy Stretch --- netbox/circuits/forms/bulk_edit.py | 5 +- netbox/dcim/forms/bulk_edit.py | 13 +---- netbox/extras/forms/__init__.py | 1 + netbox/extras/forms/misc.py | 14 ++++++ netbox/extras/urls.py | 2 + netbox/extras/views.py | 18 ++++++- netbox/ipam/forms/bulk_edit.py | 13 +---- netbox/project-static/dist/netbox-dark.css | Bin 374883 -> 375160 bytes netbox/project-static/dist/netbox-light.css | Bin 232430 -> 232605 bytes netbox/project-static/dist/netbox-print.css | Bin 726343 -> 726930 bytes netbox/project-static/dist/netbox.js | Bin 380966 -> 381466 bytes netbox/project-static/dist/netbox.js.map | Bin 353776 -> 354201 bytes netbox/project-static/src/buttons/index.ts | 2 + .../src/buttons/markdownPreview.ts | 45 ++++++++++++++++++ netbox/project-static/styles/netbox.scss | 29 ++++++++--- netbox/tenancy/forms/bulk_edit.py | 3 +- netbox/utilities/forms/fields/fields.py | 2 +- netbox/utilities/forms/widgets.py | 5 ++ .../templates/form_helpers/render_field.html | 2 +- .../templates/widgets/markdown_input.html | 22 +++++++++ netbox/virtualization/forms/bulk_edit.py | 4 +- netbox/wireless/forms/bulk_edit.py | 4 +- 22 files changed, 138 insertions(+), 46 deletions(-) create mode 100644 netbox/extras/forms/misc.py create mode 100644 netbox/project-static/src/buttons/markdownPreview.ts create mode 100644 netbox/utilities/templates/widgets/markdown_input.html diff --git a/netbox/circuits/forms/bulk_edit.py b/netbox/circuits/forms/bulk_edit.py index e1fe6338d..0449f8e99 100644 --- a/netbox/circuits/forms/bulk_edit.py +++ b/netbox/circuits/forms/bulk_edit.py @@ -7,7 +7,7 @@ from ipam.models import ASN from netbox.forms import NetBoxModelBulkEditForm from tenancy.models import Tenant from utilities.forms import ( - add_blank_choice, CommentField, DatePicker, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SmallTextarea, + add_blank_choice, CommentField, DatePicker, DynamicModelChoiceField, DynamicModelMultipleChoiceField, StaticSelect, ) @@ -35,7 +35,6 @@ class ProviderBulkEditForm(NetBoxModelBulkEditForm): required=False ) comments = CommentField( - widget=SmallTextarea, label=_('Comments') ) @@ -63,7 +62,6 @@ class ProviderNetworkBulkEditForm(NetBoxModelBulkEditForm): required=False ) comments = CommentField( - widget=SmallTextarea, label=_('Comments') ) @@ -125,7 +123,6 @@ class CircuitBulkEditForm(NetBoxModelBulkEditForm): required=False ) comments = CommentField( - widget=SmallTextarea, label=_('Comments') ) diff --git a/netbox/dcim/forms/bulk_edit.py b/netbox/dcim/forms/bulk_edit.py index 38fa55738..bd466ca48 100644 --- a/netbox/dcim/forms/bulk_edit.py +++ b/netbox/dcim/forms/bulk_edit.py @@ -11,7 +11,7 @@ from netbox.forms import NetBoxModelBulkEditForm from tenancy.models import Tenant from utilities.forms import ( add_blank_choice, BulkEditForm, BulkEditNullBooleanSelect, ColorField, CommentField, DynamicModelChoiceField, - DynamicModelMultipleChoiceField, form_from_model, SmallTextarea, StaticSelect, SelectSpeedWidget, + DynamicModelMultipleChoiceField, form_from_model, StaticSelect, SelectSpeedWidget ) __all__ = ( @@ -138,7 +138,6 @@ class SiteBulkEditForm(NetBoxModelBulkEditForm): required=False ) comments = CommentField( - widget=SmallTextarea, label='Comments' ) @@ -309,7 +308,6 @@ class RackBulkEditForm(NetBoxModelBulkEditForm): required=False ) comments = CommentField( - widget=SmallTextarea, label='Comments' ) @@ -345,7 +343,6 @@ class RackReservationBulkEditForm(NetBoxModelBulkEditForm): required=False ) comments = CommentField( - widget=SmallTextarea, label='Comments' ) @@ -406,7 +403,6 @@ class DeviceTypeBulkEditForm(NetBoxModelBulkEditForm): required=False ) comments = CommentField( - widget=SmallTextarea, label='Comments' ) @@ -441,7 +437,6 @@ class ModuleTypeBulkEditForm(NetBoxModelBulkEditForm): required=False ) comments = CommentField( - widget=SmallTextarea, label='Comments' ) @@ -551,7 +546,6 @@ class DeviceBulkEditForm(NetBoxModelBulkEditForm): required=False ) comments = CommentField( - widget=SmallTextarea, label='Comments' ) @@ -594,7 +588,6 @@ class ModuleBulkEditForm(NetBoxModelBulkEditForm): required=False ) comments = CommentField( - widget=SmallTextarea, label='Comments' ) @@ -644,7 +637,6 @@ class CableBulkEditForm(NetBoxModelBulkEditForm): required=False ) comments = CommentField( - widget=SmallTextarea, label='Comments' ) @@ -668,7 +660,6 @@ class VirtualChassisBulkEditForm(NetBoxModelBulkEditForm): required=False ) comments = CommentField( - widget=SmallTextarea, label='Comments' ) @@ -714,7 +705,6 @@ class PowerPanelBulkEditForm(NetBoxModelBulkEditForm): required=False ) comments = CommentField( - widget=SmallTextarea, label='Comments' ) @@ -776,7 +766,6 @@ class PowerFeedBulkEditForm(NetBoxModelBulkEditForm): required=False ) comments = CommentField( - widget=SmallTextarea, label=_('Comments') ) diff --git a/netbox/extras/forms/__init__.py b/netbox/extras/forms/__init__.py index af0f7cf43..0825c9ca7 100644 --- a/netbox/extras/forms/__init__.py +++ b/netbox/extras/forms/__init__.py @@ -2,6 +2,7 @@ from .model_forms import * from .filtersets import * from .bulk_edit import * from .bulk_import import * +from .misc import * from .mixins import * from .config import * from .scripts import * diff --git a/netbox/extras/forms/misc.py b/netbox/extras/forms/misc.py new file mode 100644 index 000000000..b52338e76 --- /dev/null +++ b/netbox/extras/forms/misc.py @@ -0,0 +1,14 @@ +from django import forms + +__all__ = ( + 'RenderMarkdownForm', +) + + +class RenderMarkdownForm(forms.Form): + """ + Provides basic validation for markup to be rendered. + """ + text = forms.CharField( + required=False + ) diff --git a/netbox/extras/urls.py b/netbox/extras/urls.py index f41a45f5a..304e5b9ea 100644 --- a/netbox/extras/urls.py +++ b/netbox/extras/urls.py @@ -92,4 +92,6 @@ urlpatterns = [ path('scripts/results//', views.ScriptResultView.as_view(), name='script_result'), re_path(r'^scripts/(?P.([^.]+)).(?P.(.+))/', views.ScriptView.as_view(), name='script'), + # Markdown + path('render/markdown/', views.RenderMarkdownView.as_view(), name="render_markdown") ] diff --git a/netbox/extras/views.py b/netbox/extras/views.py index 2d2608ae8..91d3b5c58 100644 --- a/netbox/extras/views.py +++ b/netbox/extras/views.py @@ -1,7 +1,7 @@ from django.contrib import messages from django.contrib.contenttypes.models import ContentType from django.db.models import Count, Q -from django.http import Http404, HttpResponseForbidden +from django.http import Http404, HttpResponseBadRequest, HttpResponseForbidden, HttpResponse from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse from django.views.generic import View @@ -10,6 +10,7 @@ from rq import Worker from netbox.views import generic from utilities.htmx import is_htmx +from utilities.templatetags.builtins.filters import render_markdown from utilities.utils import copy_safe_request, count_related, get_viewname, normalize_querydict, shallow_compare_dict from utilities.views import ContentTypePermissionRequiredMixin, register_model_view from . import filtersets, forms, tables @@ -885,3 +886,18 @@ class JobResultBulkDeleteView(generic.BulkDeleteView): queryset = JobResult.objects.all() filterset = filtersets.JobResultFilterSet table = tables.JobResultTable + + +# +# Markdown +# + +class RenderMarkdownView(View): + + def post(self, request): + form = forms.RenderMarkdownForm(request.POST) + if not form.is_valid(): + HttpResponseBadRequest() + rendered = render_markdown(form.cleaned_data['text']) + + return HttpResponse(rendered) diff --git a/netbox/ipam/forms/bulk_edit.py b/netbox/ipam/forms/bulk_edit.py index d0af43975..63352698b 100644 --- a/netbox/ipam/forms/bulk_edit.py +++ b/netbox/ipam/forms/bulk_edit.py @@ -10,7 +10,7 @@ from netbox.forms import NetBoxModelBulkEditForm from tenancy.models import Tenant from utilities.forms import ( add_blank_choice, BulkEditNullBooleanSelect, CommentField, DynamicModelChoiceField, NumericArrayField, - SmallTextarea, StaticSelect, DynamicModelMultipleChoiceField, + StaticSelect, DynamicModelMultipleChoiceField ) __all__ = ( @@ -48,7 +48,6 @@ class VRFBulkEditForm(NetBoxModelBulkEditForm): required=False ) comments = CommentField( - widget=SmallTextarea, label='Comments' ) @@ -69,7 +68,6 @@ class RouteTargetBulkEditForm(NetBoxModelBulkEditForm): required=False ) comments = CommentField( - widget=SmallTextarea, label='Comments' ) @@ -116,7 +114,6 @@ class ASNBulkEditForm(NetBoxModelBulkEditForm): required=False ) comments = CommentField( - widget=SmallTextarea, label='Comments' ) @@ -145,7 +142,6 @@ class AggregateBulkEditForm(NetBoxModelBulkEditForm): required=False ) comments = CommentField( - widget=SmallTextarea, label='Comments' ) @@ -227,7 +223,6 @@ class PrefixBulkEditForm(NetBoxModelBulkEditForm): required=False ) comments = CommentField( - widget=SmallTextarea, label='Comments' ) @@ -266,7 +261,6 @@ class IPRangeBulkEditForm(NetBoxModelBulkEditForm): required=False ) comments = CommentField( - widget=SmallTextarea, label='Comments' ) @@ -314,7 +308,6 @@ class IPAddressBulkEditForm(NetBoxModelBulkEditForm): required=False ) comments = CommentField( - widget=SmallTextarea, label='Comments' ) @@ -359,7 +352,6 @@ class FHRPGroupBulkEditForm(NetBoxModelBulkEditForm): required=False ) comments = CommentField( - widget=SmallTextarea, label='Comments' ) @@ -442,7 +434,6 @@ class VLANBulkEditForm(NetBoxModelBulkEditForm): required=False ) comments = CommentField( - widget=SmallTextarea, label='Comments' ) @@ -474,7 +465,6 @@ class ServiceTemplateBulkEditForm(NetBoxModelBulkEditForm): required=False ) comments = CommentField( - widget=SmallTextarea, label='Comments' ) @@ -504,7 +494,6 @@ class L2VPNBulkEditForm(NetBoxModelBulkEditForm): required=False ) comments = CommentField( - widget=SmallTextarea, label='Comments' ) diff --git a/netbox/project-static/dist/netbox-dark.css b/netbox/project-static/dist/netbox-dark.css index 8b6b37a4cc545892c5ab1d46c60ebe75f8bc5a40..74ab785a5794f756ce265e7c39fb9175b729c616 100644 GIT binary patch delta 139 zcmaF-LF~sTv4$4L7N!>F7M3ln+FKR%aubWPQ}WC6bjveS(o;(m^zstRbaOKEva6Hw zi&9dHrVF?+>GGqgn*Q36NqPE)HLP6hN%<+2x=HEN8ILfj@u8YEU3vqn3|JLgK~XAD Q(PYCZD%-WUvRW_$0C(my4*&oF delta 25 hcmezIN$l|lv4$4L7N!>F7M3ln+FQ3fY-6=x1^}5m3JL%K diff --git a/netbox/project-static/dist/netbox-light.css b/netbox/project-static/dist/netbox-light.css index 27e804c3084267eba5a70d9352ea5fb676a6d35f..f12291e44d1dc368699f6c51307d5d63d40dd267 100644 GIT binary patch delta 139 zcmaDiop0_;zJ?aY7N#xCOFFIfaubWPQ}WC6bjveS(o;(m^zstRbaOKEva6Hwi&9dH zbd&N+O7e593~JGpPT$(btUT>86PHp_eoCcoQo1IF5{PLfiAmERYBC9f)J}if$;`fe JaVN7k69A~~G`#=- delta 21 dcmbO`lkeShzJ?aY7N#xCOFFj)cQJc00RUrE2r2*o diff --git a/netbox/project-static/dist/netbox-print.css b/netbox/project-static/dist/netbox-print.css index f0220c0500b66f04fbdffb8e04f83bc86900c527..5d7f7342be9c1ee67fd170bc98a19cebc26f3aec 100644 GIT binary patch delta 302 zcmX@UN@vn~orV_17N!>F7M2#)7Pc1l7LFFq7OpMa|9J)UaubWPQ}WC6bjveS(o;*O ze_YO`$B!;J(UL{q9!*d|FE6o7HzzYMyE-YqC?&N>Hz~iQBtO^6pf;l~bS+R&eY(L~R!&J&iy$s8 gNlYrPo-SC!Da}?;l&YJQK3!3PQFQx%UTzO|02dEy?EnA( delta 37 tcmbQVUg!8KorV_17N!>F7M2#)7Pc1l7LFFq7OpMa|9Q8232=L`0|5V+3?Kji diff --git a/netbox/project-static/dist/netbox.js b/netbox/project-static/dist/netbox.js index ed6e127477872c2c111e2c18a555bdc54769df5f..f430604f967c52fa5ed13c20a60064e1cdff88ac 100644 GIT binary patch delta 17996 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 MAfWlv^U4VRe}M}_w*UYD delta 17547 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 M1lM}!Q8tMGA3BYxr2qf` diff --git a/netbox/project-static/dist/netbox.js.map b/netbox/project-static/dist/netbox.js.map index dd1ec47c90c838edcfb2385e1f8e8a65c560d54a..753576bc313c4c012b2bb99d14f14c2fd9eda6dc 100644 GIT binary patch delta 465 zcmYjNtxm&G6waUs6l!Q>6OLP;j^^A$S(>JlrdgKCWi+fCFjkUvU@8zOFo=b>z)d4i z2qZ6o;1M|Iv>_AsC*R-s?*092zkapfYvu z+FaSL^?q8-PdWxt#6tsJ#0vsK>Kw~G1fKzgGZ-Qc;m#mABwSy%22GI`3ij5(48vOK zb08|62%SJE$+6sc(oLmQC?XH~O>hDwPdd=ijUf^P`@0ZfD+kNd>4*D@$x)n_C?`JWeZhMNEoiSzmm423^ F*8vBm6S4pR diff --git a/netbox/project-static/src/buttons/index.ts b/netbox/project-static/src/buttons/index.ts index e677ff599..fe2ccaaef 100644 --- a/netbox/project-static/src/buttons/index.ts +++ b/netbox/project-static/src/buttons/index.ts @@ -4,6 +4,7 @@ import { initMoveButtons } from './moveOptions'; import { initReslug } from './reslug'; import { initSelectAll } from './selectAll'; import { initSelectMultiple } from './selectMultiple'; +import { initMarkdownPreviews } from './markdownPreview'; export function initButtons(): void { for (const func of [ @@ -13,6 +14,7 @@ export function initButtons(): void { initSelectAll, initSelectMultiple, initMoveButtons, + initMarkdownPreviews, ]) { func(); } diff --git a/netbox/project-static/src/buttons/markdownPreview.ts b/netbox/project-static/src/buttons/markdownPreview.ts new file mode 100644 index 000000000..224b2beab --- /dev/null +++ b/netbox/project-static/src/buttons/markdownPreview.ts @@ -0,0 +1,45 @@ +import { isTruthy } from 'src/util'; + +/** + * interface for htmx configRequest event + */ +declare global { + interface HTMLElementEventMap { + 'htmx:configRequest': CustomEvent<{ + parameters: Record; + headers: Record; + }>; + } +} + +function initMarkdownPreview(markdownWidget: HTMLDivElement) { + const previewButton = markdownWidget.querySelector('button.preview-button') as HTMLButtonElement; + const textarea = markdownWidget.querySelector('textarea') as HTMLTextAreaElement; + const preview = markdownWidget.querySelector('div.preview') as HTMLDivElement; + + /** + * Make sure the textarea has style attribute height + * So that it can be copied over to preview div. + */ + if (!isTruthy(textarea.style.height)) { + const { height } = textarea.getBoundingClientRect(); + textarea.style.height = `${height}px`; + } + + /** + * Add the value of the textarea to the body of the htmx request + * and copy the height of text are to the preview div + */ + previewButton.addEventListener('htmx:configRequest', e => { + e.detail.parameters = { text: textarea.value || '' }; + e.detail.headers['X-CSRFToken'] = window.CSRF_TOKEN; + preview.style.minHeight = textarea.style.height; + preview.innerHTML = ''; + }); +} + +export function initMarkdownPreviews(): void { + for (const markdownWidget of document.querySelectorAll('.markdown-widget')) { + initMarkdownPreview(markdownWidget); + } +} diff --git a/netbox/project-static/styles/netbox.scss b/netbox/project-static/styles/netbox.scss index e486bc7db..37f6c21c4 100644 --- a/netbox/project-static/styles/netbox.scss +++ b/netbox/project-static/styles/netbox.scss @@ -236,12 +236,12 @@ table { } th.asc > a::after { - content: "\f0140"; + content: '\f0140'; font-family: 'Material Design Icons'; } th.desc > a::after { - content: "\f0143"; + content: '\f0143'; font-family: 'Material Design Icons'; } @@ -416,18 +416,18 @@ nav.search { } } -// Styles for the quicksearch and its clear button; +// Styles for the quicksearch and its clear button; // Overrides input-group styles and adds transition effects .quicksearch { - input[type="search"] { - border-radius: $border-radius !important; + input[type='search'] { + border-radius: $border-radius !important; } button { margin-left: -32px !important; z-index: 100 !important; outline: none !important; - border-radius: $border-radius !important; + border-radius: $border-radius !important; transition: visibility 0s, opacity 0.2s linear; } @@ -998,9 +998,24 @@ div.card-overlay { padding: 8px; } +/* Markdown widget */ +.markdown-widget { + .nav-link { + border-bottom: 0; + + &.active { + background-color: var(--nbx-body-bg); + } + } + + .nav-tabs { + background-color: var(--nbx-pre-bg); + } +} + // Preformatted text blocks td pre { - margin-bottom: 0 + margin-bottom: 0; } pre.block { padding: $spacer; diff --git a/netbox/tenancy/forms/bulk_edit.py b/netbox/tenancy/forms/bulk_edit.py index 183a8e851..ab882fe7e 100644 --- a/netbox/tenancy/forms/bulk_edit.py +++ b/netbox/tenancy/forms/bulk_edit.py @@ -2,7 +2,7 @@ from django import forms from netbox.forms import NetBoxModelBulkEditForm from tenancy.models import * -from utilities.forms import CommentField, DynamicModelChoiceField, SmallTextarea +from utilities.forms import CommentField, DynamicModelChoiceField __all__ = ( 'ContactBulkEditForm', @@ -106,7 +106,6 @@ class ContactBulkEditForm(NetBoxModelBulkEditForm): required=False ) comments = CommentField( - widget=SmallTextarea, label='Comments' ) diff --git a/netbox/utilities/forms/fields/fields.py b/netbox/utilities/forms/fields/fields.py index bb6c3f73b..ee9543452 100644 --- a/netbox/utilities/forms/fields/fields.py +++ b/netbox/utilities/forms/fields/fields.py @@ -27,7 +27,7 @@ class CommentField(forms.CharField): """ A textarea with support for Markdown rendering. Exists mostly just to add a standard `help_text`. """ - widget = forms.Textarea + widget = widgets.MarkdownWidget help_text = f""" diff --git a/netbox/utilities/forms/widgets.py b/netbox/utilities/forms/widgets.py index 1802306f1..bd828bb8f 100644 --- a/netbox/utilities/forms/widgets.py +++ b/netbox/utilities/forms/widgets.py @@ -16,6 +16,7 @@ __all__ = ( 'ColorSelect', 'DatePicker', 'DateTimePicker', + 'MarkdownWidget', 'NumericArrayField', 'SelectDurationWidget', 'SelectSpeedWidget', @@ -116,6 +117,10 @@ class SelectDurationWidget(forms.NumberInput): template_name = 'widgets/select_duration.html' +class MarkdownWidget(forms.Textarea): + template_name = 'widgets/markdown_input.html' + + class NumericArrayField(SimpleArrayField): def clean(self, value): diff --git a/netbox/utilities/templates/form_helpers/render_field.html b/netbox/utilities/templates/form_helpers/render_field.html index ec9ceb09a..85c04df92 100644 --- a/netbox/utilities/templates/form_helpers/render_field.html +++ b/netbox/utilities/templates/form_helpers/render_field.html @@ -6,7 +6,7 @@ {# Render the field label, except for: #} {# 1. Checkboxes (label appears to the right of the field #} {# 2. Textareas with no label set (will expand across entire row) #} - {% if field|widget_type == 'checkboxinput' or field|widget_type == 'textarea' and not label %} + {% if field|widget_type == 'checkboxinput' or field|widget_type == 'textarea' or field|widget_type == 'markdownwidget' and not label %} {% else %}