mirror of
https://github.com/netbox-community/netbox.git
synced 2025-08-09 09:08:15 -06:00
Merge branch 'feature' into 7537-vm-serial
This commit is contained in:
commit
768d55d3be
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@ -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
|
||||||
|
2
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
2
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
@ -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
|
||||||
|
@ -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",
|
||||||
|
@ -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.
|
||||||
|
@ -195,12 +195,15 @@ Plugins can inject custom content into certain areas of core NetBox views. This
|
|||||||
|
|
||||||
| Method | View | Description |
|
| Method | View | Description |
|
||||||
|---------------------|-------------|-----------------------------------------------------|
|
|---------------------|-------------|-----------------------------------------------------|
|
||||||
|
| `navbar()` | All | Inject content inside the top navigation bar |
|
||||||
| `left_page()` | Object view | Inject content on the left side of the page |
|
| `left_page()` | Object view | Inject content on the left side of the page |
|
||||||
| `right_page()` | Object view | Inject content on the right side of the page |
|
| `right_page()` | Object view | Inject content on the right side of the page |
|
||||||
| `full_width_page()` | Object view | Inject content across the entire bottom of the page |
|
| `full_width_page()` | Object view | Inject content across the entire bottom of the page |
|
||||||
| `buttons()` | Object view | Add buttons to the top of the page |
|
| `buttons()` | Object view | Add buttons to the top of the page |
|
||||||
| `list_buttons()` | List view | Add buttons to the top of the page |
|
| `list_buttons()` | List view | Add buttons to the top of the page |
|
||||||
|
|
||||||
|
!!! info "The `navbar()` method was introduced in NetBox v4.1."
|
||||||
|
|
||||||
Additionally, a `render()` method is available for convenience. This method accepts the name of a template to render, and any additional context data you want to pass. Its use is optional, however.
|
Additionally, a `render()` method is available for convenience. This method accepts the name of a template to render, and any additional context data you want to pass. Its use is optional, however.
|
||||||
|
|
||||||
When a PluginTemplateExtension is instantiated, context data is assigned to `self.context`. Available data include:
|
When a PluginTemplateExtension is instantiated, context data is assigned to `self.context`. Available data include:
|
||||||
|
@ -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
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -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):
|
||||||
|
|
||||||
|
@ -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):
|
||||||
|
|
||||||
|
@ -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):
|
||||||
|
@ -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
|
||||||
)
|
)
|
||||||
|
@ -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')
|
||||||
|
@ -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')
|
||||||
|
@ -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):
|
||||||
|
@ -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')),
|
||||||
|
@ -32,21 +32,13 @@ def register_template_extensions(class_list):
|
|||||||
template_extension=template_extension
|
template_extension=template_extension
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
if template_extension.model is None:
|
|
||||||
raise TypeError(
|
|
||||||
_("PluginTemplateExtension class {template_extension} does not define a valid model!").format(
|
|
||||||
template_extension=template_extension
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
registry['plugins']['template_extensions'][template_extension.model].append(template_extension)
|
registry['plugins']['template_extensions'][template_extension.model].append(template_extension)
|
||||||
|
|
||||||
|
|
||||||
def register_menu(menu):
|
def register_menu(menu):
|
||||||
if not isinstance(menu, PluginMenu):
|
if not isinstance(menu, PluginMenu):
|
||||||
raise TypeError(_("{item} must be an instance of netbox.plugins.PluginMenuItem").format(
|
raise TypeError(_("{item} must be an instance of netbox.plugins.PluginMenuItem").format(item=menu))
|
||||||
item=menu_link
|
|
||||||
))
|
|
||||||
registry['plugins']['menus'].append(menu)
|
registry['plugins']['menus'].append(menu)
|
||||||
|
|
||||||
|
|
||||||
|
@ -14,7 +14,8 @@ class PluginTemplateExtension:
|
|||||||
The `model` attribute on the class defines the which model detail page this class renders content for. It
|
The `model` attribute on the class defines the which model detail page this class renders content for. It
|
||||||
should be set as a string in the form '<app_label>.<model_name>'. render() provides the following context data:
|
should be set as a string in the form '<app_label>.<model_name>'. render() provides the following context data:
|
||||||
|
|
||||||
* object - The object being viewed
|
* object - The object being viewed (object views only)
|
||||||
|
* model - The type of object being viewed (list views only)
|
||||||
* request - The current request
|
* request - The current request
|
||||||
* settings - Global NetBox settings
|
* settings - Global NetBox settings
|
||||||
* config - Plugin-specific configuration parameters
|
* config - Plugin-specific configuration parameters
|
||||||
@ -36,6 +37,13 @@ class PluginTemplateExtension:
|
|||||||
|
|
||||||
return get_template(template_name).render({**self.context, **extra_context})
|
return get_template(template_name).render({**self.context, **extra_context})
|
||||||
|
|
||||||
|
def navbar(self):
|
||||||
|
"""
|
||||||
|
Content that will be rendered inside the top navigation menu. Content should be returned as an HTML
|
||||||
|
string. Note that content does not need to be marked as safe because this is automatically handled.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
def left_page(self):
|
def left_page(self):
|
||||||
"""
|
"""
|
||||||
Content that will be rendered on the left of the detail page view. Content should be returned as an
|
Content that will be rendered on the left of the detail page view. Content should be returned as an
|
||||||
|
@ -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
|
||||||
|
@ -1,6 +1,12 @@
|
|||||||
from netbox.plugins.templates import PluginTemplateExtension
|
from netbox.plugins.templates import PluginTemplateExtension
|
||||||
|
|
||||||
|
|
||||||
|
class GlobalContent(PluginTemplateExtension):
|
||||||
|
|
||||||
|
def navbar(self):
|
||||||
|
return "GLOBAL CONTENT - NAVBAR"
|
||||||
|
|
||||||
|
|
||||||
class SiteContent(PluginTemplateExtension):
|
class SiteContent(PluginTemplateExtension):
|
||||||
model = 'dcim.site'
|
model = 'dcim.site'
|
||||||
|
|
||||||
@ -20,4 +26,4 @@ class SiteContent(PluginTemplateExtension):
|
|||||||
return "SITE CONTENT - LIST BUTTONS"
|
return "SITE CONTENT - LIST BUTTONS"
|
||||||
|
|
||||||
|
|
||||||
template_extensions = [SiteContent]
|
template_extensions = [GlobalContent, SiteContent]
|
||||||
|
@ -99,8 +99,9 @@ class PluginTest(TestCase):
|
|||||||
"""
|
"""
|
||||||
Check that plugin TemplateExtensions are registered.
|
Check that plugin TemplateExtensions are registered.
|
||||||
"""
|
"""
|
||||||
from netbox.tests.dummy_plugin.template_content import SiteContent
|
from netbox.tests.dummy_plugin.template_content import GlobalContent, SiteContent
|
||||||
|
|
||||||
|
self.assertIn(GlobalContent, registry['plugins']['template_extensions'][None])
|
||||||
self.assertIn(SiteContent, registry['plugins']['template_extensions']['dcim.site'])
|
self.assertIn(SiteContent, registry['plugins']['template_extensions']['dcim.site'])
|
||||||
|
|
||||||
def test_registered_columns(self):
|
def test_registered_columns(self):
|
||||||
@ -165,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):
|
||||||
"""
|
"""
|
||||||
@ -182,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):
|
||||||
|
@ -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",
|
||||||
|
@ -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(),
|
||||||
}))
|
}))
|
||||||
|
@ -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,
|
||||||
|
BIN
netbox/project-static/dist/netbox.css
vendored
BIN
netbox/project-static/dist/netbox.css
vendored
Binary file not shown.
BIN
netbox/project-static/dist/netbox.js
vendored
BIN
netbox/project-static/dist/netbox.js
vendored
Binary file not shown.
BIN
netbox/project-static/dist/netbox.js.map
vendored
BIN
netbox/project-static/dist/netbox.js.map
vendored
Binary file not shown.
@ -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"
|
||||||
|
@ -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,
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
@ -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
2
netbox/release.yaml
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
version: "4.1.0"
|
||||||
|
designation: "dev"
|
@ -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 %}
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
{% extends 'base/base.html' %}
|
{% extends 'base/base.html' %}
|
||||||
{% load helpers %}
|
{% load helpers %}
|
||||||
{% load navigation %}
|
{% load navigation %}
|
||||||
|
{% load plugins %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
@ -51,8 +52,12 @@ Blocks:
|
|||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
|
|
||||||
<div class="navbar-nav flex-row align-items-center order-md-last">
|
<div class="navbar-nav flex-row align-items-center order-md-last">
|
||||||
|
|
||||||
|
{# Plugin content #}
|
||||||
|
{% plugin_navbar %}
|
||||||
|
|
||||||
{# Dark/light mode toggle #}
|
{# Dark/light mode toggle #}
|
||||||
<div class="d-none d-md-flex">
|
<div class="d-none d-md-flex ms-2">
|
||||||
<button class="btn color-mode-toggle hide-theme-dark" title="{% trans "Enable dark mode" %}" data-bs-toggle="tooltip" data-bs-placement="bottom">
|
<button class="btn color-mode-toggle hide-theme-dark" title="{% trans "Enable dark mode" %}" data-bs-toggle="tooltip" data-bs-placement="bottom">
|
||||||
<i class="mdi mdi-lightbulb"></i>
|
<i class="mdi mdi-lightbulb"></i>
|
||||||
</button>
|
</button>
|
||||||
@ -183,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 #}
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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 %}
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
@ -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 %}
|
||||||
|
@ -129,7 +129,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 %}
|
||||||
@ -141,7 +141,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 %}
|
||||||
|
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@ -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
@ -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)
|
||||||
|
57
netbox/utilities/release.py
Normal file
57
netbox/utilities/release.py
Normal 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)
|
@ -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>
|
||||||
|
@ -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()
|
||||||
|
@ -22,7 +22,7 @@ def _get_registered_content(obj, method, template_context):
|
|||||||
'perms': template_context['perms'],
|
'perms': template_context['perms'],
|
||||||
}
|
}
|
||||||
|
|
||||||
model_name = obj._meta.label_lower
|
model_name = obj._meta.label_lower if obj is not None else None
|
||||||
template_extensions = registry['plugins']['template_extensions'].get(model_name, [])
|
template_extensions = registry['plugins']['template_extensions'].get(model_name, [])
|
||||||
for template_extension in template_extensions:
|
for template_extension in template_extensions:
|
||||||
|
|
||||||
@ -43,6 +43,14 @@ def _get_registered_content(obj, method, template_context):
|
|||||||
return mark_safe(html)
|
return mark_safe(html)
|
||||||
|
|
||||||
|
|
||||||
|
@register.simple_tag(takes_context=True)
|
||||||
|
def plugin_navbar(context):
|
||||||
|
"""
|
||||||
|
Render any navbar content embedded by plugins
|
||||||
|
"""
|
||||||
|
return _get_registered_content(None, 'navbar', context)
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag(takes_context=True)
|
@register.simple_tag(takes_context=True)
|
||||||
def plugin_buttons(context, obj):
|
def plugin_buttons(context, obj):
|
||||||
"""
|
"""
|
||||||
|
23
netbox/virtualization/migrations/0039_convert_disk_size.py
Normal file
23
netbox/virtualization/migrations/0039_convert_disk_size.py
Normal 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
|
||||||
|
),
|
||||||
|
]
|
@ -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)')
|
||||||
)
|
)
|
||||||
serial = models.CharField(
|
serial = models.CharField(
|
||||||
verbose_name=_('serial number'),
|
verbose_name=_('serial number'),
|
||||||
|
@ -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
|
||||||
|
@ -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')
|
||||||
|
@ -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
|
||||||
|
@ -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"
|
||||||
|
Loading…
Reference in New Issue
Block a user