diff --git a/README.md b/README.md index 68927463d..a12daa783 100644 --- a/README.md +++ b/README.md @@ -12,8 +12,7 @@ complete list of requirements, see `requirements.txt`. The code is available [on The complete documentation for NetBox can be found at [Read the Docs](https://netbox.readthedocs.io/en/stable/). -Questions? Comments? Please start a [discussion on GitHub](https://github.com/netbox-community/netbox/discussions), -or join us in the **#netbox** Slack channel on [NetworkToCode](https://networktocode.slack.com/)! +Questions? Comments? Start by perusing our [GitHub discussions](https://github.com/netbox-community/netbox/discussions) for the topic you have in mind. ### Build Status diff --git a/docs/configuration/optional-settings.md b/docs/configuration/optional-settings.md index 91c0e7597..4af83493e 100644 --- a/docs/configuration/optional-settings.md +++ b/docs/configuration/optional-settings.md @@ -56,7 +56,7 @@ BASE_PATH = 'netbox/' Default: 900 -The number of seconds to cache entries will be retained before expiring. +The number of seconds that cache entries will be retained before expiring. --- diff --git a/docs/configuration/required-settings.md b/docs/configuration/required-settings.md index 2edf6c7c7..dba8cdc8c 100644 --- a/docs/configuration/required-settings.md +++ b/docs/configuration/required-settings.md @@ -5,7 +5,7 @@ This is a list of valid fully-qualified domain names (FQDNs) and/or IP addresses that can be used to reach the NetBox service. Usually this is the same as the hostname for the NetBox server, but can also be different; for example, when using a reverse proxy serving the NetBox website under a different FQDN than the hostname of the NetBox server. To help guard against [HTTP Host header attackes](https://docs.djangoproject.com/en/3.0/topics/security/#host-headers-virtual-hosting), NetBox will not permit access to the server via any other hostnames (or IPs). !!! note - This parameter must always be defined as a list or tuple, even if only value is provided. + This parameter must always be defined as a list or tuple, even if only a single value is provided. The value of this option is also used to set `CSRF_TRUSTED_ORIGINS`, which restricts POST requests to the same set of hosts (more about this [here](https://docs.djangoproject.com/en/stable/ref/settings/#std:setting-CSRF_TRUSTED_ORIGINS)). Keep in mind that NetBox, by default, sets `USE_X_FORWARDED_HOST` to true, which means that if you're using a reverse proxy, it's the FQDN used to reach that reverse proxy which needs to be in this list (more about this [here](https://docs.djangoproject.com/en/stable/ref/settings/#allowed-hosts)). @@ -101,7 +101,7 @@ REDIS = { If you are using [Redis Sentinel](https://redis.io/topics/sentinel) for high-availability purposes, there is minimal configuration necessary to convert NetBox to recognize it. It requires the removal of the `HOST` and `PORT` keys from -above and the addition of two new keys. +above and the addition of three new keys. * `SENTINELS`: List of tuples or tuple of tuples with each inner tuple containing the name or IP address of the Redis server and port for each sentinel instance to connect to diff --git a/docs/installation/3-netbox.md b/docs/installation/3-netbox.md index 9745df1f3..f2461b40d 100644 --- a/docs/installation/3-netbox.md +++ b/docs/installation/3-netbox.md @@ -7,12 +7,12 @@ This section of the documentation discusses installing and configuring the NetBo Begin by installing all system packages required by NetBox and its dependencies. !!! note - NetBox v2.8.0 and later require Python 3.6, 3.7, or 3.8. This documentation assumes Python 3.6. + NetBox v2.8.0 and later require Python 3.6, 3.7, or 3.8. ### Ubuntu ```no-highlight -sudo apt install -y python3.6 python3-pip python3-venv python3-dev build-essential libxml2-dev libxslt1-dev libffi-dev libpq-dev libssl-dev zlib1g-dev +sudo apt install -y python3 python3-pip python3-venv python3-dev build-essential libxml2-dev libxslt1-dev libffi-dev libpq-dev libssl-dev zlib1g-dev ``` ### CentOS diff --git a/docs/installation/index.md b/docs/installation/index.md index 730176e0c..71e669295 100644 --- a/docs/installation/index.md +++ b/docs/installation/index.md @@ -11,6 +11,10 @@ The following sections detail how to set up a new instance of NetBox: 5. [HTTP server](5-http-server.md) 6. [LDAP authentication](6-ldap.md) (optional) +The video below demonstrates the installation of NetBox v2.10.3 on Ubuntu 20.04 for your reference. + + + ## Requirements | Dependency | Minimum Version | diff --git a/docs/release-notes/version-2.10.md b/docs/release-notes/version-2.10.md index a3ab5968c..b6bc33229 100644 --- a/docs/release-notes/version-2.10.md +++ b/docs/release-notes/version-2.10.md @@ -1,5 +1,30 @@ # NetBox v2.10 +## v2.10.4 (2021-01-26) + +### Enhancements + +* [#5542](https://github.com/netbox-community/netbox/issues/5542) - Show cable trace lengths in both meters and feet +* [#5570](https://github.com/netbox-community/netbox/issues/5570) - Add "management only" filter widget for interfaces list +* [#5586](https://github.com/netbox-community/netbox/issues/5586) - Allow filtering virtual chassis by name and master +* [#5612](https://github.com/netbox-community/netbox/issues/5612) - Add GG45 and TERA port types, and CAT7a and CAT8 cable types +* [#5678](https://github.com/netbox-community/netbox/issues/5678) - Show available type choices for all device component import forms + +### Bug Fixes + +* [#5232](https://github.com/netbox-community/netbox/issues/5232) - Correct swagger definition for ip_prefixes_available-ips_create API +* [#5574](https://github.com/netbox-community/netbox/issues/5574) - Restrict the creation of device bay templates on non-parent device types +* [#5584](https://github.com/netbox-community/netbox/issues/5584) - Restore power utilization panel under device view +* [#5597](https://github.com/netbox-community/netbox/issues/5597) - Fix ordering devices by primary IP address +* [#5603](https://github.com/netbox-community/netbox/issues/5603) - Fix display of white cables in trace view +* [#5639](https://github.com/netbox-community/netbox/issues/5639) - Fix filtering connection lists by device name +* [#5640](https://github.com/netbox-community/netbox/issues/5640) - Fix permissions assessment when adding VM interfaces in bulk +* [#5648](https://github.com/netbox-community/netbox/issues/5648) - Include VC member interfaces on interfaces tab count when viewing VC master +* [#5665](https://github.com/netbox-community/netbox/issues/5665) - Validate rack group is assigned to same site when creating a rack +* [#5683](https://github.com/netbox-community/netbox/issues/5683) - Correct rack elevation displayed when viewing a reservation + +--- + ## v2.10.3 (2021-01-05) ### Bug Fixes diff --git a/netbox/dcim/choices.py b/netbox/dcim/choices.py index d8f8e3e27..d4cc0ecf4 100644 --- a/netbox/dcim/choices.py +++ b/netbox/dcim/choices.py @@ -873,6 +873,10 @@ class PortTypeChoices(ChoiceSet): TYPE_8P6C = '8p6c' TYPE_8P4C = '8p4c' TYPE_8P2C = '8p2c' + TYPE_GG45 = 'gg45' + TYPE_TERA4P = 'tera-4p' + TYPE_TERA2P = 'tera-2p' + TYPE_TERA1P = 'tera-1p' TYPE_110_PUNCH = '110-punch' TYPE_BNC = 'bnc' TYPE_MRJ21 = 'mrj21' @@ -898,6 +902,10 @@ class PortTypeChoices(ChoiceSet): (TYPE_8P6C, '8P6C'), (TYPE_8P4C, '8P4C'), (TYPE_8P2C, '8P2C'), + (TYPE_GG45, 'GG45'), + (TYPE_TERA4P, 'TERA 4P'), + (TYPE_TERA2P, 'TERA 2P'), + (TYPE_TERA1P, 'TERA 1P'), (TYPE_110_PUNCH, '110 Punch'), (TYPE_BNC, 'BNC'), (TYPE_MRJ21, 'MRJ21'), @@ -936,6 +944,8 @@ class CableTypeChoices(ChoiceSet): TYPE_CAT6 = 'cat6' TYPE_CAT6A = 'cat6a' TYPE_CAT7 = 'cat7' + TYPE_CAT7A = 'cat7a' + TYPE_CAT8 = 'cat8' TYPE_DAC_ACTIVE = 'dac-active' TYPE_DAC_PASSIVE = 'dac-passive' TYPE_MRJ21_TRUNK = 'mrj21-trunk' @@ -960,6 +970,8 @@ class CableTypeChoices(ChoiceSet): (TYPE_CAT6, 'CAT6'), (TYPE_CAT6A, 'CAT6a'), (TYPE_CAT7, 'CAT7'), + (TYPE_CAT7A, 'CAT7a'), + (TYPE_CAT8, 'CAT8'), (TYPE_DAC_ACTIVE, 'Direct Attach Copper (Active)'), (TYPE_DAC_PASSIVE, 'Direct Attach Copper (Passive)'), (TYPE_MRJ21_TRUNK, 'MRJ21 Trunk'), diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index 3046a0f33..03deca2a4 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -1016,6 +1016,16 @@ class VirtualChassisFilterSet(BaseFilterSet): method='search', label='Search', ) + master_id = django_filters.ModelMultipleChoiceFilter( + queryset=Device.objects.all(), + label='Master (ID)', + ) + master = django_filters.ModelMultipleChoiceFilter( + field_name='master__name', + queryset=Device.objects.all(), + to_field_name='name', + label='Master (name)', + ) region_id = TreeNodeMultipleChoiceFilter( queryset=Region.objects.all(), field_name='master__site__region', @@ -1055,7 +1065,7 @@ class VirtualChassisFilterSet(BaseFilterSet): class Meta: model = VirtualChassis - fields = ['id', 'domain'] + fields = ['id', 'domain', 'name'] def search(self, queryset, name, value): if not value.strip(): @@ -1142,7 +1152,7 @@ class ConnectionFilterSet: def filter_device(self, queryset, name, value): if not value: return queryset - return queryset.filter(device_id__in=value) + return queryset.filter(**{f'{name}__in': value}) class ConsoleConnectionFilterSet(ConnectionFilterSet, BaseFilterSet): diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index f7eb510ec..40c16d59f 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -2352,6 +2352,11 @@ class ConsolePortCSVForm(CSVModelForm): queryset=Device.objects.all(), to_field_name='name' ) + type = CSVChoiceField( + choices=ConsolePortTypeChoices, + required=False, + help_text='Port type' + ) class Meta: model = ConsolePort @@ -2425,6 +2430,11 @@ class ConsoleServerPortCSVForm(CSVModelForm): queryset=Device.objects.all(), to_field_name='name' ) + type = CSVChoiceField( + choices=ConsolePortTypeChoices, + required=False, + help_text='Port type' + ) class Meta: model = ConsoleServerPort @@ -2510,6 +2520,11 @@ class PowerPortCSVForm(CSVModelForm): queryset=Device.objects.all(), to_field_name='name' ) + type = CSVChoiceField( + choices=PowerPortTypeChoices, + required=False, + help_text='Port type' + ) class Meta: model = PowerPort @@ -2630,6 +2645,11 @@ class PowerOutletCSVForm(CSVModelForm): queryset=Device.objects.all(), to_field_name='name' ) + type = CSVChoiceField( + choices=PowerOutletTypeChoices, + required=False, + help_text='Outlet type' + ) power_port = CSVModelChoiceField( queryset=PowerPort.objects.all(), required=False, @@ -2687,6 +2707,12 @@ class InterfaceFilterForm(DeviceComponentFilterForm): choices=BOOLEAN_WITH_BLANK_CHOICES ) ) + mgmt_only = forms.NullBooleanField( + required=False, + widget=StaticSelect2( + choices=BOOLEAN_WITH_BLANK_CHOICES + ) + ) mac_address = forms.CharField( required=False, label='MAC address' diff --git a/netbox/dcim/models/device_component_templates.py b/netbox/dcim/models/device_component_templates.py index 58233f3bf..e52fe2602 100644 --- a/netbox/dcim/models/device_component_templates.py +++ b/netbox/dcim/models/device_component_templates.py @@ -363,3 +363,9 @@ class DeviceBayTemplate(ComponentTemplateModel): name=self.name, label=self.label ) + + def clean(self): + if self.device_type and self.device_type.subdevice_role != SubdeviceRoleChoices.ROLE_PARENT: + raise ValidationError( + f"Subdevice role of device type ({self.device_type}) must be set to \"parent\" to allow device bays." + ) diff --git a/netbox/dcim/models/racks.py b/netbox/dcim/models/racks.py index ccc775954..dfaf7da61 100644 --- a/netbox/dcim/models/racks.py +++ b/netbox/dcim/models/racks.py @@ -299,6 +299,10 @@ class Rack(ChangeLoggedModel, CustomFieldModel): def clean(self): super().clean() + # Validate group/site assignment + if self.site and self.group and self.group.site != self.site: + raise ValidationError(f"Assigned rack group must belong to parent site ({self.site}).") + # Validate outer dimensions and unit if (self.outer_width is not None or self.outer_depth is not None) and not self.outer_unit: raise ValidationError("Must specify a unit when setting an outer width/depth") diff --git a/netbox/dcim/tables/devices.py b/netbox/dcim/tables/devices.py index 663206505..edd9e7a43 100644 --- a/netbox/dcim/tables/devices.py +++ b/netbox/dcim/tables/devices.py @@ -129,6 +129,7 @@ class DeviceTable(BaseTable): ) primary_ip = tables.Column( linkify=True, + order_by=('primary_ip6', 'primary_ip4'), verbose_name='IP Address' ) primary_ip4 = tables.Column( @@ -406,6 +407,7 @@ class BaseInterfaceTable(BaseTable): class InterfaceTable(DeviceComponentTable, BaseInterfaceTable, PathEndpointTable): + mgmt_only = BooleanColumn() tags = TagColumn( url_name='dcim:interface_list' ) diff --git a/netbox/dcim/tests/test_api.py b/netbox/dcim/tests/test_api.py index 77c3b4786..ad1ca930c 100644 --- a/netbox/dcim/tests/test_api.py +++ b/netbox/dcim/tests/test_api.py @@ -740,7 +740,10 @@ class DeviceBayTemplateTest(APIViewTestCases.APIViewTestCase): def setUpTestData(cls): manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1') devicetype = DeviceType.objects.create( - manufacturer=manufacturer, model='Device Type 1', slug='device-type-1' + manufacturer=manufacturer, + model='Device Type 1', + slug='device-type-1', + subdevice_role=SubdeviceRoleChoices.ROLE_PARENT ) device_bay_templates = ( diff --git a/netbox/dcim/tests/test_filters.py b/netbox/dcim/tests/test_filters.py index c701c47cf..a76788e65 100644 --- a/netbox/dcim/tests/test_filters.py +++ b/netbox/dcim/tests/test_filters.py @@ -2399,9 +2399,9 @@ class VirtualChassisTestCase(TestCase): Device.objects.bulk_create(devices) virtual_chassis = ( - VirtualChassis(master=devices[0], domain='Domain 1'), - VirtualChassis(master=devices[2], domain='Domain 2'), - VirtualChassis(master=devices[4], domain='Domain 3'), + VirtualChassis(name='VC 1', master=devices[0], domain='Domain 1'), + VirtualChassis(name='VC 2', master=devices[2], domain='Domain 2'), + VirtualChassis(name='VC 3', master=devices[4], domain='Domain 3'), ) VirtualChassis.objects.bulk_create(virtual_chassis) @@ -2417,6 +2417,17 @@ class VirtualChassisTestCase(TestCase): params = {'domain': ['Domain 1', 'Domain 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_master(self): + masters = Device.objects.all() + params = {'master_id': [masters[0].pk, masters[2].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'master': [masters[0].name, masters[2].name]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_name(self): + params = {'name': ['VC 1', 'VC 2']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_region(self): regions = Region.objects.all()[:2] params = {'region_id': [regions[0].pk, regions[1].pk]} diff --git a/netbox/dcim/tests/test_views.py b/netbox/dcim/tests/test_views.py index 91ec8776c..86518af84 100644 --- a/netbox/dcim/tests/test_views.py +++ b/netbox/dcim/tests/test_views.py @@ -396,6 +396,7 @@ manufacturer: Generic model: TEST-1000 slug: test-1000 u_height: 2 +subdevice_role: parent comments: test comment console-ports: - name: Console Port 1 @@ -831,8 +832,8 @@ class DeviceBayTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCas def setUpTestData(cls): manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1') devicetypes = ( - DeviceType(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'), - DeviceType(manufacturer=manufacturer, model='Device Type 2', slug='device-type-2'), + DeviceType(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1', subdevice_role=SubdeviceRoleChoices.ROLE_PARENT), + DeviceType(manufacturer=manufacturer, model='Device Type 2', slug='device-type-2', subdevice_role=SubdeviceRoleChoices.ROLE_PARENT), ) DeviceType.objects.bulk_create(devicetypes) diff --git a/netbox/generate_secret_key.py b/netbox/generate_secret_key.py index 3c88aa710..c3de29cee 100755 --- a/netbox/generate_secret_key.py +++ b/netbox/generate_secret_key.py @@ -1,7 +1,6 @@ #!/usr/bin/env python # This script will generate a random 50-character string suitable for use as a SECRET_KEY. -import random +import secrets charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*(-_=+)' -secure_random = random.SystemRandom() -print(''.join(secure_random.sample(charset, 50))) +print(''.join(secrets.choice(charset) for _ in range(50))) diff --git a/netbox/ipam/api/views.py b/netbox/ipam/api/views.py index d9eae69aa..c322c249d 100644 --- a/netbox/ipam/api/views.py +++ b/netbox/ipam/api/views.py @@ -178,7 +178,7 @@ class PrefixViewSet(CustomFieldModelViewSet): @swagger_auto_schema(method='get', responses={200: serializers.AvailableIPSerializer(many=True)}) @swagger_auto_schema(method='post', responses={201: serializers.AvailableIPSerializer(many=True)}, - request_body=serializers.AvailableIPSerializer(many=False)) + request_body=serializers.AvailableIPSerializer(many=True)) @action(detail=True, url_path='available-ips', methods=['get', 'post'], queryset=IPAddress.objects.all()) @advisory_lock(ADVISORY_LOCK_KEYS['available-ips']) def available_ips(self, request, pk=None): diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 4c36332fc..3dd780b26 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.10.3' +VERSION = '2.10.4' # Hostname HOSTNAME = platform.node() diff --git a/netbox/templates/dcim/cable_trace.html b/netbox/templates/dcim/cable_trace.html index a36922612..a39ada1ce 100644 --- a/netbox/templates/dcim/cable_trace.html +++ b/netbox/templates/dcim/cable_trace.html @@ -69,7 +69,8 @@
Total segments: {{ traced_path|length }}
Total length: {% if total_length %} - {{ total_length|floatformat:"-2" }} Meters + {{ total_length|floatformat:"-2" }} Meters / + {{ total_length|meters_to_feet|floatformat:"-2" }} Feet {% else %} N/A {% endif %} diff --git a/netbox/templates/dcim/device.html b/netbox/templates/dcim/device.html index 97f7c8953..55be343ac 100644 --- a/netbox/templates/dcim/device.html +++ b/netbox/templates/dcim/device.html @@ -204,7 +204,7 @@ {% plugin_left_page object %}
- {% if power_ports and poweroutlets %} + {% if object.powerports.exists and object.poweroutlets.exists %}
Power Utilization @@ -217,10 +217,10 @@ Available Utilization - {% for pp in power_ports %} - {% with utilization=pp.get_power_draw powerfeed=pp.connected_endpoint %} + {% for powerport in object.powerports.all %} + {% with utilization=powerport.get_power_draw powerfeed=powerport.connected_endpoint %} - {{ pp }} + {{ powerport }} {{ utilization.outlet_count }} {{ utilization.allocated }}VA {% if powerfeed.available_power %} diff --git a/netbox/templates/dcim/device/base.html b/netbox/templates/dcim/device/base.html index 631abde89..8f488b284 100644 --- a/netbox/templates/dcim/device/base.html +++ b/netbox/templates/dcim/device/base.html @@ -90,7 +90,7 @@ - {% with interface_count=object.interfaces.count %} + {% with interface_count=object.vc_interfaces.count %} {% if interface_count %}
- {% with rack=object.rack %} -
-
-
-

Front

-
- {% include 'dcim/inc/rack_elevation.html' with face='front' %} -
-
-
-

Rear

-
- {% include 'dcim/inc/rack_elevation.html' with face='rear' %} +
+
+
+

Front

+ {% include 'dcim/inc/rack_elevation.html' with object=object.rack face='front' %}
- {% endwith %} +
+
+

Rear

+
+ {% include 'dcim/inc/rack_elevation.html' with object=object.rack face='rear' %} +
+
{% plugin_right_page object %}
diff --git a/netbox/templates/dcim/trace/cable.html b/netbox/templates/dcim/trace/cable.html index 4235768a6..7a7fbcf55 100644 --- a/netbox/templates/dcim/trace/cable.html +++ b/netbox/templates/dcim/trace/cable.html @@ -1,6 +1,6 @@ {% load helpers %} -
+ {% endif %} diff --git a/netbox/utilities/custom_inspectors.py b/netbox/utilities/custom_inspectors.py index 95c647fb8..b110a9123 100644 --- a/netbox/utilities/custom_inspectors.py +++ b/netbox/utilities/custom_inspectors.py @@ -28,29 +28,38 @@ class NetBoxSwaggerAutoSchema(SwaggerAutoSchema): serializer = super().get_request_serializer() if serializer is not None and self.method in self.implicit_body_methods: - properties = {} - for child_name, child in serializer.fields.items(): - if isinstance(child, (ChoiceField, WritableNestedSerializer)): - properties[child_name] = None - elif isinstance(child, ManyRelatedField) and isinstance(child.child_relation, SerializedPKRelatedField): - properties[child_name] = None - - if properties: - if type(serializer) not in self.writable_serializers: - writable_name = 'Writable' + type(serializer).__name__ - meta_class = getattr(type(serializer), 'Meta', None) - if meta_class: - ref_name = 'Writable' + get_serializer_ref_name(serializer) - writable_meta = type('Meta', (meta_class,), {'ref_name': ref_name}) - properties['Meta'] = writable_meta - - self.writable_serializers[type(serializer)] = type(writable_name, (type(serializer),), properties) - - writable_class = self.writable_serializers[type(serializer)] - serializer = writable_class() - + writable_class = self.get_writable_class(serializer) + if writable_class is not None: + if hasattr(serializer, 'child'): + child_serializer = self.get_writable_class(serializer.child) + serializer = writable_class(child=child_serializer) + else: + serializer = writable_class() return serializer + def get_writable_class(self, serializer): + properties = {} + fields = {} if hasattr(serializer, 'child') else serializer.fields + for child_name, child in fields.items(): + if isinstance(child, (ChoiceField, WritableNestedSerializer)): + properties[child_name] = None + elif isinstance(child, ManyRelatedField) and isinstance(child.child_relation, SerializedPKRelatedField): + properties[child_name] = None + + if properties: + if type(serializer) not in self.writable_serializers: + writable_name = 'Writable' + type(serializer).__name__ + meta_class = getattr(type(serializer), 'Meta', None) + if meta_class: + ref_name = 'Writable' + get_serializer_ref_name(serializer) + writable_meta = type('Meta', (meta_class,), {'ref_name': ref_name}) + properties['Meta'] = writable_meta + + self.writable_serializers[type(serializer)] = type(writable_name, (type(serializer),), properties) + + writable_class = self.writable_serializers[type(serializer)] + return writable_class + class SerializedPKRelatedFieldInspector(FieldInspector): def field_to_swagger_object(self, field, swagger_object_type, use_references, **kwargs): diff --git a/netbox/utilities/templatetags/helpers.py b/netbox/utilities/templatetags/helpers.py index 29c920d4f..01dce8479 100644 --- a/netbox/utilities/templatetags/helpers.py +++ b/netbox/utilities/templatetags/helpers.py @@ -220,6 +220,14 @@ def as_range(n): return range(n) +@register.filter() +def meters_to_feet(n): + """ + Convert a length from meters to feet. + """ + return float(n) * 3.28084 + + # # Tags # diff --git a/netbox/virtualization/views.py b/netbox/virtualization/views.py index 9ef4a0863..dcf9ebcda 100644 --- a/netbox/virtualization/views.py +++ b/netbox/virtualization/views.py @@ -396,3 +396,6 @@ class VirtualMachineBulkAddInterfaceView(generic.BulkComponentCreateView): model_form = forms.VMInterfaceForm filterset = filters.VirtualMachineFilterSet table = tables.VirtualMachineTable + + def get_required_permission(self): + return f'virtualization.add_vminterface'