diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index ef31324fe2c..b0c021af2bf 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 319538cda3e..f13d45c3f5a 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 0f617e8aafe..d8099923fb1 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 0a00b6d68a9..196371ce31d 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 b3f585beeec..6608074443f 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 f3dd803372a..4fce4e037e5 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 39008f18875..644b2715c6d 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 e54bf6f3e65..eb717093d60 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 e8634490074..c860b964b1a 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 736807e2c17..40df841c0e8 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 471beffeeff..45dfcfa36ce 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 9a12e6a1917..4216d935551 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 565a75c45b2..97f8721a5ce 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 6ac0d7753ae..6b67ad302e3 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 258e712d53e..b5d1b64ed35 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 4942b27c20a..c4e552b9952 100644 --- a/netbox/dcim/tests/test_views.py +++ b/netbox/dcim/tests/test_views.py @@ -1736,10 +1736,10 @@ class InventoryItemTestCase(ViewTestCases.DeviceComponentViewTestCase): } 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 1b38b63f4c3..c19131ca6d2 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -11,8 +11,8 @@ from tenancy.forms import TenancyFilterForm, TenancyForm 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 @@ class VLANGroupForm(BootstrapMixin, CustomFieldModelForm): class VLANGroupCSVForm(CustomFieldModelCSVForm): - site = CSVModelChoiceField( - queryset=Site.objects.all(), - required=False, - to_field_name='name', - help_text='Assigned site' - ) slug = SlugField() + scope_type = CSVContentTypeField( + queryset=ContentType.objects.filter(model__in=VLANGROUP_SCOPE_TYPES), + required=False, + label='Scope type (app & model)' + ) 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 cd5b89cfe2c..b428e4be726 100644 --- a/netbox/ipam/models/ip.py +++ b/netbox/ipam/models/ip.py @@ -649,18 +649,15 @@ class IPAddress(PrimaryModel): # 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 057f9383f21..5a80fd5f1d6 100644 --- a/netbox/ipam/tests/test_views.py +++ b/netbox/ipam/tests/test_views.py @@ -333,10 +333,10 @@ class VLANGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase): } 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 a5c5521f32e..cac4a9c85e4 100644 --- a/netbox/netbox/configuration.example.py +++ b/netbox/netbox/configuration.example.py @@ -69,7 +69,7 @@ SECRET_KEY = '' # 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 f28f72a2748..f9cab85be51 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -16,7 +16,7 @@ from django.core.validators import URLValidator # 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 bd3d6300af7..ae2840a428c 100644 --- a/netbox/netbox/views/generic.py +++ b/netbox/netbox/views/generic.py @@ -306,19 +306,20 @@ class ObjectEditView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View): 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 BulkImportView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View): ) 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 4b73a25777d..c6508f6019d 100644 --- a/netbox/templates/dcim/device/config.html +++ b/netbox/templates/dcim/device/config.html @@ -39,9 +39,9 @@ $(document).ready(function() { 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 7cb2cd705f7..233549e0edd 100644 --- a/netbox/utilities/forms/fields.py +++ b/netbox/utilities/forms/fields.py @@ -269,6 +269,8 @@ class CSVContentTypeField(CSVModelChoiceField): 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 503a2e8a00e..90df55b9cea 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 1cffab1ebc0..1b43c8b6bc9 100644 --- a/netbox/utilities/utils.py +++ b/netbox/utilities/utils.py @@ -6,7 +6,7 @@ from itertools import count, groupby 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 4fdca3078ee..0d1b720517e 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 d35cba435e6..65ce1de50d9 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