Skip to content

Commit 15590f1

Browse files
committed
Merge branch 'develop' into feature
2 parents 61e2073 + 0330c65 commit 15590f1

20 files changed

+109
-38
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.4.6
17+
placeholder: v3.4.7
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.4.6
17+
placeholder: v3.4.7
1818
validations:
1919
required: true
2020
- type: dropdown

base_requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,8 @@ social-auth-core
129129

130130
# Django app for social-auth-core
131131
# https://github.com/python-social-auth/social-app-django
132-
social-auth-app-django
132+
# See https://github.com/python-social-auth/social-app-django/issues/429
133+
social-auth-app-django==5.0.0
133134

134135
# SVG image rendering (used for rack elevations)
135136
# https://github.com/mozman/svgwrite

docs/configuration/remote-authentication.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ If true, NetBox will automatically create local accounts for users authenticated
1616

1717
Default: `'netbox.authentication.RemoteUserBackend'`
1818

19-
This is the Python path to the custom [Django authentication backend](https://docs.djangoproject.com/en/stable/topics/auth/customizing/) to use for external user authentication. NetBox provides two built-in backends (listed below), though custom authentication backends may also be provided by other packages or plugins.
19+
This is the Python path to the custom [Django authentication backend](https://docs.djangoproject.com/en/stable/topics/auth/customizing/) to use for external user authentication. NetBox provides two built-in backends (listed below), though custom authentication backends may also be provided by other packages or plugins. Provide a string for a single backend, or an iterable for multiple backends, which will be attempted in the order given.
2020

2121
* `netbox.authentication.RemoteUserBackend`
2222
* `netbox.authentication.LDAPBackend`

docs/configuration/system.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ In order to send email, NetBox needs an email server configured. The following i
3838
* `SERVER` - Hostname or IP address of the email server (use `localhost` if running locally)
3939
* `PORT` - TCP port to use for the connection (default: `25`)
4040
* `USERNAME` - Username with which to authenticate
41-
* `PASSSWORD` - Password with which to authenticate
41+
* `PASSWORD` - Password with which to authenticate
4242
* `USE_SSL` - Use SSL when connecting to the server (default: `False`)
4343
* `USE_TLS` - Use TLS when connecting to the server (default: `False`)
4444
* `SSL_CERTFILE` - Path to the PEM-formatted SSL certificate file (optional)

docs/release-notes/version-3.4.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,31 @@
11
# NetBox v3.4
22

3-
## v3.4.7 (FUTURE)
3+
## v3.4.8 (FUTURE)
4+
5+
---
6+
7+
## v3.4.7 (2023-03-28)
48

59
### Enhancements
610

11+
* [#11645](https://github.com/netbox-community/netbox/issues/11645) - Automatically set the scheduled time when executing reports/scripts at a recurring interval
712
* [#11833](https://github.com/netbox-community/netbox/issues/11833) - Add fieldset support for custom script forms
13+
* [#11973](https://github.com/netbox-community/netbox/issues/11833) - Use SSID for representing wireless links, if set
14+
* [#11977](https://github.com/netbox-community/netbox/issues/11977) - Support designating multiple backends via `REMOTE_AUTH_BACKEND` config parameter
15+
* [#11990](https://github.com/netbox-community/netbox/issues/11990) - Improve error reporting for duplicate CSV column headings
16+
* [#11991](https://github.com/netbox-community/netbox/issues/11991) - Enable VDC assignment during bulk import/edit of interfaces
817

918
### Bug Fixes
1019

20+
* [#11914](https://github.com/netbox-community/netbox/issues/11914) - Include parameters when exporting saved filters
21+
* [#11933](https://github.com/netbox-community/netbox/issues/11933) - Fix cloning of saved filters
1122
* [#11984](https://github.com/netbox-community/netbox/issues/11984) - Remove erroneous 802.3az PoE type
1223
* [#11979](https://github.com/netbox-community/netbox/issues/11979) - Correct URL for tags in route targets list
24+
* [#12008](https://github.com/netbox-community/netbox/issues/12008) - Enable cloning of export templates
25+
* [#12029](https://github.com/netbox-community/netbox/issues/12029) - Restore missing description field on virtual chassis form
26+
* [#12038](https://github.com/netbox-community/netbox/issues/12038) - Correct display of zero values for virtual chassis member priority
27+
* [#12048](https://github.com/netbox-community/netbox/issues/12048) - Enable cloning of tags
28+
* [#12058](https://github.com/netbox-community/netbox/issues/12058) - Enable cloning of config contexts
1329

1430
---
1531

netbox/dcim/forms/bulk_edit.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1160,6 +1160,14 @@ class InterfaceBulkEditForm(
11601160
},
11611161
label=_('LAG')
11621162
)
1163+
vdcs = DynamicModelMultipleChoiceField(
1164+
queryset=VirtualDeviceContext.objects.all(),
1165+
required=False,
1166+
label='Virtual Device Contexts',
1167+
query_params={
1168+
'device_id': '$device',
1169+
}
1170+
)
11631171
speed = forms.IntegerField(
11641172
required=False,
11651173
widget=SelectSpeedWidget(),
@@ -1222,14 +1230,14 @@ class InterfaceBulkEditForm(
12221230
fieldsets = (
12231231
(None, ('module', 'type', 'label', 'speed', 'duplex', 'description')),
12241232
('Addressing', ('vrf', 'mac_address', 'wwn')),
1225-
('Operation', ('mtu', 'tx_power', 'enabled', 'mgmt_only', 'mark_connected')),
1233+
('Operation', ('vdcs', 'mtu', 'tx_power', 'enabled', 'mgmt_only', 'mark_connected')),
12261234
('PoE', ('poe_mode', 'poe_type')),
12271235
('Related Interfaces', ('parent', 'bridge', 'lag')),
12281236
('802.1Q Switching', ('mode', 'vlan_group', 'untagged_vlan', 'tagged_vlans')),
12291237
('Wireless', ('rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width')),
12301238
)
12311239
nullable_fields = (
1232-
'module', 'label', 'parent', 'bridge', 'lag', 'speed', 'duplex', 'mac_address', 'wwn', 'mtu', 'description',
1240+
'module', 'label', 'parent', 'bridge', 'lag', 'speed', 'duplex', 'mac_address', 'wwn', 'vdcs', 'mtu', 'description',
12331241
'poe_mode', 'poe_type', 'mode', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power',
12341242
'vlan_group', 'untagged_vlan', 'tagged_vlans', 'vrf',
12351243
)

netbox/dcim/forms/bulk_import.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@
1212
from ipam.models import VRF
1313
from netbox.forms import NetBoxModelImportForm
1414
from tenancy.models import Tenant
15-
from utilities.forms import CSVChoiceField, CSVContentTypeField, CSVModelChoiceField, CSVTypedChoiceField, SlugField
15+
from utilities.forms import (
16+
CSVChoiceField, CSVContentTypeField, CSVModelChoiceField, CSVTypedChoiceField, SlugField, CSVModelMultipleChoiceField
17+
)
1618
from virtualization.models import Cluster
1719
from wireless.choices import WirelessRoleChoices
1820
from .common import ModuleCommonForm
@@ -691,6 +693,12 @@ class InterfaceImportForm(NetBoxModelImportForm):
691693
to_field_name='name',
692694
help_text=_('Parent LAG interface')
693695
)
696+
vdcs = CSVModelMultipleChoiceField(
697+
queryset=VirtualDeviceContext.objects.all(),
698+
required=False,
699+
to_field_name='name',
700+
help_text='VDC names separated by commas, encased with double quotes (e.g. "vdc1, vdc2, vdc3")'
701+
)
694702
type = CSVChoiceField(
695703
choices=InterfaceTypeChoices,
696704
help_text=_('Physical medium')
@@ -730,7 +738,7 @@ class Meta:
730738
model = Interface
731739
fields = (
732740
'device', 'name', 'label', 'parent', 'bridge', 'lag', 'type', 'speed', 'duplex', 'enabled',
733-
'mark_connected', 'mac_address', 'wwn', 'mtu', 'mgmt_only', 'description', 'poe_mode', 'poe_type', 'mode',
741+
'mark_connected', 'mac_address', 'wwn', 'vdcs', 'mtu', 'mgmt_only', 'description', 'poe_mode', 'poe_type', 'mode',
734742
'vrf', 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'tags'
735743
)
736744

@@ -746,6 +754,7 @@ def __init__(self, data=None, *args, **kwargs):
746754
self.fields['parent'].queryset = self.fields['parent'].queryset.filter(**params)
747755
self.fields['bridge'].queryset = self.fields['bridge'].queryset.filter(**params)
748756
self.fields['lag'].queryset = self.fields['lag'].queryset.filter(**params)
757+
self.fields['vdcs'].queryset = self.fields['vdcs'].queryset.filter(**params)
749758

750759
def clean_enabled(self):
751760
# Make sure enabled is True when it's not included in the uploaded data
@@ -754,6 +763,12 @@ def clean_enabled(self):
754763
else:
755764
return self.cleaned_data['enabled']
756765

766+
def clean_vdcs(self):
767+
for vdc in self.cleaned_data['vdcs']:
768+
if vdc.device != self.cleaned_data['device']:
769+
raise forms.ValidationError(f"VDC {vdc} is not assigned to device {self.cleaned_data['device']}")
770+
return self.cleaned_data['vdcs']
771+
757772

758773
class FrontPortImportForm(NetBoxModelImportForm):
759774
device = CSVModelChoiceField(

netbox/dcim/forms/object_create.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -359,7 +359,7 @@ class VirtualChassisCreateForm(NetBoxModelForm):
359359
class Meta:
360360
model = VirtualChassis
361361
fields = [
362-
'name', 'domain', 'region', 'site_group', 'site', 'rack', 'members', 'initial_position', 'tags',
362+
'name', 'domain', 'description', 'region', 'site_group', 'site', 'rack', 'members', 'initial_position', 'tags',
363363
]
364364

365365
def clean(self):

netbox/extras/forms/model_forms.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1+
import json
2+
13
from django import forms
24
from django.contrib.contenttypes.models import ContentType
3-
from django.http import QueryDict
45
from django.utils.translation import gettext as _
56

67
from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup
@@ -147,11 +148,10 @@ class Meta:
147148

148149
def __init__(self, *args, initial=None, **kwargs):
149150

150-
# Convert any parameters delivered via initial data to a dictionary
151+
# Convert any parameters delivered via initial data to JSON data
151152
if initial and 'parameters' in initial:
152153
if type(initial['parameters']) is str:
153-
# TODO: Make a utility function for this
154-
initial['parameters'] = dict(QueryDict(initial['parameters']).lists())
154+
initial['parameters'] = json.loads(initial['parameters'])
155155

156156
super().__init__(*args, initial=initial, **kwargs)
157157

@@ -277,8 +277,14 @@ class Meta:
277277
'tenants', 'tags', 'data_source', 'data_file',
278278
)
279279

280-
def __init__(self, *args, **kwargs):
281-
super().__init__(*args, **kwargs)
280+
def __init__(self, *args, initial=None, **kwargs):
281+
282+
# Convert data delivered via initial data to JSON data
283+
if initial and 'data' in initial:
284+
if type(initial['data']) is str:
285+
initial['data'] = json.loads(initial['data'])
286+
287+
super().__init__(*args, initial=initial, **kwargs)
282288

283289
# Disable data field when a DataFile has been set
284290
if self.instance.data_file:

netbox/extras/forms/reports.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,16 @@ class ReportForm(BootstrapMixin, forms.Form):
2525
help_text=_("Interval at which this report is re-run (in minutes)")
2626
)
2727

28-
def clean_schedule_at(self):
28+
def clean(self):
2929
scheduled_time = self.cleaned_data['schedule_at']
30-
if scheduled_time and scheduled_time < timezone.now():
30+
if scheduled_time and scheduled_time < local_now():
3131
raise forms.ValidationError(_('Scheduled time must be in the future.'))
3232

33-
return scheduled_time
33+
# When interval is used without schedule at, raise an exception
34+
if self.cleaned_data['interval'] and not scheduled_time:
35+
self.cleaned_data['schedule_at'] = local_now()
36+
37+
return self.cleaned_data
3438

3539
def __init__(self, *args, **kwargs):
3640
super().__init__(*args, **kwargs)

netbox/extras/forms/scripts.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ def clean(self):
5252

5353
# When interval is used without schedule at, raise an exception
5454
if self.cleaned_data['_interval'] and not scheduled_time:
55-
raise forms.ValidationError(_('Scheduled time must be set when recurs is used.'))
55+
self.cleaned_data['_schedule_at'] = local_now()
5656

5757
return self.cleaned_data
5858

netbox/extras/models/configs.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,14 @@
22
from django.core.validators import ValidationError
33
from django.db import models
44
from django.urls import reverse
5-
from django.utils import timezone
65
from django.utils.translation import gettext as _
76
from jinja2.loaders import BaseLoader
87
from jinja2.sandbox import SandboxedEnvironment
98

109
from extras.querysets import ConfigContextQuerySet
1110
from netbox.config import get_config
1211
from netbox.models import ChangeLoggedModel
13-
from netbox.models.features import ExportTemplatesMixin, SyncedDataMixin, TagsMixin
12+
from netbox.models.features import CloningMixin, ExportTemplatesMixin, SyncedDataMixin, TagsMixin
1413
from utilities.jinja2 import ConfigTemplateLoader
1514
from utilities.utils import deepmerge
1615

@@ -25,7 +24,7 @@
2524
# Config contexts
2625
#
2726

28-
class ConfigContext(SyncedDataMixin, ChangeLoggedModel):
27+
class ConfigContext(SyncedDataMixin, CloningMixin, ChangeLoggedModel):
2928
"""
3029
A ConfigContext represents a set of arbitrary data available to any Device or VirtualMachine matching its assigned
3130
qualifiers (region, site, etc.). For example, the data stored in a ConfigContext assigned to site A and tenant B
@@ -114,6 +113,12 @@ class ConfigContext(SyncedDataMixin, ChangeLoggedModel):
114113

115114
objects = ConfigContextQuerySet.as_manager()
116115

116+
clone_fields = (
117+
'weight', 'is_active', 'regions', 'site_groups', 'sites', 'locations', 'device_types',
118+
'roles', 'platforms', 'cluster_types', 'cluster_groups', 'clusters', 'tenant_groups',
119+
'tenants', 'tags', 'data',
120+
)
121+
117122
class Meta:
118123
ordering = ['weight', 'name']
119124

netbox/extras/models/models.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,7 @@ class CustomLink(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
250250
)
251251

252252
clone_fields = (
253-
'enabled', 'weight', 'group_name', 'button_class', 'new_window',
253+
'content_types', 'enabled', 'weight', 'group_name', 'button_class', 'new_window',
254254
)
255255

256256
class Meta:
@@ -285,7 +285,7 @@ def render(self, context):
285285
}
286286

287287

288-
class ExportTemplate(SyncedDataMixin, ExportTemplatesMixin, ChangeLoggedModel):
288+
class ExportTemplate(SyncedDataMixin, CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
289289
content_types = models.ManyToManyField(
290290
to=ContentType,
291291
related_name='export_templates',
@@ -318,6 +318,10 @@ class ExportTemplate(SyncedDataMixin, ExportTemplatesMixin, ChangeLoggedModel):
318318
help_text=_("Download file as attachment")
319319
)
320320

321+
clone_fields = (
322+
'content_types', 'template_code', 'mime_type', 'file_extension', 'as_attachment',
323+
)
324+
321325
class Meta:
322326
ordering = ('name',)
323327

@@ -417,7 +421,7 @@ class SavedFilter(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
417421
parameters = models.JSONField()
418422

419423
clone_fields = (
420-
'enabled', 'weight',
424+
'content_types', 'weight', 'enabled', 'parameters',
421425
)
422426

423427
class Meta:

netbox/extras/models/tags.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from taggit.models import TagBase, GenericTaggedItemBase
66

77
from netbox.models import ChangeLoggedModel
8-
from netbox.models.features import ExportTemplatesMixin
8+
from netbox.models.features import CloningMixin, ExportTemplatesMixin
99
from utilities.choices import ColorChoices
1010
from utilities.fields import ColorField
1111

@@ -19,7 +19,7 @@
1919
# Tags
2020
#
2121

22-
class Tag(ExportTemplatesMixin, ChangeLoggedModel, TagBase):
22+
class Tag(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel, TagBase):
2323
id = models.BigAutoField(
2424
primary_key=True
2525
)
@@ -31,6 +31,10 @@ class Tag(ExportTemplatesMixin, ChangeLoggedModel, TagBase):
3131
blank=True,
3232
)
3333

34+
clone_fields = (
35+
'color', 'description',
36+
)
37+
3438
class Meta:
3539
ordering = ['name']
3640

netbox/netbox/models/features.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import json
12
from collections import defaultdict
23
from functools import cached_property
34

@@ -115,7 +116,11 @@ def clone(self):
115116
for field_name in getattr(self, 'clone_fields', []):
116117
field = self._meta.get_field(field_name)
117118
field_value = field.value_from_object(self)
118-
if field_value not in (None, ''):
119+
if field_value and isinstance(field, models.ManyToManyField):
120+
attrs[field_name] = [v.pk for v in field_value]
121+
elif field_value and isinstance(field, models.JSONField):
122+
attrs[field_name] = json.dumps(field_value)
123+
elif field_value not in (None, ''):
119124
attrs[field_name] = field_value
120125

121126
# Include tags (if applicable)

netbox/netbox/settings.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -394,8 +394,10 @@ def _setting(name, default=None):
394394
]
395395

396396
# Set up authentication backends
397+
if type(REMOTE_AUTH_BACKEND) not in (list, tuple):
398+
REMOTE_AUTH_BACKEND = [REMOTE_AUTH_BACKEND]
397399
AUTHENTICATION_BACKENDS = [
398-
REMOTE_AUTH_BACKEND,
400+
*REMOTE_AUTH_BACKEND,
399401
'netbox.authentication.ObjectPermissionBackend',
400402
]
401403

netbox/templates/dcim/device.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ <h5 class="card-header">
141141
{% if object.virtual_chassis.master == vc_member %}<i class="mdi mdi-check-bold"></i>{% endif %}
142142
</td>
143143
<td>
144-
{{ vc_member.vc_priority|default:"" }}
144+
{{ vc_member.vc_priority|placeholder }}
145145
</td>
146146
</tr>
147147
{% endfor %}

netbox/templates/dcim/virtualchassis_add.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ <h5 class="offset-sm-3">Virtual Chassis</h5>
88
</div>
99
{% render_field form.name %}
1010
{% render_field form.domain %}
11+
{% render_field form.description %}
1112
{% render_field form.tags %}
1213
</div>
1314

0 commit comments

Comments
 (0)