Skip to content

Commit

Permalink
Merge branch 'develop' into feature
Browse files Browse the repository at this point in the history
  • Loading branch information
jeremystretch committed Dec 12, 2024
2 parents 0219dd7 + e63fe23 commit 39ca3ce
Show file tree
Hide file tree
Showing 64 changed files with 89,446 additions and 76,844 deletions.
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/01-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: v4.1.7
placeholder: v4.1.8
validations:
required: true
- type: dropdown
Expand Down
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/02-bug_report.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ body:
attributes:
label: NetBox Version
description: What version of NetBox are you currently running?
placeholder: v4.1.7
placeholder: v4.1.8
validations:
required: true
- type: dropdown
Expand Down
9 changes: 9 additions & 0 deletions .github/workflows/update-translation-strings.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,17 @@ jobs:
NETBOX_CONFIGURATION: netbox.configuration_testing

steps:
- name: Create app token
uses: actions/create-github-app-token@v1
id: app-token
with:
app-id: 1076524
private-key: ${{ secrets.HOUSEKEEPING_SECRET_KEY }}

- name: Check out repo
uses: actions/checkout@v4
with:
token: ${{ steps.app-token.outputs.token }}

- name: Set up Python
uses: actions/setup-python@v5
Expand Down
4 changes: 4 additions & 0 deletions docs/development/application-registry.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ This key lists all models which have been registered in NetBox which are not des

This store maintains all registered items for plugins, such as navigation menus, template extensions, etc.

### `request_processors`

A list of context managers to invoke when processing a request e.g. in middleware or when executing a background job. Request processors can be registered with the `@register_request_processor` decorator.

### `search`

A dictionary mapping each model (identified by its app and label) to its search index class, if one has been registered for it.
Expand Down
28 changes: 27 additions & 1 deletion docs/release-notes/version-4.1.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,32 @@
# NetBox v4.1

## v4.1.7 (FUTURE)
## v4.1.8 (2024-12-12)

### Enhancements

* [#17071](https://github.com/netbox-community/netbox/issues/17071) - Enable OOB IP address designation during bulk import
* [#17465](https://github.com/netbox-community/netbox/issues/17465) - Enable designation of rack type during bulk import & bulk edit
* [#17889](https://github.com/netbox-community/netbox/issues/17889) - Enable designating an IP address as out-of-band for a device upon creation
* [#17960](https://github.com/netbox-community/netbox/issues/17960) - Add L2TP, PPTP, Wireguard, and OpenVPN tunnel types
* [#18021](https://github.com/netbox-community/netbox/issues/18021) - Automatically clear cache on restart when `DEBUG` is enabled
* [#18061](https://github.com/netbox-community/netbox/issues/18061) - Omit stack trace from rendered device/VM configuration when an exception is raised
* [#18065](https://github.com/netbox-community/netbox/issues/18065) - Include status in device details when hovering on rack elevation
* [#18211](https://github.com/netbox-community/netbox/issues/18211) - Enable the dynamic registration of context managers for request processing

### Bug Fixes

* [#14044](https://github.com/netbox-community/netbox/issues/14044) - Fix unhandled AttributeError exception when bulk renaming objects
* [#17490](https://github.com/netbox-community/netbox/issues/17490) - Fix dynamic inclusion support for config templates
* [#17810](https://github.com/netbox-community/netbox/issues/17810) - Fix validation of racked device fields when modifying via REST API
* [#17820](https://github.com/netbox-community/netbox/issues/17820) - Ensure default custom field values are populated when creating new modules
* [#18044](https://github.com/netbox-community/netbox/issues/18044) - Show plugin-generated alerts within UI views for custom scripts
* [#18150](https://github.com/netbox-community/netbox/issues/18150) - Fix REST API pagination for low `MAX_PAGE_SIZE` values
* [#18183](https://github.com/netbox-community/netbox/issues/18183) - Omit UI navigation bar when printing
* [#18213](https://github.com/netbox-community/netbox/issues/18213) - Fix searching for ASN ranges by name

---

## v4.1.7 (2024-11-21)

### Enhancements

Expand Down
6 changes: 6 additions & 0 deletions netbox/core/apps.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from django.apps import AppConfig
from django.conf import settings
from django.core.cache import cache
from django.db import models
from django.db.migrations.operations import AlterModelOptions

Expand All @@ -22,3 +24,7 @@ def ready(self):

# Register models
register_models(*self.get_models())

# Clear Redis cache on startup in development mode
if settings.DEBUG:
cache.clear()
7 changes: 6 additions & 1 deletion netbox/dcim/forms/bulk_edit.py
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,11 @@ class RackBulkEditForm(NetBoxModelBulkEditForm):
queryset=RackRole.objects.all(),
required=False
)
rack_type = DynamicModelChoiceField(
label=_('Rack type'),
queryset=RackType.objects.all(),
required=False,
)
serial = forms.CharField(
max_length=50,
required=False,
Expand Down Expand Up @@ -441,7 +446,7 @@ class RackBulkEditForm(NetBoxModelBulkEditForm):

model = Rack
fieldsets = (
FieldSet('status', 'role', 'tenant', 'serial', 'asset_tag', 'description', name=_('Rack')),
FieldSet('status', 'role', 'tenant', 'serial', 'asset_tag', 'rack_type', 'description', name=_('Rack')),
FieldSet('region', 'site_group', 'site', 'location', name=_('Location')),
FieldSet(
'form_factor', 'width', 'u_height', 'desc_units', 'airflow', 'outer_width', 'outer_depth', 'outer_unit',
Expand Down
28 changes: 25 additions & 3 deletions netbox/dcim/forms/bulk_import.py
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,13 @@ class RackImportForm(NetBoxModelImportForm):
to_field_name='name',
help_text=_('Name of assigned role')
)
rack_type = CSVModelChoiceField(
label=_('Rack type'),
queryset=RackType.objects.all(),
to_field_name='model',
required=False,
help_text=_('Rack type model')
)
form_factor = CSVChoiceField(
label=_('Type'),
choices=RackFormFactorChoices,
Expand All @@ -267,8 +274,13 @@ class RackImportForm(NetBoxModelImportForm):
width = forms.ChoiceField(
label=_('Width'),
choices=RackWidthChoices,
required=False,
help_text=_('Rail-to-rail width (in inches)')
)
u_height = forms.IntegerField(
required=False,
label=_('Height (U)')
)
outer_unit = CSVChoiceField(
label=_('Outer unit'),
choices=RackDimensionUnitChoices,
Expand All @@ -291,9 +303,9 @@ class RackImportForm(NetBoxModelImportForm):
class Meta:
model = Rack
fields = (
'site', 'location', 'name', 'facility_id', 'tenant', 'status', 'role', 'form_factor', 'serial', 'asset_tag',
'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'mounting_depth', 'airflow',
'weight', 'max_weight', 'weight_unit', 'description', 'comments', 'tags',
'site', 'location', 'name', 'facility_id', 'tenant', 'status', 'role', 'rack_type', 'form_factor', 'serial',
'asset_tag', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit',
'mounting_depth', 'airflow', 'weight', 'max_weight', 'weight_unit', 'description', 'comments', 'tags',
)

def __init__(self, data=None, *args, **kwargs):
Expand All @@ -305,6 +317,16 @@ def __init__(self, data=None, *args, **kwargs):
params = {f"site__{self.fields['site'].to_field_name}": data.get('site')}
self.fields['location'].queryset = self.fields['location'].queryset.filter(**params)

def clean(self):
super().clean()

# width & u_height must be set if not specifying a rack type on import
if not self.instance.pk:
if not self.cleaned_data.get('rack_type') and not self.cleaned_data.get('width'):
raise forms.ValidationError(_("Width must be set if not specifying a rack type."))
if not self.cleaned_data.get('rack_type') and not self.cleaned_data.get('u_height'):
raise forms.ValidationError(_("U height must be set if not specifying a rack type."))


class RackReservationImportForm(NetBoxModelImportForm):
site = CSVModelChoiceField(
Expand Down
5 changes: 5 additions & 0 deletions netbox/dcim/models/devices.py
Original file line number Diff line number Diff line change
Expand Up @@ -1277,6 +1277,11 @@ def save(self, *args, **kwargs):
if not disable_replication:
create_instances.append(template_instance)

# Set default values for any applicable custom fields
if cf_defaults := CustomField.objects.get_defaults_for_model(component_model):
for component in create_instances:
component.custom_field_data = cf_defaults

if component_model is not ModuleBay:
component_model.objects.bulk_create(create_instances)
# Emit the post_save signal for each newly created object
Expand Down
2 changes: 2 additions & 0 deletions netbox/dcim/svg/racks.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,15 @@ def get_device_description(device):
Name: <name>
Role: <role>
Status: <status>
Device Type: <manufacturer> <model> (<u_height>)
Asset tag: <asset_tag> (if defined)
Serial: <serial> (if defined)
Description: <description> (if defined)
"""
description = f'Name: {device.name}'
description += f'\nRole: {device.role}'
description += f'\nStatus: {device.get_status_display()}'
u_height = f'{floatformat(device.device_type.u_height)}U'
description += f'\nDevice Type: {device.device_type.manufacturer.name} {device.device_type.model} ({u_height})'
if device.asset_tag:
Expand Down
10 changes: 5 additions & 5 deletions netbox/dcim/views.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import traceback

from django.contrib import messages
from django.contrib.contenttypes.models import ContentType
from django.core.paginator import EmptyPage, PageNotAnInteger
Expand Down Expand Up @@ -2238,7 +2236,8 @@ def get(self, request, **kwargs):
# If a direct export has been requested, return the rendered template content as a
# downloadable file.
if request.GET.get('export'):
response = HttpResponse(context['rendered_config'], content_type='text')
content = context['rendered_config'] or context['error_message']
response = HttpResponse(content, content_type='text')
filename = f"{instance.name or 'config'}.txt"
response['Content-Disposition'] = f'attachment; filename="{filename}"'
return response
Expand All @@ -2256,17 +2255,18 @@ def get_extra_context(self, request, instance):

# Render the config template
rendered_config = None
error_message = None
if config_template := instance.get_config_template():
try:
rendered_config = config_template.render(context=context_data)
except TemplateError as e:
messages.error(request, _("An error occurred while rendering the template: {error}").format(error=e))
rendered_config = traceback.format_exc()
error_message = _("An error occurred while rendering the template: {error}").format(error=e)

return {
'config_template': config_template,
'context_data': context_data,
'rendered_config': rendered_config,
'error_message': error_message,
}


Expand Down
4 changes: 4 additions & 0 deletions netbox/extras/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -1210,12 +1210,14 @@ def get(self, request, **kwargs):
script_class = self._get_script_class(script)
if not script_class:
return render(request, 'extras/script.html', {
'object': script,
'script': script,
})

form = script_class.as_form(initial=normalize_querydict(request.GET))

return render(request, 'extras/script.html', {
'object': script,
'script': script,
'script_class': script_class,
'form': form,
Expand All @@ -1231,6 +1233,7 @@ def post(self, request, **kwargs):
script_class = self._get_script_class(script)
if not script_class:
return render(request, 'extras/script.html', {
'object': script,
'script': script,
})

Expand All @@ -1255,6 +1258,7 @@ def post(self, request, **kwargs):
return redirect('extras:script_result', job_pk=job.pk)

return render(request, 'extras/script.html', {
'object': script,
'script': script,
'script_class': script.python_class(),
'form': form,
Expand Down
6 changes: 4 additions & 2 deletions netbox/ipam/filtersets.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,8 +214,10 @@ class Meta:
def search(self, queryset, name, value):
if not value.strip():
return queryset
qs_filter = Q(description__icontains=value)
return queryset.filter(qs_filter)
return queryset.filter(
Q(name__icontains=value) |
Q(description__icontains=value)
)


class ASNFilterSet(OrganizationalModelFilterSet, TenancyFilterSet):
Expand Down
30 changes: 27 additions & 3 deletions netbox/ipam/forms/bulk_import.py
Original file line number Diff line number Diff line change
Expand Up @@ -325,12 +325,17 @@ class IPAddressImportForm(NetBoxModelImportForm):
help_text=_('Make this the primary IP for the assigned device'),
required=False
)
is_oob = forms.BooleanField(
label=_('Is out-of-band'),
help_text=_('Designate this as the out-of-band IP address for the assigned device'),
required=False
)

class Meta:
model = IPAddress
fields = [
'address', 'vrf', 'tenant', 'status', 'role', 'device', 'virtual_machine', 'interface', 'is_primary',
'dns_name', 'description', 'comments', 'tags',
'is_oob', 'dns_name', 'description', 'comments', 'tags',
]

def __init__(self, data=None, *args, **kwargs):
Expand All @@ -344,7 +349,7 @@ def __init__(self, data=None, *args, **kwargs):
**{f"device__{self.fields['device'].to_field_name}": data['device']}
)

# Limit interface queryset by assigned device
# Limit interface queryset by assigned VM
elif data.get('virtual_machine'):
self.fields['interface'].queryset = VMInterface.objects.filter(
**{f"virtual_machine__{self.fields['virtual_machine'].to_field_name}": data['virtual_machine']}
Expand All @@ -357,16 +362,29 @@ def clean(self):
virtual_machine = self.cleaned_data.get('virtual_machine')
interface = self.cleaned_data.get('interface')
is_primary = self.cleaned_data.get('is_primary')
is_oob = self.cleaned_data.get('is_oob')

# Validate is_primary
# Validate is_primary and is_oob
if is_primary and not device and not virtual_machine:
raise forms.ValidationError({
"is_primary": _("No device or virtual machine specified; cannot set as primary IP")
})
if is_oob and not device:
raise forms.ValidationError({
"is_oob": _("No device specified; cannot set as out-of-band IP")
})
if is_oob and virtual_machine:
raise forms.ValidationError({
"is_oob": _("Cannot set out-of-band IP for virtual machines")
})
if is_primary and not interface:
raise forms.ValidationError({
"is_primary": _("No interface specified; cannot set as primary IP")
})
if is_oob and not interface:
raise forms.ValidationError({
"is_oob": _("No interface specified; cannot set as out-of-band IP")
})

def save(self, *args, **kwargs):

Expand All @@ -385,6 +403,12 @@ def save(self, *args, **kwargs):
parent.primary_ip6 = ipaddress
parent.save()

# Set as OOB for device
if self.cleaned_data.get('is_oob'):
parent = self.cleaned_data.get('device')
parent.oob_ip = ipaddress
parent.save()

return ipaddress


Expand Down
Loading

0 comments on commit 39ca3ce

Please sign in to comment.