Skip to content

Commit 593874b

Browse files
Merge pull request netbox-community#7130 from netbox-community/develop
Release v3.0.1
2 parents fd16c47 + b207f28 commit 593874b

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

61 files changed

+550
-242
lines changed

.github/ISSUE_TEMPLATE/bug_report.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ body:
1717
What version of NetBox are you currently running? (If you don't have access to the most
1818
recent NetBox release, consider testing on our [demo instance](https://demo.netbox.dev/)
1919
before opening a bug report to see if your issue has already been addressed.)
20-
placeholder: v3.0.0
20+
placeholder: v3.0.1
2121
validations:
2222
required: true
2323
- 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.0.0
17+
placeholder: v3.0.1
1818
validations:
1919
required: true
2020
- type: dropdown

docs/release-notes/version-3.0.md

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

3+
## v3.0.1 (2021-09-01)
4+
5+
### Bug Fixes
6+
7+
* [#7041](https://github.com/netbox-community/netbox/issues/7041) - Properly format JSON config object returned from a NAPALM device
8+
* [#7070](https://github.com/netbox-community/netbox/issues/7070) - Fix exception when filtering by prefix max length in UI
9+
* [#7071](https://github.com/netbox-community/netbox/issues/7071) - Fix exception when removing a primary IP from a device/VM
10+
* [#7072](https://github.com/netbox-community/netbox/issues/7072) - Fix table configuration under prefix child object views
11+
* [#7075](https://github.com/netbox-community/netbox/issues/7075) - Fix UI bug when a custom field has a space in the name
12+
* [#7080](https://github.com/netbox-community/netbox/issues/7080) - Fix missing image previews
13+
* [#7081](https://github.com/netbox-community/netbox/issues/7081) - Fix UI bug that did not properly request and handle paginated data
14+
* [#7082](https://github.com/netbox-community/netbox/issues/7082) - Avoid exception when referencing invalid content type in table
15+
* [#7083](https://github.com/netbox-community/netbox/issues/7083) - Correct labeling for VM memory attribute
16+
* [#7084](https://github.com/netbox-community/netbox/issues/7084) - Fix KeyError exception when editing access VLAN on an interface
17+
* [#7084](https://github.com/netbox-community/netbox/issues/7084) - Fix issue where hidden VLAN form fields were incorrectly included in the form submission
18+
* [#7089](https://github.com/netbox-community/netbox/issues/7089) - Fix filtering of change log by content type
19+
* [#7090](https://github.com/netbox-community/netbox/issues/7090) - Allow decimal input on length field when bulk editing cables
20+
* [#7091](https://github.com/netbox-community/netbox/issues/7091) - Ensure API requests from the UI are aware of `BASE_PATH`
21+
* [#7092](https://github.com/netbox-community/netbox/issues/7092) - Fix missing bulk edit buttons on Prefix IP Addresses table
22+
* [#7093](https://github.com/netbox-community/netbox/issues/7093) - Multi-select custom field filters should employ exact match
23+
* [#7096](https://github.com/netbox-community/netbox/issues/7096) - Home links should honor `BASE_PATH` configuration
24+
* [#7101](https://github.com/netbox-community/netbox/issues/7101) - Enforce `MAX_PAGE_SIZE` for table and REST API pagination
25+
* [#7106](https://github.com/netbox-community/netbox/issues/7106) - Fix incorrect "Map It" button URL on a site's physical address field
26+
* [#7107](https://github.com/netbox-community/netbox/issues/7107) - Fix missing search button and search results in IP address assignment "Assign IP" tab
27+
* [#7109](https://github.com/netbox-community/netbox/issues/7109) - Ensure human readability of exceptions raised during REST API requests
28+
* [#7113](https://github.com/netbox-community/netbox/issues/7113) - Show bulk edit/delete actions for prefix child objects
29+
* [#7123](https://github.com/netbox-community/netbox/issues/7123) - Remove "Global" placeholder for null VRF field
30+
* [#7124](https://github.com/netbox-community/netbox/issues/7124) - Fix duplicate static query param values in API Select
31+
32+
---
33+
334
## v3.0.0 (2021-08-30)
435

536
!!! warning "Existing Deployments Must Upgrade from v2.11"

netbox/dcim/api/views.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
from netbox.api.exceptions import ServiceUnavailable
2323
from netbox.api.metadata import ContentTypeMetadata
2424
from utilities.api import get_serializer_for_model
25-
from utilities.utils import count_related
25+
from utilities.utils import count_related, decode_dict
2626
from virtualization.models import VirtualMachine
2727
from . import serializers
2828
from .exceptions import MissingFilterException
@@ -498,7 +498,7 @@ def napalm(self, request, pk):
498498
response[method] = {'error': 'Only get_* NAPALM methods are supported'}
499499
continue
500500
try:
501-
response[method] = getattr(d, method)()
501+
response[method] = decode_dict(getattr(d, method)())
502502
except NotImplementedError:
503503
response[method] = {'error': 'Method {} not implemented for NAPALM driver {}'.format(method, driver)}
504504
except Exception as e:

netbox/dcim/forms.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ def clean(self):
129129
super().clean()
130130

131131
parent_field = 'device' if 'device' in self.cleaned_data else 'virtual_machine'
132-
tagged_vlans = self.cleaned_data['tagged_vlans']
132+
tagged_vlans = self.cleaned_data.get('tagged_vlans')
133133

134134
# Untagged interfaces cannot be assigned tagged VLANs
135135
if self.cleaned_data['mode'] == InterfaceModeChoices.MODE_ACCESS and tagged_vlans:
@@ -142,7 +142,7 @@ def clean(self):
142142
self.cleaned_data['tagged_vlans'] = []
143143

144144
# Validate tagged VLANs; must be a global VLAN or in the same site
145-
elif self.cleaned_data['mode'] == InterfaceModeChoices.MODE_TAGGED:
145+
elif self.cleaned_data['mode'] == InterfaceModeChoices.MODE_TAGGED and tagged_vlans:
146146
valid_sites = [None, self.cleaned_data[parent_field].site]
147147
invalid_vlans = [str(v) for v in tagged_vlans if v.site not in valid_sites]
148148

@@ -4586,8 +4586,8 @@ class CableBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkE
45864586
color = ColorField(
45874587
required=False
45884588
)
4589-
length = forms.IntegerField(
4590-
min_value=1,
4589+
length = forms.DecimalField(
4590+
min_value=0,
45914591
required=False
45924592
)
45934593
length_unit = forms.ChoiceField(

netbox/extras/filters.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
CustomFieldTypeChoices.TYPE_DATE,
1515
CustomFieldTypeChoices.TYPE_INTEGER,
1616
CustomFieldTypeChoices.TYPE_SELECT,
17+
CustomFieldTypeChoices.TYPE_MULTISELECT,
1718
)
1819

1920

@@ -35,7 +36,9 @@ def __init__(self, custom_field, *args, **kwargs):
3536

3637
self.field_name = f'custom_field_data__{self.field_name}'
3738

38-
if custom_field.type not in EXACT_FILTER_TYPES:
39+
if custom_field.type == CustomFieldTypeChoices.TYPE_MULTISELECT:
40+
self.lookup_expr = 'has_key'
41+
elif custom_field.type not in EXACT_FILTER_TYPES:
3942
if custom_field.filter_logic == CustomFieldFilterLogicChoices.FILTER_LOOSE:
4043
self.lookup_expr = 'icontains'
4144

netbox/extras/filtersets.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,7 +367,19 @@ def search(self, queryset, name, value):
367367
#
368368

369369
class ContentTypeFilterSet(django_filters.FilterSet):
370+
q = django_filters.CharFilter(
371+
method='search',
372+
label='Search',
373+
)
370374

371375
class Meta:
372376
model = ContentType
373377
fields = ['id', 'app_label', 'model']
378+
379+
def search(self, queryset, name, value):
380+
if not value.strip():
381+
return queryset
382+
return queryset.filter(
383+
Q(app_label__icontains=value) |
384+
Q(model__icontains=value)
385+
)

netbox/extras/tests/test_customfields.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -681,7 +681,12 @@ def setUpTestData(cls):
681681
cf.content_types.set([obj_type])
682682

683683
# Selection filtering
684-
cf = CustomField(name='cf8', type=CustomFieldTypeChoices.TYPE_URL, choices=['Foo', 'Bar', 'Baz'])
684+
cf = CustomField(name='cf8', type=CustomFieldTypeChoices.TYPE_SELECT, choices=['Foo', 'Bar', 'Baz'])
685+
cf.save()
686+
cf.content_types.set([obj_type])
687+
688+
# Multiselect filtering
689+
cf = CustomField(name='cf9', type=CustomFieldTypeChoices.TYPE_MULTISELECT, choices=['A', 'AA', 'B', 'C'])
685690
cf.save()
686691
cf.content_types.set([obj_type])
687692

@@ -695,6 +700,7 @@ def setUpTestData(cls):
695700
'cf6': 'http://foo.example.com/',
696701
'cf7': 'http://foo.example.com/',
697702
'cf8': 'Foo',
703+
'cf9': ['A', 'B'],
698704
}),
699705
Site(name='Site 2', slug='site-2', custom_field_data={
700706
'cf1': 200,
@@ -705,9 +711,9 @@ def setUpTestData(cls):
705711
'cf6': 'http://bar.example.com/',
706712
'cf7': 'http://bar.example.com/',
707713
'cf8': 'Bar',
714+
'cf9': ['AA', 'B'],
708715
}),
709-
Site(name='Site 3', slug='site-3', custom_field_data={
710-
}),
716+
Site(name='Site 3', slug='site-3'),
711717
])
712718

713719
def test_filter_integer(self):
@@ -730,3 +736,10 @@ def test_filter_url(self):
730736

731737
def test_filter_select(self):
732738
self.assertEqual(self.filterset({'cf_cf8': 'Foo'}, self.queryset).qs.count(), 1)
739+
self.assertEqual(self.filterset({'cf_cf8': 'Bar'}, self.queryset).qs.count(), 1)
740+
self.assertEqual(self.filterset({'cf_cf8': 'Baz'}, self.queryset).qs.count(), 0)
741+
742+
def test_filter_multiselect(self):
743+
self.assertEqual(self.filterset({'cf_cf9': 'A'}, self.queryset).qs.count(), 1)
744+
self.assertEqual(self.filterset({'cf_cf9': 'B'}, self.queryset).qs.count(), 2)
745+
self.assertEqual(self.filterset({'cf_cf9': 'C'}, self.queryset).qs.count(), 0)

netbox/ipam/filtersets.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ class PrefixFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
216216
children = MultiValueNumberFilter(
217217
field_name='_children'
218218
)
219-
mask_length = django_filters.NumberFilter(
219+
mask_length = MultiValueNumberFilter(
220220
field_name='prefix',
221221
lookup_expr='net_mask_length'
222222
)

netbox/ipam/forms.py

Lines changed: 3 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -491,11 +491,6 @@ class Meta:
491491
'status': StaticSelect(),
492492
}
493493

494-
def __init__(self, *args, **kwargs):
495-
super().__init__(*args, **kwargs)
496-
497-
self.fields['vrf'].empty_label = 'Global'
498-
499494

500495
class PrefixCSVForm(CustomFieldModelCSVForm):
501496
vrf = CSVModelChoiceField(
@@ -658,11 +653,11 @@ class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilter
658653
label=_('Address family'),
659654
widget=StaticSelect()
660655
)
661-
mask_length = forms.ChoiceField(
656+
mask_length = forms.MultipleChoiceField(
662657
required=False,
663658
choices=PREFIX_MASK_LENGTH_CHOICES,
664659
label=_('Mask length'),
665-
widget=StaticSelect()
660+
widget=StaticSelectMultiple()
666661
)
667662
vrf_id = DynamicModelMultipleChoiceField(
668663
queryset=VRF.objects.all(),
@@ -760,11 +755,6 @@ class Meta:
760755
'status': StaticSelect(),
761756
}
762757

763-
def __init__(self, *args, **kwargs):
764-
super().__init__(*args, **kwargs)
765-
766-
self.fields['vrf'].empty_label = 'Global'
767-
768758

769759
class IPRangeCSVForm(CustomFieldModelCSVForm):
770760
vrf = CSVModelChoiceField(
@@ -1026,8 +1016,6 @@ def __init__(self, *args, **kwargs):
10261016

10271017
super().__init__(*args, **kwargs)
10281018

1029-
self.fields['vrf'].empty_label = 'Global'
1030-
10311019
# Initialize primary_for_parent if IP address is already assigned
10321020
if self.instance.pk and self.instance.assigned_object:
10331021
parent = self.instance.assigned_object.parent_object
@@ -1102,10 +1090,6 @@ class Meta:
11021090
'role': StaticSelect(),
11031091
}
11041092

1105-
def __init__(self, *args, **kwargs):
1106-
super().__init__(*args, **kwargs)
1107-
self.fields['vrf'].empty_label = 'Global'
1108-
11091093

11101094
class IPAddressCSVForm(CustomFieldModelCSVForm):
11111095
vrf = CSVModelChoiceField(
@@ -1256,8 +1240,7 @@ class IPAddressAssignForm(BootstrapMixin, forms.Form):
12561240
vrf_id = DynamicModelChoiceField(
12571241
queryset=VRF.objects.all(),
12581242
required=False,
1259-
label='VRF',
1260-
empty_label='Global'
1243+
label='VRF'
12611244
)
12621245
q = forms.CharField(
12631246
required=False,

netbox/ipam/models/ip.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -825,9 +825,9 @@ def clean(self):
825825
if self.pk:
826826
for cls, attr in ((Device, 'device'), (VirtualMachine, 'virtual_machine')):
827827
parent = cls.objects.filter(Q(primary_ip4=self) | Q(primary_ip6=self)).first()
828-
if parent and getattr(self.assigned_object, attr) != parent:
828+
if parent and getattr(self.assigned_object, attr, None) != parent:
829829
# Check for a NAT relationship
830-
if not self.nat_inside or getattr(self.nat_inside.assigned_object, attr) != parent:
830+
if not self.nat_inside or getattr(self.nat_inside.assigned_object, attr, None) != parent:
831831
raise ValidationError({
832832
'interface': f"IP address is primary for {cls._meta.model_name} {parent} but "
833833
f"not assigned to it!"

netbox/ipam/tests/test_filtersets.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -451,7 +451,7 @@ def test_children(self):
451451
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
452452

453453
def test_mask_length(self):
454-
params = {'mask_length': '24'}
454+
params = {'mask_length': ['24']}
455455
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
456456

457457
def test_vrf(self):

netbox/ipam/views.py

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -403,13 +403,19 @@ def get_extra_context(self, request, instance):
403403

404404
bulk_querystring = 'vrf_id={}&within={}'.format(instance.vrf.pk if instance.vrf else '0', instance.prefix)
405405

406+
# Compile permissions list for rendering the object table
407+
permissions = {
408+
'change': request.user.has_perm('ipam.change_prefix'),
409+
'delete': request.user.has_perm('ipam.delete_prefix'),
410+
}
411+
406412
return {
407-
'first_available_prefix': instance.get_first_available_prefix(),
408413
'table': table,
414+
'permissions': permissions,
409415
'bulk_querystring': bulk_querystring,
410416
'active_tab': 'prefixes',
417+
'first_available_prefix': instance.get_first_available_prefix(),
411418
'show_available': request.GET.get('show_available', 'true') == 'true',
412-
'table_config_form': TableConfigForm(table=table),
413419
}
414420

415421

@@ -421,15 +427,22 @@ def get_extra_context(self, request, instance):
421427
# Find all IPRanges belonging to this Prefix
422428
ip_ranges = instance.get_child_ranges().restrict(request.user, 'view').prefetch_related('vrf')
423429

424-
table = tables.IPRangeTable(ip_ranges)
430+
table = tables.IPRangeTable(ip_ranges, user=request.user)
425431
if request.user.has_perm('ipam.change_iprange') or request.user.has_perm('ipam.delete_iprange'):
426432
table.columns.show('pk')
427433
paginate_table(table, request)
428434

429435
bulk_querystring = 'vrf_id={}&parent={}'.format(instance.vrf.pk if instance.vrf else '0', instance.prefix)
430436

437+
# Compile permissions list for rendering the object table
438+
permissions = {
439+
'change': request.user.has_perm('ipam.change_iprange'),
440+
'delete': request.user.has_perm('ipam.delete_iprange'),
441+
}
442+
431443
return {
432444
'table': table,
445+
'permissions': permissions,
433446
'bulk_querystring': bulk_querystring,
434447
'active_tab': 'ip-ranges',
435448
}
@@ -449,18 +462,25 @@ def get_extra_context(self, request, instance):
449462
if request.GET.get('show_available', 'true') == 'true':
450463
ipaddresses = add_available_ipaddresses(instance.prefix, ipaddresses, instance.is_pool)
451464

452-
table = tables.IPAddressTable(ipaddresses)
465+
table = tables.IPAddressTable(ipaddresses, user=request.user)
453466
if request.user.has_perm('ipam.change_ipaddress') or request.user.has_perm('ipam.delete_ipaddress'):
454467
table.columns.show('pk')
455468
paginate_table(table, request)
456469

457470
bulk_querystring = 'vrf_id={}&parent={}'.format(instance.vrf.pk if instance.vrf else '0', instance.prefix)
458471

472+
# Compile permissions list for rendering the object table
473+
permissions = {
474+
'change': request.user.has_perm('ipam.change_ipaddress'),
475+
'delete': request.user.has_perm('ipam.delete_ipaddress'),
476+
}
477+
459478
return {
460-
'first_available_ip': instance.get_first_available_ip(),
461479
'table': table,
480+
'permissions': permissions,
462481
'bulk_querystring': bulk_querystring,
463482
'active_tab': 'ip-addresses',
483+
'first_available_ip': instance.get_first_available_ip(),
464484
'show_available': request.GET.get('show_available', 'true') == 'true',
465485
}
466486

netbox/netbox/api/pagination.py

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -34,23 +34,13 @@ def paginate_queryset(self, queryset, request, view=None):
3434
return list(queryset[self.offset:])
3535

3636
def get_limit(self, request):
37+
limit = super().get_limit(request)
3738

38-
if self.limit_query_param:
39-
try:
40-
limit = int(request.query_params[self.limit_query_param])
41-
if limit < 0:
42-
raise ValueError()
43-
# Enforce maximum page size, if defined
44-
if settings.MAX_PAGE_SIZE:
45-
if limit == 0:
46-
return settings.MAX_PAGE_SIZE
47-
else:
48-
return min(limit, settings.MAX_PAGE_SIZE)
49-
return limit
50-
except (KeyError, ValueError):
51-
pass
52-
53-
return self.default_limit
39+
# Enforce maximum page size
40+
if settings.MAX_PAGE_SIZE:
41+
limit = min(limit, settings.MAX_PAGE_SIZE)
42+
43+
return limit
5444

5545
def get_next_link(self):
5646

0 commit comments

Comments
 (0)