diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index ef31324fe2..b0c021af2b 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -17,7 +17,7 @@ body: What version of NetBox are you currently running? (If you don't have access to the most recent NetBox release, consider testing on our [demo instance](https://demo.netbox.dev/) before opening a bug report to see if your issue has already been addressed.) - placeholder: v2.11.10 + placeholder: v2.11.11 validations: required: true - type: dropdown diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml index 319538cda3..f13d45c3f5 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: v2.11.10 + placeholder: v2.11.11 validations: required: true - type: dropdown diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 0f617e8aaf..d8099923fb 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -8,7 +8,7 @@ jobs: stale: runs-on: ubuntu-latest steps: - - uses: actions/stale@v3 + - uses: actions/stale@v4 with: close-issue-message: > This issue has been automatically closed due to lack of activity. In an diff --git a/docs/additional-features/custom-links.md b/docs/additional-features/custom-links.md index 0a00b6d68a..196371ce31 100644 --- a/docs/additional-features/custom-links.md +++ b/docs/additional-features/custom-links.md @@ -17,6 +17,9 @@ When viewing a device named Router4, this link would render as: Custom links appear as buttons at the top right corner of the page. Numeric weighting can be used to influence the ordering of links. +!!! warning + Custom links rely on user-created code to generate arbitrary HTML output, which may be dangerous. Only grant permission to create or modify custom links to trusted users. + ## Context Data The following context data is available within the template when rendering a custom link's text or URL. diff --git a/docs/additional-features/export-templates.md b/docs/additional-features/export-templates.md index b3f585beee..6608074443 100644 --- a/docs/additional-features/export-templates.md +++ b/docs/additional-features/export-templates.md @@ -4,10 +4,13 @@ NetBox allows users to define custom templates that can be used when exporting o Each export template is associated with a certain type of object. For instance, if you create an export template for VLANs, your custom template will appear under the "Export" button on the VLANs list. Each export template must have a name, and may optionally designate a specific export [MIME type](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types) and/or file extension. +Export templates must be written in [Jinja2](https://jinja.palletsprojects.com/). + !!! note The name `table` is reserved for internal use. -Export templates must be written in [Jinja2](https://jinja.palletsprojects.com/). +!!! warning + Export templates are rendered using user-submitted code, which may pose security risks under certain conditions. Only grant permission to create or modify export templates to trusted users. The list of objects returned from the database when rendering an export template is stored in the `queryset` variable, which you'll typically want to iterate through using a `for` loop. Object properties can be access by name. For example: diff --git a/docs/additional-features/webhooks.md b/docs/additional-features/webhooks.md index f3dd803372..4fce4e037e 100644 --- a/docs/additional-features/webhooks.md +++ b/docs/additional-features/webhooks.md @@ -2,6 +2,9 @@ A webhook is a mechanism for conveying to some external system a change that took place in NetBox. For example, you may want to notify a monitoring system whenever the status of a device is updated in NetBox. This can be done by creating a webhook for the device model in NetBox and identifying the webhook receiver. When NetBox detects a change to a device, an HTTP request containing the details of the change and who made it be sent to the specified receiver. Webhooks are configured in the admin UI under Extras > Webhooks. +!!! warning + Webhooks support the inclusion of user-submitted code to generate custom headers and payloads, which may pose security risks under certain conditions. Only grant permission to create or modify webhooks to trusted users. + ## Configuration * **Name** - A unique name for the webhook. The name is not included with outbound messages. diff --git a/docs/installation/1-postgresql.md b/docs/installation/1-postgresql.md index 39008f1887..644b2715c6 100644 --- a/docs/installation/1-postgresql.md +++ b/docs/installation/1-postgresql.md @@ -11,13 +11,13 @@ This section entails the installation and configuration of a local PostgreSQL da ```no-highlight sudo apt update - sudo apt install -y postgresql libpq-dev + sudo apt install -y postgresql ``` === "CentOS" ```no-highlight - sudo yum install -y postgresql-server libpq-devel + sudo yum install -y postgresql-server sudo postgresql-setup --initdb ``` diff --git a/docs/installation/3-netbox.md b/docs/installation/3-netbox.md index e54bf6f3e6..eb717093d6 100644 --- a/docs/installation/3-netbox.md +++ b/docs/installation/3-netbox.md @@ -18,7 +18,7 @@ Begin by installing all system packages required by NetBox and its dependencies. === "CentOS" ```no-highlight - sudo yum install -y gcc python36 python36-devel python3-pip libxml2-devel libxslt-devel libffi-devel openssl-devel redhat-rpm-config + sudo yum install -y gcc python36 python36-devel python3-pip libxml2-devel libxslt-devel libffi-devel libpq-devel openssl-devel redhat-rpm-config ``` Before continuing with either platform, update pip (Python's package management tool) to its latest release: diff --git a/docs/media/installation/netbox_application_stack.png b/docs/media/installation/netbox_application_stack.png index e863449007..c860b964b1 100644 Binary files a/docs/media/installation/netbox_application_stack.png and b/docs/media/installation/netbox_application_stack.png differ diff --git a/docs/release-notes/version-2.11.md b/docs/release-notes/version-2.11.md index 736807e2c1..40df841c0e 100644 --- a/docs/release-notes/version-2.11.md +++ b/docs/release-notes/version-2.11.md @@ -1,5 +1,27 @@ # NetBox v2.11 +## v2.11.11 (2021-08-12) + +### Enhancements + +* [#6883](https://github.com/netbox-community/netbox/issues/6883) - Add C21 & C22 power types +* [#6921](https://github.com/netbox-community/netbox/issues/6921) - Employ a sandbox when rendering Jinja2 code for increased security + +### Bug Fixes + +* [#6740](https://github.com/netbox-community/netbox/issues/6740) - Add import button to VM interfaces list +* [#6892](https://github.com/netbox-community/netbox/issues/6892) - Fix validation of unit ranges when creating a rack reservation +* [#6896](https://github.com/netbox-community/netbox/issues/6896) - Fix validation of IP address assigned as device/VM primary via NAT relation +* [#6902](https://github.com/netbox-community/netbox/issues/6902) - Populate device field when cloning device components +* [#6908](https://github.com/netbox-community/netbox/issues/6908) - Allow assignment of scope to VLAN groups upon import +* [#6909](https://github.com/netbox-community/netbox/issues/6909) - Remove extraneous `site` column from VLAN group import form +* [#6910](https://github.com/netbox-community/netbox/issues/6910) - Fix exception on invalid CSV import column name +* [#6918](https://github.com/netbox-community/netbox/issues/6918) - Fix return URL persistence when adding multiple objects sequentially +* [#6935](https://github.com/netbox-community/netbox/issues/6935) - Remove extraneous columns from inventory item and device bay tables +* [#6936](https://github.com/netbox-community/netbox/issues/6936) - Add missing `parent` column to inventory item import form + +--- + ## v2.11.10 (2021-07-28) ### Enhancements diff --git a/docs/rest-api/filtering.md b/docs/rest-api/filtering.md index 471beffeef..45dfcfa36c 100644 --- a/docs/rest-api/filtering.md +++ b/docs/rest-api/filtering.md @@ -69,6 +69,12 @@ Numeric based fields (ASN, VLAN ID, etc) support these lookup expressions: | `gt` | Greater than | | `gte` | Greater than or equal to | +Here is an example of a numeric field lookup expression that will return all VLANs with a VLAN ID greater than 900: + +```no-highlight +GET /api/ipam/vlans/?vid__gt=900 +``` + ### String Fields String based (char) fields (Name, Address, etc) support these lookup expressions: @@ -86,7 +92,17 @@ String based (char) fields (Name, Address, etc) support these lookup expressions | `nie` | Inverse exact match (case-insensitive) | | `empty` | Is empty (boolean) | +Here is an example of a lookup expression on a string field that will return all devices with `switch` in the name: + +```no-highlight +GET /api/dcim/devices/?name__ic=switch +``` + ### Foreign Keys & Other Fields Certain other fields, namely foreign key relationships support just the negation -expression: `n`. +expression: `n`. Here is an example of a lookup expression on a foreign key, it would return all the VLANs that don't have a VLAN Group ID of 3203: + +```no-highlight +GET /api/ipam/vlans/?group_id__n=3203 +``` diff --git a/netbox/dcim/choices.py b/netbox/dcim/choices.py index 9a12e6a191..4216d93555 100644 --- a/netbox/dcim/choices.py +++ b/netbox/dcim/choices.py @@ -252,6 +252,7 @@ class PowerPortTypeChoices(ChoiceSet): TYPE_IEC_C14 = 'iec-60320-c14' TYPE_IEC_C16 = 'iec-60320-c16' TYPE_IEC_C20 = 'iec-60320-c20' + TYPE_IEC_C22 = 'iec-60320-c22' # IEC 60309 TYPE_IEC_PNE4H = 'iec-60309-p-n-e-4h' TYPE_IEC_PNE6H = 'iec-60309-p-n-e-6h' @@ -351,6 +352,7 @@ class PowerPortTypeChoices(ChoiceSet): (TYPE_IEC_C14, 'C14'), (TYPE_IEC_C16, 'C16'), (TYPE_IEC_C20, 'C20'), + (TYPE_IEC_C22, 'C22'), )), ('IEC 60309', ( (TYPE_IEC_PNE4H, 'P+N+E 4H'), @@ -467,6 +469,7 @@ class PowerOutletTypeChoices(ChoiceSet): TYPE_IEC_C13 = 'iec-60320-c13' TYPE_IEC_C15 = 'iec-60320-c15' TYPE_IEC_C19 = 'iec-60320-c19' + TYPE_IEC_C21 = 'iec-60320-c21' # IEC 60309 TYPE_IEC_PNE4H = 'iec-60309-p-n-e-4h' TYPE_IEC_PNE6H = 'iec-60309-p-n-e-6h' @@ -558,6 +561,7 @@ class PowerOutletTypeChoices(ChoiceSet): (TYPE_IEC_C13, 'C13'), (TYPE_IEC_C15, 'C15'), (TYPE_IEC_C19, 'C19'), + (TYPE_IEC_C21, 'C21'), )), ('IEC 60309', ( (TYPE_IEC_PNE4H, 'P+N+E 4H'), diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 565a75c45b..97f8721a5c 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -3852,11 +3852,32 @@ class InventoryItemCSVForm(CustomFieldModelCSVForm): to_field_name='name', required=False ) + parent = CSVModelChoiceField( + queryset=Device.objects.all(), + to_field_name='name', + required=False, + help_text='Parent inventory item' + ) class Meta: model = InventoryItem fields = InventoryItem.csv_headers + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # Limit parent choices to inventory items belonging to this device + device = None + if self.is_bound and 'device' in self.data: + try: + device = self.fields['device'].to_python(self.data['device']) + except forms.ValidationError: + pass + if device: + self.fields['parent'].queryset = InventoryItem.objects.filter(device=device) + else: + self.fields['parent'].queryset = InventoryItem.objects.none() + class InventoryItemBulkCreateForm( form_from_model(InventoryItem, ['manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered']), diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index 6ac0d7753a..6b67ad302e 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -230,6 +230,7 @@ class ConsolePort(ComponentModel, CableTermination, PathEndpoint): ) csv_headers = ['device', 'name', 'label', 'type', 'speed', 'mark_connected', 'description'] + clone_fields = ['device', 'type', 'speed'] class Meta: ordering = ('device', '_name') @@ -273,6 +274,7 @@ class ConsoleServerPort(ComponentModel, CableTermination, PathEndpoint): ) csv_headers = ['device', 'name', 'label', 'type', 'speed', 'mark_connected', 'description'] + clone_fields = ['device', 'type', 'speed'] class Meta: ordering = ('device', '_name') @@ -324,6 +326,7 @@ class PowerPort(ComponentModel, CableTermination, PathEndpoint): csv_headers = [ 'device', 'name', 'label', 'type', 'mark_connected', 'maximum_draw', 'allocated_draw', 'description', ] + clone_fields = ['device', 'maximum_draw', 'allocated_draw'] class Meta: ordering = ('device', '_name') @@ -434,6 +437,7 @@ class PowerOutlet(ComponentModel, CableTermination, PathEndpoint): ) csv_headers = ['device', 'name', 'label', 'type', 'mark_connected', 'power_port', 'feed_leg', 'description'] + clone_fields = ['device', 'type', 'power_port', 'feed_leg'] class Meta: ordering = ('device', '_name') @@ -577,6 +581,7 @@ class Interface(ComponentModel, BaseInterface, CableTermination, PathEndpoint): 'device', 'name', 'label', 'parent', 'lag', 'type', 'enabled', 'mark_connected', 'mac_address', 'mtu', 'mgmt_only', 'description', 'mode', ] + clone_fields = ['device', 'parent', 'lag', 'type', 'mgmt_only'] class Meta: ordering = ('device', CollateAsChar('_name')) @@ -711,6 +716,7 @@ class FrontPort(ComponentModel, CableTermination): csv_headers = [ 'device', 'name', 'label', 'type', 'mark_connected', 'rear_port', 'rear_port_position', 'description', ] + clone_fields = ['device', 'type'] class Meta: ordering = ('device', '_name') @@ -767,6 +773,7 @@ class RearPort(ComponentModel, CableTermination): MaxValueValidator(REARPORT_POSITIONS_MAX) ] ) + clone_fields = ['device', 'type', 'positions'] csv_headers = ['device', 'name', 'label', 'type', 'mark_connected', 'positions', 'description'] @@ -818,6 +825,7 @@ class DeviceBay(ComponentModel): ) csv_headers = ['device', 'name', 'label', 'installed_device', 'description'] + clone_fields = ['device'] class Meta: ordering = ('device', '_name') @@ -913,6 +921,7 @@ class InventoryItem(MPTTModel, ComponentModel): csv_headers = [ 'device', 'name', 'label', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered', 'description', ] + clone_fields = ['device', 'parent', 'manufacturer', 'part_id'] class Meta: ordering = ('device__id', 'parent__id', '_name') diff --git a/netbox/dcim/tables/devices.py b/netbox/dcim/tables/devices.py index 258e712d53..b5d1b64ed3 100644 --- a/netbox/dcim/tables/devices.py +++ b/netbox/dcim/tables/devices.py @@ -232,10 +232,6 @@ class DeviceComponentTable(BaseTable): linkify=True, order_by=('_name',) ) - cable = tables.Column( - linkify=True - ) - mark_connected = BooleanColumn() class Meta(BaseTable.Meta): order_by = ('device', 'name') diff --git a/netbox/dcim/tests/test_views.py b/netbox/dcim/tests/test_views.py index 4942b27c20..c4e552b995 100644 --- a/netbox/dcim/tests/test_views.py +++ b/netbox/dcim/tests/test_views.py @@ -1736,10 +1736,10 @@ def setUpTestData(cls): } cls.csv_data = ( - "device,name", - "Device 1,Inventory Item 4", - "Device 1,Inventory Item 5", - "Device 1,Inventory Item 6", + "device,name,parent", + "Device 1,Inventory Item 4,Inventory Item 1", + "Device 1,Inventory Item 5,Inventory Item 2", + "Device 1,Inventory Item 6,Inventory Item 3", ) diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index 1b38b63f4c..c19131ca6d 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -11,8 +11,8 @@ from tenancy.models import Tenant from utilities.forms import ( add_blank_choice, BootstrapMixin, BulkEditNullBooleanSelect, ContentTypeChoiceField, CSVChoiceField, - CSVModelChoiceField, DatePicker, DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableIPAddressField, - NumericArrayField, ReturnURLForm, SlugField, StaticSelect2, StaticSelect2Multiple, TagFilterField, + CSVContentTypeField, CSVModelChoiceField, DatePicker, DynamicModelChoiceField, DynamicModelMultipleChoiceField, + ExpandableIPAddressField, NumericArrayField, SlugField, StaticSelect2, StaticSelect2Multiple, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES, ) from virtualization.models import Cluster, ClusterGroup, VirtualMachine, VMInterface @@ -682,7 +682,7 @@ class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm) # IP addresses # -class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldModelForm): +class IPAddressForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): device = DynamicModelChoiceField( queryset=Device.objects.all(), required=False, @@ -1238,17 +1238,19 @@ def clean(self): class VLANGroupCSVForm(CustomFieldModelCSVForm): - site = CSVModelChoiceField( - queryset=Site.objects.all(), + slug = SlugField() + scope_type = CSVContentTypeField( + queryset=ContentType.objects.filter(model__in=VLANGROUP_SCOPE_TYPES), required=False, - to_field_name='name', - help_text='Assigned site' + label='Scope type (app & model)' ) - slug = SlugField() class Meta: model = VLANGroup fields = VLANGroup.csv_headers + labels = { + 'scope_id': 'Scope ID', + } class VLANGroupBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm): diff --git a/netbox/ipam/models/ip.py b/netbox/ipam/models/ip.py index cd5b89cfe2..b428e4be72 100644 --- a/netbox/ipam/models/ip.py +++ b/netbox/ipam/models/ip.py @@ -649,18 +649,15 @@ def clean(self): # Check for primary IP assignment that doesn't match the assigned device/VM if self.pk: - device = Device.objects.filter(Q(primary_ip4=self) | Q(primary_ip6=self)).first() - if device: - if getattr(self.assigned_object, 'device', None) != device: - raise ValidationError({ - 'interface': f"IP address is primary for device {device} but not assigned to it!" - }) - vm = VirtualMachine.objects.filter(Q(primary_ip4=self) | Q(primary_ip6=self)).first() - if vm: - if getattr(self.assigned_object, 'virtual_machine', None) != vm: - raise ValidationError({ - 'vminterface': f"IP address is primary for virtual machine {vm} but not assigned to it!" - }) + for cls, attr in ((Device, 'device'), (VirtualMachine, 'virtual_machine')): + parent = cls.objects.filter(Q(primary_ip4=self) | Q(primary_ip6=self)).first() + if parent and getattr(self.assigned_object, attr) != parent: + # Check for a NAT relationship + if not self.nat_inside or getattr(self.nat_inside.assigned_object, attr) != parent: + raise ValidationError({ + 'interface': f"IP address is primary for {cls._meta.model_name} {parent} but " + f"not assigned to it!" + }) # Validate IP status selection if self.status == IPAddressStatusChoices.STATUS_SLAAC and self.family != 6: diff --git a/netbox/ipam/tests/test_views.py b/netbox/ipam/tests/test_views.py index 057f9383f2..5a80fd5f1d 100644 --- a/netbox/ipam/tests/test_views.py +++ b/netbox/ipam/tests/test_views.py @@ -333,10 +333,10 @@ def setUpTestData(cls): } cls.csv_data = ( - "name,slug,description", - "VLAN Group 4,vlan-group-4,Fourth VLAN group", - "VLAN Group 5,vlan-group-5,Fifth VLAN group", - "VLAN Group 6,vlan-group-6,Sixth VLAN group", + f"name,slug,scope_type,scope_id,description", + f"VLAN Group 4,vlan-group-4,,,Fourth VLAN group", + f"VLAN Group 5,vlan-group-5,dcim.site,{sites[0].pk},Fifth VLAN group", + f"VLAN Group 6,vlan-group-6,dcim.site,{sites[1].pk},Sixth VLAN group", ) cls.bulk_edit_data = { diff --git a/netbox/netbox/configuration.example.py b/netbox/netbox/configuration.example.py index a5c5521f32..cac4a9c85e 100644 --- a/netbox/netbox/configuration.example.py +++ b/netbox/netbox/configuration.example.py @@ -69,7 +69,7 @@ # Specify one or more name and email address tuples representing NetBox administrators. These people will be notified of # application errors (assuming correct email settings are provided). ADMINS = [ - # ['John Doe', 'jdoe@example.com'], + # ('John Doe', 'jdoe@example.com'), ] # URL schemes that are allowed within links in NetBox diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index f28f72a274..f9cab85be5 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -16,7 +16,7 @@ # Environment setup # -VERSION = '2.11.10' +VERSION = '2.11.11' # Hostname HOSTNAME = platform.node() diff --git a/netbox/netbox/views/generic.py b/netbox/netbox/views/generic.py index bd3d6300af..ae2840a428 100644 --- a/netbox/netbox/views/generic.py +++ b/netbox/netbox/views/generic.py @@ -306,19 +306,20 @@ def post(self, request, *args, **kwargs): messages.success(request, mark_safe(msg)) if '_addanother' in request.POST: + redirect_url = request.path + return_url = request.GET.get('return_url') + if return_url is not None and is_safe_url(url=return_url, allowed_hosts=request.get_host()): + redirect_url = f'{redirect_url}?return_url={return_url}' # If the object has clone_fields, pre-populate a new instance of the form if hasattr(obj, 'clone_fields'): - url = '{}?{}'.format(request.path, prepare_cloned_fields(obj)) - return redirect(url) + redirect_url += f"{'&' if return_url else '?'}{prepare_cloned_fields(obj)}" - return redirect(request.get_full_path()) + return redirect(redirect_url) - return_url = form.cleaned_data.get('return_url') - if return_url is not None and is_safe_url(url=return_url, allowed_hosts=request.get_host()): - return redirect(return_url) - else: - return redirect(self.get_return_url(request, obj)) + return_url = self.get_return_url(request, obj) + + return redirect(return_url) except PermissionsViolation: msg = "Object save failed due to object-level permissions violation" @@ -675,7 +676,7 @@ class ImportForm(BootstrapMixin, Form): ) def clean(self): - csv_rows = self.cleaned_data['csv'][1] + csv_rows = self.cleaned_data['csv'][1] if 'csv' in self.cleaned_data else None csv_file = self.files.get('csv_file') # Check that the user has not submitted both text data and a file diff --git a/netbox/templates/dcim/device/config.html b/netbox/templates/dcim/device/config.html index 4b73a25777..c6508f6019 100644 --- a/netbox/templates/dcim/device/config.html +++ b/netbox/templates/dcim/device/config.html @@ -39,9 +39,9 @@ url: "{% url 'dcim-api:device-napalm' pk=object.pk %}?method=get_config", dataType: 'json', success: function(json) { - $('#running_config').html($.trim(json['get_config']['running'])); - $('#startup_config').html($.trim(json['get_config']['startup'])); - $('#candidate_config').html($.trim(json['get_config']['candidate'])); + $('#running_config').text($.trim(json['get_config']['running'])); + $('#startup_config').text($.trim(json['get_config']['startup'])); + $('#candidate_config').text($.trim(json['get_config']['candidate'])); }, error: function(xhr) { alert(xhr.responseText); diff --git a/netbox/utilities/forms/fields.py b/netbox/utilities/forms/fields.py index 7cb2cd705f..233549e0ed 100644 --- a/netbox/utilities/forms/fields.py +++ b/netbox/utilities/forms/fields.py @@ -269,6 +269,8 @@ def prepare_value(self, value): return f'{value.app_label}.{value.model}' def to_python(self, value): + if not value: + return None try: app_label, model = value.split('.') except ValueError: diff --git a/netbox/utilities/forms/utils.py b/netbox/utilities/forms/utils.py index 503a2e8a00..90df55b9ce 100644 --- a/netbox/utilities/forms/utils.py +++ b/netbox/utilities/forms/utils.py @@ -32,7 +32,10 @@ def parse_numeric_range(string, base=10): begin, end = dash_range.split('-') except ValueError: begin, end = dash_range, dash_range - begin, end = int(begin.strip(), base=base), int(end.strip(), base=base) + 1 + try: + begin, end = int(begin.strip(), base=base), int(end.strip(), base=base) + 1 + except ValueError: + raise forms.ValidationError(f'Range "{dash_range}" is invalid.') values.extend(range(begin, end)) return list(set(values)) @@ -64,7 +67,7 @@ def parse_alphanumeric_range(string): else: # Not a valid range (more than a single character) if not len(begin) == len(end) == 1: - raise forms.ValidationError('Range "{}" is invalid.'.format(dash_range)) + raise forms.ValidationError(f'Range "{dash_range}" is invalid.') for n in list(range(ord(begin), ord(end) + 1)): values.append(chr(n)) return values diff --git a/netbox/utilities/utils.py b/netbox/utilities/utils.py index 1cffab1ebc..1b43c8b6bc 100644 --- a/netbox/utilities/utils.py +++ b/netbox/utilities/utils.py @@ -6,7 +6,7 @@ from django.core.serializers import serialize from django.db.models import Count, OuterRef, Subquery from django.db.models.functions import Coalesce -from jinja2 import Environment +from jinja2.sandbox import SandboxedEnvironment from mptt.models import MPTTModel from dcim.choices import CableLengthUnitChoices @@ -213,7 +213,7 @@ def render_jinja2(template_code, context): """ Render a Jinja2 template with the provided context. Return the rendered content. """ - return Environment().from_string(source=template_code).render(**context) + return SandboxedEnvironment().from_string(source=template_code).render(**context) def prepare_cloned_fields(instance): diff --git a/netbox/virtualization/views.py b/netbox/virtualization/views.py index 4fdca3078e..0d1b720517 100644 --- a/netbox/virtualization/views.py +++ b/netbox/virtualization/views.py @@ -414,7 +414,7 @@ class VMInterfaceListView(generic.ObjectListView): filterset = filtersets.VMInterfaceFilterSet filterset_form = forms.VMInterfaceFilterForm table = tables.VMInterfaceTable - action_buttons = ('export',) + action_buttons = ('import', 'export') class VMInterfaceView(generic.ObjectView): diff --git a/requirements.txt b/requirements.txt index d35cba435e..65ce1de50d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -Django==3.2.5 +Django==3.2.6 django-cacheops==6.0 django-cors-headers==3.7.0 django-debug-toolbar==3.2.1