Compare commits

...

19 Commits

Author SHA1 Message Date
Jeremy Stretch
f845b2cf07 Release v4.2.2 2025-01-17 15:05:09 -05:00
atownson
2ed4a2b005 Fixes: #18369 - Remove the json filter for protection rules (#18388)
* Remove the json filter for protection rules

* Configure PROTECTION_RULE config attribute to use ConfigJSONEncoder as serializer

* Tweak getattr()

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
2025-01-17 14:02:12 -05:00
Jeremy Stretch
5b9210dfa5 Fixes #18392: Exclude config contexts assigned to locations for VMs 2025-01-17 13:14:05 -05:00
Jeremy Stretch
4a13664e0f Closes #18425: Remove the triage priority field from GitHub issue templates 2025-01-17 11:06:17 -05:00
Jeremy Stretch
a9f3c74b0c Fixes #18379: Ensure RSS feed content within dashboard widget is sanitized 2025-01-17 10:25:22 -05:00
Brian Tiemann
50b7f46fc0 Migrate DEFAULT_FILE_STORAGE to STORAGES 2025-01-17 09:04:51 -05:00
Brian Tiemann
07ad4c1321 Make GFK scope field sortable=False on tables where it appears 2025-01-17 08:52:12 -05:00
bctiemann
4a1fea3504 Fixes: #18336 - Perform Rack object validation of u_height and starting_unit on rack_type if present (#18395)
* Perform Rack object validation of u_height and starting_unit on rack_type if present

* Calculate effective values before doing validation
2025-01-17 08:45:17 -05:00
bctiemann
993d8f1480 Fixes: #18373 - Fix validation of site in Assign Device to Cluster flow (#18375)
* Fix validation of site in Assign Device to Cluster flow

* Validate Location as well as Site scope
2025-01-17 08:35:17 -05:00
bctiemann
c3efa2149c Fixes: #18350 - Remove 'site' and 'provider_network' from CircuitTerminationIndex.display_attrs (#18351)
* Remove 'site' and 'provider_network' from CircuitTerminationIndex.display_attrs

* Use '_site' and '_provider_network' in display_attrs

* Replace private fields with 'termination'
2025-01-17 08:28:43 -05:00
Jeremy Stretch
a75fa53d4d Closes #18348: Disable legacy pre-commit hook script 2025-01-13 08:50:34 -05:00
Jeremy Stretch
e75d327f38 Fixes #18376: Include tagged VLANs in interfaces list for Q-in-Q interfaces 2025-01-10 09:10:34 -05:00
github-actions
a79d869bd8 Update source translation strings 2025-01-10 05:02:08 +00:00
Brian Tiemann
32422d1683 Don't cache CACHE_KEY_CATALOG_ERROR if ISOLATED_DEPLOYMENT is True 2025-01-09 15:21:27 -05:00
Jeremy Stretch
571f604ce8 Fixes #18368: Restore missing fields on REST API serializer for MAC addresses 2025-01-09 14:53:03 -05:00
Jeremy Stretch
b12c8c880f Fixes #18363: Fix assignment of MAC addresses to interfaces via REST API (#18367)
* Fixes #18363: Fix assignment of MAC addresses to interfaces via REST API

* Add missing API & view tests
2025-01-09 13:55:19 -05:00
Jeremy Stretch
b11f179527 Closes #18362: Create a system job for census reporting 2025-01-09 11:56:09 -05:00
Brian Tiemann
80e1fd02bb Update docs to indicate PostgreSQL 13+ requirement 2025-01-09 10:58:51 -05:00
github-actions
4090afbf24 Update source translation strings 2025-01-09 05:02:09 +00:00
32 changed files with 301 additions and 199 deletions

View File

@@ -14,7 +14,7 @@ body:
attributes:
label: NetBox version
description: What version of NetBox are you currently running?
placeholder: v4.2.1
placeholder: v4.2.2
validations:
required: true
- type: dropdown
@@ -27,19 +27,6 @@ body:
- Other
validations:
required: true
- type: dropdown
attributes:
label: Triage priority
description: >
Issue triage may be prioritized in some cases. Select whichever of the following
conditions applies, if any.
options:
- I volunteer to perform this work (if approved)
- I'm a NetBox Labs customer
- N/A
default: 2
validations:
required: true
- type: textarea
attributes:
label: Proposed functionality

View File

@@ -22,24 +22,11 @@ body:
- Self-hosted
validations:
required: true
- type: dropdown
attributes:
label: Triage priority
description: >
Issue triage may be prioritized in some cases. Select whichever of the following
conditions applies, if any.
options:
- I volunteer to perform this work (if approved)
- I'm a NetBox Labs customer
- N/A
default: 2
validations:
required: true
- type: input
attributes:
label: NetBox Version
description: What version of NetBox are you currently running?
placeholder: v4.2.1
placeholder: v4.2.2
validations:
required: true
- type: dropdown

View File

@@ -8,8 +8,6 @@ django-cors-headers
# Runtime UI tool for debugging Django
# https://github.com/jazzband/django-debug-toolbar/blob/main/docs/changes.rst
# Pinned for DNS looukp bug; see https://github.com/netbox-community/netbox/issues/16454
# and https://github.com/jazzband/django-debug-toolbar/issues/1927
django-debug-toolbar
# Library for writing reusable URL query filters

View File

@@ -21,7 +21,7 @@ The following sections detail how to set up a new instance of NetBox:
| Dependency | Supported Versions |
|------------|--------------------|
| Python | 3.10, 3.11, 3.12 |
| PostgreSQL | 12+ |
| PostgreSQL | 13+ |
| Redis | 4.0+ |
Below is a simplified overview of the NetBox application stack for reference:

View File

@@ -20,7 +20,7 @@ NetBox requires the following dependencies:
| Dependency | Supported Versions |
|------------|--------------------|
| Python | 3.10, 3.11, 3.12 |
| PostgreSQL | 12+ |
| PostgreSQL | 13+ |
| Redis | 4.0+ |
## 3. Install the Latest Release

View File

@@ -1,5 +1,25 @@
# NetBox v4.2
## v4.2.2 (2025-01-17)
### Bug Fixes
* [#18336](https://github.com/netbox-community/netbox/issues/18336) - Validate new rack height against installed devices when changing a rack's type
* [#18350](https://github.com/netbox-community/netbox/issues/18350) - Fix `FieldDoesNotExist` exception when global search results include a circuit termination
* [#18353](https://github.com/netbox-community/netbox/issues/18353) - Disable fetching of plugin catalog data when `ISOLATED_DEPLOYMENT` is enabled
* [#18362](https://github.com/netbox-community/netbox/issues/18362) - Avoid transmitting census data on every worker restart
* [#18363](https://github.com/netbox-community/netbox/issues/18363) - Fix support for assigning a MAC address to an interface via the REST API
* [#18368](https://github.com/netbox-community/netbox/issues/18368) - Restore missing attributes from REST API serializer for MAC addresses (`tags`, `created`, `last_updated`, and custom fields)
* [#18369](https://github.com/netbox-community/netbox/issues/18369) - Fix `TypeError` exception when rendering the system configuration view with one or more custom classes defined under `PROTECTION_RULES`
* [#18373](https://github.com/netbox-community/netbox/issues/18373) - Fix `AttributeError` exception when attempting to assign host devices to a cluster
* [#18376](https://github.com/netbox-community/netbox/issues/18376) - Fix the display of tagged VLANs in interfaces list for Q-in-Q interfaces
* [#18379](https://github.com/netbox-community/netbox/issues/18379) - Ensure RSS feed dashboard widget content is sanitized
* [#18392](https://github.com/netbox-community/netbox/issues/18392) - Virtual machines should not inherit config contexts assigned to locations
* [#18400](https://github.com/netbox-community/netbox/issues/18400) - Fix support for `STORAGE_BACKEND` configuration parameter
* [#18406](https://github.com/netbox-community/netbox/issues/18406) - Scope column headers in object lists should not be orderable
---
## v4.2.1 (2025-01-08)
### Bug Fixes

View File

@@ -34,7 +34,7 @@ class CircuitTerminationIndex(SearchIndex):
('port_speed', 2000),
('upstream_speed', 2000),
)
display_attrs = ('circuit', 'site', 'provider_network', 'description')
display_attrs = ('circuit', 'termination', 'description')
@register_search

View File

@@ -1,8 +1,11 @@
import logging
import requests
import sys
from netbox.jobs import JobRunner
from django.conf import settings
from netbox.jobs import JobRunner, system_job
from netbox.search.backends import search_backend
from .choices import DataSourceStatusChoices
from .choices import DataSourceStatusChoices, JobIntervalChoices
from .exceptions import SyncError
from .models import DataSource
@@ -31,3 +34,44 @@ class SyncDataSourceJob(JobRunner):
if type(e) is SyncError:
logging.error(e)
raise e
@system_job(interval=JobIntervalChoices.INTERVAL_DAILY)
class SystemHousekeepingJob(JobRunner):
"""
Perform daily system housekeeping functions.
"""
class Meta:
name = "System Housekeeping"
def run(self, *args, **kwargs):
# Skip if running in development or test mode
if settings.DEBUG or 'test' in sys.argv:
return
# TODO: Migrate other housekeeping functions from the `housekeeping` management command.
self.send_census_report()
@staticmethod
def send_census_report():
"""
Send a census report (if enabled).
"""
# Skip if census reporting is disabled
if settings.ISOLATED_DEPLOYMENT or not settings.CENSUS_REPORTING_ENABLED:
return
census_data = {
'version': settings.RELEASE.full_version,
'python_version': sys.version.split()[0],
'deployment_id': settings.DEPLOYMENT_ID,
}
try:
requests.get(
url=settings.CENSUS_URL,
params=census_data,
timeout=3,
proxies=settings.HTTP_PROXIES
)
except requests.exceptions.RequestException:
pass

View File

@@ -570,8 +570,9 @@ class SystemView(UserPassesTestMixin, View):
return response
# Serialize any CustomValidator classes
if hasattr(config, 'CUSTOM_VALIDATORS') and config.CUSTOM_VALIDATORS:
config.CUSTOM_VALIDATORS = json.dumps(config.CUSTOM_VALIDATORS, cls=ConfigJSONEncoder, indent=4)
for attr in ['CUSTOM_VALIDATORS', 'PROTECTION_RULES']:
if hasattr(config, attr) and getattr(config, attr, None):
setattr(config, attr, json.dumps(getattr(config, attr), cls=ConfigJSONEncoder, indent=4))
return render(request, 'core/system.html', {
'stats': stats,
@@ -594,7 +595,7 @@ class BasePluginView(UserPassesTestMixin, View):
catalog_plugins_error = cache.get(self.CACHE_KEY_CATALOG_ERROR, default=False)
if not catalog_plugins_error:
catalog_plugins = get_catalog_plugins()
if not catalog_plugins:
if not catalog_plugins and not settings.ISOLATED_DEPLOYMENT:
# Cache for 5 minutes to avoid spamming connection
cache.set(self.CACHE_KEY_CATALOG_ERROR, True, 300)
messages.warning(request, _("Plugins catalog could not be loaded"))

View File

@@ -170,8 +170,8 @@ class MACAddressSerializer(NetBoxModelSerializer):
class Meta:
model = MACAddress
fields = [
'id', 'url', 'display_url', 'display', 'mac_address', 'assigned_object_type', 'assigned_object',
'description', 'comments',
'id', 'url', 'display_url', 'display', 'mac_address', 'assigned_object_type', 'assigned_object_id',
'assigned_object', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
]
brief_fields = ('id', 'url', 'display', 'mac_address', 'description')

View File

@@ -374,22 +374,27 @@ class Rack(ContactsMixin, ImageAttachmentsMixin, RackBase):
if not self._state.adding:
mounted_devices = Device.objects.filter(rack=self).exclude(position__isnull=True).order_by('position')
effective_u_height = self.rack_type.u_height if self.rack_type else self.u_height
effective_starting_unit = self.rack_type.starting_unit if self.rack_type else self.starting_unit
# Validate that Rack is tall enough to house the highest mounted Device
if top_device := mounted_devices.last():
min_height = top_device.position + top_device.device_type.u_height - self.starting_unit
if self.u_height < min_height:
min_height = top_device.position + top_device.device_type.u_height - effective_starting_unit
if effective_u_height < min_height:
field = 'rack_type' if self.rack_type else 'u_height'
raise ValidationError({
'u_height': _(
field: _(
"Rack must be at least {min_height}U tall to house currently installed devices."
).format(min_height=min_height)
})
# Validate that the Rack's starting unit is less than or equal to the position of the lowest mounted Device
if last_device := mounted_devices.first():
if self.starting_unit > last_device.position:
if effective_starting_unit > last_device.position:
field = 'rack_type' if self.rack_type else 'starting_unit'
raise ValidationError({
'starting_unit': _("Rack unit numbering must begin at {position} or less to house "
"currently installed devices.").format(position=last_device.position)
field: _("Rack unit numbering must begin at {position} or less to house "
"currently installed devices.").format(position=last_device.position)
})
# Validate that Rack was assigned a Location of its same site, if applicable

View File

@@ -69,16 +69,18 @@ INTERFACE_FHRPGROUPS = """
"""
INTERFACE_TAGGED_VLANS = """
{% if record.mode == 'tagged' %}
{% load i18n %}
{% if record.mode == 'access' %}
{% elif record.mode == 'tagged-all' %}
{% trans "All" %}
{% else %}
{% if value.count > 3 %}
<a href="{% url 'ipam:vlan_list' %}?{{ record|meta:"model_name" }}_id={{ record.pk }}">{{ value.count }} VLANs</a>
{% else %}
{% for vlan in value.all %}
<a href="{{ vlan.get_absolute_url }}">{{ vlan }}</a><br />
<a href="{{ vlan.get_absolute_url }}">{{ vlan }}</a><br />
{% endfor %}
{% endif %}
{% elif record.mode == 'tagged-all' %}
All
{% endif %}
"""

View File

@@ -2447,3 +2447,46 @@ class VirtualDeviceContextTest(APIViewTestCases.APIViewTestCase):
# Omit identifier to test uniqueness constraint
},
]
class MACAddressTest(APIViewTestCases.APIViewTestCase):
model = MACAddress
brief_fields = ['description', 'display', 'id', 'mac_address', 'url']
bulk_update_data = {
'description': 'New description',
}
@classmethod
def setUpTestData(cls):
device = create_test_device(name='Device 1')
interfaces = (
Interface(device=device, name='Interface 1', type='1000base-t'),
Interface(device=device, name='Interface 2', type='1000base-t'),
Interface(device=device, name='Interface 3', type='1000base-t'),
Interface(device=device, name='Interface 4', type='1000base-t'),
Interface(device=device, name='Interface 5', type='1000base-t'),
)
Interface.objects.bulk_create(interfaces)
mac_addresses = (
MACAddress(mac_address='00:00:00:00:00:01', assigned_object=interfaces[0]),
MACAddress(mac_address='00:00:00:00:00:02', assigned_object=interfaces[1]),
MACAddress(mac_address='00:00:00:00:00:03', assigned_object=interfaces[2]),
)
MACAddress.objects.bulk_create(mac_addresses)
cls.create_data = [
{
'mac_address': '00:00:00:00:00:04',
'assigned_object_type': 'dcim.interface',
'assigned_object_id': interfaces[3].pk,
},
{
'mac_address': '00:00:00:00:00:05',
'assigned_object_type': 'dcim.interface',
'assigned_object_id': interfaces[4].pk,
},
{
'mac_address': '00:00:00:00:00:06',
},
]

View File

@@ -3470,3 +3470,54 @@ class VirtualDeviceContextTestCase(ViewTestCases.PrimaryObjectViewTestCase):
cls.bulk_edit_data = {
'status': VirtualDeviceContextStatusChoices.STATUS_OFFLINE,
}
class MACAddressTestCase(ViewTestCases.PrimaryObjectViewTestCase):
model = MACAddress
@classmethod
def setUpTestData(cls):
device = create_test_device(name='Device 1')
interfaces = (
Interface(device=device, name='Interface 1', type='1000base-t'),
Interface(device=device, name='Interface 2', type='1000base-t'),
Interface(device=device, name='Interface 3', type='1000base-t'),
Interface(device=device, name='Interface 4', type='1000base-t'),
Interface(device=device, name='Interface 5', type='1000base-t'),
Interface(device=device, name='Interface 6', type='1000base-t'),
)
Interface.objects.bulk_create(interfaces)
mac_addresses = (
MACAddress(mac_address='00:00:00:00:00:01', assigned_object=interfaces[0]),
MACAddress(mac_address='00:00:00:00:00:02', assigned_object=interfaces[1]),
MACAddress(mac_address='00:00:00:00:00:03', assigned_object=interfaces[2]),
)
MACAddress.objects.bulk_create(mac_addresses)
tags = create_tags('Alpha', 'Bravo', 'Charlie')
cls.form_data = {
'mac_address': EUI('00:00:00:00:00:04'),
'description': 'New MAC address',
'interface_id': interfaces[3].pk,
'tags': [t.pk for t in tags],
}
cls.csv_data = (
"mac_address,device,interface",
"00:00:00:00:00:04,Device 1,Interface 4",
"00:00:00:00:00:05,Device 1,Interface 5",
"00:00:00:00:00:06,Device 1,Interface 6",
)
cls.csv_update_data = (
"id,mac_address",
f"{mac_addresses[0].pk},00:00:00:00:00:0a",
f"{mac_addresses[1].pk},00:00:00:00:00:0b",
f"{mac_addresses[2].pk},00:00:00:00:00:0c",
)
cls.bulk_edit_data = {
'description': 'New description',
}

View File

@@ -120,11 +120,12 @@ class ConfigContextModelQuerySet(RestrictedQuerySet):
is_active=True,
)
# Apply Location & DeviceType filters only for VirtualMachines
if self.model._meta.model_name == 'device':
base_query.add((Q(locations=OuterRef('location')) | Q(locations=None)), Q.AND)
base_query.add((Q(device_types=OuterRef('device_type')) | Q(device_types=None)), Q.AND)
elif self.model._meta.model_name == 'virtualmachine':
base_query.add(Q(locations=None), Q.AND)
base_query.add(Q(device_types=None), Q.AND)
base_query.add((Q(roles=OuterRef('role')) | Q(roles=None)), Q.AND)

View File

@@ -192,7 +192,8 @@ class PrefixTable(TenancyColumnsMixin, NetBoxTable):
)
scope = tables.Column(
verbose_name=_('Scope'),
linkify=True
linkify=True,
orderable=False
)
vlan_group = tables.Column(
accessor='vlan__group',

View File

@@ -5,9 +5,7 @@ import os
import platform
import sys
import warnings
from urllib.parse import urlencode
import requests
from django.contrib.messages import constants as messages
from django.core.exceptions import ImproperlyConfigured, ValidationError
from django.core.validators import URLValidator
@@ -224,8 +222,18 @@ DATABASES = {
# Storage backend
#
# Default STORAGES for Django
STORAGES = {
"default": {
"BACKEND": "django.core.files.storage.FileSystemStorage",
},
"staticfiles": {
"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
},
}
if STORAGE_BACKEND is not None:
DEFAULT_FILE_STORAGE = STORAGE_BACKEND
STORAGES['default']['BACKEND'] = STORAGE_BACKEND
# django-storages
if STORAGE_BACKEND.startswith('storages.'):
@@ -583,17 +591,6 @@ if SENTRY_ENABLED:
# Calculate a unique deployment ID from the secret key
DEPLOYMENT_ID = hashlib.sha256(SECRET_KEY.encode('utf-8')).hexdigest()[:16]
CENSUS_URL = 'https://census.netbox.oss.netboxlabs.com/api/v1/'
CENSUS_PARAMS = {
'version': RELEASE.full_version,
'python_version': sys.version.split()[0],
'deployment_id': DEPLOYMENT_ID,
}
if CENSUS_REPORTING_ENABLED and not ISOLATED_DEPLOYMENT and not DEBUG and 'test' not in sys.argv:
try:
# Report anonymous census data
requests.get(f'{CENSUS_URL}?{urlencode(CENSUS_PARAMS)}', timeout=3, proxies=HTTP_PROXIES)
except requests.exceptions.RequestException:
pass
#

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -27,10 +27,10 @@
"bootstrap": "5.3.3",
"clipboard": "2.0.11",
"flatpickr": "4.6.13",
"gridstack": "11.2.0",
"gridstack": "11.3.0",
"htmx.org": "1.9.12",
"query-string": "9.1.1",
"sass": "1.83.1",
"sass": "1.83.4",
"tom-select": "2.4.1",
"typeface-inter": "3.18.1",
"typeface-roboto-mono": "1.1.13"

View File

@@ -1905,10 +1905,10 @@ graphql@16.10.0:
resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.10.0.tgz#24c01ae0af6b11ea87bf55694429198aaa8e220c"
integrity sha512-AjqGKbDGUFRKIRCP9tCKiIGHyriz2oHEbPIbEtcSLSs4YjReZOIPQQWek4+6hjw62H9QShXHyaGivGiYVLeYFQ==
gridstack@11.2.0:
version "11.2.0"
resolved "https://registry.yarnpkg.com/gridstack/-/gridstack-11.2.0.tgz#8977a6632c521260f064ef171b92c7a8df4f58a9"
integrity sha512-ajwUzd9spR8NXDxfJotHWq9WOYoDOV9o6UJR3ksevNz8cvXNxDtI9H/lC+RN6ijM2DexureLlsG0RpYjBZiOtg==
gridstack@11.3.0:
version "11.3.0"
resolved "https://registry.yarnpkg.com/gridstack/-/gridstack-11.3.0.tgz#b110c66bafc64c920fc54933e2c9df4f7b2cfffe"
integrity sha512-Z0eRovKcZTRTs3zetJwjO6CNwrgIy845WfOeZGk8ybpeMCE8fMA8tScyKU72Y2M6uGHkjgwnjflglvPiv+RcBQ==
has-bigints@^1.0.1, has-bigints@^1.0.2:
version "1.0.2"
@@ -2667,10 +2667,10 @@ safe-regex-test@^1.0.3:
es-errors "^1.3.0"
is-regex "^1.1.4"
sass@1.83.1:
version "1.83.1"
resolved "https://registry.yarnpkg.com/sass/-/sass-1.83.1.tgz#dee1ab94b47a6f9993d3195d36f556bcbda64846"
integrity sha512-EVJbDaEs4Rr3F0glJzFSOvtg2/oy2V/YrGFPqPY24UqcLDWcI9ZY5sN+qyO3c/QCZwzgfirvhXvINiJCE/OLcA==
sass@1.83.4:
version "1.83.4"
resolved "https://registry.yarnpkg.com/sass/-/sass-1.83.4.tgz#5ccf60f43eb61eeec300b780b8dcb85f16eec6d1"
integrity sha512-B1bozCeNQiOgDcLd33e2Cs2U60wZwjUUXzh900ZyQF5qUasvMdDZYbQ566LJu7cqR+sAHlAfO6RMkaID5s6qpA==
dependencies:
chokidar "^4.0.0"
immutable "^5.0.2"

View File

@@ -1,3 +1,3 @@
version: "4.2.1"
version: "4.2.2"
edition: "Community"
published: "2025-01-08"
published: "2025-01-17"

View File

@@ -103,7 +103,7 @@
<tr>
<th scope="row" class="border-0 ps-3">{% trans "Protection rules" %}</th>
{% if config.PROTECTION_RULES %}
<td class="border-0"><pre>{{ config.PROTECTION_RULES|json }}</pre></td>
<td class="border-0"><pre>{{ config.PROTECTION_RULES }}</pre></td>
{% else %}
<td class="border-0">{{ ''|placeholder }}</td>
{% endif %}

View File

@@ -5,7 +5,7 @@
<div class="list-group-item px-1 py-2">
<a href="{{ entry.link }}" class="text-body">{{ entry.title }}</a>
<div class="text-secondary">
{{ entry.summary|safe }}
{{ entry.summary }}
</div>
</div>
{% empty %}

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-01-08 05:02+0000\n"
"POT-Creation-Date: 2025-01-10 05:01+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -766,8 +766,8 @@ msgstr ""
#: netbox/ipam/forms/filtersets.py:212 netbox/ipam/forms/filtersets.py:284
#: netbox/ipam/forms/filtersets.py:358 netbox/ipam/forms/filtersets.py:542
#: netbox/ipam/forms/model_forms.py:503 netbox/ipam/tables/ip.py:183
#: netbox/ipam/tables/ip.py:262 netbox/ipam/tables/ip.py:313
#: netbox/ipam/tables/ip.py:376 netbox/ipam/tables/ip.py:403
#: netbox/ipam/tables/ip.py:263 netbox/ipam/tables/ip.py:314
#: netbox/ipam/tables/ip.py:377 netbox/ipam/tables/ip.py:404
#: netbox/ipam/tables/vlans.py:95 netbox/ipam/tables/vlans.py:208
#: netbox/templates/circuits/circuit.html:34
#: netbox/templates/circuits/virtualcircuit.html:43
@@ -844,7 +844,7 @@ msgstr ""
#: netbox/ipam/forms/filtersets.py:122 netbox/ipam/forms/filtersets.py:145
#: netbox/ipam/forms/filtersets.py:176 netbox/ipam/forms/filtersets.py:270
#: netbox/ipam/forms/filtersets.py:313 netbox/ipam/forms/filtersets.py:510
#: netbox/ipam/tables/ip.py:406 netbox/ipam/tables/vlans.py:205
#: netbox/ipam/tables/ip.py:407 netbox/ipam/tables/vlans.py:205
#: netbox/templates/circuits/circuit.html:48
#: netbox/templates/circuits/circuitgroup.html:36
#: netbox/templates/circuits/virtualcircuit.html:47
@@ -1078,8 +1078,8 @@ msgstr ""
#: netbox/ipam/forms/filtersets.py:292 netbox/ipam/forms/filtersets.py:363
#: netbox/ipam/forms/filtersets.py:550 netbox/ipam/forms/model_forms.py:194
#: netbox/ipam/forms/model_forms.py:220 netbox/ipam/forms/model_forms.py:251
#: netbox/ipam/forms/model_forms.py:678 netbox/ipam/tables/ip.py:207
#: netbox/ipam/tables/ip.py:266 netbox/ipam/tables/ip.py:317
#: netbox/ipam/forms/model_forms.py:678 netbox/ipam/tables/ip.py:208
#: netbox/ipam/tables/ip.py:267 netbox/ipam/tables/ip.py:318
#: netbox/ipam/tables/vlans.py:99 netbox/ipam/tables/vlans.py:211
#: netbox/templates/circuits/virtualcircuittermination.html:42
#: netbox/templates/dcim/device.html:182
@@ -1191,7 +1191,7 @@ msgstr ""
#: netbox/dcim/tables/connections.py:65 netbox/dcim/tables/devices.py:1140
#: netbox/ipam/forms/bulk_import.py:317 netbox/ipam/forms/model_forms.py:282
#: netbox/ipam/forms/model_forms.py:291 netbox/ipam/tables/fhrp.py:64
#: netbox/ipam/tables/ip.py:322 netbox/ipam/tables/vlans.py:145
#: netbox/ipam/tables/ip.py:323 netbox/ipam/tables/vlans.py:145
#: netbox/templates/circuits/inc/circuit_termination_fields.html:52
#: netbox/templates/circuits/virtualcircuittermination.html:53
#: netbox/templates/circuits/virtualcircuittermination.html:60
@@ -1831,8 +1831,8 @@ msgstr ""
#: netbox/dcim/tables/racks.py:224 netbox/dcim/tables/sites.py:108
#: netbox/extras/tables/tables.py:582 netbox/ipam/tables/asn.py:69
#: netbox/ipam/tables/fhrp.py:34 netbox/ipam/tables/ip.py:82
#: netbox/ipam/tables/ip.py:224 netbox/ipam/tables/ip.py:279
#: netbox/ipam/tables/ip.py:347 netbox/ipam/tables/services.py:24
#: netbox/ipam/tables/ip.py:225 netbox/ipam/tables/ip.py:280
#: netbox/ipam/tables/ip.py:348 netbox/ipam/tables/services.py:24
#: netbox/ipam/tables/services.py:54 netbox/ipam/tables/vlans.py:121
#: netbox/ipam/tables/vrfs.py:47 netbox/ipam/tables/vrfs.py:72
#: netbox/templates/dcim/htmx/cable_edit.html:89
@@ -3009,7 +3009,7 @@ msgstr ""
#: netbox/dcim/tables/devices.py:689 netbox/dcim/tables/devices.py:899
#: netbox/dcim/tables/devices.py:986 netbox/dcim/tables/devices.py:1146
#: netbox/extras/tables/tables.py:223 netbox/ipam/tables/fhrp.py:59
#: netbox/ipam/tables/ip.py:328 netbox/ipam/tables/services.py:44
#: netbox/ipam/tables/ip.py:329 netbox/ipam/tables/services.py:44
#: netbox/templates/dcim/interface.html:108
#: netbox/templates/dcim/interface.html:366
#: netbox/templates/dcim/location.html:41 netbox/templates/dcim/region.html:37
@@ -3688,8 +3688,8 @@ msgstr ""
#: netbox/ipam/forms/model_forms.py:480 netbox/ipam/forms/model_forms.py:494
#: netbox/ipam/models/ip.py:217 netbox/ipam/models/ip.py:498
#: netbox/ipam/models/ip.py:719 netbox/ipam/models/vrfs.py:61
#: netbox/ipam/tables/ip.py:188 netbox/ipam/tables/ip.py:259
#: netbox/ipam/tables/ip.py:310 netbox/ipam/tables/ip.py:400
#: netbox/ipam/tables/ip.py:188 netbox/ipam/tables/ip.py:260
#: netbox/ipam/tables/ip.py:311 netbox/ipam/tables/ip.py:401
#: netbox/templates/dcim/interface.html:152
#: netbox/templates/ipam/ipaddress.html:18
#: netbox/templates/ipam/iprange.html:40 netbox/templates/ipam/prefix.html:19
@@ -6994,8 +6994,8 @@ msgstr ""
#: netbox/dcim/tables/devices.py:197 netbox/dcim/tables/devices.py:1099
#: netbox/ipam/forms/bulk_import.py:562 netbox/ipam/forms/model_forms.py:308
#: netbox/ipam/forms/model_forms.py:321 netbox/ipam/tables/ip.py:306
#: netbox/ipam/tables/ip.py:373 netbox/ipam/tables/ip.py:396
#: netbox/ipam/forms/model_forms.py:321 netbox/ipam/tables/ip.py:307
#: netbox/ipam/tables/ip.py:374 netbox/ipam/tables/ip.py:397
#: netbox/templates/ipam/ipaddress.html:11
#: netbox/virtualization/tables/virtualmachines.py:65
msgid "IP Address"
@@ -9658,7 +9658,7 @@ msgstr ""
#: netbox/ipam/forms/bulk_edit.py:218 netbox/ipam/forms/bulk_import.py:181
#: netbox/ipam/forms/filtersets.py:259 netbox/ipam/forms/model_forms.py:217
#: netbox/ipam/models/vlans.py:272 netbox/ipam/tables/ip.py:204
#: netbox/ipam/models/vlans.py:272 netbox/ipam/tables/ip.py:205
#: netbox/templates/ipam/prefix.html:56 netbox/templates/ipam/vlan.html:12
#: netbox/templates/ipam/vlan/base.html:6
#: netbox/templates/ipam/vlan_edit.html:10
@@ -10516,8 +10516,8 @@ msgstr ""
msgid "Prefixes"
msgstr ""
#: netbox/ipam/tables/ip.py:77 netbox/ipam/tables/ip.py:219
#: netbox/ipam/tables/ip.py:274 netbox/ipam/tables/vlans.py:55
#: netbox/ipam/tables/ip.py:77 netbox/ipam/tables/ip.py:220
#: netbox/ipam/tables/ip.py:275 netbox/ipam/tables/vlans.py:55
#: netbox/templates/dcim/device.html:260
#: netbox/templates/ipam/aggregate.html:24
#: netbox/templates/ipam/iprange.html:29 netbox/templates/ipam/prefix.html:102
@@ -10542,31 +10542,31 @@ msgstr ""
msgid "Scope Type"
msgstr ""
#: netbox/ipam/tables/ip.py:211
#: netbox/ipam/tables/ip.py:212
msgid "Pool"
msgstr ""
#: netbox/ipam/tables/ip.py:215 netbox/ipam/tables/ip.py:270
#: netbox/ipam/tables/ip.py:216 netbox/ipam/tables/ip.py:271
msgid "Marked Utilized"
msgstr ""
#: netbox/ipam/tables/ip.py:254
#: netbox/ipam/tables/ip.py:255
msgid "Start address"
msgstr ""
#: netbox/ipam/tables/ip.py:333
#: netbox/ipam/tables/ip.py:334
msgid "NAT (Inside)"
msgstr ""
#: netbox/ipam/tables/ip.py:338
#: netbox/ipam/tables/ip.py:339
msgid "NAT (Outside)"
msgstr ""
#: netbox/ipam/tables/ip.py:343
#: netbox/ipam/tables/ip.py:344
msgid "Assigned"
msgstr ""
#: netbox/ipam/tables/ip.py:379 netbox/templates/vpn/l2vpntermination.html:16
#: netbox/ipam/tables/ip.py:380 netbox/templates/vpn/l2vpntermination.html:16
#: netbox/vpn/forms/filtersets.py:240
msgid "Assigned Object"
msgstr ""
@@ -11582,63 +11582,63 @@ msgstr ""
msgid "Cannot delete stores from registry"
msgstr ""
#: netbox/netbox/settings.py:755
#: netbox/netbox/settings.py:742
msgid "Czech"
msgstr ""
#: netbox/netbox/settings.py:756
#: netbox/netbox/settings.py:743
msgid "Danish"
msgstr ""
#: netbox/netbox/settings.py:757
#: netbox/netbox/settings.py:744
msgid "German"
msgstr ""
#: netbox/netbox/settings.py:758
#: netbox/netbox/settings.py:745
msgid "English"
msgstr ""
#: netbox/netbox/settings.py:759
#: netbox/netbox/settings.py:746
msgid "Spanish"
msgstr ""
#: netbox/netbox/settings.py:760
#: netbox/netbox/settings.py:747
msgid "French"
msgstr ""
#: netbox/netbox/settings.py:761
#: netbox/netbox/settings.py:748
msgid "Italian"
msgstr ""
#: netbox/netbox/settings.py:762
#: netbox/netbox/settings.py:749
msgid "Japanese"
msgstr ""
#: netbox/netbox/settings.py:763
#: netbox/netbox/settings.py:750
msgid "Dutch"
msgstr ""
#: netbox/netbox/settings.py:764
#: netbox/netbox/settings.py:751
msgid "Polish"
msgstr ""
#: netbox/netbox/settings.py:765
#: netbox/netbox/settings.py:752
msgid "Portuguese"
msgstr ""
#: netbox/netbox/settings.py:766
#: netbox/netbox/settings.py:753
msgid "Russian"
msgstr ""
#: netbox/netbox/settings.py:767
#: netbox/netbox/settings.py:754
msgid "Turkish"
msgstr ""
#: netbox/netbox/settings.py:768
#: netbox/netbox/settings.py:755
msgid "Ukrainian"
msgstr ""
#: netbox/netbox/settings.py:769
#: netbox/netbox/settings.py:756
msgid "Chinese"
msgstr ""

View File

@@ -12,8 +12,8 @@
# Alexander Ryazanov (alryaz) <alryaz@xavux.com>, 2024
# Vladyslav V. Prodan, 2024
# Jeremy Stretch, 2024
# Michail Tatarinov, 2024
# Artem Kotik, 2025
# Michail Tatarinov, 2025
#
#, fuzzy
msgid ""
@@ -22,7 +22,7 @@ msgstr ""
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-01-04 05:02+0000\n"
"PO-Revision-Date: 2023-10-30 17:48+0000\n"
"Last-Translator: Artem Kotik, 2025\n"
"Last-Translator: Michail Tatarinov, 2025\n"
"Language-Team: Russian (https://app.transifex.com/netbox-community/teams/178115/ru/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@@ -6334,7 +6334,7 @@ msgstr "распределительный щит"
#: netbox/dcim/models/power.py:56
msgid "power panels"
msgstr "распределительный щиты"
msgstr "распределительные щиты"
#: netbox/dcim/models/power.py:70
#, python-brace-format
@@ -9256,7 +9256,7 @@ msgstr "SLAAC"
#: netbox/ipam/choices.py:89
msgid "Loopback"
msgstr "Обратная петля"
msgstr "Loopback"
#: netbox/ipam/choices.py:91
msgid "Anycast"
@@ -11185,7 +11185,7 @@ msgstr "Сети провайдеров"
#: netbox/netbox/navigation/menu.py:298
msgid "Power Panels"
msgstr "Распределительный щиты"
msgstr "Распределительные щиты"
#: netbox/netbox/navigation/menu.py:309
msgid "Configurations"

View File

@@ -1,4 +1,5 @@
from django import forms
from django.apps import apps
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
@@ -143,19 +144,26 @@ class ClusterAddDevicesForm(forms.Form):
def clean(self):
super().clean()
# If the Cluster is assigned to a Site, all Devices must be assigned to that Site.
if self.cluster.site is not None:
# If the Cluster is assigned to a Site or Location, all Devices must be assigned to that same scope.
if self.cluster.scope is not None:
for device in self.cleaned_data.get('devices', []):
if device.site != self.cluster.site:
raise ValidationError({
'devices': _(
"{device} belongs to a different site ({device_site}) than the cluster ({cluster_site})"
).format(
device=device,
device_site=device.site,
cluster_site=self.cluster.site
)
})
for scope_field in ['site', 'location']:
device_scope = getattr(device, scope_field)
if (
self.cluster.scope_type.model_class() == apps.get_model('dcim', scope_field)
and device_scope != self.cluster.scope
):
raise ValidationError({
'devices': _(
"{device} belongs to a different {scope_field} ({device_scope}) than the "
"cluster ({cluster_scope})"
).format(
device=device,
scope_field=scope_field,
device_scope=device_scope,
cluster_scope=self.cluster.scope
)
})
class ClusterRemoveDevicesForm(ConfirmationForm):

View File

@@ -78,7 +78,8 @@ class ClusterTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
)
scope = tables.Column(
verbose_name=_('Scope'),
linkify=True
linkify=True,
orderable=False
)
device_count = columns.LinkedCountColumn(
viewname='dcim:device_list',

View File

@@ -56,7 +56,8 @@ class WirelessLANTable(TenancyColumnsMixin, NetBoxTable):
)
scope = tables.Column(
verbose_name=_('Scope'),
linkify=True
linkify=True,
orderable=False
)
interface_count = tables.Column(
verbose_name=_('Interfaces')

View File

@@ -1,6 +1,6 @@
Django==5.1.4
Django==5.1.5
django-cors-headers==4.6.0
django-debug-toolbar==4.4.6
django-debug-toolbar==5.0.1
django-filter==24.3
django-htmx==1.21.0
django-graphiql-debug-toolbar==0.2.0
@@ -12,7 +12,7 @@ django-rich==1.13.0
django-rq==3.0
django-taggit==6.1.0
django-tables2==2.7.5
django-timezone-field==7.0
django-timezone-field==7.1
djangorestframework==3.15.2
drf-spectacular==0.28.0
drf-spectacular-sidecar==2024.12.1
@@ -25,13 +25,13 @@ mkdocstrings[python-legacy]==0.27.0
netaddr==1.3.0
nh3==0.2.20
Pillow==11.1.0
psycopg[c,pool]==3.2.3
psycopg[c,pool]==3.2.4
PyYAML==6.0.2
requests==2.32.3
rq==2.1.0
social-auth-app-django==5.4.2
social-auth-core==4.5.4
strawberry-graphql==0.256.1
strawberry-graphql==0.258.0
strawberry-graphql-django==0.52.0
svgwrite==1.4.3
tablib==3.7.0

View File

@@ -1,11 +1,6 @@
#!/bin/sh
# Create a link to this file at .git/hooks/pre-commit to
# force PEP8 validation prior to committing
#
# Ignored violations:
#
# W504: Line break after binary operator
# E501: Line too long
# TODO: Remove this file in NetBox v4.3
# This script has been maintained to ease transition to the pre-commit tool.
exec 1>&2
@@ -14,48 +9,8 @@ RED='\033[0;31m'
YELLOW='\033[0;33m'
NOCOLOR='\033[0m'
printf "${YELLOW}This script is obsolete and will be removed in a future release.\n"
printf "Please use pre-commit instead:\n"
printf "${YELLOW}The pre-commit hook script is obsolete. Please use pre-commit instead:${NOCOLOR}\n"
printf " pip install pre-commit\n"
printf " pre-commit install${NOCOLOR}\n"
if [ -d ./venv/ ]; then
VENV="$PWD/venv"
if [ -e $VENV/bin/python ]; then
PATH=$VENV/bin:$PATH
elif [ -e $VENV/Scripts/python.exe ]; then
PATH=$VENV/Scripts:$PATH
fi
fi
if [ ${NOVALIDATE} ]; then
echo "${YELLOW}Skipping validation checks${NOCOLOR}"
exit $EXIT
fi
echo "Linting with ruff..."
ruff check netbox/
if [ $? != 0 ]; then
EXIT=1
fi
echo "Checking for missing migrations..."
python netbox/manage.py makemigrations --dry-run --check
if [ $? != 0 ]; then
EXIT=1
fi
git diff --cached --name-only | if grep --quiet 'netbox/project-static/'
then
echo "Checking UI ESLint, TypeScript, and Prettier compliance..."
yarn --cwd "$PWD/netbox/project-static" validate
if [ $? != 0 ]; then
EXIT=1
fi
fi
if [ $EXIT != 0 ]; then
printf "${RED}COMMIT FAILED${NOCOLOR}\n"
fi
exit $EXIT
exit 1