Skip to content

Commit 2a4e3dd

Browse files
committed
Merge branch 'develop' into feature
2 parents 7d7e812 + 0dbfbf6 commit 2a4e3dd

File tree

42 files changed

+569
-1129
lines changed

Some content is hidden

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

42 files changed

+569
-1129
lines changed

.github/ISSUE_TEMPLATE/bug_report.yaml

+1-1
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.5.8
17+
placeholder: v3.5.9
1818
validations:
1919
required: true
2020
- type: dropdown

.github/ISSUE_TEMPLATE/feature_request.yaml

+1-1
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.5.8
17+
placeholder: v3.5.9
1818
validations:
1919
required: true
2020
- type: dropdown

contrib/generated_schema.json

+1
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,7 @@
332332
"100gbase-x-cfp",
333333
"100gbase-x-cfp2",
334334
"200gbase-x-cfp2",
335+
"400gbase-x-cfp2",
335336
"100gbase-x-cfp4",
336337
"100gbase-x-cxp",
337338
"100gbase-x-cpak",

docs/customization/reports.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ The following methods are available to log results within a report:
111111

112112
The recording of one or more failure messages will automatically flag a report as failed. It is advised to log a success for each object that is evaluated so that the results will reflect how many objects are being reported on. (The inclusion of a log message is optional for successes.) Messages recorded with `log()` will appear in a report's results but are not associated with a particular object or status. Log messages also support using markdown syntax and will be rendered on the report result page.
113113

114-
To perform additional tasks, such as sending an email or calling a webhook, before or after a report is run, extend the `pre_run()` and/or `post_run()` methods, respectively. The status of a completed report is available as `self.failed` and the results object is `self.result`.
114+
To perform additional tasks, such as sending an email or calling a webhook, before or after a report is run, extend the `pre_run()` and/or `post_run()` methods, respectively.
115115

116116
By default, reports within a module are ordered alphabetically in the reports list page. To return reports in a specific order, you can define the `report_order` variable at the end of your module. The `report_order` variable is a tuple which contains each Report class in the desired order. Any reports that are omitted from this list will be listed last.
117117

docs/installation/upgrading.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ Copy `local_requirements.txt`, `configuration.py`, and `ldap_config.py` (if pres
5959

6060
```no-highlight
6161
# Set $OLDVER to the NetBox version currently installed
62-
NEWVER=3.4.9
62+
OLDVER=3.4.9
6363
sudo cp /opt/netbox-$OLDVER/local_requirements.txt /opt/netbox/
6464
sudo cp /opt/netbox-$OLDVER/netbox/netbox/configuration.py /opt/netbox/netbox/netbox/
6565
sudo cp /opt/netbox-$OLDVER/netbox/netbox/ldap_config.py /opt/netbox/netbox/netbox/

docs/reference/filtering.md

+20-19
Original file line numberDiff line numberDiff line change
@@ -61,13 +61,14 @@ These lookup expressions can be applied by adding a suffix to the desired field'
6161

6262
Numeric based fields (ASN, VLAN ID, etc) support these lookup expressions:
6363

64-
| Filter | Description |
65-
|--------|-------------|
66-
| `n` | Not equal to |
67-
| `lt` | Less than |
68-
| `lte` | Less than or equal to |
69-
| `gt` | Greater than |
70-
| `gte` | Greater than or equal to |
64+
| Filter | Description |
65+
|---------|--------------------------|
66+
| `n` | Not equal to |
67+
| `lt` | Less than |
68+
| `lte` | Less than or equal to |
69+
| `gt` | Greater than |
70+
| `gte` | Greater than or equal to |
71+
| `empty` | Is empty/null (boolean) |
7172

7273
Here is an example of a numeric field lookup expression that will return all VLANs with a VLAN ID greater than 900:
7374

@@ -79,18 +80,18 @@ GET /api/ipam/vlans/?vid__gt=900
7980

8081
String based (char) fields (Name, Address, etc) support these lookup expressions:
8182

82-
| Filter | Description |
83-
|--------|-------------|
84-
| `n` | Not equal to |
85-
| `ic` | Contains (case-insensitive) |
86-
| `nic` | Does not contain (case-insensitive) |
87-
| `isw` | Starts with (case-insensitive) |
88-
| `nisw` | Does not start with (case-insensitive) |
89-
| `iew` | Ends with (case-insensitive) |
90-
| `niew` | Does not end with (case-insensitive) |
91-
| `ie` | Exact match (case-insensitive) |
92-
| `nie` | Inverse exact match (case-insensitive) |
93-
| `empty` | Is empty (boolean) |
83+
| Filter | Description |
84+
|---------|----------------------------------------|
85+
| `n` | Not equal to |
86+
| `ic` | Contains (case-insensitive) |
87+
| `nic` | Does not contain (case-insensitive) |
88+
| `isw` | Starts with (case-insensitive) |
89+
| `nisw` | Does not start with (case-insensitive) |
90+
| `iew` | Ends with (case-insensitive) |
91+
| `niew` | Does not end with (case-insensitive) |
92+
| `ie` | Exact match (case-insensitive) |
93+
| `nie` | Inverse exact match (case-insensitive) |
94+
| `empty` | Is empty/null (boolean) |
9495

9596
Here is an example of a lookup expression on a string field that will return all devices with `switch` in the name:
9697

docs/release-notes/version-3.5.md

+27-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,32 @@
11
# NetBox v3.5
22

3-
## v3.5.9 (FUTURE)
3+
## v3.5.9 (2023-08-28)
4+
5+
### Enhancements
6+
7+
* [#12489](https://github.com/netbox-community/netbox/issues/12489) - Dynamically render location and device lists under site and location views
8+
* [#12825](https://github.com/netbox-community/netbox/issues/12825) - Display assigned values count per obejct type under custom field view
9+
* [#13313](https://github.com/netbox-community/netbox/issues/13313) - Enable filtering IP ranges by containing prefix
10+
* [#13415](https://github.com/netbox-community/netbox/issues/13415) - Include request object in custom link renderer on tables
11+
* [#13536](https://github.com/netbox-community/netbox/issues/13536) - Move child VLANs list to a separate tab under VLAN group view
12+
* [#13542](https://github.com/netbox-community/netbox/issues/13542) - Pass additional HTTP headers through to custom script context
13+
* [#13585](https://github.com/netbox-community/netbox/issues/13585) - Introduce `empty` lookup for numeric value filters
14+
15+
### Bug Fixes
16+
17+
* [#11272](https://github.com/netbox-community/netbox/issues/11272) - Fix localization support for device position field
18+
* [#13358](https://github.com/netbox-community/netbox/issues/13358) - Git backend should send HTTP auth headers only if credentials have been defined
19+
* [#13477](https://github.com/netbox-community/netbox/issues/13477) - Fix filtering of modified objects after bulk import/update
20+
* [#13478](https://github.com/netbox-community/netbox/issues/13478) - Fix filtering of export templates by content type under web UI
21+
* [#13500](https://github.com/netbox-community/netbox/issues/13500) - Fix form validation for bulk update of L2VPN terminations via bulk import form
22+
* [#13503](https://github.com/netbox-community/netbox/issues/13503) - Fix utilization graph proportions when localization is enabled
23+
* [#13507](https://github.com/netbox-community/netbox/issues/13507) - Avoid raising exception for invalid content type during global search
24+
* [#13516](https://github.com/netbox-community/netbox/issues/13516) - Plugin utility functions should be importable from `extras.plugins`
25+
* [#13530](https://github.com/netbox-community/netbox/issues/13530) - Ensure script log messages can be serialized as JSON data
26+
* [#13543](https://github.com/netbox-community/netbox/issues/13543) - Config context tab under device/VM view should not require `extras.view_configcontext` permission
27+
* [#13544](https://github.com/netbox-community/netbox/issues/13544) - Ensure `reindex` command clears all cached values when not in lazy mode
28+
* [#13556](https://github.com/netbox-community/netbox/issues/13556) - Correct REST API representation of VDC status choice
29+
* [#13569](https://github.com/netbox-community/netbox/issues/13569) - Fix selection widgets for related interfaces when bulk editing interfaces under device view
430

531
---
632

netbox/core/data_backends.py

+7-6
Original file line numberDiff line numberDiff line change
@@ -125,12 +125,13 @@ def fetch(self):
125125
}
126126

127127
if self.url_scheme in ('http', 'https'):
128-
clone_args.update(
129-
{
130-
"username": self.params.get('username'),
131-
"password": self.params.get('password'),
132-
}
133-
)
128+
if self.params.get('username'):
129+
clone_args.update(
130+
{
131+
"username": self.params.get('username'),
132+
"password": self.params.get('password'),
133+
}
134+
)
134135

135136
logger.debug(f"Cloning git repo: {self.url}")
136137
try:

netbox/dcim/api/serializers.py

+1
Original file line numberDiff line numberDiff line change
@@ -758,6 +758,7 @@ class VirtualDeviceContextSerializer(NetBoxModelSerializer):
758758
primary_ip = NestedIPAddressSerializer(read_only=True, allow_null=True)
759759
primary_ip4 = NestedIPAddressSerializer(required=False, allow_null=True)
760760
primary_ip6 = NestedIPAddressSerializer(required=False, allow_null=True)
761+
status = ChoiceField(choices=VirtualDeviceContextStatusChoices)
761762

762763
# Related object counts
763764
interface_count = serializers.IntegerField(read_only=True)

netbox/dcim/forms/model_forms.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -421,12 +421,13 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
421421
label=_('Position'),
422422
required=False,
423423
help_text=_("The lowest-numbered unit occupied by the device"),
424+
localize=True,
424425
widget=APISelect(
425426
api_url='/api/dcim/racks/{{rack}}/elevation/',
426427
attrs={
427428
'disabled-indicator': 'device',
428429
'data-dynamic-params': '[{"fieldName":"face","queryParam":"face"}]'
429-
}
430+
},
430431
)
431432
)
432433
device_type = DynamicModelChoiceField(

netbox/dcim/views.py

-33
Original file line numberDiff line numberDiff line change
@@ -398,32 +398,8 @@ def get_extra_context(self, request, instance):
398398
(Circuit.objects.restrict(request.user, 'view').filter(terminations__site=instance).distinct(), 'site_id'),
399399
)
400400

401-
locations = Location.objects.add_related_count(
402-
Location.objects.all(),
403-
Rack,
404-
'location',
405-
'rack_count',
406-
cumulative=True
407-
)
408-
locations = Location.objects.add_related_count(
409-
locations,
410-
Device,
411-
'location',
412-
'device_count',
413-
cumulative=True
414-
).restrict(request.user, 'view').filter(site=instance)
415-
416-
nonracked_devices = Device.objects.filter(
417-
site=instance,
418-
rack__isnull=True,
419-
parent_bay__isnull=True
420-
).prefetch_related('device_type__manufacturer', 'parent_bay', 'role')
421-
422401
return {
423402
'related_models': related_models,
424-
'locations': locations,
425-
'nonracked_devices': nonracked_devices.order_by('-pk')[:10],
426-
'total_nonracked_devices_count': nonracked_devices.count(),
427403
}
428404

429405

@@ -495,16 +471,8 @@ def get_extra_context(self, request, instance):
495471
(Device.objects.restrict(request.user, 'view').filter(location__in=locations), 'location_id'),
496472
)
497473

498-
nonracked_devices = Device.objects.filter(
499-
location=instance,
500-
rack__isnull=True,
501-
parent_bay__isnull=True
502-
).prefetch_related('device_type__manufacturer', 'parent_bay', 'role')
503-
504474
return {
505475
'related_models': related_models,
506-
'nonracked_devices': nonracked_devices.order_by('-pk')[:10],
507-
'total_nonracked_devices_count': nonracked_devices.count(),
508476
}
509477

510478

@@ -2055,7 +2023,6 @@ class DeviceConfigContextView(ObjectConfigContextView):
20552023
base_template = 'dcim/device/base.html'
20562024
tab = ViewTab(
20572025
label=_('Config Context'),
2058-
permission='extras.view_configcontext',
20592026
weight=2000
20602027
)
20612028

netbox/extras/forms/filtersets.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ class ExportTemplateFilterForm(SavedFiltersMixin, FilterForm):
136136
fieldsets = (
137137
(None, ('q', 'filter_id')),
138138
(_('Data'), ('data_source_id', 'data_file_id')),
139-
(_('Attributes'), ('content_types', 'mime_type', 'file_extension', 'as_attachment')),
139+
(_('Attributes'), ('content_type_id', 'mime_type', 'file_extension', 'as_attachment')),
140140
)
141141
data_source_id = DynamicModelMultipleChoiceField(
142142
queryset=DataSource.objects.all(),
@@ -151,10 +151,10 @@ class ExportTemplateFilterForm(SavedFiltersMixin, FilterForm):
151151
'source_id': '$data_source_id'
152152
}
153153
)
154-
content_types = ContentTypeMultipleChoiceField(
155-
label=_('Content types'),
154+
content_type_id = ContentTypeMultipleChoiceField(
156155
queryset=ContentType.objects.filter(FeatureQuery('export_templates').get_query()),
157-
required=False
156+
required=False,
157+
label=_('Content types')
158158
)
159159
mime_type = forms.CharField(
160160
required=False,

netbox/extras/management/commands/reindex.py

+1-4
Original file line numberDiff line numberDiff line change
@@ -69,10 +69,7 @@ def handle(self, *model_labels, **kwargs):
6969
if not kwargs['lazy']:
7070
self.stdout.write('Clearing cached values... ', ending='')
7171
self.stdout.flush()
72-
content_types = [
73-
ContentType.objects.get_for_model(model) for model in indexers.keys()
74-
]
75-
deleted_count = search_backend.clear(content_types)
72+
deleted_count = search_backend.clear()
7673
self.stdout.write(f'{deleted_count} entries deleted.')
7774

7875
# Index models

netbox/extras/plugins/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from .navigation import *
1212
from .registration import *
1313
from .templates import *
14+
from .utils import *
1415

1516
# Initialize plugin registry
1617
registry['plugins'].update({

netbox/extras/scripts.py

+5-5
Original file line numberDiff line numberDiff line change
@@ -401,23 +401,23 @@ def as_form(self, data=None, files=None, initial=None):
401401

402402
def log_debug(self, message):
403403
self.logger.log(logging.DEBUG, message)
404-
self.log.append((LogLevelChoices.LOG_DEFAULT, message))
404+
self.log.append((LogLevelChoices.LOG_DEFAULT, str(message)))
405405

406406
def log_success(self, message):
407407
self.logger.log(logging.INFO, message) # No syslog equivalent for SUCCESS
408-
self.log.append((LogLevelChoices.LOG_SUCCESS, message))
408+
self.log.append((LogLevelChoices.LOG_SUCCESS, str(message)))
409409

410410
def log_info(self, message):
411411
self.logger.log(logging.INFO, message)
412-
self.log.append((LogLevelChoices.LOG_INFO, message))
412+
self.log.append((LogLevelChoices.LOG_INFO, str(message)))
413413

414414
def log_warning(self, message):
415415
self.logger.log(logging.WARNING, message)
416-
self.log.append((LogLevelChoices.LOG_WARNING, message))
416+
self.log.append((LogLevelChoices.LOG_WARNING, str(message)))
417417

418418
def log_failure(self, message):
419419
self.logger.log(logging.ERROR, message)
420-
self.log.append((LogLevelChoices.LOG_FAILURE, message))
420+
self.log.append((LogLevelChoices.LOG_FAILURE, str(message)))
421421

422422
# Convenience functions
423423

netbox/extras/tests/test_filtersets.py

+31-6
Original file line numberDiff line numberDiff line change
@@ -1109,11 +1109,13 @@ def setUpTestData(cls):
11091109
Site(name='Site 1', slug='site-1'),
11101110
Site(name='Site 2', slug='site-2'),
11111111
Site(name='Site 3', slug='site-3'),
1112+
Site(name='Site 4', slug='site-4'),
11121113
)
11131114
Site.objects.bulk_create(sites)
11141115

11151116
# Simulate *creation* changelog records for two of the sites
11161117
request_id = uuid.uuid4()
1118+
cls.create_request_id = request_id
11171119
objectchanges = (
11181120
ObjectChange(
11191121
changed_object_type=content_type,
@@ -1132,6 +1134,7 @@ def setUpTestData(cls):
11321134

11331135
# Simulate *update* changelog records for two of the sites
11341136
request_id = uuid.uuid4()
1137+
cls.update_request_id = request_id
11351138
objectchanges = (
11361139
ObjectChange(
11371140
changed_object_type=content_type,
@@ -1148,14 +1151,36 @@ def setUpTestData(cls):
11481151
)
11491152
ObjectChange.objects.bulk_create(objectchanges)
11501153

1154+
# Simulate *create* and *update* changelog records for two of the sites
1155+
request_id = uuid.uuid4()
1156+
cls.create_update_request_id = request_id
1157+
objectchanges = (
1158+
ObjectChange(
1159+
changed_object_type=content_type,
1160+
changed_object_id=sites[2].pk,
1161+
action=ObjectChangeActionChoices.ACTION_CREATE,
1162+
request_id=request_id
1163+
),
1164+
ObjectChange(
1165+
changed_object_type=content_type,
1166+
changed_object_id=sites[3].pk,
1167+
action=ObjectChangeActionChoices.ACTION_UPDATE,
1168+
request_id=request_id
1169+
),
1170+
)
1171+
ObjectChange.objects.bulk_create(objectchanges)
1172+
11511173
def test_created_by_request(self):
1152-
request_id = ObjectChange.objects.filter(action=ObjectChangeActionChoices.ACTION_CREATE).first().request_id
1153-
params = {'created_by_request': request_id}
1174+
params = {'created_by_request': self.create_request_id}
11541175
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
1155-
self.assertEqual(self.queryset.count(), 3)
1176+
self.assertEqual(self.queryset.count(), 4)
11561177

11571178
def test_updated_by_request(self):
1158-
request_id = ObjectChange.objects.filter(action=ObjectChangeActionChoices.ACTION_UPDATE).first().request_id
1159-
params = {'updated_by_request': request_id}
1179+
params = {'updated_by_request': self.update_request_id}
1180+
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
1181+
self.assertEqual(self.queryset.count(), 4)
1182+
1183+
def test_modified_by_request(self):
1184+
params = {'modified_by_request': self.create_update_request_id}
11601185
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
1161-
self.assertEqual(self.queryset.count(), 3)
1186+
self.assertEqual(self.queryset.count(), 4)

netbox/extras/views.py

+15
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,21 @@ class CustomFieldListView(generic.ObjectListView):
4646
class CustomFieldView(generic.ObjectView):
4747
queryset = CustomField.objects.select_related('choice_set')
4848

49+
def get_extra_context(self, request, instance):
50+
related_models = ()
51+
52+
for content_type in instance.content_types.all():
53+
related_models += (
54+
content_type.model_class().objects.restrict(request.user, 'view').exclude(
55+
Q(**{f'custom_field_data__{instance.name}': ''}) |
56+
Q(**{f'custom_field_data__{instance.name}': None})
57+
),
58+
)
59+
60+
return {
61+
'related_models': related_models
62+
}
63+
4964

5065
@register_model_view(CustomField, 'edit')
5166
class CustomFieldEditView(generic.ObjectEditView):

0 commit comments

Comments
 (0)