Skip to content

Commit 6ac25ee

Browse files
Merge pull request #14238 from netbox-community/develop
Release v3.6.5
2 parents d195f9c + 41eae1b commit 6ac25ee

32 files changed

+333
-54
lines changed

.github/ISSUE_TEMPLATE/bug_report.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ body:
1414
attributes:
1515
label: NetBox version
1616
description: What version of NetBox are you currently running?
17-
placeholder: v3.6.4
17+
placeholder: v3.6.5
1818
validations:
1919
required: true
2020
- type: dropdown

.github/ISSUE_TEMPLATE/feature_request.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ body:
1414
attributes:
1515
label: NetBox version
1616
description: What version of NetBox are you currently running?
17-
placeholder: v3.6.4
17+
placeholder: v3.6.5
1818
validations:
1919
required: true
2020
- type: dropdown
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
---
2+
name: 🌍 Translation
3+
description: Request support for a new language in the user interface
4+
labels: ["type: translation"]
5+
body:
6+
- type: markdown
7+
attributes:
8+
value: >
9+
**NOTE:** This template is used only for proposing the addition of *new* languages. Please do
10+
not use it to request changes to existing translations.
11+
- type: input
12+
attributes:
13+
label: Language
14+
description: What is the name of the language in English?
15+
validations:
16+
required: true
17+
- type: input
18+
attributes:
19+
label: ISO 639-1 code
20+
description: >
21+
What is the two-letter [ISO 639-1 code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes)
22+
assigned to the language?
23+
validations:
24+
required: true
25+
- type: dropdown
26+
attributes:
27+
label: Volunteer
28+
description: Are you a fluent speaker of this language **and** willing to contribute a translation map?
29+
options:
30+
- "Yes"
31+
- "No"
32+
validations:
33+
required: true
34+
- type: textarea
35+
attributes:
36+
label: Comments
37+
description: Any other notes you would like to share

base_requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@ django-tables2
5353

5454
# User-defined tags for objects
5555
# https://github.com/jazzband/django-taggit/blob/master/CHANGELOG.rst
56-
django-taggit
56+
# TODO: Upgrade to v5.0 for NetBox v3.7 beta
57+
django-taggit<5.0
5758

5859
# A Django field for representing time zones
5960
# https://github.com/mfogel/django-timezone-field/

docs/reference/conditions.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ Multiple conditions can be combined into nested sets using AND or OR logic. This
116116
]
117117
},
118118
{
119-
"attr": "tags",
119+
"attr": "tags.slug",
120120
"value": "exempt",
121121
"op": "contains"
122122
}

docs/release-notes/version-3.6.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,35 @@
11
# NetBox v3.6
22

3+
## v3.6.5 (2023-11-09)
4+
5+
### Enhancements
6+
7+
* [#12741](https://github.com/netbox-community/netbox/issues/12741) - Add selector widget to platform field on device & virtual machine forms
8+
* [#13022](https://github.com/netbox-community/netbox/issues/13022) - Introduce support for assigning IP addresses when bulk importing services
9+
* [#13587](https://github.com/netbox-community/netbox/issues/13587) - Annotate units of measurement on power port table columns
10+
* [#13669](https://github.com/netbox-community/netbox/issues/13669) - Add bulk import button to contact assignments list view
11+
* [#13723](https://github.com/netbox-community/netbox/issues/13723) - Add inventory items column to interfaces table
12+
* [#13743](https://github.com/netbox-community/netbox/issues/13743) - Add site column to power feeds table
13+
* [#13936](https://github.com/netbox-community/netbox/issues/13936) - Add primary IPv4 and IPv6 filters for virtual machines and VDCs
14+
* [#13951](https://github.com/netbox-community/netbox/issues/13951) - Add device & virtual machine fields to service filter form
15+
* [#14085](https://github.com/netbox-community/netbox/issues/14085) - Strip trailing port number from value returned by `get_client_ip()`
16+
* [#14101](https://github.com/netbox-community/netbox/issues/14101) - Add greater/less than mask length filters for IP addresses
17+
* [#14112](https://github.com/netbox-community/netbox/issues/14112) - Add tab listing child items under inventory item view
18+
* [#14113](https://github.com/netbox-community/netbox/issues/14113) - Add optional parent column to inventory items table
19+
* [#14220](https://github.com/netbox-community/netbox/issues/14220) - Order available columns alphabetically in table configuration form
20+
* [#14221](https://github.com/netbox-community/netbox/issues/14221) - Add contact group column on contact assignments table
21+
22+
### Bug Fixes
23+
24+
* [#14033](https://github.com/netbox-community/netbox/issues/14033) - Avoid exception when attempting to connect both ends of a cable to the same object
25+
* [#14117](https://github.com/netbox-community/netbox/issues/14117) - Check that enough rear port positions have been selected to accommodate the number of front ports being created
26+
* [#14166](https://github.com/netbox-community/netbox/issues/14166) - Permit user login when maintenance mode is enabled
27+
* [#14182](https://github.com/netbox-community/netbox/issues/14182) - Ensure the active configuration is restored upon clearing cache
28+
* [#14195](https://github.com/netbox-community/netbox/issues/14195) - Correct permissions evaluation for ASN range child ASNs view
29+
* [#14223](https://github.com/netbox-community/netbox/issues/14223) - Disable ordering of jobs by assigned object
30+
31+
---
32+
333
## v3.6.4 (2023-10-17)
434

535
### Enhancements
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
11
from django.core.cache import cache
22
from django.core.management.base import BaseCommand
33

4+
from extras.models import ConfigRevision
5+
46

57
class Command(BaseCommand):
68
"""Command to clear the entire cache."""
79
help = 'Clears the cache.'
810

911
def handle(self, *args, **kwargs):
12+
# Fetch the current config revision from the cache
13+
config_version = cache.get('config_version')
14+
# Clear the cache
1015
cache.clear()
1116
self.stdout.write('Cache has been cleared.', ending="\n")
17+
if config_version:
18+
# Activate the current config revision
19+
ConfigRevision.objects.get(id=config_version).activate()
20+
self.stdout.write(f'Config revision ({config_version}) has been restored.', ending="\n")

netbox/core/tables/jobs.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ class JobTable(NetBoxTable):
1919
)
2020
object = tables.Column(
2121
verbose_name=_('Object'),
22-
linkify=True
22+
linkify=True,
23+
orderable=False
2324
)
2425
status = columns.ChoiceFieldColumn(
2526
verbose_name=_('Status'),

netbox/dcim/filtersets.py

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from extras.filtersets import LocalConfigContextFilterSet
66
from extras.models import ConfigTemplate
7+
from ipam.filtersets import PrimaryIPFilterSet
78
from ipam.models import ASN, L2VPN, IPAddress, VRF
89
from netbox.filtersets import (
910
BaseFilterSet, ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, NetBoxModelFilterSet,
@@ -817,7 +818,13 @@ class Meta:
817818
fields = ['id', 'name', 'slug', 'description']
818819

819820

820-
class DeviceFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSet, LocalConfigContextFilterSet):
821+
class DeviceFilterSet(
822+
NetBoxModelFilterSet,
823+
TenancyFilterSet,
824+
ContactModelFilterSet,
825+
LocalConfigContextFilterSet,
826+
PrimaryIPFilterSet,
827+
):
821828
manufacturer_id = django_filters.ModelMultipleChoiceFilter(
822829
field_name='device_type__manufacturer',
823830
queryset=Manufacturer.objects.all(),
@@ -993,16 +1000,6 @@ class DeviceFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilter
9931000
method='_device_bays',
9941001
label=_('Has device bays'),
9951002
)
996-
primary_ip4_id = django_filters.ModelMultipleChoiceFilter(
997-
field_name='primary_ip4',
998-
queryset=IPAddress.objects.all(),
999-
label=_('Primary IPv4 (ID)'),
1000-
)
1001-
primary_ip6_id = django_filters.ModelMultipleChoiceFilter(
1002-
field_name='primary_ip6',
1003-
queryset=IPAddress.objects.all(),
1004-
label=_('Primary IPv6 (ID)'),
1005-
)
10061003
oob_ip_id = django_filters.ModelMultipleChoiceFilter(
10071004
field_name='oob_ip',
10081005
queryset=IPAddress.objects.all(),
@@ -1069,7 +1066,7 @@ def _device_bays(self, queryset, name, value):
10691066
return queryset.exclude(devicebays__isnull=value)
10701067

10711068

1072-
class VirtualDeviceContextFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
1069+
class VirtualDeviceContextFilterSet(NetBoxModelFilterSet, TenancyFilterSet, PrimaryIPFilterSet):
10731070
device_id = django_filters.ModelMultipleChoiceFilter(
10741071
field_name='device',
10751072
queryset=Device.objects.all(),

netbox/dcim/forms/model_forms.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -442,7 +442,8 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
442442
platform = DynamicModelChoiceField(
443443
label=_('Platform'),
444444
queryset=Platform.objects.all(),
445-
required=False
445+
required=False,
446+
selector=True
446447
)
447448
cluster = DynamicModelChoiceField(
448449
label=_('Cluster'),

netbox/dcim/forms/object_create.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,23 @@ def __init__(self, *args, **kwargs):
151151
)
152152
self.fields['rear_port'].choices = choices
153153

154+
def clean(self):
155+
156+
# Check that the number of FrontPortTemplates to be created matches the selected number of RearPortTemplate
157+
# positions
158+
frontport_count = len(self.cleaned_data['name'])
159+
rearport_count = len(self.cleaned_data['rear_port'])
160+
if frontport_count != rearport_count:
161+
raise forms.ValidationError({
162+
'rear_port': _(
163+
"The number of front port templates to be created ({frontport_count}) must match the selected "
164+
"number of rear port positions ({rearport_count})."
165+
).format(
166+
frontport_count=frontport_count,
167+
rearport_count=rearport_count
168+
)
169+
})
170+
154171
def get_iterative_data(self, iteration):
155172

156173
# Assign rear port and position from selected set
@@ -291,6 +308,22 @@ def __init__(self, *args, **kwargs):
291308
)
292309
self.fields['rear_port'].choices = choices
293310

311+
def clean(self):
312+
313+
# Check that the number of FrontPorts to be created matches the selected number of RearPort positions
314+
frontport_count = len(self.cleaned_data['name'])
315+
rearport_count = len(self.cleaned_data['rear_port'])
316+
if frontport_count != rearport_count:
317+
raise forms.ValidationError({
318+
'rear_port': _(
319+
"The number of front ports to be created ({frontport_count}) must match the selected number of "
320+
"rear port positions ({rearport_count})."
321+
).format(
322+
frontport_count=frontport_count,
323+
rearport_count=rearport_count
324+
)
325+
})
326+
294327
def get_iterative_data(self, iteration):
295328

296329
# Assign rear port and position from selected set

netbox/dcim/models/cables.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,17 @@ def clean(self):
180180
if b_type not in COMPATIBLE_TERMINATION_TYPES.get(a_type):
181181
raise ValidationError(f"Incompatible termination types: {a_type} and {b_type}")
182182

183+
if a_type == b_type:
184+
# can't directly use self.a_terminations here as possible they
185+
# don't have pk yet
186+
a_pks = set(obj.pk for obj in self.a_terminations if obj.pk)
187+
b_pks = set(obj.pk for obj in self.b_terminations if obj.pk)
188+
189+
if (a_pks & b_pks):
190+
raise ValidationError(
191+
_("A and B terminations cannot connect to the same object.")
192+
)
193+
183194
# Run clean() on any new CableTerminations
184195
for termination in self.a_terminations:
185196
CableTermination(cable=self, cable_end='A', termination=termination).clean()

netbox/dcim/tables/devices.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -466,6 +466,12 @@ class PowerPortTable(ModularDeviceComponentTable, PathEndpointTable):
466466
'args': [Accessor('device_id')],
467467
}
468468
)
469+
maximum_draw = tables.Column(
470+
verbose_name=_('Maximum draw (W)')
471+
)
472+
allocated_draw = tables.Column(
473+
verbose_name=_('Allocated draw (W)')
474+
)
469475
tags = columns.TagColumn(
470476
url_name='dcim:powerport_list'
471477
)
@@ -625,6 +631,10 @@ class InterfaceTable(ModularDeviceComponentTable, BaseInterfaceTable, PathEndpoi
625631
verbose_name=_('VRF'),
626632
linkify=True
627633
)
634+
inventory_items = tables.ManyToManyColumn(
635+
linkify_item=True,
636+
verbose_name=_('Inventory Items'),
637+
)
628638
tags = columns.TagColumn(
629639
url_name='dcim:interface_list'
630640
)
@@ -636,7 +646,7 @@ class Meta(DeviceComponentTable.Meta):
636646
'speed', 'speed_formatted', 'duplex', 'mode', 'mac_address', 'wwn', 'poe_mode', 'poe_type', 'rf_role', 'rf_channel',
637647
'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'description', 'mark_connected', 'cable',
638648
'cable_color', 'wireless_link', 'wireless_lans', 'link_peer', 'connection', 'tags', 'vdcs', 'vrf', 'l2vpn',
639-
'ip_addresses', 'fhrp_groups', 'untagged_vlan', 'tagged_vlans', 'created', 'last_updated',
649+
'ip_addresses', 'fhrp_groups', 'untagged_vlan', 'tagged_vlans', 'inventory_items', 'created', 'last_updated',
640650
)
641651
default_columns = ('pk', 'name', 'device', 'label', 'enabled', 'type', 'description')
642652

@@ -933,6 +943,10 @@ class InventoryItemTable(DeviceComponentTable):
933943
discovered = columns.BooleanColumn(
934944
verbose_name=_('Discovered'),
935945
)
946+
parent = tables.Column(
947+
linkify=True,
948+
verbose_name=_('Parent'),
949+
)
936950
tags = columns.TagColumn(
937951
url_name='dcim:inventoryitem_list'
938952
)
@@ -941,7 +955,7 @@ class InventoryItemTable(DeviceComponentTable):
941955
class Meta(NetBoxTable.Meta):
942956
model = models.InventoryItem
943957
fields = (
944-
'pk', 'id', 'name', 'device', 'component', 'label', 'role', 'manufacturer', 'part_id', 'serial',
958+
'pk', 'id', 'name', 'device', 'parent', 'component', 'label', 'role', 'manufacturer', 'part_id', 'serial',
945959
'asset_tag', 'description', 'discovered', 'tags', 'created', 'last_updated',
946960
)
947961
default_columns = (

netbox/dcim/tables/power.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,11 @@ class PowerFeedTable(TenancyColumnsMixin, CableTerminationTable):
8787
linkify=True,
8888
verbose_name=_('Tenant')
8989
)
90+
site = tables.Column(
91+
accessor='rack__site',
92+
linkify=True,
93+
verbose_name=_('Site'),
94+
)
9095
comments = columns.MarkdownColumn(
9196
verbose_name=_('Comments'),
9297
)
@@ -97,9 +102,9 @@ class PowerFeedTable(TenancyColumnsMixin, CableTerminationTable):
97102
class Meta(NetBoxTable.Meta):
98103
model = PowerFeed
99104
fields = (
100-
'pk', 'id', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase',
101-
'max_utilization', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'available_power', 'tenant',
102-
'tenant_group', 'description', 'comments', 'tags', 'created', 'last_updated',
105+
'pk', 'id', 'name', 'power_panel', 'site', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage',
106+
'phase', 'max_utilization', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'available_power',
107+
'tenant', 'tenant_group', 'description', 'comments', 'tags', 'created', 'last_updated',
103108
)
104109
default_columns = (
105110
'pk', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase', 'cable',

netbox/dcim/tests/test_filtersets.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4712,12 +4712,18 @@ def setUpTestData(cls):
47124712
addresses = (
47134713
IPAddress(assigned_object=interfaces[0], address='10.1.1.1/24'),
47144714
IPAddress(assigned_object=interfaces[1], address='10.1.1.2/24'),
4715+
IPAddress(assigned_object=None, address='10.1.1.3/24'),
4716+
IPAddress(assigned_object=interfaces[0], address='2001:db8::1/64'),
4717+
IPAddress(assigned_object=interfaces[1], address='2001:db8::2/64'),
4718+
IPAddress(assigned_object=None, address='2001:db8::3/64'),
47154719
)
47164720
IPAddress.objects.bulk_create(addresses)
47174721

47184722
vdcs[0].primary_ip4 = addresses[0]
4723+
vdcs[0].primary_ip6 = addresses[3]
47194724
vdcs[0].save()
47204725
vdcs[1].primary_ip4 = addresses[1]
4726+
vdcs[1].primary_ip6 = addresses[4]
47214727
vdcs[1].save()
47224728

47234729
def test_device(self):
@@ -4738,3 +4744,17 @@ def test_has_primary_ip(self):
47384744
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
47394745
params = {'has_primary_ip': False}
47404746
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
4747+
4748+
def test_primary_ip4(self):
4749+
addresses = IPAddress.objects.filter(address__family=4)
4750+
params = {'primary_ip4_id': [addresses[0].pk, addresses[1].pk]}
4751+
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
4752+
params = {'primary_ip4_id': [addresses[2].pk]}
4753+
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 0)
4754+
4755+
def test_primary_ip6(self):
4756+
addresses = IPAddress.objects.filter(address__family=6)
4757+
params = {'primary_ip6_id': [addresses[0].pk, addresses[1].pk]}
4758+
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
4759+
params = {'primary_ip6_id': [addresses[2].pk]}
4760+
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 0)

0 commit comments

Comments
 (0)