mirror of
https://github.com/netbox-community/netbox.git
synced 2025-08-08 00:28:16 -06:00
Merge branch 'feature' into 10500-nested-modules
This commit is contained in:
commit
c7c6e78cb4
@ -39,3 +39,7 @@ An alternative part number to uniquely identify the module type.
|
||||
### Weight
|
||||
|
||||
The numeric weight of the module, including a unit designation (e.g. 3 kilograms or 1 pound).
|
||||
|
||||
### Airflow
|
||||
|
||||
The direction in which air circulates through the device chassis for cooling.
|
||||
|
@ -54,4 +54,7 @@ The maximum total weight capacity for all installed devices, inclusive of the ra
|
||||
|
||||
If selected, the rack's elevation will display unit 1 at the top of the rack. (Most racks use ascending numbering, with unit 1 assigned to the bottommost position.)
|
||||
|
||||
### Airflow
|
||||
|
||||
The direction in which air circulates through the rack for cooling.
|
||||
|
||||
|
209
netbox/core/plugins.py
Normal file
209
netbox/core/plugins.py
Normal file
@ -0,0 +1,209 @@
|
||||
import datetime
|
||||
import importlib
|
||||
import importlib.util
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Optional
|
||||
|
||||
import requests
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from netbox.plugins import PluginConfig
|
||||
from utilities.datetime import datetime_from_timestamp
|
||||
|
||||
USER_AGENT_STRING = f'NetBox/{settings.RELEASE.version} {settings.RELEASE.edition}'
|
||||
|
||||
|
||||
@dataclass
|
||||
class PluginAuthor:
|
||||
"""
|
||||
Identifying information for the author of a plugin.
|
||||
"""
|
||||
name: str
|
||||
org_id: str = ''
|
||||
url: str = ''
|
||||
|
||||
|
||||
@dataclass
|
||||
class PluginVersion:
|
||||
"""
|
||||
Details for a specific versioned release of a plugin.
|
||||
"""
|
||||
date: datetime.datetime = None
|
||||
version: str = ''
|
||||
netbox_min_version: str = ''
|
||||
netbox_max_version: str = ''
|
||||
has_model: bool = False
|
||||
is_certified: bool = False
|
||||
is_feature: bool = False
|
||||
is_integration: bool = False
|
||||
is_netboxlabs_supported: bool = False
|
||||
|
||||
|
||||
@dataclass
|
||||
class Plugin:
|
||||
"""
|
||||
The representation of a NetBox plugin in the catalog API.
|
||||
"""
|
||||
id: str = ''
|
||||
status: str = ''
|
||||
title_short: str = ''
|
||||
title_long: str = ''
|
||||
tag_line: str = ''
|
||||
description_short: str = ''
|
||||
slug: str = ''
|
||||
author: Optional[PluginAuthor] = None
|
||||
created_at: datetime.datetime = None
|
||||
updated_at: datetime.datetime = None
|
||||
license_type: str = ''
|
||||
homepage_url: str = ''
|
||||
package_name_pypi: str = ''
|
||||
config_name: str = ''
|
||||
is_certified: bool = False
|
||||
release_latest: PluginVersion = field(default_factory=PluginVersion)
|
||||
release_recent_history: list[PluginVersion] = field(default_factory=list)
|
||||
is_local: bool = False # extra field for locally installed plugins
|
||||
is_installed: bool = False
|
||||
installed_version: str = ''
|
||||
|
||||
|
||||
def get_local_plugins():
|
||||
"""
|
||||
Return a dictionary of all locally-installed plugins, mapped by name.
|
||||
"""
|
||||
plugins = {}
|
||||
for plugin_name in settings.PLUGINS:
|
||||
plugin = importlib.import_module(plugin_name)
|
||||
plugin_config: PluginConfig = plugin.config
|
||||
|
||||
plugins[plugin_config.name] = Plugin(
|
||||
slug=plugin_config.name,
|
||||
title_short=plugin_config.verbose_name,
|
||||
tag_line=plugin_config.description,
|
||||
description_short=plugin_config.description,
|
||||
is_local=True,
|
||||
is_installed=True,
|
||||
installed_version=plugin_config.version,
|
||||
)
|
||||
|
||||
return plugins
|
||||
|
||||
|
||||
def get_catalog_plugins():
|
||||
"""
|
||||
Return a dictionary of all entries in the plugins catalog, mapped by name.
|
||||
"""
|
||||
session = requests.Session()
|
||||
plugins = {}
|
||||
|
||||
def get_pages():
|
||||
# TODO: pagination is currently broken in API
|
||||
payload = {'page': '1', 'per_page': '50'}
|
||||
first_page = session.get(
|
||||
settings.PLUGIN_CATALOG_URL,
|
||||
headers={'User-Agent': USER_AGENT_STRING},
|
||||
proxies=settings.HTTP_PROXIES,
|
||||
timeout=3,
|
||||
params=payload
|
||||
).json()
|
||||
yield first_page
|
||||
num_pages = first_page['metadata']['pagination']['last_page']
|
||||
|
||||
for page in range(2, num_pages + 1):
|
||||
payload['page'] = page
|
||||
next_page = session.get(
|
||||
settings.PLUGIN_CATALOG_URL,
|
||||
headers={'User-Agent': USER_AGENT_STRING},
|
||||
proxies=settings.HTTP_PROXIES,
|
||||
timeout=3,
|
||||
params=payload
|
||||
).json()
|
||||
yield next_page
|
||||
|
||||
for page in get_pages():
|
||||
for data in page['data']:
|
||||
|
||||
# Populate releases
|
||||
releases = []
|
||||
for version in data['release_recent_history']:
|
||||
releases.append(
|
||||
PluginVersion(
|
||||
date=datetime_from_timestamp(version['date']),
|
||||
version=version['version'],
|
||||
netbox_min_version=version['netbox_min_version'],
|
||||
netbox_max_version=version['netbox_max_version'],
|
||||
has_model=version['has_model'],
|
||||
is_certified=version['is_certified'],
|
||||
is_feature=version['is_feature'],
|
||||
is_integration=version['is_integration'],
|
||||
is_netboxlabs_supported=version['is_netboxlabs_supported'],
|
||||
)
|
||||
)
|
||||
releases = sorted(releases, key=lambda x: x.date, reverse=True)
|
||||
latest_release = PluginVersion(
|
||||
date=datetime_from_timestamp(data['release_latest']['date']),
|
||||
version=data['release_latest']['version'],
|
||||
netbox_min_version=data['release_latest']['netbox_min_version'],
|
||||
netbox_max_version=data['release_latest']['netbox_max_version'],
|
||||
has_model=data['release_latest']['has_model'],
|
||||
is_certified=data['release_latest']['is_certified'],
|
||||
is_feature=data['release_latest']['is_feature'],
|
||||
is_integration=data['release_latest']['is_integration'],
|
||||
is_netboxlabs_supported=data['release_latest']['is_netboxlabs_supported'],
|
||||
)
|
||||
|
||||
# Populate author (if any)
|
||||
if data['author']:
|
||||
print(data['author'])
|
||||
author = PluginAuthor(
|
||||
name=data['author']['name'],
|
||||
org_id=data['author']['org_id'],
|
||||
url=data['author']['url'],
|
||||
)
|
||||
else:
|
||||
author = None
|
||||
|
||||
# Populate plugin data
|
||||
plugins[data['slug']] = Plugin(
|
||||
id=data['id'],
|
||||
status=data['status'],
|
||||
title_short=data['title_short'],
|
||||
title_long=data['title_long'],
|
||||
tag_line=data['tag_line'],
|
||||
description_short=data['description_short'],
|
||||
slug=data['slug'],
|
||||
author=author,
|
||||
created_at=datetime_from_timestamp(data['created_at']),
|
||||
updated_at=datetime_from_timestamp(data['updated_at']),
|
||||
license_type=data['license_type'],
|
||||
homepage_url=data['homepage_url'],
|
||||
package_name_pypi=data['package_name_pypi'],
|
||||
config_name=data['config_name'],
|
||||
is_certified=data['is_certified'],
|
||||
release_latest=latest_release,
|
||||
release_recent_history=releases,
|
||||
)
|
||||
|
||||
return plugins
|
||||
|
||||
|
||||
def get_plugins():
|
||||
"""
|
||||
Return a dictionary of all plugins (both catalog and locally installed), mapped by name.
|
||||
"""
|
||||
local_plugins = get_local_plugins()
|
||||
catalog_plugins = cache.get('plugins-catalog-feed')
|
||||
if not catalog_plugins:
|
||||
catalog_plugins = get_catalog_plugins()
|
||||
cache.set('plugins-catalog-feed', catalog_plugins, 3600)
|
||||
|
||||
plugins = catalog_plugins
|
||||
for k, v in local_plugins.items():
|
||||
if k in plugins:
|
||||
plugins[k].is_local = True
|
||||
plugins[k].is_installed = True
|
||||
else:
|
||||
plugins[k] = v
|
||||
|
||||
return plugins
|
@ -1,39 +1,80 @@
|
||||
import django_tables2 as tables
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from netbox.tables import BaseTable
|
||||
|
||||
from netbox.tables import BaseTable, columns
|
||||
|
||||
__all__ = (
|
||||
'PluginTable',
|
||||
'CatalogPluginTable',
|
||||
'PluginVersionTable',
|
||||
)
|
||||
|
||||
|
||||
class PluginTable(BaseTable):
|
||||
name = tables.Column(
|
||||
accessor=tables.A('verbose_name'),
|
||||
verbose_name=_('Name')
|
||||
)
|
||||
class PluginVersionTable(BaseTable):
|
||||
version = tables.Column(
|
||||
verbose_name=_('Version')
|
||||
)
|
||||
package = tables.Column(
|
||||
accessor=tables.A('name'),
|
||||
verbose_name=_('Package')
|
||||
last_updated = columns.DateTimeColumn(
|
||||
accessor=tables.A('date'),
|
||||
timespec='minutes',
|
||||
verbose_name=_('Last Updated')
|
||||
)
|
||||
author = tables.Column(
|
||||
verbose_name=_('Author')
|
||||
min_version = tables.Column(
|
||||
accessor=tables.A('netbox_min_version'),
|
||||
verbose_name=_('Minimum NetBox Version')
|
||||
)
|
||||
author_email = tables.Column(
|
||||
verbose_name=_('Author Email')
|
||||
)
|
||||
description = tables.Column(
|
||||
verbose_name=_('Description')
|
||||
max_version = tables.Column(
|
||||
accessor=tables.A('netbox_max_version'),
|
||||
verbose_name=_('Maximum NetBox Version')
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
empty_text = _('No plugins found')
|
||||
empty_text = _('No plugin data found')
|
||||
fields = (
|
||||
'name', 'version', 'package', 'author', 'author_email', 'description',
|
||||
'version', 'last_updated', 'min_version', 'max_version',
|
||||
)
|
||||
default_columns = (
|
||||
'name', 'version', 'package', 'description',
|
||||
'version', 'last_updated', 'min_version', 'max_version',
|
||||
)
|
||||
orderable = False
|
||||
|
||||
|
||||
class CatalogPluginTable(BaseTable):
|
||||
title_short = tables.Column(
|
||||
linkify=('core:plugin', [tables.A('slug')]),
|
||||
verbose_name=_('Name')
|
||||
)
|
||||
author = tables.Column(
|
||||
accessor=tables.A('author.name'),
|
||||
verbose_name=_('Author')
|
||||
)
|
||||
is_local = columns.BooleanColumn(
|
||||
verbose_name=_('Local')
|
||||
)
|
||||
is_installed = columns.BooleanColumn(
|
||||
verbose_name=_('Installed')
|
||||
)
|
||||
is_certified = columns.BooleanColumn(
|
||||
verbose_name=_('Certified')
|
||||
)
|
||||
created_at = columns.DateTimeColumn(
|
||||
verbose_name=_('Published')
|
||||
)
|
||||
updated_at = columns.DateTimeColumn(
|
||||
verbose_name=_('Updated')
|
||||
)
|
||||
installed_version = tables.Column(
|
||||
verbose_name=_('Installed version')
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
empty_text = _('No plugin data found')
|
||||
fields = (
|
||||
'title_short', 'author', 'is_local', 'is_installed', 'is_certified', 'created_at', 'updated_at',
|
||||
'installed_version',
|
||||
)
|
||||
default_columns = (
|
||||
'title_short', 'author', 'is_local', 'is_installed', 'is_certified', 'created_at', 'updated_at',
|
||||
)
|
||||
# List installed plugins first, then certified plugins, then
|
||||
# everything else (with each tranche ordered alphabetically)
|
||||
order_by = ('-is_installed', '-is_certified', 'name')
|
||||
|
@ -49,4 +49,8 @@ urlpatterns = (
|
||||
|
||||
# System
|
||||
path('system/', views.SystemView.as_view(), name='system'),
|
||||
|
||||
# Plugins
|
||||
path('plugins/', views.PluginListView.as_view(), name='plugin_list'),
|
||||
path('plugins/<str:name>/', views.PluginView.as_view(), name='plugin'),
|
||||
)
|
||||
|
@ -2,7 +2,6 @@ import json
|
||||
import platform
|
||||
|
||||
from django import __version__ as DJANGO_VERSION
|
||||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.mixins import UserPassesTestMixin
|
||||
@ -36,6 +35,8 @@ from utilities.query import count_related
|
||||
from utilities.views import ContentTypePermissionRequiredMixin, GetRelatedModelsMixin, register_model_view
|
||||
from . import filtersets, forms, tables
|
||||
from .models import *
|
||||
from .plugins import get_plugins
|
||||
from .tables import CatalogPluginTable, PluginVersionTable
|
||||
|
||||
|
||||
#
|
||||
@ -581,7 +582,7 @@ class WorkerView(BaseRQView):
|
||||
|
||||
|
||||
#
|
||||
# Plugins
|
||||
# System
|
||||
#
|
||||
|
||||
class SystemView(UserPassesTestMixin, View):
|
||||
@ -614,12 +615,6 @@ class SystemView(UserPassesTestMixin, View):
|
||||
'rq_worker_count': Worker.count(get_connection('default')),
|
||||
}
|
||||
|
||||
# Plugins
|
||||
plugins = [
|
||||
# Look up app config by package name
|
||||
apps.get_app_config(plugin.rsplit('.', 1)[-1]) for plugin in settings.PLUGINS
|
||||
]
|
||||
|
||||
# Configuration
|
||||
try:
|
||||
config = ConfigRevision.objects.get(pk=cache.get('config_version'))
|
||||
@ -631,9 +626,6 @@ class SystemView(UserPassesTestMixin, View):
|
||||
if 'export' in request.GET:
|
||||
data = {
|
||||
**stats,
|
||||
'plugins': {
|
||||
plugin.name: plugin.version for plugin in plugins
|
||||
},
|
||||
'config': {
|
||||
k: config.data[k] for k in sorted(config.data)
|
||||
},
|
||||
@ -642,11 +634,58 @@ class SystemView(UserPassesTestMixin, View):
|
||||
response['Content-Disposition'] = 'attachment; filename="netbox.json"'
|
||||
return response
|
||||
|
||||
plugins_table = tables.PluginTable(plugins, orderable=False)
|
||||
plugins_table.configure(request)
|
||||
|
||||
return render(request, 'core/system.html', {
|
||||
'stats': stats,
|
||||
'plugins_table': plugins_table,
|
||||
'config': config,
|
||||
})
|
||||
|
||||
|
||||
#
|
||||
# Plugins
|
||||
#
|
||||
|
||||
class PluginListView(UserPassesTestMixin, View):
|
||||
|
||||
def test_func(self):
|
||||
return self.request.user.is_staff
|
||||
|
||||
def get(self, request):
|
||||
q = request.GET.get('q', None)
|
||||
|
||||
plugins = get_plugins().values()
|
||||
if q:
|
||||
plugins = [obj for obj in plugins if q.casefold() in obj.title_short.casefold()]
|
||||
|
||||
table = CatalogPluginTable(plugins, user=request.user)
|
||||
table.configure(request)
|
||||
|
||||
# If this is an HTMX request, return only the rendered table HTML
|
||||
if htmx_partial(request):
|
||||
return render(request, 'htmx/table.html', {
|
||||
'table': table,
|
||||
})
|
||||
|
||||
return render(request, 'core/plugin_list.html', {
|
||||
'table': table,
|
||||
})
|
||||
|
||||
|
||||
class PluginView(UserPassesTestMixin, View):
|
||||
|
||||
def test_func(self):
|
||||
return self.request.user.is_staff
|
||||
|
||||
def get(self, request, name):
|
||||
|
||||
plugins = get_plugins()
|
||||
if name not in plugins:
|
||||
raise Http404(_("Plugin {name} not found").format(name=name))
|
||||
plugin = plugins[name]
|
||||
|
||||
table = PluginVersionTable(plugin.release_recent_history, user=request.user)
|
||||
table.configure(request)
|
||||
|
||||
return render(request, 'core/plugin.html', {
|
||||
'plugin': plugin,
|
||||
'table': table,
|
||||
})
|
||||
|
@ -62,13 +62,27 @@ class DeviceTypeSerializer(NetBoxModelSerializer):
|
||||
|
||||
|
||||
class ModuleTypeSerializer(NetBoxModelSerializer):
|
||||
manufacturer = ManufacturerSerializer(nested=True)
|
||||
weight_unit = ChoiceField(choices=WeightUnitChoices, allow_blank=True, required=False, allow_null=True)
|
||||
manufacturer = ManufacturerSerializer(
|
||||
nested=True
|
||||
)
|
||||
weight_unit = ChoiceField(
|
||||
choices=WeightUnitChoices,
|
||||
allow_blank=True,
|
||||
required=False,
|
||||
allow_null=True
|
||||
)
|
||||
airflow = ChoiceField(
|
||||
choices=ModuleAirflowChoices,
|
||||
allow_blank=True,
|
||||
required=False,
|
||||
allow_null=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ModuleType
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'manufacturer', 'model', 'part_number', 'weight', 'weight_unit',
|
||||
'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||
'id', 'url', 'display_url', 'display', 'manufacturer', 'model', 'part_number', 'airflow',
|
||||
'weight', 'weight_unit', 'description', 'comments', 'tags', 'custom_fields',
|
||||
'created', 'last_updated',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'manufacturer', 'model', 'description')
|
||||
|
@ -64,14 +64,19 @@ class RackTypeSerializer(RackBaseSerializer):
|
||||
manufacturer = ManufacturerSerializer(
|
||||
nested=True
|
||||
)
|
||||
airflow = ChoiceField(
|
||||
choices=RackAirflowChoices,
|
||||
allow_blank=True,
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = RackType
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'manufacturer', 'name', 'slug', 'description', 'form_factor',
|
||||
'width', 'u_height', 'starting_unit', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'weight',
|
||||
'max_weight', 'weight_unit', 'mounting_depth', 'description', 'comments', 'tags', 'custom_fields',
|
||||
'created', 'last_updated',
|
||||
'max_weight', 'weight_unit', 'mounting_depth', 'airflow', 'description', 'comments', 'tags',
|
||||
'custom_fields', 'created', 'last_updated',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'manufacturer', 'name', 'slug', 'description')
|
||||
|
||||
@ -95,6 +100,11 @@ class RackSerializer(RackBaseSerializer):
|
||||
choices=RackStatusChoices,
|
||||
required=False
|
||||
)
|
||||
airflow = ChoiceField(
|
||||
choices=RackAirflowChoices,
|
||||
allow_blank=True,
|
||||
required=False
|
||||
)
|
||||
role = RackRoleSerializer(
|
||||
nested=True,
|
||||
required=False,
|
||||
@ -124,7 +134,7 @@ class RackSerializer(RackBaseSerializer):
|
||||
'id', 'url', 'display_url', 'display', 'name', 'facility_id', 'site', 'location', 'tenant', 'status',
|
||||
'role', 'serial', 'asset_tag', 'rack_type', 'form_factor', 'width', 'u_height', 'starting_unit', 'weight',
|
||||
'max_weight', 'weight_unit', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'mounting_depth',
|
||||
'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'device_count',
|
||||
'airflow', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'device_count',
|
||||
'powerfeed_count',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'description', 'device_count')
|
||||
|
@ -127,6 +127,17 @@ class RackElevationDetailRenderChoices(ChoiceSet):
|
||||
)
|
||||
|
||||
|
||||
class RackAirflowChoices(ChoiceSet):
|
||||
|
||||
AIRFLOW_FRONT_TO_REAR = 'front-to-rear'
|
||||
AIRFLOW_REAR_TO_FRONT = 'rear-to-front'
|
||||
|
||||
CHOICES = (
|
||||
(AIRFLOW_FRONT_TO_REAR, _('Front to rear')),
|
||||
(AIRFLOW_REAR_TO_FRONT, _('Rear to front')),
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# DeviceTypes
|
||||
#
|
||||
@ -224,6 +235,25 @@ class ModuleStatusChoices(ChoiceSet):
|
||||
]
|
||||
|
||||
|
||||
class ModuleAirflowChoices(ChoiceSet):
|
||||
|
||||
AIRFLOW_FRONT_TO_REAR = 'front-to-rear'
|
||||
AIRFLOW_REAR_TO_FRONT = 'rear-to-front'
|
||||
AIRFLOW_LEFT_TO_RIGHT = 'left-to-right'
|
||||
AIRFLOW_RIGHT_TO_LEFT = 'right-to-left'
|
||||
AIRFLOW_SIDE_TO_REAR = 'side-to-rear'
|
||||
AIRFLOW_PASSIVE = 'passive'
|
||||
|
||||
CHOICES = (
|
||||
(AIRFLOW_FRONT_TO_REAR, _('Front to rear')),
|
||||
(AIRFLOW_REAR_TO_FRONT, _('Rear to front')),
|
||||
(AIRFLOW_LEFT_TO_RIGHT, _('Left to right')),
|
||||
(AIRFLOW_RIGHT_TO_LEFT, _('Right to left')),
|
||||
(AIRFLOW_SIDE_TO_REAR, _('Side to rear')),
|
||||
(AIRFLOW_PASSIVE, _('Passive')),
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# ConsolePorts
|
||||
#
|
||||
|
@ -312,7 +312,7 @@ class RackTypeFilterSet(NetBoxModelFilterSet):
|
||||
model = RackType
|
||||
fields = (
|
||||
'id', 'name', 'slug', 'u_height', 'starting_unit', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit',
|
||||
'mounting_depth', 'weight', 'max_weight', 'weight_unit', 'description',
|
||||
'mounting_depth', 'airflow', 'weight', 'max_weight', 'weight_unit', 'description',
|
||||
)
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
@ -413,7 +413,8 @@ class RackFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSe
|
||||
model = Rack
|
||||
fields = (
|
||||
'id', 'name', 'facility_id', 'asset_tag', 'u_height', 'starting_unit', 'desc_units', 'outer_width',
|
||||
'outer_depth', 'outer_unit', 'mounting_depth', 'weight', 'max_weight', 'weight_unit', 'description',
|
||||
'outer_depth', 'outer_unit', 'mounting_depth', 'airflow', 'weight', 'max_weight', 'weight_unit',
|
||||
'description',
|
||||
)
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
@ -698,7 +699,7 @@ class ModuleTypeFilterSet(NetBoxModelFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = ModuleType
|
||||
fields = ('id', 'model', 'part_number', 'weight', 'weight_unit', 'description')
|
||||
fields = ('id', 'model', 'part_number', 'airflow', 'weight', 'weight_unit', 'description')
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
|
@ -268,6 +268,11 @@ class RackTypeBulkEditForm(NetBoxModelBulkEditForm):
|
||||
required=False,
|
||||
min_value=1
|
||||
)
|
||||
airflow = forms.ChoiceField(
|
||||
label=_('Airflow'),
|
||||
choices=add_blank_choice(RackAirflowChoices),
|
||||
required=False
|
||||
)
|
||||
weight = forms.DecimalField(
|
||||
label=_('Weight'),
|
||||
min_value=0,
|
||||
@ -293,10 +298,8 @@ class RackTypeBulkEditForm(NetBoxModelBulkEditForm):
|
||||
|
||||
model = RackType
|
||||
fieldsets = (
|
||||
FieldSet('manufacturer', 'description', 'form_factor', name=_('Rack Type')),
|
||||
FieldSet('manufacturer', 'description', 'form_factor', 'width', 'u_height', 'airflow', name=_('Rack Type')),
|
||||
FieldSet(
|
||||
'width',
|
||||
'u_height',
|
||||
InlineFields('outer_width', 'outer_depth', 'outer_unit', label=_('Outer Dimensions')),
|
||||
InlineFields('weight', 'max_weight', 'weight_unit', label=_('Weight')),
|
||||
'mounting_depth',
|
||||
@ -409,6 +412,11 @@ class RackBulkEditForm(NetBoxModelBulkEditForm):
|
||||
required=False,
|
||||
min_value=1
|
||||
)
|
||||
airflow = forms.ChoiceField(
|
||||
label=_('Airflow'),
|
||||
choices=add_blank_choice(RackAirflowChoices),
|
||||
required=False
|
||||
)
|
||||
weight = forms.DecimalField(
|
||||
label=_('Weight'),
|
||||
min_value=0,
|
||||
@ -437,7 +445,7 @@ class RackBulkEditForm(NetBoxModelBulkEditForm):
|
||||
FieldSet('status', 'role', 'tenant', 'serial', 'asset_tag', 'description', name=_('Rack')),
|
||||
FieldSet('region', 'site_group', 'site', 'location', name=_('Location')),
|
||||
FieldSet(
|
||||
'form_factor', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit',
|
||||
'form_factor', 'width', 'u_height', 'desc_units', 'airflow', 'outer_width', 'outer_depth', 'outer_unit',
|
||||
'mounting_depth', name=_('Hardware')
|
||||
),
|
||||
FieldSet('weight', 'max_weight', 'weight_unit', name=_('Weight')),
|
||||
@ -563,6 +571,11 @@ class ModuleTypeBulkEditForm(NetBoxModelBulkEditForm):
|
||||
label=_('Part number'),
|
||||
required=False
|
||||
)
|
||||
airflow = forms.ChoiceField(
|
||||
label=_('Airflow'),
|
||||
choices=add_blank_choice(ModuleAirflowChoices),
|
||||
required=False
|
||||
)
|
||||
weight = forms.DecimalField(
|
||||
label=_('Weight'),
|
||||
min_value=0,
|
||||
@ -584,7 +597,11 @@ class ModuleTypeBulkEditForm(NetBoxModelBulkEditForm):
|
||||
model = ModuleType
|
||||
fieldsets = (
|
||||
FieldSet('manufacturer', 'part_number', 'description', name=_('Module Type')),
|
||||
FieldSet('weight', 'weight_unit', name=_('Weight')),
|
||||
FieldSet(
|
||||
'airflow',
|
||||
InlineFields('weight', 'max_weight', 'weight_unit', label=_('Weight')),
|
||||
name=_('Chassis')
|
||||
),
|
||||
)
|
||||
nullable_fields = ('part_number', 'weight', 'weight_unit', 'description', 'comments')
|
||||
|
||||
|
@ -206,6 +206,12 @@ class RackTypeImportForm(NetBoxModelImportForm):
|
||||
required=False,
|
||||
help_text=_('Unit for outer dimensions')
|
||||
)
|
||||
airflow = CSVChoiceField(
|
||||
label=_('Airflow'),
|
||||
choices=RackAirflowChoices,
|
||||
required=False,
|
||||
help_text=_('Airflow direction')
|
||||
)
|
||||
weight_unit = CSVChoiceField(
|
||||
label=_('Weight unit'),
|
||||
choices=WeightUnitChoices,
|
||||
@ -217,8 +223,8 @@ class RackTypeImportForm(NetBoxModelImportForm):
|
||||
model = RackType
|
||||
fields = (
|
||||
'manufacturer', 'name', 'slug', 'form_factor', 'width', 'u_height', 'starting_unit', 'desc_units',
|
||||
'outer_width', 'outer_depth', 'outer_unit', 'mounting_depth', 'weight', 'max_weight', 'weight_unit',
|
||||
'description', 'comments', 'tags',
|
||||
'outer_width', 'outer_depth', 'outer_unit', 'mounting_depth', 'airflow', 'weight', 'max_weight',
|
||||
'weight_unit', 'description', 'comments', 'tags',
|
||||
)
|
||||
|
||||
def __init__(self, data=None, *args, **kwargs):
|
||||
@ -273,6 +279,12 @@ class RackImportForm(NetBoxModelImportForm):
|
||||
required=False,
|
||||
help_text=_('Unit for outer dimensions')
|
||||
)
|
||||
airflow = CSVChoiceField(
|
||||
label=_('Airflow'),
|
||||
choices=RackAirflowChoices,
|
||||
required=False,
|
||||
help_text=_('Airflow direction')
|
||||
)
|
||||
weight_unit = CSVChoiceField(
|
||||
label=_('Weight unit'),
|
||||
choices=WeightUnitChoices,
|
||||
@ -284,8 +296,8 @@ class RackImportForm(NetBoxModelImportForm):
|
||||
model = Rack
|
||||
fields = (
|
||||
'site', 'location', 'name', 'facility_id', 'tenant', 'status', 'role', 'form_factor', 'serial', 'asset_tag',
|
||||
'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'mounting_depth', 'weight',
|
||||
'max_weight', 'weight_unit', 'description', 'comments', 'tags',
|
||||
'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'mounting_depth', 'airflow',
|
||||
'weight', 'max_weight', 'weight_unit', 'description', 'comments', 'tags',
|
||||
)
|
||||
|
||||
def __init__(self, data=None, *args, **kwargs):
|
||||
@ -400,6 +412,12 @@ class ModuleTypeImportForm(NetBoxModelImportForm):
|
||||
queryset=Manufacturer.objects.all(),
|
||||
to_field_name='name'
|
||||
)
|
||||
airflow = CSVChoiceField(
|
||||
label=_('Airflow'),
|
||||
choices=ModuleAirflowChoices,
|
||||
required=False,
|
||||
help_text=_('Airflow direction')
|
||||
)
|
||||
weight = forms.DecimalField(
|
||||
label=_('Weight'),
|
||||
required=False,
|
||||
@ -414,7 +432,7 @@ class ModuleTypeImportForm(NetBoxModelImportForm):
|
||||
|
||||
class Meta:
|
||||
model = ModuleType
|
||||
fields = ['manufacturer', 'model', 'part_number', 'description', 'weight', 'weight_unit', 'comments', 'tags']
|
||||
fields = ['manufacturer', 'model', 'part_number', 'description', 'airflow', 'weight', 'weight_unit', 'comments', 'tags']
|
||||
|
||||
|
||||
class DeviceRoleImportForm(NetBoxModelImportForm):
|
||||
|
@ -267,6 +267,11 @@ class RackBaseFilterForm(NetBoxModelFilterSetForm):
|
||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
)
|
||||
airflow = forms.MultipleChoiceField(
|
||||
label=_('Airflow'),
|
||||
choices=add_blank_choice(RackAirflowChoices),
|
||||
required=False
|
||||
)
|
||||
weight = forms.DecimalField(
|
||||
label=_('Weight'),
|
||||
required=False,
|
||||
@ -288,7 +293,7 @@ class RackTypeFilterForm(RackBaseFilterForm):
|
||||
model = RackType
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('form_factor', 'width', 'u_height', name=_('Rack Type')),
|
||||
FieldSet('form_factor', 'width', 'u_height', 'airflow', name=_('Rack Type')),
|
||||
FieldSet('starting_unit', 'desc_units', name=_('Numbering')),
|
||||
FieldSet('weight', 'max_weight', 'weight_unit', name=_('Weight')),
|
||||
)
|
||||
@ -308,7 +313,7 @@ class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, RackBaseFilterFo
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', name=_('Location')),
|
||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||
FieldSet('status', 'role_id', 'serial', 'asset_tag', name=_('Rack')),
|
||||
FieldSet('form_factor', 'width', 'u_height', name=_('Rack Type')),
|
||||
FieldSet('form_factor', 'width', 'u_height', 'airflow', name=_('Rack Type')),
|
||||
FieldSet('starting_unit', 'desc_units', name=_('Numbering')),
|
||||
FieldSet('weight', 'max_weight', 'weight_unit', name=_('Weight')),
|
||||
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
||||
@ -578,7 +583,7 @@ class ModuleTypeFilterForm(NetBoxModelFilterSetForm):
|
||||
model = ModuleType
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('manufacturer_id', 'part_number', name=_('Hardware')),
|
||||
FieldSet('manufacturer_id', 'part_number', 'airflow', name=_('Hardware')),
|
||||
FieldSet(
|
||||
'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces',
|
||||
'pass_through_ports', name=_('Components')
|
||||
@ -638,6 +643,11 @@ class ModuleTypeFilterForm(NetBoxModelFilterSetForm):
|
||||
)
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
airflow = forms.MultipleChoiceField(
|
||||
label=_('Airflow'),
|
||||
choices=add_blank_choice(ModuleAirflowChoices),
|
||||
required=False
|
||||
)
|
||||
weight = forms.DecimalField(
|
||||
label=_('Weight'),
|
||||
required=False
|
||||
|
@ -211,7 +211,7 @@ class RackTypeForm(NetBoxModelForm):
|
||||
slug = SlugField()
|
||||
|
||||
fieldsets = (
|
||||
FieldSet('manufacturer', 'name', 'slug', 'description', 'form_factor', 'tags', name=_('Rack Type')),
|
||||
FieldSet('manufacturer', 'name', 'slug', 'description', 'form_factor', 'airflow', 'tags', name=_('Rack Type')),
|
||||
FieldSet(
|
||||
'width', 'u_height',
|
||||
InlineFields('outer_width', 'outer_depth', 'outer_unit', label=_('Outer Dimensions')),
|
||||
@ -226,7 +226,7 @@ class RackTypeForm(NetBoxModelForm):
|
||||
fields = [
|
||||
'manufacturer', 'name', 'slug', 'form_factor', 'width', 'u_height', 'starting_unit', 'desc_units',
|
||||
'outer_width', 'outer_depth', 'outer_unit', 'mounting_depth', 'weight', 'max_weight', 'weight_unit',
|
||||
'description', 'comments', 'tags',
|
||||
'airflow', 'description', 'comments', 'tags',
|
||||
]
|
||||
|
||||
|
||||
@ -268,8 +268,8 @@ class RackForm(TenancyForm, NetBoxModelForm):
|
||||
fields = [
|
||||
'site', 'location', 'name', 'facility_id', 'tenant_group', 'tenant', 'status', 'role', 'serial',
|
||||
'asset_tag', 'rack_type', 'form_factor', 'width', 'u_height', 'starting_unit', 'desc_units', 'outer_width',
|
||||
'outer_depth', 'outer_unit', 'mounting_depth', 'weight', 'max_weight', 'weight_unit', 'description',
|
||||
'comments', 'tags',
|
||||
'outer_depth', 'outer_unit', 'mounting_depth', 'airflow', 'weight', 'max_weight', 'weight_unit',
|
||||
'description', 'comments', 'tags',
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@ -290,7 +290,7 @@ class RackForm(TenancyForm, NetBoxModelForm):
|
||||
self.fieldsets = (
|
||||
*self.fieldsets,
|
||||
FieldSet(
|
||||
'form_factor', 'width', 'starting_unit', 'u_height',
|
||||
'form_factor', 'width', 'starting_unit', 'u_height', 'airflow',
|
||||
InlineFields('outer_width', 'outer_depth', 'outer_unit', label=_('Outer Dimensions')),
|
||||
InlineFields('weight', 'max_weight', 'weight_unit', label=_('Weight')),
|
||||
'mounting_depth', 'desc_units', name=_('Dimensions')
|
||||
@ -398,13 +398,14 @@ class ModuleTypeForm(NetBoxModelForm):
|
||||
|
||||
fieldsets = (
|
||||
FieldSet('manufacturer', 'model', 'part_number', 'description', 'tags', name=_('Module Type')),
|
||||
FieldSet('weight', 'weight_unit', name=_('Weight'))
|
||||
FieldSet('airflow', 'weight', 'weight_unit', name=_('Chassis'))
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ModuleType
|
||||
fields = [
|
||||
'manufacturer', 'model', 'part_number', 'weight', 'weight_unit', 'description', 'comments', 'tags',
|
||||
'manufacturer', 'model', 'part_number', 'airflow', 'weight', 'weight_unit', 'description',
|
||||
'comments', 'tags',
|
||||
]
|
||||
|
||||
|
||||
|
@ -0,0 +1,28 @@
|
||||
# Generated by Django 5.0.7 on 2024-07-25 07:00
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0188_racktype'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='moduletype',
|
||||
name='airflow',
|
||||
field=models.CharField(blank=True, max_length=50),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rack',
|
||||
name='airflow',
|
||||
field=models.CharField(blank=True, max_length=50),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='racktype',
|
||||
name='airflow',
|
||||
field=models.CharField(blank=True, max_length=50),
|
||||
),
|
||||
]
|
@ -388,8 +388,14 @@ class ModuleType(ImageAttachmentsMixin, PrimaryModel, WeightMixin):
|
||||
blank=True,
|
||||
help_text=_('Discrete part number (optional)')
|
||||
)
|
||||
airflow = models.CharField(
|
||||
verbose_name=_('airflow'),
|
||||
max_length=50,
|
||||
choices=ModuleAirflowChoices,
|
||||
blank=True
|
||||
)
|
||||
|
||||
clone_fields = ('manufacturer', 'weight', 'weight_unit',)
|
||||
clone_fields = ('manufacturer', 'weight', 'weight_unit', 'airflow')
|
||||
prerequisite_models = (
|
||||
'dcim.Manufacturer',
|
||||
)
|
||||
|
@ -53,6 +53,12 @@ class RackBase(WeightMixin, PrimaryModel):
|
||||
verbose_name=_('width'),
|
||||
help_text=_('Rail-to-rail width')
|
||||
)
|
||||
airflow = models.CharField(
|
||||
verbose_name=_('airflow'),
|
||||
max_length=50,
|
||||
choices=RackAirflowChoices,
|
||||
blank=True
|
||||
)
|
||||
|
||||
# Numbering
|
||||
u_height = models.PositiveSmallIntegerField(
|
||||
@ -232,10 +238,10 @@ class Rack(ContactsMixin, ImageAttachmentsMixin, RackBase):
|
||||
Each Rack is assigned to a Site and (optionally) a Location.
|
||||
"""
|
||||
# Fields which cannot be set locally if a RackType is assigned
|
||||
RACKTYPE_FIELDS = [
|
||||
'form_factor', 'width', 'u_height', 'starting_unit', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit',
|
||||
'mounting_depth', 'weight', 'weight_unit', 'max_weight'
|
||||
]
|
||||
RACKTYPE_FIELDS = (
|
||||
'form_factor', 'width', 'airflow', 'u_height', 'starting_unit', 'desc_units', 'outer_width', 'outer_depth',
|
||||
'outer_unit', 'mounting_depth', 'weight', 'weight_unit', 'max_weight',
|
||||
)
|
||||
|
||||
rack_type = models.ForeignKey(
|
||||
to='dcim.RackType',
|
||||
@ -316,8 +322,8 @@ class Rack(ContactsMixin, ImageAttachmentsMixin, RackBase):
|
||||
)
|
||||
|
||||
clone_fields = (
|
||||
'site', 'location', 'tenant', 'status', 'role', 'form_factor', 'width', 'u_height', 'desc_units', 'outer_width',
|
||||
'outer_depth', 'outer_unit', 'mounting_depth', 'weight', 'max_weight', 'weight_unit',
|
||||
'site', 'location', 'tenant', 'status', 'role', 'form_factor', 'width', 'airflow', 'u_height', 'desc_units',
|
||||
'outer_width', 'outer_depth', 'outer_unit', 'mounting_depth', 'weight', 'max_weight', 'weight_unit',
|
||||
)
|
||||
prerequisite_models = (
|
||||
'dcim.Site',
|
||||
|
@ -40,7 +40,7 @@ class ModuleTypeTable(NetBoxTable):
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = ModuleType
|
||||
fields = (
|
||||
'pk', 'id', 'model', 'manufacturer', 'part_number', 'weight', 'description', 'comments', 'tags',
|
||||
'pk', 'id', 'model', 'manufacturer', 'part_number', 'airflow', 'weight', 'description', 'comments', 'tags',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'model', 'manufacturer', 'part_number',
|
||||
|
@ -92,8 +92,8 @@ class RackTypeTable(NetBoxTable):
|
||||
model = RackType
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'manufacturer', 'form_factor', 'u_height', 'starting_unit', 'width', 'outer_width',
|
||||
'outer_depth', 'mounting_depth', 'weight', 'max_weight', 'description', 'comments', 'tags', 'created',
|
||||
'last_updated',
|
||||
'outer_depth', 'mounting_depth', 'airflow', 'weight', 'max_weight', 'description', 'comments', 'tags',
|
||||
'created', 'last_updated',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'name', 'manufacturer', 'type', 'u_height', 'description',
|
||||
@ -171,7 +171,7 @@ class RackTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'site', 'location', 'status', 'facility_id', 'tenant', 'tenant_group', 'role', 'serial',
|
||||
'asset_tag', 'form_factor', 'u_height', 'starting_unit', 'width', 'outer_width', 'outer_depth',
|
||||
'mounting_depth', 'weight', 'max_weight', 'comments', 'device_count', 'get_utilization',
|
||||
'mounting_depth', 'airflow', 'weight', 'max_weight', 'comments', 'device_count', 'get_utilization',
|
||||
'get_power_utilization', 'description', 'contacts', 'tags', 'created', 'last_updated',
|
||||
)
|
||||
default_columns = (
|
||||
|
@ -437,6 +437,11 @@ ADMIN_MENU = Menu(
|
||||
link_text=_('System'),
|
||||
auth_required=True
|
||||
),
|
||||
MenuItem(
|
||||
link='core:plugin_list',
|
||||
link_text=_('Plugins'),
|
||||
auth_required=True
|
||||
),
|
||||
MenuItem(
|
||||
link='core:configrevision_list',
|
||||
link_text=_('Configuration History'),
|
||||
|
@ -769,6 +769,8 @@ STRAWBERRY_DJANGO = {
|
||||
# Plugins
|
||||
#
|
||||
|
||||
PLUGIN_CATALOG_URL = 'https://api.netbox.oss.netboxlabs.com/v1/plugins'
|
||||
|
||||
# Register any configured plugins
|
||||
for plugin_name in PLUGINS:
|
||||
try:
|
||||
|
29
netbox/templates/core/inc/plugin_installation.html
Normal file
29
netbox/templates/core/inc/plugin_installation.html
Normal file
@ -0,0 +1,29 @@
|
||||
<p>You can install this plugin from the command line with PyPI.</p>
|
||||
<p>The following commands may be helpful; always refer to <a href="{{ plugin.homepage_url }}" target="_blank">the plugin's own documentation <i class="mdi mdi-launch"></i></a> and the <a href="https://netboxlabs.com/docs/netbox/en/stable/plugins/installation/" target="_blank">Installing a Plugin unit <i class="mdi mdi-launch"></i></a> of the NetBox documentation.</p>
|
||||
<p>1. Enter the NetBox virtual environment and install the plugin package:</p>
|
||||
|
||||
<pre class="block">
|
||||
source /opt/netbox/venv/bin/activate
|
||||
pip install {{ plugin.slug }}
|
||||
</pre>
|
||||
|
||||
<p>2. In /opt/netbox/netbox/netbox/configuration.py, add the plugin to the PLUGINS list:</p>
|
||||
|
||||
<pre class="block">
|
||||
PLUGINS=[
|
||||
"{{ plugin.config_name }}",
|
||||
]
|
||||
</pre>
|
||||
|
||||
<p>3. Still from the NetBox virtual environment, run database migrations and collect static files:</p>
|
||||
|
||||
<pre class="block">
|
||||
python3 /opt/netbox/netbox/netbox/manage.py migrate
|
||||
python3 /opt/netbox/netbox/netbox/manage.py collectstatic
|
||||
</pre>
|
||||
|
||||
<p>4. Restart the NetBox services to complete the plugin installation:</p>
|
||||
|
||||
<pre class="block">
|
||||
sudo systemctl restart netbox netbox-rq
|
||||
</pre>
|
113
netbox/templates/core/plugin.html
Normal file
113
netbox/templates/core/plugin.html
Normal file
@ -0,0 +1,113 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load helpers %}
|
||||
{% load form_helpers %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}{{ plugin.title_short }}{% endblock %}
|
||||
|
||||
{% block object_identifier %}
|
||||
{% endblock object_identifier %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
<li class="breadcrumb-item"><a href="{% url 'core:plugin_list' %}">{% trans "Plugins" %}</a></li>
|
||||
{% endblock breadcrumbs %}
|
||||
|
||||
{% block subtitle %}
|
||||
<span class="text-secondary fs-5">
|
||||
{% checkmark plugin.is_installed %}
|
||||
{% if plugin.is_installed %}
|
||||
v{{ plugin.installed_version }} {% trans "installed" %}
|
||||
{% else %}
|
||||
{% trans "Not installed" %}
|
||||
{% endif %}
|
||||
</span>
|
||||
{% endblock %}
|
||||
|
||||
{% block controls %}{% endblock %}
|
||||
|
||||
{% block tabs %}
|
||||
<ul class="nav nav-tabs" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link active" id="overview-tab" data-bs-toggle="tab" data-bs-target="#overview" type="button" role="tab" aria-controls="edit-form" aria-selected="true">
|
||||
{% trans "Overview" %}
|
||||
</a>
|
||||
</li>
|
||||
{% if True or not plugin.is_local and 'commercial' not in settings.RELEASE.features %}
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="install-tab" data-bs-toggle="tab" data-bs-target="#install" type="button" role="tab" aria-controls="object-list" aria-selected="false">
|
||||
{% trans "Install" %}
|
||||
</button>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
{% endblock tabs %}
|
||||
|
||||
{% block content %}
|
||||
<div class="tab-pane show active" id="overview" role="tabpanel" aria-labelledby="overview-tab">
|
||||
<div class="row">
|
||||
<div class="col col-6">
|
||||
<div class="card">
|
||||
<h5 class="card-header">{% trans "Plugin Details" %}</h5>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Name" %}</th>
|
||||
<td>{{ plugin.title_short }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Summary" %}</th>
|
||||
<td>{{ plugin.tag_line|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Author" %}</th>
|
||||
<td>{{ plugin.author.name|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "URL" %}</th>
|
||||
<td>
|
||||
{% if plugin.homepage_url %}
|
||||
<a href="{{ plugin.homepage_url }}">{{ plugin.homepage_url }}</a>
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "License" %}</th>
|
||||
<td>{{ plugin.license_type|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ plugin.description_short|markdown }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Certified" %}</th>
|
||||
<td>{% checkmark plugin.is_certified %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Local" %}</th>
|
||||
<td>{% checkmark plugin.is_local %}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col col-6">
|
||||
<div class="card">
|
||||
<h5 class="card-header">{% trans "Version History" %}</h5>
|
||||
<div class="htmx-container table-responsive" id="object_list">
|
||||
{% include 'htmx/table.html' %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if True or not plugin.is_local and 'commercial' not in settings.RELEASE.features %}
|
||||
<div class="tab-pane" id="install" role="tabpanel" aria-labelledby="install-tab">
|
||||
<div class="card">
|
||||
<h5 class="card-header">{% trans "Local Installation Instructions" %}</h5>
|
||||
<div class="card-body">
|
||||
{% include 'core/inc/plugin_installation.html' %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock content %}
|
16
netbox/templates/core/plugin_list.html
Normal file
16
netbox/templates/core/plugin_list.html
Normal file
@ -0,0 +1,16 @@
|
||||
{% extends 'generic/object_list.html' %}
|
||||
{% load buttons %}
|
||||
{% load helpers %}
|
||||
{% load i18n %}
|
||||
{% load render_table from django_tables2 %}
|
||||
|
||||
{% block title %}{% trans "Plugins" %}{% endblock %}
|
||||
|
||||
{% block tabs %}
|
||||
<ul class="nav nav-tabs px-3">
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link active" role="tab">{% trans "Plugins" %}</a>
|
||||
</li>
|
||||
</ul>
|
||||
{% endblock tabs %}
|
||||
|
@ -78,16 +78,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Plugins #}
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-12">
|
||||
<div class="card">
|
||||
<h5 class="card-header">{% trans "Plugins" %}</h5>
|
||||
{% render_table plugins_table %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Configuration #}
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-12">
|
||||
|
@ -26,6 +26,12 @@
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.description|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Airflow" %}</th>
|
||||
<td>
|
||||
{{ object.get_airflow_display|placeholder }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Weight" %}</th>
|
||||
<td>
|
||||
|
@ -61,6 +61,10 @@
|
||||
<th scope="row">{% trans "Asset Tag" %}</th>
|
||||
<td class="font-monospace">{{ object.asset_tag|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Airflow" %}</th>
|
||||
<td>{{ object.get_airflow_display|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Space Utilization" %}</th>
|
||||
<td>{% utilization_graph object.get_utilization %}</td>
|
||||
|
@ -24,6 +24,10 @@
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.description|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Airflow" %}</th>
|
||||
<td>{{ object.get_airflow_display|placeholder }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% include 'dcim/inc/panels/racktype_dimensions.html' %}
|
||||
|
@ -1,11 +1,18 @@
|
||||
{% load i18n %}
|
||||
{% load navigation %}
|
||||
|
||||
{% if 'help-center' in settings.RELEASE.features %}
|
||||
{# Help center control #}
|
||||
<a href="#" class="nav-link px-1" aria-label="{% trans "Help center" %}">
|
||||
<i class="mdi mdi-forum-outline"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% if request.user.is_authenticated %}
|
||||
{# Notifications #}
|
||||
{% with notifications=request.user.notifications.unread.exists %}
|
||||
<div class="nav-item dropdown">
|
||||
<a href="#" class="nav-link" data-bs-toggle="dropdown" hx-get="{% url 'extras:notifications' %}" hx-target="next .notifications" aria-label="Notifications">
|
||||
<div class="dropdown">
|
||||
<a href="#" class="nav-link px-1" data-bs-toggle="dropdown" hx-get="{% url 'extras:notifications' %}" hx-target="next .notifications" aria-label="{% trans "Notifications" %}">
|
||||
{% include 'inc/notification_bell.html' %}
|
||||
</a>
|
||||
<div class="dropdown-menu dropdown-menu-end dropdown-menu-arrow notifications"></div>
|
||||
|
@ -1,4 +1,4 @@
|
||||
from django.urls import include, path
|
||||
from django.urls import include, path, re_path
|
||||
|
||||
from utilities.urls import get_model_urls
|
||||
from . import views
|
||||
@ -49,12 +49,16 @@ urlpatterns = [
|
||||
path('virtual-machines/interfaces/add/', views.VirtualMachineBulkAddInterfaceView.as_view(), name='virtualmachine_bulk_add_vminterface'),
|
||||
|
||||
# Virtual disks
|
||||
path('disks/', views.VirtualDiskListView.as_view(), name='virtualdisk_list'),
|
||||
path('disks/add/', views.VirtualDiskCreateView.as_view(), name='virtualdisk_add'),
|
||||
path('disks/import/', views.VirtualDiskBulkImportView.as_view(), name='virtualdisk_import'),
|
||||
path('disks/edit/', views.VirtualDiskBulkEditView.as_view(), name='virtualdisk_bulk_edit'),
|
||||
path('disks/rename/', views.VirtualDiskBulkRenameView.as_view(), name='virtualdisk_bulk_rename'),
|
||||
path('disks/delete/', views.VirtualDiskBulkDeleteView.as_view(), name='virtualdisk_bulk_delete'),
|
||||
path('disks/<int:pk>/', include(get_model_urls('virtualization', 'virtualdisk'))),
|
||||
path('virtual-disks/', views.VirtualDiskListView.as_view(), name='virtualdisk_list'),
|
||||
path('virtual-disks/add/', views.VirtualDiskCreateView.as_view(), name='virtualdisk_add'),
|
||||
path('virtual-disks/import/', views.VirtualDiskBulkImportView.as_view(), name='virtualdisk_import'),
|
||||
path('virtual-disks/edit/', views.VirtualDiskBulkEditView.as_view(), name='virtualdisk_bulk_edit'),
|
||||
path('virtual-disks/rename/', views.VirtualDiskBulkRenameView.as_view(), name='virtualdisk_bulk_rename'),
|
||||
path('virtual-disks/delete/', views.VirtualDiskBulkDeleteView.as_view(), name='virtualdisk_bulk_delete'),
|
||||
path('virtual-disks/<int:pk>/', include(get_model_urls('virtualization', 'virtualdisk'))),
|
||||
path('virtual-machines/disks/add/', views.VirtualMachineBulkAddVirtualDiskView.as_view(), name='virtualmachine_bulk_add_virtualdisk'),
|
||||
|
||||
# TODO: Remove in v4.2
|
||||
# Redirect old (pre-v4.1) URLs for VirtualDisk views
|
||||
re_path('disks/(?P<path>[a-z0-9/-]*)', views.VirtualDiskRedirectView.as_view()),
|
||||
]
|
||||
|
@ -7,6 +7,7 @@ from django.http import HttpResponse
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext as _
|
||||
from django.views.generic.base import RedirectView
|
||||
from jinja2.exceptions import TemplateError
|
||||
|
||||
from dcim.filtersets import DeviceFilterSet
|
||||
@ -630,6 +631,15 @@ class VirtualDiskBulkDeleteView(generic.BulkDeleteView):
|
||||
table = tables.VirtualDiskTable
|
||||
|
||||
|
||||
# TODO: Remove in v4.2
|
||||
class VirtualDiskRedirectView(RedirectView):
|
||||
"""
|
||||
Redirect old (pre-v4.1) URLs for VirtualDisk views.
|
||||
"""
|
||||
def get_redirect_url(self, path):
|
||||
return f"{reverse('virtualization:virtualdisk_list')}{path}"
|
||||
|
||||
|
||||
#
|
||||
# Bulk Device component creation
|
||||
#
|
||||
|
Loading…
Reference in New Issue
Block a user