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!
+ [](https://netboxlabs.com)
+
[](https://try.digitalocean.com/developer-cloud)
- [](https://metal.equinix.com/)
-
- [](https://ns1.com/)
+ [](https://ns1.com)
- [](https://sentry.io/)
+ [](https://sentry.io)
- [](https://stellar.tech/)
+ [](https://metal.equinix.com)
+
+ [](https://stellar.tech)
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
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/
diff --git a/docs/release-notes/version-3.4.md b/docs/release-notes/version-3.4.md
index bf32f2d26..5134c9972 100644
--- a/docs/release-notes/version-3.4.md
+++ b/docs/release-notes/version-3.4.md
@@ -2,6 +2,18 @@
## 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
+* [#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
+
---
## v3.4.3 (2023-01-20)
@@ -34,8 +46,9 @@
* [#11483](https://github.com/netbox-community/netbox/issues/11483) - Apply configured formatting to custom date fields
* [#11488](https://github.com/netbox-community/netbox/issues/11488) - Add missing `description` fields to several REST API serializers
* [#11497](https://github.com/netbox-community/netbox/issues/11497) - Enforce `run_script` permission when executing scripts via REST API
-* [#11516](https://github.com/netbox-community/netbox/issues/11516) - Prevent text highlight utility from interpreting match as regex
+* ~[#11516](https://github.com/netbox-community/netbox/issues/11516) - Prevent text highlight utility from interpreting match as regex~
* [#11522](https://github.com/netbox-community/netbox/issues/11522) - Correct tag links under contact & tenant list views
+* [#11537](https://github.com/netbox-community/netbox/issues/11537) - Remove obsolete "Connection" column from power feeds table
* [#11544](https://github.com/netbox-community/netbox/issues/11544) - Catch ValidationError exception when filtering by invalid MAC address
---
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/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',
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 1fabc4bd9..3b5602baa 100644
--- a/netbox/dcim/views.py
+++ b/netbox/dcim/views.py
@@ -1994,22 +1994,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/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 = {
diff --git a/netbox/netbox/forms/base.py b/netbox/netbox/forms/base.py
index a8b41d139..83c238e0f 100644
--- a/netbox/netbox/forms/base.py
+++ b/netbox/netbox/forms/base.py
@@ -122,7 +122,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)
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 @@
-
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
)
diff --git a/netbox/utilities/utils.py b/netbox/utilities/utils.py
index b6f626eb4..23c2666df 100644
--- a/netbox/utilities/utils.py
+++ b/netbox/utilities/utils.py
@@ -527,6 +527,7 @@ def highlight_string(value, highlight, trim_pre=None, trim_post=None, trim_place
if type(highlight) is re.Pattern:
pre, match, post = highlight.split(value, maxsplit=1)
else:
+ highlight = re.escape(highlight)
pre, match, post = re.split(fr'({highlight})', value, maxsplit=1, flags=re.IGNORECASE)
except ValueError as e:
# Match not found