Skip to content

Commit

Permalink
Merge pull request #14057 from netbox-community/develop
Browse files Browse the repository at this point in the history
Release v3.6.4
  • Loading branch information
jeremystretch authored Oct 17, 2023
2 parents ccc9e89 + de29822 commit d195f9c
Show file tree
Hide file tree
Showing 44 changed files with 282 additions and 89 deletions.
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/bug_report.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ body:
attributes:
label: NetBox version
description: What version of NetBox are you currently running?
placeholder: v3.6.3
placeholder: v3.6.4
validations:
required: true
- type: dropdown
Expand Down
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/feature_request.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ body:
attributes:
label: NetBox version
description: What version of NetBox are you currently running?
placeholder: v3.6.3
placeholder: v3.6.4
validations:
required: true
- type: dropdown
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,23 +31,23 @@ jobs:

steps:
- name: Check out repo
uses: actions/checkout@v2
uses: actions/checkout@v4

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}

- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}

- name: Install Yarn Package Manager
run: npm install -g yarn

- name: Setup Node.js with Yarn Caching
uses: actions/setup-node@v2
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: yarn
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/lock.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
lock:
runs-on: ubuntu-latest
steps:
- uses: dessant/lock-threads@v3
- uses: dessant/lock-threads@v4
with:
issue-inactive-days: 90
pr-inactive-days: 30
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/stale.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:

runs-on: ubuntu-latest
steps:
- uses: actions/stale@v6
- uses: actions/stale@v8
with:
close-issue-message: >
This issue has been automatically closed due to lack of activity. In an
Expand Down
7 changes: 6 additions & 1 deletion base_requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@ django-filter
django-graphiql-debug-toolbar

# Modified Preorder Tree Traversal (recursive nesting of objects)
# Pinned to 0.14.0; 0.15.0 requires Python 3.9+
# https://github.com/django-mptt/django-mptt/blob/main/CHANGELOG.rst
django-mptt
django-mptt==0.14.0

# Context managers for PostgreSQL advisory locks
# https://github.com/Xof/django-pglocks/blob/master/CHANGES.txt
Expand Down Expand Up @@ -120,6 +121,10 @@ psycopg[binary,pool]
# https://github.com/yaml/pyyaml/blob/master/CHANGES
PyYAML

# Requests
# https://github.com/psf/requests/blob/main/HISTORY.md
requests

# Sentry SDK
# https://github.com/getsentry/sentry-python/blob/master/CHANGELOG.md
sentry-sdk
Expand Down
12 changes: 10 additions & 2 deletions docs/configuration/miscellaneous.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,14 @@ changes in the database indefinitely.

---

## DATA_UPLOAD_MAX_MEMORY_SIZE

Default: `2621440` (2.5 MB)

The maximum size (in bytes) of an incoming HTTP request (i.e. `GET` or `POST` data). Requests which exceed this size will raise a `RequestDataTooBig` exception.

---

## ENFORCE_GLOBAL_UNIQUE

!!! tip "Dynamic Configuration Parameter"
Expand All @@ -90,9 +98,9 @@ By default, NetBox will permit users to create duplicate prefixes and IP address

---

## `FILE_UPLOAD_MAX_MEMORY_SIZE`
## FILE_UPLOAD_MAX_MEMORY_SIZE

Default: `2621440` (2.5 MB).
Default: `2621440` (2.5 MB)

The maximum amount (in bytes) of uploaded data that will be held in memory before being written to the filesystem. Changing this setting can be useful for example to be able to upload files bigger than 2.5MB to custom scripts for processing.

Expand Down
2 changes: 1 addition & 1 deletion docs/customization/custom-scripts.md
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,7 @@ An IPv4 or IPv6 network with a mask. Returns a `netaddr.IPNetwork` object. Two a
## Running Custom Scripts

!!! note
To run a custom script, a user must be assigned the `extras.run_script` permission. This is achieved by assigning the user (or group) a permission on the Script object and specifying the `run` action in the admin UI as shown below.
To run a custom script, a user must be assigned via permissions for `Extras > Script`, `Extras > ScriptModule`, and `Core > ManagedFile` objects. They must also be assigned the `extras.run_script` permission. This is achieved by assigning the user (or group) a permission on the Script object and specifying the `run` action in the admin UI as shown below.

![Adding the run action to a permission](../media/admin_ui_run_permission.png)

Expand Down
2 changes: 1 addition & 1 deletion docs/customization/reports.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ Once you have created a report, it will appear in the reports list. Initially, r
## Running Reports

!!! note
To run a report, a user must be assigned the `extras.run_report` permission. This is achieved by assigning the user (or group) a permission on the Report object and specifying the `run` action in the admin UI as shown below.
To run a report, a user must be assigned via permissions for `Extras > Report`, `Extras > ReportModule`, and `Core > ManagedFile` objects. They must also be assigned the `extras.run_report` permission. This is achieved by assigning the user (or group) a permission on the Report object and specifying the `run` action in the admin UI as shown below.

![Adding the run action to a permission](../media/admin_ui_run_permission.png)

Expand Down
32 changes: 32 additions & 0 deletions docs/release-notes/version-3.6.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,37 @@
# NetBox v3.6

## v3.6.4 (2023-10-17)

### Enhancements

* [#12831](https://github.com/netbox-community/netbox/issues/12831) - Include circuit description in cable trace SVG image
* [#12872](https://github.com/netbox-community/netbox/issues/12872) - Introduce the `DATA_UPLOAD_MAX_MEMORY_SIZE` configuration parameter
* [#13950](https://github.com/netbox-community/netbox/issues/13950) - Display custom choice field labels rather than values in UI
* [#13957](https://github.com/netbox-community/netbox/issues/13957) - Add DNS name filter on IP addresses list
* [#13962](https://github.com/netbox-community/netbox/issues/13962) - Add a copy-to-clipboard button for API tokens
* [#13972](https://github.com/netbox-community/netbox/issues/13972) - Introduce a filter to find unterminated cables

### Bug Fixes

* [#11987](https://github.com/netbox-community/netbox/issues/11987) - Fix validation of bulk cable updates via bulk import form
* [#12328](https://github.com/netbox-community/netbox/issues/12328) - Ensure generic foreign key relationships are populated in REST API serializations of objects
* [#12336](https://github.com/netbox-community/netbox/issues/12336) - Employ PostgreSQL advisory locks to avoid duplicate MPTT tree IDs when bulk creating objects
* [#13064](https://github.com/netbox-community/netbox/issues/13064) - Fix resetting of checkbox fields triggered by HTMX form re-rendering
* [#13440](https://github.com/netbox-community/netbox/issues/13440) - Fix support for assigning a tenant when creating "next available" VLANs via the REST API
* [#13746](https://github.com/netbox-community/netbox/issues/13746) - Fix support for setting custom field values when creating "next available" IP addresses via the REST API
* [#13872](https://github.com/netbox-community/netbox/issues/13872) - Add CSV delimiter field to file upload tab under bulk object upload views
* [#13876](https://github.com/netbox-community/netbox/issues/13876) - Fix support for assigning an interface when creating "next available" IP addresses via the REST API
* [#13910](https://github.com/netbox-community/netbox/issues/13910) - Correct "add device" button link under platform view
* [#13944](https://github.com/netbox-community/netbox/issues/13944) - Correct serialization of several report attributes in the REST API
* [#13966](https://github.com/netbox-community/netbox/issues/13966) - Restore "last login" column on users table
* [#14013](https://github.com/netbox-community/netbox/issues/14013) - Fix device role filter choices under inventory items list filters
* [#14023](https://github.com/netbox-community/netbox/issues/14023) - Fix exception when bulk disconnecting interfaces connected to the same cable
* [#14025](https://github.com/netbox-community/netbox/issues/14025) - Fix exception when viewing a script that begins with the same name as another
* [#14026](https://github.com/netbox-community/netbox/issues/14026) - Optimize the automatic creation of available IP addresses for large prefixes
* [#14042](https://github.com/netbox-community/netbox/issues/14042) - Fix duplicated child object count decrements when removing objects in bulk

---

## v3.6.3 (2023-09-26)

### Enhancements
Expand Down
12 changes: 6 additions & 6 deletions netbox/dcim/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from netbox.api.metadata import ContentTypeMetadata
from netbox.api.pagination import StripCountAnnotationsPaginator
from netbox.api.renderers import TextRenderer
from netbox.api.viewsets import NetBoxModelViewSet
from netbox.api.viewsets import NetBoxModelViewSet, MPTTLockedMixin
from netbox.api.viewsets.mixins import SequentialBulkCreatesMixin
from netbox.constants import NESTED_SERIALIZER_PREFIX
from utilities.api import get_serializer_for_model
Expand Down Expand Up @@ -98,7 +98,7 @@ def paths(self, request, pk):
# Regions
#

class RegionViewSet(NetBoxModelViewSet):
class RegionViewSet(MPTTLockedMixin, NetBoxModelViewSet):
queryset = Region.objects.add_related_count(
Region.objects.all(),
Site,
Expand All @@ -114,7 +114,7 @@ class RegionViewSet(NetBoxModelViewSet):
# Site groups
#

class SiteGroupViewSet(NetBoxModelViewSet):
class SiteGroupViewSet(MPTTLockedMixin, NetBoxModelViewSet):
queryset = SiteGroup.objects.add_related_count(
SiteGroup.objects.all(),
Site,
Expand Down Expand Up @@ -149,7 +149,7 @@ class SiteViewSet(NetBoxModelViewSet):
# Locations
#

class LocationViewSet(NetBoxModelViewSet):
class LocationViewSet(MPTTLockedMixin, NetBoxModelViewSet):
queryset = Location.objects.add_related_count(
Location.objects.add_related_count(
Location.objects.all(),
Expand Down Expand Up @@ -350,7 +350,7 @@ class DeviceBayTemplateViewSet(NetBoxModelViewSet):
filterset_class = filtersets.DeviceBayTemplateFilterSet


class InventoryItemTemplateViewSet(NetBoxModelViewSet):
class InventoryItemTemplateViewSet(MPTTLockedMixin, NetBoxModelViewSet):
queryset = InventoryItemTemplate.objects.prefetch_related('device_type__manufacturer', 'role')
serializer_class = serializers.InventoryItemTemplateSerializer
filterset_class = filtersets.InventoryItemTemplateFilterSet
Expand Down Expand Up @@ -538,7 +538,7 @@ class DeviceBayViewSet(NetBoxModelViewSet):
brief_prefetch_fields = ['device']


class InventoryItemViewSet(NetBoxModelViewSet):
class InventoryItemViewSet(MPTTLockedMixin, NetBoxModelViewSet):
queryset = InventoryItem.objects.prefetch_related('device', 'manufacturer', 'tags')
serializer_class = serializers.InventoryItemSerializer
filterset_class = filtersets.InventoryItemFilterSet
Expand Down
17 changes: 17 additions & 0 deletions netbox/dcim/filtersets.py
Original file line number Diff line number Diff line change
Expand Up @@ -1745,6 +1745,10 @@ class CableFilterSet(TenancyFilterSet, NetBoxModelFilterSet):
method='filter_by_cable_end_b',
field_name='terminations__termination_id'
)
unterminated = django_filters.BooleanFilter(
method='_unterminated',
label=_('Unterminated'),
)
type = django_filters.MultipleChoiceFilter(
choices=CableTypeChoices
)
Expand Down Expand Up @@ -1812,6 +1816,19 @@ def filter_by_cable_end_b(self, queryset, name, value):
# Filter by termination id and cable_end type
return self.filter_by_cable_end(queryset, name, value, CableEndChoices.SIDE_B)

def _unterminated(self, queryset, name, value):
if value:
terminated_ids = (
queryset.filter(terminations__cable_end=CableEndChoices.SIDE_A)
.filter(terminations__cable_end=CableEndChoices.SIDE_B)
.values("id")
)
return queryset.exclude(id__in=terminated_ids)
else:
return queryset.filter(terminations__cable_end=CableEndChoices.SIDE_A).filter(
terminations__cable_end=CableEndChoices.SIDE_B
)


class CableTerminationFilterSet(BaseFilterSet):
termination_type = ContentTypeFilter()
Expand Down
2 changes: 1 addition & 1 deletion netbox/dcim/forms/bulk_import.py
Original file line number Diff line number Diff line change
Expand Up @@ -1192,7 +1192,7 @@ def _clean_side(self, side):
termination_object = model.objects.get(device__in=device.virtual_chassis.members.all(), name=name)
else:
termination_object = model.objects.get(device=device, name=name)
if termination_object.cable is not None:
if termination_object.cable is not None and termination_object.cable != self.instance:
raise forms.ValidationError(f"Side {side.upper()}: {device} {termination_object} is already connected")
except ObjectDoesNotExist:
raise forms.ValidationError(f"{side.upper()} side termination not found: {device} {name}")
Expand Down
31 changes: 19 additions & 12 deletions netbox/dcim/forms/filtersets.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ class DeviceComponentFilterForm(NetBoxModelFilterSetForm):
required=False,
label=_('Device type')
)
role_id = DynamicModelMultipleChoiceField(
device_role_id = DynamicModelMultipleChoiceField(
queryset=DeviceRole.objects.all(),
required=False,
label=_('Device role')
Expand Down Expand Up @@ -910,7 +910,7 @@ class CableFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
fieldsets = (
(None, ('q', 'filter_id', 'tag')),
(_('Location'), ('site_id', 'location_id', 'rack_id', 'device_id')),
(_('Attributes'), ('type', 'status', 'color', 'length', 'length_unit')),
(_('Attributes'), ('type', 'status', 'color', 'length', 'length_unit', 'unterminated')),
(_('Tenant'), ('tenant_group_id', 'tenant_id')),
)
region_id = DynamicModelMultipleChoiceField(
Expand Down Expand Up @@ -979,6 +979,13 @@ class CableFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
choices=add_blank_choice(CableLengthUnitChoices),
required=False
)
unterminated = forms.NullBooleanField(
label=_('Unterminated'),
required=False,
widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
tag = TagFilterField(model)


Expand Down Expand Up @@ -1136,7 +1143,7 @@ class ConsolePortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
(None, ('q', 'filter_id', 'tag')),
(_('Attributes'), ('name', 'label', 'type', 'speed')),
(_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
(_('Device'), ('device_type_id', 'role_id', 'device_id', 'virtual_chassis_id')),
(_('Device'), ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')),
(_('Connection'), ('cabled', 'connected', 'occupied')),
)
type = forms.MultipleChoiceField(
Expand All @@ -1158,7 +1165,7 @@ class ConsoleServerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterF
(None, ('q', 'filter_id', 'tag')),
(_('Attributes'), ('name', 'label', 'type', 'speed')),
(_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
(_('Device'), ('device_type_id', 'role_id', 'device_id', 'virtual_chassis_id')),
(_('Device'), ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')),
(_('Connection'), ('cabled', 'connected', 'occupied')),
)
type = forms.MultipleChoiceField(
Expand All @@ -1180,7 +1187,7 @@ class PowerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
(None, ('q', 'filter_id', 'tag')),
(_('Attributes'), ('name', 'label', 'type')),
(_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
(_('Device'), ('device_type_id', 'role_id', 'device_id', 'virtual_chassis_id')),
(_('Device'), ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')),
(_('Connection'), ('cabled', 'connected', 'occupied')),
)
type = forms.MultipleChoiceField(
Expand All @@ -1197,7 +1204,7 @@ class PowerOutletFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
(None, ('q', 'filter_id', 'tag')),
(_('Attributes'), ('name', 'label', 'type')),
(_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
(_('Device'), ('device_type_id', 'role_id', 'device_id', 'virtual_chassis_id')),
(_('Device'), ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')),
(_('Connection'), ('cabled', 'connected', 'occupied')),
)
type = forms.MultipleChoiceField(
Expand All @@ -1217,7 +1224,7 @@ class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
(_('PoE'), ('poe_mode', 'poe_type')),
(_('Wireless'), ('rf_role', 'rf_channel', 'rf_channel_width', 'tx_power')),
(_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
(_('Device'), ('device_type_id', 'role_id', 'device_id', 'virtual_chassis_id', 'vdc_id')),
(_('Device'), ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id', 'vdc_id')),
(_('Connection'), ('cabled', 'connected', 'occupied')),
)
vdc_id = DynamicModelMultipleChoiceField(
Expand Down Expand Up @@ -1324,7 +1331,7 @@ class FrontPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
(None, ('q', 'filter_id', 'tag')),
(_('Attributes'), ('name', 'label', 'type', 'color')),
(_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
(_('Device'), ('device_type_id', 'role_id', 'device_id', 'virtual_chassis_id')),
(_('Device'), ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')),
(_('Cable'), ('cabled', 'occupied')),
)
model = FrontPort
Expand All @@ -1346,7 +1353,7 @@ class RearPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
(None, ('q', 'filter_id', 'tag')),
(_('Attributes'), ('name', 'label', 'type', 'color')),
(_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
(_('Device'), ('device_type_id', 'role_id', 'device_id', 'virtual_chassis_id')),
(_('Device'), ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')),
(_('Cable'), ('cabled', 'occupied')),
)
type = forms.MultipleChoiceField(
Expand All @@ -1367,7 +1374,7 @@ class ModuleBayFilterForm(DeviceComponentFilterForm):
(None, ('q', 'filter_id', 'tag')),
(_('Attributes'), ('name', 'label', 'position')),
(_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
(_('Device'), ('device_type_id', 'role_id', 'device_id', 'virtual_chassis_id')),
(_('Device'), ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')),
)
tag = TagFilterField(model)
position = forms.CharField(
Expand All @@ -1382,7 +1389,7 @@ class DeviceBayFilterForm(DeviceComponentFilterForm):
(None, ('q', 'filter_id', 'tag')),
(_('Attributes'), ('name', 'label')),
(_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
(_('Device'), ('device_type_id', 'role_id', 'device_id', 'virtual_chassis_id')),
(_('Device'), ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')),
)
tag = TagFilterField(model)

Expand All @@ -1393,7 +1400,7 @@ class InventoryItemFilterForm(DeviceComponentFilterForm):
(None, ('q', 'filter_id', 'tag')),
(_('Attributes'), ('name', 'label', 'role_id', 'manufacturer_id', 'serial', 'asset_tag', 'discovered')),
(_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
(_('Device'), ('device_type_id', 'role_id', 'device_id', 'virtual_chassis_id')),
(_('Device'), ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')),
)
role_id = DynamicModelMultipleChoiceField(
queryset=InventoryItemRole.objects.all(),
Expand Down
2 changes: 2 additions & 0 deletions netbox/dcim/svg/cables.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,8 @@ def _get_labels(cls, instance):
elif instance._meta.model_name == 'circuit':
labels[0] = f'Circuit {instance}'
labels.append(instance.provider)
if instance.description:
labels.append(instance.description)
elif instance._meta.model_name == 'circuittermination':
if instance.xconnect_id:
labels.append(f'{instance.xconnect_id}')
Expand Down
Loading

0 comments on commit d195f9c

Please sign in to comment.