Merge branch 'feature' into 8984-script-log

This commit is contained in:
Arthur 2024-06-11 13:01:38 -07:00
commit ca6de26d67
53 changed files with 28192 additions and 24589 deletions

View File

@ -26,7 +26,7 @@ body:
attributes: attributes:
label: NetBox Version label: NetBox Version
description: What version of NetBox are you currently running? description: What version of NetBox are you currently running?
placeholder: v4.0.3 placeholder: v4.0.5
validations: validations:
required: true required: true
- type: dropdown - type: dropdown

View File

@ -14,7 +14,7 @@ body:
attributes: attributes:
label: NetBox version label: NetBox version
description: What version of NetBox are you currently running? description: What version of NetBox are you currently running?
placeholder: v4.0.3 placeholder: v4.0.5
validations: validations:
required: true required: true
- type: dropdown - type: dropdown

View File

@ -323,6 +323,7 @@
"100base-tx", "100base-tx",
"100base-t1", "100base-t1",
"1000base-t", "1000base-t",
"1000base-tx",
"2.5gbase-t", "2.5gbase-t",
"5gbase-t", "5gbase-t",
"10gbase-t", "10gbase-t",

View File

@ -65,12 +65,6 @@ class AnotherCustomScript(Script):
script_order = (MyCustomScript, AnotherCustomScript) script_order = (MyCustomScript, AnotherCustomScript)
``` ```
## Module Attributes
### `name`
You can define `name` within a script module (the Python file which contains one or more scripts) to set the module name. If `name` is not defined, the module's file name will be used.
## Script Attributes ## Script Attributes
Script attributes are defined under a class named `Meta` within the script. These are optional, but encouraged. Script attributes are defined under a class named `Meta` within the script. These are optional, but encouraged.

View File

@ -1,18 +1,34 @@
# NetBox v4.0 # NetBox v4.0
## v4.0.4 (FUTURE) ## v4.0.6 (FUTURE)
---
## v4.0.5 (2024-06-06)
### Enhancements ### Enhancements
* [#14810](https://github.com/netbox-community/netbox/issues/14810) - Enable contact assignment for services * [#14810](https://github.com/netbox-community/netbox/issues/14810) - Enable contact assignment for services
* [#15489](https://github.com/netbox-community/netbox/issues/15489) - Add 1000Base-TX interface type * [#15489](https://github.com/netbox-community/netbox/issues/15489) - Add 1000Base-TX interface type
* [#15873](https://github.com/netbox-community/netbox/issues/15873) - Improve readability of allocates resource numbers for clusters
* [#16290](https://github.com/netbox-community/netbox/issues/16290) - Capture entire object in changelog data (but continue to display only non-internal attributes) * [#16290](https://github.com/netbox-community/netbox/issues/16290) - Capture entire object in changelog data (but continue to display only non-internal attributes)
* [#16353](https://github.com/netbox-community/netbox/issues/16353) - Enable plugins to extend object change view with custom content
### Bug Fixes ### Bug Fixes
* [#13422](https://github.com/netbox-community/netbox/issues/13422) - Rebuild MPTT trees for applicable models after merging staged changes * [#13422](https://github.com/netbox-community/netbox/issues/13422) - Rebuild MPTT trees for applicable models after merging staged changes
* [#14567](https://github.com/netbox-community/netbox/issues/14567) - Apply active quicksearch value when exporting "current view" from object list
* [#15194](https://github.com/netbox-community/netbox/issues/15194) - Avoid enqueuing duplicate event triggers for a modified object
* [#16039](https://github.com/netbox-community/netbox/issues/16039) - Fix row highlighting for front & rear port connections under device view
* [#16050](https://github.com/netbox-community/netbox/issues/16050) - Fix display of names & descriptions defined for custom scripts
* [#16083](https://github.com/netbox-community/netbox/issues/16083) - Disable font ligatures to avoid peculiarities in rendered text
* [#16202](https://github.com/netbox-community/netbox/issues/16202) - Fix site map button URL for certain localizations * [#16202](https://github.com/netbox-community/netbox/issues/16202) - Fix site map button URL for certain localizations
* [#16261](https://github.com/netbox-community/netbox/issues/16261) - Fix GraphQL filtering for certain multi-value filters
* [#16286](https://github.com/netbox-community/netbox/issues/16286) - Fix global search support for provider accounts * [#16286](https://github.com/netbox-community/netbox/issues/16286) - Fix global search support for provider accounts
* [#16312](https://github.com/netbox-community/netbox/issues/16312) - Fix object list navigation for dashboard widgets
* [#16315](https://github.com/netbox-community/netbox/issues/16315) - Fix filtering change log & journal entries by object type in UI
* [#16376](https://github.com/netbox-community/netbox/issues/16376) - Update change log for the terminating object (e.g. interface) when attaching a cable
* [#16400](https://github.com/netbox-community/netbox/issues/16400) - Fix AttributeError when attempting to restore a previous configuration revision after deleting the current one
--- ---

View File

@ -18,7 +18,7 @@ BANNER_TEXT = """### NetBox interactive shell ({node})
node=platform.node(), node=platform.node(),
python=platform.python_version(), python=platform.python_version(),
django=get_version(), django=get_version(),
netbox=settings.VERSION netbox=settings.RELEASE.name
) )

View File

@ -224,7 +224,7 @@ class ConfigRevisionRestoreView(ContentTypePermissionRequiredMixin, View):
for param in PARAMS: for param in PARAMS:
params.append(( params.append((
param.name, param.name,
current_config.data.get(param.name, None), current_config.data.get(param.name, None) if current_config else None,
candidate_config.data.get(param.name, None) candidate_config.data.get(param.name, None)
)) ))
@ -539,7 +539,7 @@ class SystemView(UserPassesTestMixin, View):
except (ProgrammingError, IndexError): except (ProgrammingError, IndexError):
pass pass
stats = { stats = {
'netbox_version': settings.VERSION, 'netbox_release': settings.RELEASE,
'django_version': DJANGO_VERSION, 'django_version': DJANGO_VERSION,
'python_version': platform.python_version(), 'python_version': platform.python_version(),
'postgresql_version': psql_version, 'postgresql_version': psql_version,

View File

@ -698,9 +698,6 @@ class DeviceTypeComponentFilterSet(django_filters.FilterSet):
label=_('Device type (ID)'), label=_('Device type (ID)'),
) )
# TODO: Remove in v4.1
devicetype_id = device_type_id
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
return queryset return queryset
@ -717,9 +714,6 @@ class ModularDeviceTypeComponentFilterSet(DeviceTypeComponentFilterSet):
label=_('Module type (ID)'), label=_('Module type (ID)'),
) )
# TODO: Remove in v4.1
moduletype_id = module_type_id
class ConsolePortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet): class ConsolePortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):

View File

@ -355,11 +355,11 @@ class CableTermination(ChangeLoggedModel):
super().save(*args, **kwargs) super().save(*args, **kwargs)
# Set the cable on the terminating object # Set the cable on the terminating object
termination_model = self.termination._meta.model termination = self.termination._meta.model.objects.get(pk=self.termination_id)
termination_model.objects.filter(pk=self.termination_id).update( termination.snapshot()
cable=self.cable, termination.cable = self.cable
cable_end=self.cable_end termination.cable_end = self.cable_end
) termination.save()
def delete(self, *args, **kwargs): def delete(self, *args, **kwargs):

View File

@ -50,9 +50,9 @@ class DeviceComponentTemplateFilterSetTests:
params = {'description': ['foobar1', 'foobar2']} params = {'description': ['foobar1', 'foobar2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_devicetype_id(self): def test_device_type_id(self):
device_types = DeviceType.objects.all()[:2] device_types = DeviceType.objects.all()[:2]
params = {'devicetype_id': [device_types[0].pk, device_types[1].pk]} params = {'device_type_id': [device_types[0].pk, device_types[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@ -1753,9 +1753,9 @@ class InventoryItemTemplateTestCase(TestCase, DeviceComponentTemplateFilterSetTe
params = {'name': ['Inventory Item 1', 'Inventory Item 2']} params = {'name': ['Inventory Item 1', 'Inventory Item 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_devicetype_id(self): def test_device_type_id(self):
device_types = DeviceType.objects.all()[:2] device_types = DeviceType.objects.all()[:2]
params = {'devicetype_id': [device_types[0].pk, device_types[1].pk]} params = {'device_type_id': [device_types[0].pk, device_types[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
def test_label(self): def test_label(self):

View File

@ -329,7 +329,7 @@ class RSSFeedWidget(DashboardWidget):
try: try:
response = requests.get( response = requests.get(
url=self.config['feed_url'], url=self.config['feed_url'],
headers={'User-Agent': f'NetBox/{settings.VERSION}'}, headers={'User-Agent': f'NetBox/{settings.RELEASE.version}'},
proxies=settings.HTTP_PROXIES, proxies=settings.HTTP_PROXIES,
timeout=3 timeout=3
) )

View File

@ -589,10 +589,6 @@ class ConfigContextFilterSet(ChangeLoggedModelFilterSet):
label=_('Data file (ID)'), label=_('Data file (ID)'),
) )
# TODO: Remove in v4.1
role = device_role
role_id = device_role_id
class Meta: class Meta:
model = ConfigContext model = ConfigContext
fields = ('id', 'name', 'is_active', 'description', 'weight', 'auto_sync_enabled', 'data_synced') fields = ('id', 'name', 'is_active', 'description', 'weight', 'auto_sync_enabled', 'data_synced')

View File

@ -912,10 +912,6 @@ class VLANGroupFilterSet(OrganizationalModelFilterSet):
method='filter_scope' method='filter_scope'
) )
# TODO: Remove in v4.1
sitegroup = site_group
clustergroup = cluster_group
class Meta: class Meta:
model = VLANGroup model = VLANGroup
fields = ('id', 'name', 'slug', 'min_vid', 'max_vid', 'description', 'scope_id') fields = ('id', 'name', 'slug', 'min_vid', 'max_vid', 'description', 'scope_id')
@ -1106,10 +1102,6 @@ class ServiceFilterSet(NetBoxModelFilterSet):
lookup_expr='contains' lookup_expr='contains'
) )
# TODO: Remove in v4.1
ipaddress = ip_address
ipaddress_id = ip_address_id
class Meta: class Meta:
model = Service model = Service
fields = ('id', 'name', 'protocol', 'description') fields = ('id', 'name', 'protocol', 'description')

View File

@ -1525,8 +1525,8 @@ class VLANGroupTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'region': Region.objects.first().pk} params = {'region': Region.objects.first().pk}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_sitegroup(self): def test_site_group(self):
params = {'sitegroup': SiteGroup.objects.first().pk} params = {'site_group': SiteGroup.objects.first().pk}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_site(self): def test_site(self):
@ -1541,8 +1541,8 @@ class VLANGroupTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'rack': Rack.objects.first().pk} params = {'rack': Rack.objects.first().pk}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_clustergroup(self): def test_cluster_group(self):
params = {'clustergroup': ClusterGroup.objects.first().pk} params = {'cluster_group': ClusterGroup.objects.first().pk}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_cluster(self): def test_cluster(self):

View File

@ -66,7 +66,7 @@ class StatusView(APIView):
return Response({ return Response({
'django-version': DJANGO_VERSION, 'django-version': DJANGO_VERSION,
'installed-apps': installed_apps, 'installed-apps': installed_apps,
'netbox-version': settings.VERSION, 'netbox-version': settings.RELEASE.full_version,
'plugins': get_installed_plugins(), 'plugins': get_installed_plugins(),
'python-version': platform.python_version(), 'python-version': platform.python_version(),
'rq-workers-running': Worker.count(get_connection('default')), 'rq-workers-running': Worker.count(get_connection('default')),

View File

@ -18,6 +18,7 @@ from django.utils.translation import gettext_lazy as _
from netbox.config import PARAMS as CONFIG_PARAMS from netbox.config import PARAMS as CONFIG_PARAMS
from netbox.constants import RQ_QUEUE_DEFAULT, RQ_QUEUE_HIGH, RQ_QUEUE_LOW from netbox.constants import RQ_QUEUE_DEFAULT, RQ_QUEUE_HIGH, RQ_QUEUE_LOW
from netbox.plugins import PluginConfig from netbox.plugins import PluginConfig
from utilities.release import load_release_data
from utilities.string import trailing_slash from utilities.string import trailing_slash
@ -25,7 +26,8 @@ from utilities.string import trailing_slash
# Environment setup # Environment setup
# #
VERSION = '4.0.4-dev' RELEASE = load_release_data()
VERSION = RELEASE.full_version # Retained for backward compatibility
HOSTNAME = platform.node() HOSTNAME = platform.node()
# Set the base directory two levels up # Set the base directory two levels up
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
@ -368,6 +370,8 @@ INSTALLED_APPS = [
'drf_spectacular', 'drf_spectacular',
'drf_spectacular_sidecar', 'drf_spectacular_sidecar',
] ]
if not DEBUG:
INSTALLED_APPS.remove('debug_toolbar')
if not DJANGO_ADMIN_ENABLED: if not DJANGO_ADMIN_ENABLED:
INSTALLED_APPS.remove('django.contrib.admin') INSTALLED_APPS.remove('django.contrib.admin')
@ -531,7 +535,7 @@ if SENTRY_ENABLED:
# Initialize the SDK # Initialize the SDK
sentry_sdk.init( sentry_sdk.init(
dsn=SENTRY_DSN, dsn=SENTRY_DSN,
release=VERSION, release=RELEASE.full_version,
sample_rate=SENTRY_SAMPLE_RATE, sample_rate=SENTRY_SAMPLE_RATE,
traces_sample_rate=SENTRY_TRACES_SAMPLE_RATE, traces_sample_rate=SENTRY_TRACES_SAMPLE_RATE,
send_default_pii=True, send_default_pii=True,
@ -551,7 +555,7 @@ if SENTRY_ENABLED:
DEPLOYMENT_ID = hashlib.sha256(SECRET_KEY.encode('utf-8')).hexdigest()[:16] DEPLOYMENT_ID = hashlib.sha256(SECRET_KEY.encode('utf-8')).hexdigest()[:16]
CENSUS_URL = 'https://census.netbox.dev/api/v1/' CENSUS_URL = 'https://census.netbox.dev/api/v1/'
CENSUS_PARAMS = { CENSUS_PARAMS = {
'version': VERSION, 'version': RELEASE.full_version,
'python_version': sys.version.split()[0], 'python_version': sys.version.split()[0],
'deployment_id': DEPLOYMENT_ID, 'deployment_id': DEPLOYMENT_ID,
} }
@ -609,7 +613,7 @@ FILTERS_NULL_CHOICE_VALUE = 'null'
# Django REST framework (API) # Django REST framework (API)
# #
REST_FRAMEWORK_VERSION = '.'.join(VERSION.split('-')[0].split('.')[:2]) # Use major.minor as API version REST_FRAMEWORK_VERSION = '.'.join(RELEASE.version.split('-')[0].split('.')[:2]) # Use major.minor as API version
REST_FRAMEWORK = { REST_FRAMEWORK = {
'ALLOWED_VERSIONS': [REST_FRAMEWORK_VERSION], 'ALLOWED_VERSIONS': [REST_FRAMEWORK_VERSION],
'COERCE_DECIMAL_TO_STRING': False, 'COERCE_DECIMAL_TO_STRING': False,
@ -654,7 +658,7 @@ REST_FRAMEWORK = {
SPECTACULAR_SETTINGS = { SPECTACULAR_SETTINGS = {
'TITLE': 'NetBox REST API', 'TITLE': 'NetBox REST API',
'LICENSE': {'name': 'Apache v2 License'}, 'LICENSE': {'name': 'Apache v2 License'},
'VERSION': VERSION, 'VERSION': RELEASE.full_version,
'COMPONENT_SPLIT_REQUEST': True, 'COMPONENT_SPLIT_REQUEST': True,
'REDOC_DIST': 'SIDECAR', 'REDOC_DIST': 'SIDECAR',
'SERVERS': [{ 'SERVERS': [{
@ -800,7 +804,7 @@ for plugin_name in PLUGINS:
# Validate user-provided configuration settings and assign defaults # Validate user-provided configuration settings and assign defaults
if plugin_name not in PLUGINS_CONFIG: if plugin_name not in PLUGINS_CONFIG:
PLUGINS_CONFIG[plugin_name] = {} PLUGINS_CONFIG[plugin_name] = {}
plugin_config.validate(PLUGINS_CONFIG[plugin_name], VERSION) plugin_config.validate(PLUGINS_CONFIG[plugin_name], RELEASE.version)
# Add middleware # Add middleware
plugin_middleware = plugin_config.middleware plugin_middleware = plugin_config.middleware

View File

@ -166,11 +166,11 @@ class PluginTest(TestCase):
required_settings = ['foo'] required_settings = ['foo']
# Validation should pass when all required settings are present # Validation should pass when all required settings are present
DummyConfigWithRequiredSettings.validate({'foo': True}, settings.VERSION) DummyConfigWithRequiredSettings.validate({'foo': True}, settings.RELEASE.version)
# Validation should fail when a required setting is missing # Validation should fail when a required setting is missing
with self.assertRaises(ImproperlyConfigured): with self.assertRaises(ImproperlyConfigured):
DummyConfigWithRequiredSettings.validate({}, settings.VERSION) DummyConfigWithRequiredSettings.validate({}, settings.RELEASE.version)
def test_default_settings(self): def test_default_settings(self):
""" """
@ -183,12 +183,12 @@ class PluginTest(TestCase):
# Populate the default value if setting has not been specified # Populate the default value if setting has not been specified
user_config = {} user_config = {}
DummyConfigWithDefaultSettings.validate(user_config, settings.VERSION) DummyConfigWithDefaultSettings.validate(user_config, settings.RELEASE.version)
self.assertEqual(user_config['bar'], 123) self.assertEqual(user_config['bar'], 123)
# Don't overwrite specified values # Don't overwrite specified values
user_config = {'bar': 456} user_config = {'bar': 456}
DummyConfigWithDefaultSettings.validate(user_config, settings.VERSION) DummyConfigWithDefaultSettings.validate(user_config, settings.RELEASE.version)
self.assertEqual(user_config['bar'], 456) self.assertEqual(user_config['bar'], 456)
def test_graphql(self): def test_graphql(self):

View File

@ -57,7 +57,7 @@ _patterns = [
path( path(
"api/schema/", "api/schema/",
cache_page(timeout=86400, key_prefix=f"api_schema_{settings.VERSION}")( cache_page(timeout=86400, key_prefix=f"api_schema_{settings.RELEASE.version}")(
SpectacularAPIView.as_view() SpectacularAPIView.as_view()
), ),
name="schema", name="schema",

View File

@ -54,7 +54,7 @@ def handler_500(request, template_name=ERROR_500_TEMPLATE_NAME):
return HttpResponseServerError(template.render({ return HttpResponseServerError(template.render({
'error': error, 'error': error,
'exception': str(type_), 'exception': str(type_),
'netbox_version': settings.VERSION, 'netbox_version': settings.RELEASE.full_version,
'python_version': platform.python_version(), 'python_version': platform.python_version(),
'plugins': get_installed_plugins(), 'plugins': get_installed_plugins(),
})) }))

View File

@ -50,7 +50,7 @@ class HomeView(View):
latest_release = cache.get('latest_release') latest_release = cache.get('latest_release')
if latest_release: if latest_release:
release_version, release_url = latest_release release_version, release_url = latest_release
if release_version > version.parse(settings.VERSION): if release_version > version.parse(settings.RELEASE.version):
new_release = { new_release = {
'version': str(release_version), 'version': str(release_version),
'url': release_url, 'url': release_url,

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -27,10 +27,10 @@
"bootstrap": "5.3.3", "bootstrap": "5.3.3",
"clipboard": "2.0.11", "clipboard": "2.0.11",
"flatpickr": "4.6.13", "flatpickr": "4.6.13",
"gridstack": "10.1.2", "gridstack": "10.2.0",
"htmx.org": "1.9.12", "htmx.org": "1.9.12",
"query-string": "9.0.0", "query-string": "9.0.0",
"sass": "1.77.2", "sass": "1.77.4",
"tom-select": "2.3.1", "tom-select": "2.3.1",
"typeface-inter": "3.18.1", "typeface-inter": "3.18.1",
"typeface-roboto-mono": "1.1.13" "typeface-roboto-mono": "1.1.13"

View File

@ -7,38 +7,74 @@ import { isTruthy } from './util';
*/ */
function quickSearchEventHandler(event: Event): void { function quickSearchEventHandler(event: Event): void {
const quicksearch = event.currentTarget as HTMLInputElement; const quicksearch = event.currentTarget as HTMLInputElement;
const clearbtn = document.getElementById("quicksearch_clear") as HTMLAnchorElement; const clearbtn = document.getElementById('quicksearch_clear') as HTMLAnchorElement;
if (isTruthy(clearbtn)) { if (isTruthy(clearbtn)) {
if (quicksearch.value === "") { if (quicksearch.value === '') {
clearbtn.classList.add("invisible"); clearbtn.classList.add('invisible');
} else { } else {
clearbtn.classList.remove("invisible"); clearbtn.classList.remove('invisible');
} }
} }
} }
/**
* Clear the existing search parameters in the link to export Current View.
*/
function clearLinkParams(): void {
const link = document.getElementById('export_current_view') as HTMLLinkElement;
const linkUpdated = link?.href.split('&')[0];
link.setAttribute('href', linkUpdated);
}
/**
* Update the Export View link to add the Quick Search parameters.
* @param event
*/
function handleQuickSearchParams(event: Event): void {
const quickSearchParameters = event.currentTarget as HTMLInputElement;
// Clear the existing search parameters
clearLinkParams();
if (quickSearchParameters != null) {
const link = document.getElementById('export_current_view') as HTMLLinkElement;
const search_parameter = `q=${quickSearchParameters.value}`;
const linkUpdated = link?.href + '&' + search_parameter;
link.setAttribute('href', linkUpdated);
}
}
/** /**
* Initialize Quicksearch Event listener/handlers. * Initialize Quicksearch Event listener/handlers.
*/ */
export function initQuickSearch(): void { export function initQuickSearch(): void {
const quicksearch = document.getElementById("quicksearch") as HTMLInputElement; const quicksearch = document.getElementById('quicksearch') as HTMLInputElement;
const clearbtn = document.getElementById("quicksearch_clear") as HTMLAnchorElement; const clearbtn = document.getElementById('quicksearch_clear') as HTMLAnchorElement;
if (isTruthy(quicksearch)) { if (isTruthy(quicksearch)) {
quicksearch.addEventListener("keyup", quickSearchEventHandler, { quicksearch.addEventListener('keyup', quickSearchEventHandler, {
passive: true passive: true,
}) });
quicksearch.addEventListener("search", quickSearchEventHandler, { quicksearch.addEventListener('search', quickSearchEventHandler, {
passive: true passive: true,
}) });
quicksearch.addEventListener('change', handleQuickSearchParams, {
passive: true,
});
if (isTruthy(clearbtn)) { if (isTruthy(clearbtn)) {
clearbtn.addEventListener("click", async () => { clearbtn.addEventListener(
const search = new Event('search'); 'click',
quicksearch.value = ''; async () => {
await new Promise(f => setTimeout(f, 100)); const search = new Event('search');
quicksearch.dispatchEvent(search); quicksearch.value = '';
}, { await new Promise(f => setTimeout(f, 100));
passive: true quicksearch.dispatchEvent(search);
}) clearLinkParams();
},
{
passive: true,
},
);
} }
} }
} }

View File

@ -39,3 +39,8 @@ table a {
// Adjust table anchor link contrast as not enough contrast in dark mode // Adjust table anchor link contrast as not enough contrast in dark mode
filter: brightness(110%); filter: brightness(110%);
} }
// Override background color alpha value
[data-bs-theme=dark] ::selection {
background-color: rgba(var(--tblr-primary-rgb),.48)
}

View File

@ -1754,10 +1754,10 @@ graphql@16.8.1:
resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.8.1.tgz#1930a965bef1170603702acdb68aedd3f3cf6f07" resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.8.1.tgz#1930a965bef1170603702acdb68aedd3f3cf6f07"
integrity sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw== integrity sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==
gridstack@10.1.2: gridstack@10.2.0:
version "10.1.2" version "10.2.0"
resolved "https://registry.yarnpkg.com/gridstack/-/gridstack-10.1.2.tgz#58b5ae0057a8aa5e4f6563041c4ca2def3aa4268" resolved "https://registry.yarnpkg.com/gridstack/-/gridstack-10.2.0.tgz#4ba9c7ee69a730851721a9f5cb33dc55026ded1f"
integrity sha512-Nn27XGQ68WtBC513cKQQ4t/dA2uuN/xnNUU50puXEJv6IFk5SzT0Dnsq68GpopO1n0tXUKZKm1Rw7uOUMDz1KQ== integrity sha512-svKAOq/dfinpvhe/nnxdyZOOEd9qynXiOPHvL96PALE0yWChWp/6lechnqKwud0tL/rRyAfMJ6Hh/z2fS13pBA==
has-bigints@^1.0.1, has-bigints@^1.0.2: has-bigints@^1.0.1, has-bigints@^1.0.2:
version "1.0.2" version "1.0.2"
@ -2482,10 +2482,10 @@ safe-regex-test@^1.0.3:
es-errors "^1.3.0" es-errors "^1.3.0"
is-regex "^1.1.4" is-regex "^1.1.4"
sass@1.77.2: sass@1.77.4:
version "1.77.2" version "1.77.4"
resolved "https://registry.yarnpkg.com/sass/-/sass-1.77.2.tgz#18d4ed2eefc260cdc8099c5439ec1303fd5863aa" resolved "https://registry.yarnpkg.com/sass/-/sass-1.77.4.tgz#92059c7bfc56b827c56eb116778d157ec017a5cd"
integrity sha512-eb4GZt1C3avsX3heBNlrc7I09nyT00IUuo4eFhAbeXWU2fvA7oXI53SxODVAA+zgZCk9aunAZgO+losjR3fAwA== integrity sha512-vcF3Ckow6g939GMA4PeU7b2K/9FALXk2KF9J87txdHzXbUF9XRQRwSxcAs/fGaTnJeBFd7UoV22j3lzMLdM0Pw==
dependencies: dependencies:
chokidar ">=3.0.0 <4.0.0" chokidar ">=3.0.0 <4.0.0"
immutable "^4.0.0" immutable "^4.0.0"

2
netbox/release.yaml Normal file
View File

@ -0,0 +1,2 @@
version: "4.1.0"
designation: "dev"

View File

@ -20,7 +20,7 @@
{# Initialize color mode #} {# Initialize color mode #}
<script <script
type="text/javascript" type="text/javascript"
src="{% static 'setmode.js' %}?v={{ settings.VERSION }}" src="{% static 'setmode.js' %}?v={{ settings.RELEASE.version }}"
onerror="window.location='{% url 'media_failure' %}?filename=setmode.js'"> onerror="window.location='{% url 'media_failure' %}?filename=setmode.js'">
</script> </script>
<script type="text/javascript"> <script type="text/javascript">
@ -33,12 +33,12 @@
{# Static resources #} {# Static resources #}
<link <link
rel="stylesheet" rel="stylesheet"
href="{% static 'netbox-external.css'%}?v={{ settings.VERSION }}" href="{% static 'netbox-external.css'%}?v={{ settings.RELEASE.version }}"
onerror="window.location='{% url 'media_failure' %}?filename=netbox-external.css'" onerror="window.location='{% url 'media_failure' %}?filename=netbox-external.css'"
/> />
<link <link
rel="stylesheet" rel="stylesheet"
href="{% static 'netbox.css'%}?v={{ settings.VERSION }}" href="{% static 'netbox.css'%}?v={{ settings.RELEASE.version }}"
onerror="window.location='{% url 'media_failure' %}?filename=netbox.css'" onerror="window.location='{% url 'media_failure' %}?filename=netbox.css'"
/> />
<link rel="icon" type="image/png" href="{% static 'netbox.ico' %}" /> <link rel="icon" type="image/png" href="{% static 'netbox.ico' %}" />
@ -47,7 +47,7 @@
{# Javascript #} {# Javascript #}
<script <script
type="text/javascript" type="text/javascript"
src="{% static 'netbox.js' %}?v={{ settings.VERSION }}" src="{% static 'netbox.js' %}?v={{ settings.RELEASE.version }}"
onerror="window.location='{% url 'media_failure' %}?filename=netbox.js'"> onerror="window.location='{% url 'media_failure' %}?filename=netbox.js'">
</script> </script>
{% django_htmx_script %} {% django_htmx_script %}

View File

@ -188,12 +188,9 @@ Blocks:
{# Footer text #} {# Footer text #}
<ul class="list-inline list-inline-dots fs-5 mb-0" id="footer-stamp" hx-swap-oob="true"> <ul class="list-inline list-inline-dots fs-5 mb-0" id="footer-stamp" hx-swap-oob="true">
<li class="list-inline-item"> <li class="list-inline-item">{% now 'Y-m-d H:i:s T' %}</li>
{% now 'Y-m-d H:i:s T' %} <li class="list-inline-item">{{ settings.HOSTNAME }}</li>
</li> <li class="list-inline-item">{{ settings.RELEASE.name }}</li>
<li class="list-inline-item">
{{ settings.HOSTNAME }} (v{{ settings.VERSION }})
</li>
</ul> </ul>
{# /Footer text #} {# /Footer text #}

View File

@ -5,7 +5,7 @@
<div class="row mb-3"> <div class="row mb-3">
<div class="col col-md-12"> <div class="col col-md-12">
<div class="card"> <div class="card">
<div class="card-body table-responsive"> <div class="table-responsive">
{% render_table table 'inc/table.html' %} {% render_table table 'inc/table.html' %}
{% include 'inc/paginator.html' with paginator=table.paginator page=table.page %} {% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
</div> </div>

View File

@ -28,8 +28,13 @@
<h5 class="card-header">{% trans "System Status" %}</h5> <h5 class="card-header">{% trans "System Status" %}</h5>
<table class="table table-hover attr-table"> <table class="table table-hover attr-table">
<tr> <tr>
<th scope="row">{% trans "NetBox version" %}</th> <th scope="row">{% trans "NetBox release" %}</th>
<td>{{ stats.netbox_version }}</td> <td>
{{ stats.netbox_release.name }}
{% if stats.netbox_release.published %}
({{ stats.netbox_release.published|isodate }})
{% endif %}
</td>
</tr> </tr>
<tr> <tr>
<th scope="row">{% trans "Python version" %}</th> <th scope="row">{% trans "Python version" %}</th>

View File

@ -4,7 +4,7 @@
{% load log_levels %} {% load log_levels %}
{% load i18n %} {% load i18n %}
{% block title %}{{ script }}{% endblock %} {% block title %}{{ script.python_class.name }}{% endblock %}
{% block object_identifier %} {% block object_identifier %}
{{ script.full_name }} {{ script.full_name }}
@ -17,7 +17,7 @@
{% block subtitle %} {% block subtitle %}
<div class="text-secondary fs-5"> <div class="text-secondary fs-5">
{{ script.Meta.description|markdown }} {{ script.python_class.Meta.description|markdown }}
</div> </div>
{% endblock subtitle %} {% endblock subtitle %}

View File

@ -56,15 +56,15 @@
<tr> <tr>
<td> <td>
{% if script.is_executable %} {% if script.is_executable %}
<a href="{% url 'extras:script' script.pk %}" id="{{ script.module }}.{{ script.class_name }}">{{ script.name }}</a> <a href="{% url 'extras:script' script.pk %}" id="{{ script.module }}.{{ script.class_name }}">{{ script.python_class.name }}</a>
{% else %} {% else %}
<a href="{% url 'extras:script_jobs' script.pk %}" id="{{ script.module }}.{{ script.class_name }}">{{ script.name }}</a> <a href="{% url 'extras:script_jobs' script.pk %}" id="{{ script.module }}.{{ script.class_name }}">{{ script.python_class.name }}</a>
<span class="text-danger"> <span class="text-danger">
<i class="mdi mdi-alert" title="{% trans "Script is no longer present in the source file" %}"></i> <i class="mdi mdi-alert" title="{% trans "Script is no longer present in the source file" %}"></i>
</span> </span>
{% endif %} {% endif %}
</td> </td>
<td>{{ script.description|markdown|placeholder }}</td> <td>{{ script.python_class.Meta.description|markdown|placeholder }}</td>
{% if last_job %} {% if last_job %}
<td> <td>
<a href="{% url 'extras:script_result' job_pk=last_job.pk %}">{{ last_job.created|isodatetime }}</a> <a href="{% url 'extras:script_result' job_pk=last_job.pk %}">{{ last_job.created|isodatetime }}</a>

View File

@ -59,7 +59,7 @@
<th scope="row"><i class="mdi mdi-chip"></i> {% trans "Memory" %}</th> <th scope="row"><i class="mdi mdi-chip"></i> {% trans "Memory" %}</th>
<td> <td>
{% if memory_sum %} {% if memory_sum %}
{{ memory_sum|humanize_megabytes }} <span title={{ memory_sum }}>{{ memory_sum|humanize_megabytes }}</span>
{% else %} {% else %}
{{ ''|placeholder }} {{ ''|placeholder }}
{% endif %} {% endif %}

View File

@ -125,7 +125,7 @@
<th scope="row"><i class="mdi mdi-chip"></i> {% trans "Memory" %}</th> <th scope="row"><i class="mdi mdi-chip"></i> {% trans "Memory" %}</th>
<td> <td>
{% if object.memory %} {% if object.memory %}
{{ object.memory|humanize_megabytes }} <span title={{ object.memory }}>{{ object.memory|humanize_megabytes }}</span>
{% else %} {% else %}
{{ ''|placeholder }} {{ ''|placeholder }}
{% endif %} {% endif %}
@ -137,7 +137,7 @@
</th> </th>
<td> <td>
{% if object.disk %} {% if object.disk %}
{{ object.disk }} {% trans "GB" context "Abbreviation for gigabyte" %} {{ object.disk|humanize_megabytes }}
{% else %} {% else %}
{{ ''|placeholder }} {{ ''|placeholder }}
{% endif %} {% endif %}

File diff suppressed because it is too large Load Diff

View File

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-06-04 05:02+0000\n" "POT-Creation-Date: 2024-06-05 05:02+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -965,7 +965,7 @@ msgstr ""
#: netbox/extras/forms/filtersets.py:143 netbox/extras/forms/filtersets.py:183 #: netbox/extras/forms/filtersets.py:143 netbox/extras/forms/filtersets.py:183
#: netbox/extras/forms/filtersets.py:199 netbox/extras/forms/filtersets.py:230 #: netbox/extras/forms/filtersets.py:199 netbox/extras/forms/filtersets.py:230
#: netbox/extras/forms/filtersets.py:254 netbox/extras/forms/filtersets.py:450 #: netbox/extras/forms/filtersets.py:254 netbox/extras/forms/filtersets.py:450
#: netbox/extras/forms/filtersets.py:488 netbox/ipam/forms/filtersets.py:99 #: netbox/extras/forms/filtersets.py:485 netbox/ipam/forms/filtersets.py:99
#: netbox/ipam/forms/filtersets.py:266 netbox/ipam/forms/filtersets.py:307 #: netbox/ipam/forms/filtersets.py:266 netbox/ipam/forms/filtersets.py:307
#: netbox/ipam/forms/filtersets.py:382 netbox/ipam/forms/filtersets.py:475 #: netbox/ipam/forms/filtersets.py:382 netbox/ipam/forms/filtersets.py:475
#: netbox/ipam/forms/filtersets.py:534 netbox/ipam/forms/filtersets.py:552 #: netbox/ipam/forms/filtersets.py:534 netbox/ipam/forms/filtersets.py:552
@ -1577,9 +1577,9 @@ msgid "Creation"
msgstr "" msgstr ""
#: netbox/core/forms/filtersets.py:71 netbox/extras/forms/filtersets.py:470 #: netbox/core/forms/filtersets.py:71 netbox/extras/forms/filtersets.py:470
#: netbox/extras/forms/filtersets.py:513 netbox/extras/tables/tables.py:183 #: netbox/extras/forms/filtersets.py:510 netbox/extras/tables/tables.py:183
#: netbox/extras/tables/tables.py:504 netbox/templates/core/job.html:20 #: netbox/extras/tables/tables.py:504 netbox/templates/core/job.html:20
#: netbox/templates/extras/objectchange.html:51 #: netbox/templates/extras/objectchange.html:52
#: netbox/tenancy/tables/contacts.py:90 netbox/vpn/tables/l2vpn.py:59 #: netbox/tenancy/tables/contacts.py:90 netbox/vpn/tables/l2vpn.py:59
msgid "Object Type" msgid "Object Type"
msgstr "" msgstr ""
@ -1619,9 +1619,9 @@ msgstr ""
#: netbox/core/forms/filtersets.py:123 netbox/dcim/forms/bulk_edit.py:361 #: netbox/core/forms/filtersets.py:123 netbox/dcim/forms/bulk_edit.py:361
#: netbox/dcim/forms/filtersets.py:353 netbox/dcim/forms/filtersets.py:397 #: netbox/dcim/forms/filtersets.py:353 netbox/dcim/forms/filtersets.py:397
#: netbox/dcim/forms/model_forms.py:258 netbox/extras/forms/filtersets.py:465 #: netbox/dcim/forms/model_forms.py:258 netbox/extras/forms/filtersets.py:465
#: netbox/extras/forms/filtersets.py:508 #: netbox/extras/forms/filtersets.py:505
#: netbox/templates/dcim/rackreservation.html:58 #: netbox/templates/dcim/rackreservation.html:58
#: netbox/templates/extras/objectchange.html:35 #: netbox/templates/extras/objectchange.html:36
#: netbox/templates/extras/savedfilter.html:21 #: netbox/templates/extras/savedfilter.html:21
#: netbox/templates/inc/user_menu.html:15 netbox/templates/users/token.html:21 #: netbox/templates/inc/user_menu.html:15 netbox/templates/users/token.html:21
#: netbox/templates/users/user.html:6 netbox/templates/users/user.html:14 #: netbox/templates/users/user.html:6 netbox/templates/users/user.html:14
@ -1976,7 +1976,7 @@ msgstr ""
#: netbox/extras/tables/tables.py:509 netbox/extras/tables/tables.py:574 #: netbox/extras/tables/tables.py:509 netbox/extras/tables/tables.py:574
#: netbox/netbox/tables/tables.py:243 netbox/templates/extras/eventrule.html:84 #: netbox/netbox/tables/tables.py:243 netbox/templates/extras/eventrule.html:84
#: netbox/templates/extras/journalentry.html:18 #: netbox/templates/extras/journalentry.html:18
#: netbox/templates/extras/objectchange.html:57 #: netbox/templates/extras/objectchange.html:58
#: netbox/tenancy/tables/contacts.py:93 netbox/vpn/tables/l2vpn.py:64 #: netbox/tenancy/tables/contacts.py:93 netbox/vpn/tables/l2vpn.py:64
msgid "Object" msgid "Object"
msgstr "" msgstr ""
@ -4172,7 +4172,7 @@ msgid "Connection"
msgstr "" msgstr ""
#: netbox/dcim/forms/filtersets.py:1254 netbox/extras/forms/bulk_edit.py:316 #: netbox/dcim/forms/filtersets.py:1254 netbox/extras/forms/bulk_edit.py:316
#: netbox/extras/forms/bulk_import.py:242 netbox/extras/forms/filtersets.py:476 #: netbox/extras/forms/bulk_import.py:242 netbox/extras/forms/filtersets.py:473
#: netbox/extras/forms/model_forms.py:551 netbox/extras/tables/tables.py:512 #: netbox/extras/forms/model_forms.py:551 netbox/extras/tables/tables.py:512
#: netbox/templates/extras/journalentry.html:30 #: netbox/templates/extras/journalentry.html:30
msgid "Kind" msgid "Kind"
@ -7144,23 +7144,23 @@ msgstr ""
msgid "Tenant groups" msgid "Tenant groups"
msgstr "" msgstr ""
#: netbox/extras/forms/filtersets.py:454 netbox/extras/forms/filtersets.py:492 #: netbox/extras/forms/filtersets.py:454 netbox/extras/forms/filtersets.py:489
msgid "After" msgid "After"
msgstr "" msgstr ""
#: netbox/extras/forms/filtersets.py:459 netbox/extras/forms/filtersets.py:497 #: netbox/extras/forms/filtersets.py:459 netbox/extras/forms/filtersets.py:494
msgid "Before" msgid "Before"
msgstr "" msgstr ""
#: netbox/extras/forms/filtersets.py:487 netbox/extras/tables/tables.py:456 #: netbox/extras/forms/filtersets.py:484 netbox/extras/tables/tables.py:456
#: netbox/extras/tables/tables.py:542 netbox/extras/tables/tables.py:567 #: netbox/extras/tables/tables.py:542 netbox/extras/tables/tables.py:567
#: netbox/templates/extras/objectchange.html:31 #: netbox/templates/extras/objectchange.html:32
msgid "Time" msgid "Time"
msgstr "" msgstr ""
#: netbox/extras/forms/filtersets.py:501 netbox/extras/forms/model_forms.py:282 #: netbox/extras/forms/filtersets.py:498 netbox/extras/forms/model_forms.py:282
#: netbox/extras/tables/tables.py:470 netbox/templates/extras/eventrule.html:77 #: netbox/extras/tables/tables.py:470 netbox/templates/extras/eventrule.html:77
#: netbox/templates/extras/objectchange.html:45 #: netbox/templates/extras/objectchange.html:46
msgid "Action" msgid "Action"
msgstr "" msgstr ""
@ -8256,7 +8256,7 @@ msgid "Full Name"
msgstr "" msgstr ""
#: netbox/extras/tables/tables.py:483 #: netbox/extras/tables/tables.py:483
#: netbox/templates/extras/objectchange.html:67 #: netbox/templates/extras/objectchange.html:68
msgid "Request ID" msgid "Request ID"
msgstr "" msgstr ""
@ -10275,7 +10275,7 @@ msgid "Journal Entries"
msgstr "" msgstr ""
#: netbox/netbox/navigation/menu.py:359 #: netbox/netbox/navigation/menu.py:359
#: netbox/templates/extras/objectchange.html:8 #: netbox/templates/extras/objectchange.html:9
#: netbox/templates/extras/objectchange_list.html:4 #: netbox/templates/extras/objectchange_list.html:4
msgid "Change Log" msgid "Change Log"
msgstr "" msgstr ""
@ -10734,8 +10734,8 @@ msgstr ""
#: netbox/templates/extras/configcontext.html:70 #: netbox/templates/extras/configcontext.html:70
#: netbox/templates/extras/eventrule.html:72 #: netbox/templates/extras/eventrule.html:72
#: netbox/templates/extras/htmx/script_result.html:56 #: netbox/templates/extras/htmx/script_result.html:56
#: netbox/templates/extras/objectchange.html:123 #: netbox/templates/extras/objectchange.html:124
#: netbox/templates/extras/objectchange.html:141 #: netbox/templates/extras/objectchange.html:142
#: netbox/templates/extras/webhook.html:67 #: netbox/templates/extras/webhook.html:67
#: netbox/templates/extras/webhook.html:79 #: netbox/templates/extras/webhook.html:79
#: netbox/templates/inc/panel_table.html:13 #: netbox/templates/inc/panel_table.html:13
@ -12308,48 +12308,48 @@ msgstr ""
msgid "New Journal Entry" msgid "New Journal Entry"
msgstr "" msgstr ""
#: netbox/templates/extras/objectchange.html:28 #: netbox/templates/extras/objectchange.html:29
#: netbox/templates/users/objectpermission.html:42 #: netbox/templates/users/objectpermission.html:42
msgid "Change" msgid "Change"
msgstr "" msgstr ""
#: netbox/templates/extras/objectchange.html:78 #: netbox/templates/extras/objectchange.html:79
msgid "Difference" msgid "Difference"
msgstr "" msgstr ""
#: netbox/templates/extras/objectchange.html:81 #: netbox/templates/extras/objectchange.html:82
msgid "Previous" msgid "Previous"
msgstr "" msgstr ""
#: netbox/templates/extras/objectchange.html:84 #: netbox/templates/extras/objectchange.html:85
msgid "Next" msgid "Next"
msgstr "" msgstr ""
#: netbox/templates/extras/objectchange.html:92 #: netbox/templates/extras/objectchange.html:93
msgid "Object Created" msgid "Object Created"
msgstr "" msgstr ""
#: netbox/templates/extras/objectchange.html:94 #: netbox/templates/extras/objectchange.html:95
msgid "Object Deleted" msgid "Object Deleted"
msgstr "" msgstr ""
#: netbox/templates/extras/objectchange.html:96 #: netbox/templates/extras/objectchange.html:97
msgid "No Changes" msgid "No Changes"
msgstr "" msgstr ""
#: netbox/templates/extras/objectchange.html:110 #: netbox/templates/extras/objectchange.html:111
msgid "Pre-Change Data" msgid "Pre-Change Data"
msgstr "" msgstr ""
#: netbox/templates/extras/objectchange.html:121 #: netbox/templates/extras/objectchange.html:122
msgid "Warning: Comparing non-atomic change to previous change record" msgid "Warning: Comparing non-atomic change to previous change record"
msgstr "" msgstr ""
#: netbox/templates/extras/objectchange.html:130 #: netbox/templates/extras/objectchange.html:131
msgid "Post-Change Data" msgid "Post-Change Data"
msgstr "" msgstr ""
#: netbox/templates/extras/objectchange.html:153 #: netbox/templates/extras/objectchange.html:162
#, python-format #, python-format
msgid "See All %(count)s Changes" msgid "See All %(count)s Changes"
msgstr "" msgstr ""

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -53,7 +53,7 @@ def handle_rest_api_exception(request, *args, **kwargs):
data = { data = {
'error': str(error), 'error': str(error),
'exception': type_.__name__, 'exception': type_.__name__,
'netbox_version': settings.VERSION, 'netbox_version': settings.RELEASE.full_version,
'python_version': platform.python_version(), 'python_version': platform.python_version(),
} }
return JsonResponse(data, status=status.HTTP_500_INTERNAL_SERVER_ERROR) return JsonResponse(data, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

View File

@ -0,0 +1,57 @@
import datetime
import os
import yaml
from dataclasses import dataclass
from typing import Union
from django.core.exceptions import ImproperlyConfigured
RELEASE_PATH = 'release.yaml'
LOCAL_RELEASE_PATH = 'local/release.yaml'
@dataclass
class ReleaseInfo:
version: str
edition: str = 'Community'
published: Union[datetime.date, None] = None
designation: Union[str, None] = None
@property
def full_version(self):
if self.designation:
return f"{self.version}-{self.designation}"
return self.version
@property
def name(self):
return f"NetBox {self.edition} v{self.full_version}"
def load_release_data():
"""
Load any locally-defined release attributes and return a ReleaseInfo instance.
"""
base_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Load canonical release attributes
with open(os.path.join(base_path, RELEASE_PATH), 'r') as release_file:
data = yaml.safe_load(release_file)
# Overlay any local release date (if defined)
try:
with open(os.path.join(base_path, LOCAL_RELEASE_PATH), 'r') as release_file:
local_data = yaml.safe_load(release_file)
except FileNotFoundError:
local_data = {}
if type(local_data) is not dict:
raise ImproperlyConfigured(
f"{LOCAL_RELEASE_PATH}: Local release data must be defined as a dictionary."
)
data.update(local_data)
# Convert the published date to a date object
if 'published' in data:
data['published'] = datetime.date.fromisoformat(data['published'])
return ReleaseInfo(**data)

View File

@ -4,7 +4,7 @@
<i class="mdi mdi-download"></i> {% trans "Export" %} <i class="mdi mdi-download"></i> {% trans "Export" %}
</button> </button>
<ul class="dropdown-menu dropdown-menu-end"> <ul class="dropdown-menu dropdown-menu-end">
<li><a class="dropdown-item" href="?{% if url_params %}{{ url_params }}&{% endif %}export=table">{% trans "Current View" %}</a></li> <li><a id="export_current_view" class="dropdown-item" href="?{% if url_params %}{{ url_params }}&{% endif %}export=table">{% trans "Current View" %}</a></li>
<li><a class="dropdown-item" href="?{% if url_params %}{{ url_params }}&{% endif %}export">{% trans "All Data" %} ({{ data_format }})</a></li> <li><a class="dropdown-item" href="?{% if url_params %}{{ url_params }}&{% endif %}export">{% trans "All Data" %} ({{ data_format }})</a></li>
{% if export_templates %} {% if export_templates %}
<li> <li>

View File

@ -1,14 +1,9 @@
import datetime
import json import json
from typing import Dict, Any from typing import Dict, Any
from urllib.parse import quote from urllib.parse import quote
from django import template from django import template
from django.conf import settings
from django.template.defaultfilters import date
from django.urls import NoReverseMatch, reverse from django.urls import NoReverseMatch, reverse
from django.utils import timezone
from django.utils.safestring import mark_safe
from core.models import ObjectType from core.models import ObjectType
from utilities.forms import get_selected_values, TableConfigForm from utilities.forms import get_selected_values, TableConfigForm
@ -92,15 +87,22 @@ def humanize_speed(speed):
@register.filter() @register.filter()
def humanize_megabytes(mb): def humanize_megabytes(mb):
""" """
Express a number of megabytes in the most suitable unit (e.g. gigabytes or terabytes). Express a number of megabytes in the most suitable unit (e.g. gigabytes, terabytes, etc.).
""" """
if not mb: if not mb:
return '' return ""
if not mb % 1048576: # 1024^2
return f'{int(mb / 1048576)} TB' PB_SIZE = 1000000000
if not mb % 1024: TB_SIZE = 1000000
return f'{int(mb / 1024)} GB' GB_SIZE = 1000
return f'{mb} MB'
if mb >= PB_SIZE:
return f"{mb / PB_SIZE:.2f} PB"
if mb >= TB_SIZE:
return f"{mb / TB_SIZE:.2f} TB"
if mb >= GB_SIZE:
return f"{mb / GB_SIZE:.2f} GB"
return f"{mb} MB"
@register.filter() @register.filter()

View File

@ -0,0 +1,23 @@
# Generated by Django 5.0.6 on 2024-06-06 17:46
from django.db import migrations
from django.db.models import F
def convert_disk_size(apps, schema_editor):
VirtualMachine = apps.get_model('virtualization', 'VirtualMachine')
VirtualMachine.objects.filter(disk__isnull=False).update(disk=F('disk') * 1000)
class Migration(migrations.Migration):
dependencies = [
('virtualization', '0038_virtualdisk'),
]
operations = [
migrations.RunPython(
code=convert_disk_size,
reverse_code=migrations.RunPython.noop
),
]

View File

@ -125,7 +125,7 @@ class VirtualMachine(ContactsMixin, ImageAttachmentsMixin, RenderConfigMixin, Co
disk = models.PositiveIntegerField( disk = models.PositiveIntegerField(
blank=True, blank=True,
null=True, null=True,
verbose_name=_('disk (GB)') verbose_name=_('disk (MB)')
) )
# Counter fields # Counter fields

View File

@ -4,6 +4,7 @@ from django.utils.translation import gettext_lazy as _
from dcim.tables.devices import BaseInterfaceTable from dcim.tables.devices import BaseInterfaceTable
from netbox.tables import NetBoxTable, columns from netbox.tables import NetBoxTable, columns
from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin
from utilities.templatetags.helpers import humanize_megabytes
from virtualization.models import VirtualDisk, VirtualMachine, VMInterface from virtualization.models import VirtualDisk, VirtualMachine, VMInterface
__all__ = ( __all__ = (
@ -106,6 +107,9 @@ class VirtualMachineTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable)
verbose_name=_('Config Template'), verbose_name=_('Config Template'),
linkify=True linkify=True
) )
disk = tables.Column(
verbose_name=_('Disk'),
)
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = VirtualMachine model = VirtualMachine
@ -118,6 +122,9 @@ class VirtualMachineTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable)
'pk', 'name', 'status', 'site', 'cluster', 'role', 'tenant', 'vcpus', 'memory', 'disk', 'primary_ip', 'pk', 'name', 'status', 'site', 'cluster', 'role', 'tenant', 'vcpus', 'memory', 'disk', 'primary_ip',
) )
def render_disk(self, value):
return humanize_megabytes(value)
# #
# VM components # VM components

View File

@ -190,10 +190,6 @@ class IKEPolicyFilterSet(NetBoxModelFilterSet):
to_field_name='name' to_field_name='name'
) )
# TODO: Remove in v4.1
proposal = ike_proposal
proposal_id = ike_proposal_id
class Meta: class Meta:
model = IKEPolicy model = IKEPolicy
fields = ('id', 'name', 'preshared_key', 'description') fields = ('id', 'name', 'preshared_key', 'description')
@ -255,10 +251,6 @@ class IPSecPolicyFilterSet(NetBoxModelFilterSet):
to_field_name='name' to_field_name='name'
) )
# TODO: Remove in v4.1
proposal = ipsec_proposal
proposal_id = ipsec_proposal_id
class Meta: class Meta:
model = IPSecPolicy model = IPSecPolicy
fields = ('id', 'name', 'description') fields = ('id', 'name', 'description')

View File

@ -1,6 +1,6 @@
Django==5.0.6 Django==5.0.6
django-cors-headers==4.3.1 django-cors-headers==4.3.1
django-debug-toolbar==4.3.0 django-debug-toolbar==4.4.2
django-filter==24.2 django-filter==24.2
django-htmx==1.17.3 django-htmx==1.17.3
django-graphiql-debug-toolbar==0.2.0 django-graphiql-debug-toolbar==0.2.0
@ -15,23 +15,23 @@ django-tables2==2.7.0
django-timezone-field==6.1.0 django-timezone-field==6.1.0
djangorestframework==3.15.1 djangorestframework==3.15.1
drf-spectacular==0.27.2 drf-spectacular==0.27.2
drf-spectacular-sidecar==2024.5.1 drf-spectacular-sidecar==2024.6.1
feedparser==6.0.11 feedparser==6.0.11
gunicorn==22.0.0 gunicorn==22.0.0
Jinja2==3.1.4 Jinja2==3.1.4
Markdown==3.6 Markdown==3.6
mkdocs-material==9.5.24 mkdocs-material==9.5.26
mkdocstrings[python-legacy]==0.25.1 mkdocstrings[python-legacy]==0.25.1
netaddr==1.2.1 netaddr==1.3.0
nh3==0.2.17 nh3==0.2.17
Pillow==10.3.0 Pillow==10.3.0
psycopg[c,pool]==3.1.19 psycopg[c,pool]==3.1.19
PyYAML==6.0.1 PyYAML==6.0.1
requests==2.32.2 requests==2.32.3
social-auth-app-django==5.4.1 social-auth-app-django==5.4.1
social-auth-core==4.5.4 social-auth-core==4.5.4
strawberry-graphql==0.230.0 strawberry-graphql==0.234.0
strawberry-graphql-django==0.40.0 strawberry-graphql-django==0.42.0
svgwrite==1.4.3 svgwrite==1.4.3
tablib==3.6.1 tablib==3.6.1
tzdata==2024.1 tzdata==2024.1

View File

@ -8,7 +8,7 @@
cd "$(dirname "$0")" cd "$(dirname "$0")"
NETBOX_VERSION="$(grep ^VERSION netbox/netbox/settings.py | cut -d\' -f2)" NETBOX_VERSION="$(grep ^version netbox/release.yaml | cut -d \" -f2)"
echo "You are installing (or upgrading to) NetBox version ${NETBOX_VERSION}" echo "You are installing (or upgrading to) NetBox version ${NETBOX_VERSION}"
VIRTUALENV="$(pwd -P)/venv" VIRTUALENV="$(pwd -P)/venv"