mirror of
https://github.com/netbox-community/netbox.git
synced 2026-02-15 13:27:47 +01:00
Compare commits
16 Commits
feature
...
21412-defe
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5db8c04c64 | ||
|
|
0bb22dee0c | ||
|
|
6c383f293c | ||
|
|
5bf516c63d | ||
|
|
7df062d590 | ||
|
|
4b22be03a0 | ||
|
|
24769ce127 | ||
|
|
164e9db98d | ||
|
|
23f1c86e9c | ||
|
|
02ffdd9d5d | ||
|
|
5013297326 | ||
|
|
584e0a9b8c | ||
|
|
3ac9d0b8bf | ||
|
|
b387ea5f58 | ||
|
|
ba9f6bf359 | ||
|
|
ee6cbdcefe |
@@ -200,6 +200,48 @@ REDIS = {
|
|||||||
!!! note
|
!!! note
|
||||||
It is permissible to use Sentinel for only one database and not the other.
|
It is permissible to use Sentinel for only one database and not the other.
|
||||||
|
|
||||||
|
### SSL Configuration
|
||||||
|
|
||||||
|
If you need to configure SSL/TLS for Redis beyond the basic `SSL`, `CA_CERT_PATH`, and `INSECURE_SKIP_TLS_VERIFY` options (for example, client certificates, a specific TLS version, or custom ciphers), you can pass additional parameters via the `KWARGS` key in either the `tasks` or `caching` subsection.
|
||||||
|
|
||||||
|
NetBox already maps `CA_CERT_PATH` to `ssl_ca_certs` and (for caching) `INSECURE_SKIP_TLS_VERIFY` to `ssl_cert_reqs`; only add `KWARGS` when you need to override or extend those settings (for example, to supply client certificates or restrict TLS version or ciphers).
|
||||||
|
|
||||||
|
* `KWARGS` - Optional dictionary of additional SSL/TLS (or other) parameters passed to the Redis client. These are passed directly to the underlying Redis client: for `tasks` to [redis-py](https://redis-py.readthedocs.io/en/stable/connections.html), and for `caching` to the [django-redis](https://github.com/jazzband/django-redis#configure-as-cache-backend) connection pool.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```python
|
||||||
|
REDIS = {
|
||||||
|
'tasks': {
|
||||||
|
'HOST': 'redis.example.com',
|
||||||
|
'PORT': 1234,
|
||||||
|
'SSL': True,
|
||||||
|
'CA_CERT_PATH': '/etc/ssl/certs/ca.crt',
|
||||||
|
'KWARGS': {
|
||||||
|
'ssl_certfile': '/path/to/client-cert.pem',
|
||||||
|
'ssl_keyfile': '/path/to/client-key.pem',
|
||||||
|
'ssl_min_version': ssl.TLSVersion.TLSv1_2,
|
||||||
|
'ssl_ciphers': 'HIGH:!aNULL',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'caching': {
|
||||||
|
'HOST': 'redis.example.com',
|
||||||
|
'PORT': 1234,
|
||||||
|
'SSL': True,
|
||||||
|
'CA_CERT_PATH': '/etc/ssl/certs/ca.crt',
|
||||||
|
'KWARGS': {
|
||||||
|
'ssl_certfile': '/path/to/client-cert.pem',
|
||||||
|
'ssl_keyfile': '/path/to/client-key.pem',
|
||||||
|
'ssl_min_version': ssl.TLSVersion.TLSv1_2,
|
||||||
|
'ssl_ciphers': 'HIGH:!aNULL',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! note
|
||||||
|
If you use `ssl.TLSVersion` in your configuration (e.g. `ssl_min_version`), add `import ssl` at the top of your configuration file.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## SECRET_KEY
|
## SECRET_KEY
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import json
|
import json
|
||||||
import platform
|
import platform
|
||||||
|
|
||||||
|
from copy import deepcopy
|
||||||
from django import __version__ as django_version
|
from django import __version__ as django_version
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
@@ -310,6 +311,22 @@ class ConfigRevisionListView(generic.ObjectListView):
|
|||||||
class ConfigRevisionView(generic.ObjectView):
|
class ConfigRevisionView(generic.ObjectView):
|
||||||
queryset = ConfigRevision.objects.all()
|
queryset = ConfigRevision.objects.all()
|
||||||
|
|
||||||
|
def get_extra_context(self, request, instance):
|
||||||
|
"""
|
||||||
|
Retrieve additional context for a given request and instance.
|
||||||
|
"""
|
||||||
|
# Copy the revision data to avoid modifying the original
|
||||||
|
config = deepcopy(instance.data or {})
|
||||||
|
|
||||||
|
# Serialize any JSON-based classes
|
||||||
|
for attr in ['CUSTOM_VALIDATORS', 'DEFAULT_USER_PREFERENCES', 'PROTECTION_RULES']:
|
||||||
|
if attr in config:
|
||||||
|
config[attr] = json.dumps(config[attr], cls=ConfigJSONEncoder, indent=4)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'config': config,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(ConfigRevision, 'add', detail=False)
|
@register_model_view(ConfigRevision, 'add', detail=False)
|
||||||
class ConfigRevisionEditView(generic.ObjectEditView):
|
class ConfigRevisionEditView(generic.ObjectEditView):
|
||||||
@@ -617,8 +634,8 @@ class SystemView(UserPassesTestMixin, View):
|
|||||||
response['Content-Disposition'] = 'attachment; filename="netbox.json"'
|
response['Content-Disposition'] = 'attachment; filename="netbox.json"'
|
||||||
return response
|
return response
|
||||||
|
|
||||||
# Serialize any CustomValidator classes
|
# Serialize any JSON-based classes
|
||||||
for attr in ['CUSTOM_VALIDATORS', 'PROTECTION_RULES']:
|
for attr in ['CUSTOM_VALIDATORS', 'DEFAULT_USER_PREFERENCES', 'PROTECTION_RULES']:
|
||||||
if hasattr(config, attr) and getattr(config, attr, None):
|
if hasattr(config, attr) and getattr(config, attr, None):
|
||||||
setattr(config, attr, json.dumps(getattr(config, attr), cls=ConfigJSONEncoder, indent=4))
|
setattr(config, attr, json.dumps(getattr(config, attr), cls=ConfigJSONEncoder, indent=4))
|
||||||
|
|
||||||
|
|||||||
@@ -373,7 +373,7 @@ class Rack(ContactsMixin, ImageAttachmentsMixin, TrackingModelMixin, RackBase):
|
|||||||
super().clean()
|
super().clean()
|
||||||
|
|
||||||
# Validate location/site assignment
|
# Validate location/site assignment
|
||||||
if self.site and self.location and self.location.site != self.site:
|
if self.site_id and self.location_id and self.location.site_id != self.site_id:
|
||||||
raise ValidationError(_("Assigned location must belong to parent site ({site}).").format(site=self.site))
|
raise ValidationError(_("Assigned location must belong to parent site ({site}).").format(site=self.site))
|
||||||
|
|
||||||
# Validate outer dimensions and unit
|
# Validate outer dimensions and unit
|
||||||
|
|||||||
@@ -584,6 +584,15 @@ class BaseInterfaceTable(NetBoxTable):
|
|||||||
orderable=False,
|
orderable=False,
|
||||||
verbose_name=_('IP Addresses')
|
verbose_name=_('IP Addresses')
|
||||||
)
|
)
|
||||||
|
primary_mac_address = tables.Column(
|
||||||
|
verbose_name=_('Primary MAC'),
|
||||||
|
linkify=True
|
||||||
|
)
|
||||||
|
mac_addresses = columns.ManyToManyColumn(
|
||||||
|
orderable=False,
|
||||||
|
linkify_item=True,
|
||||||
|
verbose_name=_('MAC Addresses')
|
||||||
|
)
|
||||||
fhrp_groups = tables.TemplateColumn(
|
fhrp_groups = tables.TemplateColumn(
|
||||||
accessor=Accessor('fhrp_group_assignments'),
|
accessor=Accessor('fhrp_group_assignments'),
|
||||||
template_code=INTERFACE_FHRPGROUPS,
|
template_code=INTERFACE_FHRPGROUPS,
|
||||||
@@ -615,10 +624,6 @@ class BaseInterfaceTable(NetBoxTable):
|
|||||||
verbose_name=_('Q-in-Q SVLAN'),
|
verbose_name=_('Q-in-Q SVLAN'),
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
primary_mac_address = tables.Column(
|
|
||||||
verbose_name=_('MAC Address'),
|
|
||||||
linkify=True
|
|
||||||
)
|
|
||||||
|
|
||||||
def value_ip_addresses(self, value):
|
def value_ip_addresses(self, value):
|
||||||
return ",".join([str(obj.address) for obj in value.all()])
|
return ",".join([str(obj.address) for obj in value.all()])
|
||||||
@@ -681,11 +686,12 @@ class InterfaceTable(BaseInterfaceTable, ModularDeviceComponentTable, PathEndpoi
|
|||||||
model = models.Interface
|
model = models.Interface
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'enabled', 'type', 'mgmt_only', 'mtu',
|
'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'enabled', 'type', 'mgmt_only', 'mtu',
|
||||||
'speed', 'speed_formatted', 'duplex', 'mode', 'primary_mac_address', 'wwn', 'poe_mode', 'poe_type',
|
'speed', 'speed_formatted', 'duplex', 'mode', 'mac_addresses', 'primary_mac_address', 'wwn',
|
||||||
'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'description',
|
'poe_mode', 'poe_type', 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power',
|
||||||
'mark_connected', 'cable', 'cable_color', 'wireless_link', 'wireless_lans', 'link_peer', 'connection',
|
'description', 'mark_connected', 'cable', 'cable_color', 'wireless_link', 'wireless_lans', 'link_peer',
|
||||||
'tags', 'vdcs', 'vrf', 'l2vpn', 'tunnel', 'ip_addresses', 'fhrp_groups', 'untagged_vlan', 'tagged_vlans',
|
'connection', 'tags', 'vdcs', 'vrf', 'l2vpn', 'tunnel', 'ip_addresses', 'fhrp_groups',
|
||||||
'qinq_svlan', 'inventory_items', 'created', 'last_updated', 'vlan_translation_policy'
|
'untagged_vlan', 'tagged_vlans', 'qinq_svlan', 'inventory_items', 'created', 'last_updated',
|
||||||
|
'vlan_translation_policy',
|
||||||
)
|
)
|
||||||
default_columns = ('pk', 'name', 'device', 'label', 'enabled', 'type', 'description')
|
default_columns = ('pk', 'name', 'device', 'label', 'enabled', 'type', 'description')
|
||||||
|
|
||||||
@@ -746,10 +752,11 @@ class DeviceInterfaceTable(InterfaceTable):
|
|||||||
model = models.Interface
|
model = models.Interface
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'name', 'module_bay', 'module', 'label', 'enabled', 'type', 'parent', 'bridge', 'lag',
|
'pk', 'id', 'name', 'module_bay', 'module', 'label', 'enabled', 'type', 'parent', 'bridge', 'lag',
|
||||||
'mgmt_only', 'mtu', 'mode', 'primary_mac_address', 'wwn', 'rf_role', 'rf_channel', 'rf_channel_frequency',
|
'mgmt_only', 'mtu', 'mode', 'mac_addresses', 'primary_mac_address', 'wwn', 'rf_role', 'rf_channel',
|
||||||
'rf_channel_width', 'tx_power', 'description', 'mark_connected', 'cable', 'cable_color', 'wireless_link',
|
'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'description', 'mark_connected', 'cable',
|
||||||
'wireless_lans', 'link_peer', 'connection', 'tags', 'vdcs', 'vrf', 'l2vpn', 'tunnel', 'ip_addresses',
|
'cable_color', 'wireless_link', 'wireless_lans', 'link_peer', 'connection', 'tags', 'vdcs', 'vrf',
|
||||||
'fhrp_groups', 'untagged_vlan', 'tagged_vlans', 'qinq_svlan', 'actions',
|
'l2vpn', 'tunnel', 'ip_addresses', 'fhrp_groups', 'untagged_vlan', 'tagged_vlans', 'qinq_svlan',
|
||||||
|
'actions',
|
||||||
)
|
)
|
||||||
default_columns = (
|
default_columns = (
|
||||||
'pk', 'name', 'label', 'enabled', 'type', 'parent', 'lag', 'mtu', 'mode', 'description', 'ip_addresses',
|
'pk', 'name', 'label', 'enabled', 'type', 'parent', 'lag', 'mtu', 'mode', 'description', 'ip_addresses',
|
||||||
@@ -880,24 +887,36 @@ class DeviceBayTable(DeviceComponentTable):
|
|||||||
'args': [Accessor('device_id')],
|
'args': [Accessor('device_id')],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
role = columns.ColoredLabelColumn(
|
|
||||||
accessor=Accessor('installed_device__role'),
|
|
||||||
verbose_name=_('Role')
|
|
||||||
)
|
|
||||||
device_type = tables.Column(
|
|
||||||
accessor=Accessor('installed_device__device_type'),
|
|
||||||
linkify=True,
|
|
||||||
verbose_name=_('Type')
|
|
||||||
)
|
|
||||||
status = tables.TemplateColumn(
|
status = tables.TemplateColumn(
|
||||||
verbose_name=_('Status'),
|
verbose_name=_('Status'),
|
||||||
template_code=DEVICEBAY_STATUS,
|
template_code=DEVICEBAY_STATUS,
|
||||||
order_by=Accessor('installed_device__status')
|
order_by=Accessor('installed_device__status')
|
||||||
)
|
)
|
||||||
installed_device = tables.Column(
|
installed_device = tables.Column(
|
||||||
verbose_name=_('Installed device'),
|
verbose_name=_('Installed Device'),
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
|
installed_role = columns.ColoredLabelColumn(
|
||||||
|
accessor=Accessor('installed_device__role'),
|
||||||
|
verbose_name=_('Installed Role')
|
||||||
|
)
|
||||||
|
installed_device_type = tables.Column(
|
||||||
|
accessor=Accessor('installed_device__device_type'),
|
||||||
|
linkify=True,
|
||||||
|
verbose_name=_('Installed Type')
|
||||||
|
)
|
||||||
|
installed_description = tables.Column(
|
||||||
|
accessor=Accessor('installed_device__description'),
|
||||||
|
verbose_name=_('Installed Description')
|
||||||
|
)
|
||||||
|
installed_serial = tables.Column(
|
||||||
|
accessor=Accessor('installed_device__serial'),
|
||||||
|
verbose_name=_('Installed Serial')
|
||||||
|
)
|
||||||
|
installed_asset_tag = tables.Column(
|
||||||
|
accessor=Accessor('installed_device__asset_tag'),
|
||||||
|
verbose_name=_('Installed Asset Tag')
|
||||||
|
)
|
||||||
tags = columns.TagColumn(
|
tags = columns.TagColumn(
|
||||||
url_name='dcim:devicebay_list'
|
url_name='dcim:devicebay_list'
|
||||||
)
|
)
|
||||||
@@ -905,8 +924,9 @@ class DeviceBayTable(DeviceComponentTable):
|
|||||||
class Meta(DeviceComponentTable.Meta):
|
class Meta(DeviceComponentTable.Meta):
|
||||||
model = models.DeviceBay
|
model = models.DeviceBay
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'name', 'device', 'label', 'status', 'role', 'device_type', 'installed_device', 'description',
|
'pk', 'id', 'name', 'device', 'label', 'status', 'description', 'installed_device', 'installed_role',
|
||||||
'tags', 'created', 'last_updated',
|
'installed_device_type', 'installed_description', 'installed_serial', 'installed_asset_tag', 'tags',
|
||||||
|
'created', 'last_updated',
|
||||||
)
|
)
|
||||||
|
|
||||||
default_columns = ('pk', 'name', 'device', 'label', 'status', 'installed_device', 'description')
|
default_columns = ('pk', 'name', 'device', 'label', 'status', 'installed_device', 'description')
|
||||||
@@ -1199,4 +1219,6 @@ class MACAddressTable(PrimaryModelTable):
|
|||||||
'pk', 'id', 'mac_address', 'assigned_object_parent', 'assigned_object', 'description', 'is_primary',
|
'pk', 'id', 'mac_address', 'assigned_object_parent', 'assigned_object', 'description', 'is_primary',
|
||||||
'comments', 'tags', 'created', 'last_updated',
|
'comments', 'tags', 'created', 'last_updated',
|
||||||
)
|
)
|
||||||
default_columns = ('pk', 'mac_address', 'assigned_object_parent', 'assigned_object', 'description')
|
default_columns = (
|
||||||
|
'pk', 'mac_address', 'is_primary', 'assigned_object_parent', 'assigned_object', 'description',
|
||||||
|
)
|
||||||
|
|||||||
@@ -90,7 +90,6 @@ class DevicePanel(panels.ObjectAttributesPanel):
|
|||||||
parent_device = attrs.TemplatedAttr('parent_bay', template_name='dcim/device/attrs/parent_device.html')
|
parent_device = attrs.TemplatedAttr('parent_bay', template_name='dcim/device/attrs/parent_device.html')
|
||||||
gps_coordinates = attrs.GPSCoordinatesAttr()
|
gps_coordinates = attrs.GPSCoordinatesAttr()
|
||||||
tenant = attrs.RelatedObjectAttr('tenant', linkify=True, grouped_by='group')
|
tenant = attrs.RelatedObjectAttr('tenant', linkify=True, grouped_by='group')
|
||||||
device_type = attrs.RelatedObjectAttr('device_type', linkify=True, grouped_by='manufacturer')
|
|
||||||
description = attrs.TextAttr('description')
|
description = attrs.TextAttr('description')
|
||||||
airflow = attrs.ChoiceAttr('airflow')
|
airflow = attrs.ChoiceAttr('airflow')
|
||||||
serial = attrs.TextAttr('serial', label=_('Serial number'), style='font-monospace', copy_button=True)
|
serial = attrs.TextAttr('serial', label=_('Serial number'), style='font-monospace', copy_button=True)
|
||||||
@@ -122,10 +121,19 @@ class DeviceManagementPanel(panels.ObjectAttributesPanel):
|
|||||||
cluster = attrs.RelatedObjectAttr('cluster', linkify=True)
|
cluster = attrs.RelatedObjectAttr('cluster', linkify=True)
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceDeviceTypePanel(panels.ObjectAttributesPanel):
|
||||||
|
title = _('Device Type')
|
||||||
|
|
||||||
|
manufacturer = attrs.RelatedObjectAttr('device_type.manufacturer', linkify=True)
|
||||||
|
model = attrs.RelatedObjectAttr('device_type', linkify=True)
|
||||||
|
height = attrs.TextAttr('device_type.u_height', format_string='{}U')
|
||||||
|
front_image = attrs.ImageAttr('device_type.front_image')
|
||||||
|
rear_image = attrs.ImageAttr('device_type.rear_image')
|
||||||
|
|
||||||
|
|
||||||
class DeviceDimensionsPanel(panels.ObjectAttributesPanel):
|
class DeviceDimensionsPanel(panels.ObjectAttributesPanel):
|
||||||
title = _('Dimensions')
|
title = _('Dimensions')
|
||||||
|
|
||||||
height = attrs.TextAttr('device_type.u_height', format_string='{}U')
|
|
||||||
total_weight = attrs.TemplatedAttr('total_weight', template_name='dcim/device/attrs/total_weight.html')
|
total_weight = attrs.TemplatedAttr('total_weight', template_name='dcim/device/attrs/total_weight.html')
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
from django.views.generic import View
|
from django.views.generic import View
|
||||||
|
|
||||||
from circuits.models import Circuit, CircuitTermination
|
from circuits.models import Circuit, CircuitTermination
|
||||||
from dcim.ui import panels
|
|
||||||
from extras.ui.panels import CustomFieldsPanel, ImageAttachmentsPanel, TagsPanel
|
from extras.ui.panels import CustomFieldsPanel, ImageAttachmentsPanel, TagsPanel
|
||||||
from extras.views import ObjectConfigContextView, ObjectRenderConfigView
|
from extras.views import ObjectConfigContextView, ObjectRenderConfigView
|
||||||
from ipam.models import ASN, IPAddress, Prefix, VLAN, VLANGroup
|
from ipam.models import ASN, IPAddress, Prefix, VLAN, VLANGroup
|
||||||
@@ -44,6 +43,7 @@ from .choices import DeviceFaceChoices, InterfaceModeChoices
|
|||||||
from .models import *
|
from .models import *
|
||||||
from .models.device_components import PortMapping
|
from .models.device_components import PortMapping
|
||||||
from .object_actions import BulkAddComponents, BulkDisconnect
|
from .object_actions import BulkAddComponents, BulkDisconnect
|
||||||
|
from .ui import panels
|
||||||
|
|
||||||
CABLE_TERMINATION_TYPES = {
|
CABLE_TERMINATION_TYPES = {
|
||||||
'dcim.consoleport': ConsolePort,
|
'dcim.consoleport': ConsolePort,
|
||||||
@@ -2470,6 +2470,7 @@ class DeviceView(generic.ObjectView):
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
ImageAttachmentsPanel(),
|
ImageAttachmentsPanel(),
|
||||||
|
panels.DeviceDeviceTypePanel(),
|
||||||
panels.DeviceDimensionsPanel(),
|
panels.DeviceDimensionsPanel(),
|
||||||
TemplatePanel('dcim/panels/device_rack_elevations.html'),
|
TemplatePanel('dcim/panels/device_rack_elevations.html'),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -39,9 +39,20 @@ __all__ = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
IMAGEATTACHMENT_IMAGE = """
|
IMAGEATTACHMENT_IMAGE = """
|
||||||
|
{% load thumbnail %}
|
||||||
{% if record.image %}
|
{% if record.image %}
|
||||||
<a href="{{ record.image.url }}" target="_blank" class="image-preview" data-bs-placement="top">
|
{% thumbnail record.image "400x400" as tn %}
|
||||||
<i class="mdi mdi-image"></i></a>
|
<a href="{{ record.get_absolute_url }}"
|
||||||
|
class="image-preview"
|
||||||
|
data-preview-url="{{ tn.url }}"
|
||||||
|
data-bs-placement="left"
|
||||||
|
title="{{ record.filename }}"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
aria-label="{{ record.filename }}">
|
||||||
|
<i class="mdi mdi-image"></i>
|
||||||
|
</a>
|
||||||
|
{% endthumbnail %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a href="{{ record.get_absolute_url }}">{{ record.filename|truncate_middle:16 }}</a>
|
<a href="{{ record.get_absolute_url }}">{{ record.filename|truncate_middle:16 }}</a>
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -11,14 +11,10 @@ from django.core.exceptions import ImproperlyConfigured, ValidationError
|
|||||||
from django.core.validators import URLValidator
|
from django.core.validators import URLValidator
|
||||||
from django.utils.module_loading import import_string
|
from django.utils.module_loading import import_string
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from rest_framework.utils import field_mapping
|
|
||||||
from strawberry_django import pagination
|
|
||||||
from strawberry_django.fields.field import StrawberryDjangoField
|
|
||||||
|
|
||||||
from core.exceptions import IncompatiblePluginError
|
from core.exceptions import IncompatiblePluginError
|
||||||
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.graphql.pagination import OffsetPaginationInput, apply_pagination
|
|
||||||
from netbox.plugins import PluginConfig
|
from netbox.plugins import PluginConfig
|
||||||
from netbox.registry import registry
|
from netbox.registry import registry
|
||||||
import storages.utils # type: ignore
|
import storages.utils # type: ignore
|
||||||
@@ -28,21 +24,6 @@ from utilities.string import trailing_slash
|
|||||||
from .monkey import get_unique_validators
|
from .monkey import get_unique_validators
|
||||||
|
|
||||||
|
|
||||||
#
|
|
||||||
# Monkey-patching
|
|
||||||
#
|
|
||||||
|
|
||||||
# TODO: Remove this once #20547 has been implemented
|
|
||||||
# Override DRF's get_unique_validators() function with our own (see bug #19302)
|
|
||||||
field_mapping.get_unique_validators = get_unique_validators
|
|
||||||
|
|
||||||
# Override strawberry-django's OffsetPaginationInput class to add the `start` parameter
|
|
||||||
pagination.OffsetPaginationInput = OffsetPaginationInput
|
|
||||||
|
|
||||||
# Patch StrawberryDjangoField to use our custom `apply_pagination()` method with support for cursor-based pagination
|
|
||||||
StrawberryDjangoField.apply_pagination = apply_pagination
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Environment setup
|
# Environment setup
|
||||||
#
|
#
|
||||||
@@ -408,6 +389,11 @@ if CACHING_REDIS_CA_CERT_PATH:
|
|||||||
CACHES['default']['OPTIONS'].setdefault('CONNECTION_POOL_KWARGS', {})
|
CACHES['default']['OPTIONS'].setdefault('CONNECTION_POOL_KWARGS', {})
|
||||||
CACHES['default']['OPTIONS']['CONNECTION_POOL_KWARGS']['ssl_ca_certs'] = CACHING_REDIS_CA_CERT_PATH
|
CACHES['default']['OPTIONS']['CONNECTION_POOL_KWARGS']['ssl_ca_certs'] = CACHING_REDIS_CA_CERT_PATH
|
||||||
|
|
||||||
|
# Merge in KWARGS for additional parameters
|
||||||
|
if caching_redis_kwargs := REDIS['caching'].get('KWARGS'):
|
||||||
|
CACHES['default']['OPTIONS'].setdefault('CONNECTION_POOL_KWARGS', {})
|
||||||
|
CACHES['default']['OPTIONS']['CONNECTION_POOL_KWARGS'].update(caching_redis_kwargs)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Sessions
|
# Sessions
|
||||||
@@ -773,7 +759,7 @@ SPECTACULAR_SETTINGS = {
|
|||||||
'COMPONENT_SPLIT_REQUEST': True,
|
'COMPONENT_SPLIT_REQUEST': True,
|
||||||
'REDOC_DIST': 'SIDECAR',
|
'REDOC_DIST': 'SIDECAR',
|
||||||
'SERVERS': [{
|
'SERVERS': [{
|
||||||
'url': BASE_PATH,
|
'url': '',
|
||||||
'description': 'NetBox',
|
'description': 'NetBox',
|
||||||
}],
|
}],
|
||||||
'SWAGGER_UI_DIST': 'SIDECAR',
|
'SWAGGER_UI_DIST': 'SIDECAR',
|
||||||
@@ -817,6 +803,11 @@ if TASKS_REDIS_CA_CERT_PATH:
|
|||||||
RQ_PARAMS.setdefault('REDIS_CLIENT_KWARGS', {})
|
RQ_PARAMS.setdefault('REDIS_CLIENT_KWARGS', {})
|
||||||
RQ_PARAMS['REDIS_CLIENT_KWARGS']['ssl_ca_certs'] = TASKS_REDIS_CA_CERT_PATH
|
RQ_PARAMS['REDIS_CLIENT_KWARGS']['ssl_ca_certs'] = TASKS_REDIS_CA_CERT_PATH
|
||||||
|
|
||||||
|
# Merge in KWARGS for additional parameters
|
||||||
|
if tasks_redis_kwargs := TASKS_REDIS.get('KWARGS'):
|
||||||
|
RQ_PARAMS.setdefault('REDIS_CLIENT_KWARGS', {})
|
||||||
|
RQ_PARAMS['REDIS_CLIENT_KWARGS'].update(tasks_redis_kwargs)
|
||||||
|
|
||||||
# Define named RQ queues
|
# Define named RQ queues
|
||||||
RQ_QUEUES = {
|
RQ_QUEUES = {
|
||||||
RQ_QUEUE_HIGH: RQ_PARAMS,
|
RQ_QUEUE_HIGH: RQ_PARAMS,
|
||||||
@@ -959,6 +950,26 @@ for plugin_name in PLUGINS:
|
|||||||
raise ImproperlyConfigured(f"events_pipline in plugin: {plugin_name} must be a list or tuple")
|
raise ImproperlyConfigured(f"events_pipline in plugin: {plugin_name} must be a list or tuple")
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Monkey-patching
|
||||||
|
#
|
||||||
|
|
||||||
|
from rest_framework.utils import field_mapping # noqa: E402
|
||||||
|
from strawberry_django import pagination # noqa: E402
|
||||||
|
from strawberry_django.fields.field import StrawberryDjangoField # noqa: E402
|
||||||
|
from netbox.graphql.pagination import OffsetPaginationInput, apply_pagination # noqa: E402
|
||||||
|
|
||||||
|
# TODO: Remove this once #20547 has been implemented
|
||||||
|
# Override DRF's get_unique_validators() function with our own (see bug #19302)
|
||||||
|
field_mapping.get_unique_validators = get_unique_validators
|
||||||
|
|
||||||
|
# Override strawberry-django's OffsetPaginationInput class to add the `start` parameter
|
||||||
|
pagination.OffsetPaginationInput = OffsetPaginationInput
|
||||||
|
|
||||||
|
# Patch StrawberryDjangoField to use our custom `apply_pagination()` method with support for cursor-based pagination
|
||||||
|
StrawberryDjangoField.apply_pagination = apply_pagination
|
||||||
|
|
||||||
|
|
||||||
# UNSUPPORTED FUNCTIONALITY: Import any local overrides.
|
# UNSUPPORTED FUNCTIONALITY: Import any local overrides.
|
||||||
try:
|
try:
|
||||||
from .local_settings import *
|
from .local_settings import *
|
||||||
|
|||||||
2
netbox/project-static/dist/netbox.css
vendored
2
netbox/project-static/dist/netbox.css
vendored
File diff suppressed because one or more lines are too long
2
netbox/project-static/dist/netbox.js
vendored
2
netbox/project-static/dist/netbox.js
vendored
File diff suppressed because one or more lines are too long
6
netbox/project-static/dist/netbox.js.map
vendored
6
netbox/project-static/dist/netbox.js.map
vendored
File diff suppressed because one or more lines are too long
@@ -150,20 +150,22 @@ function initSidebarAccordions(): void {
|
|||||||
*/
|
*/
|
||||||
function initImagePreview(): void {
|
function initImagePreview(): void {
|
||||||
for (const element of getElements<HTMLAnchorElement>('a.image-preview')) {
|
for (const element of getElements<HTMLAnchorElement>('a.image-preview')) {
|
||||||
// Generate a max-width that's a quarter of the screen's width (note - the actual element
|
// Prefer a thumbnail URL for the popover (so we don't preload full-size images),
|
||||||
// width will be slightly larger due to the popover body's padding).
|
// but fall back to the link target if no thumbnail was provided.
|
||||||
const maxWidth = `${Math.round(window.innerWidth / 4)}px`;
|
const previewUrl = element.dataset.previewUrl ?? element.href;
|
||||||
|
const image = createElement('img', { src: previewUrl });
|
||||||
|
|
||||||
// Create an image element that uses the linked image as its `src`.
|
// Ensure lazy loading and async decoding
|
||||||
const image = createElement('img', { src: element.href });
|
image.loading = 'lazy';
|
||||||
image.style.maxWidth = maxWidth;
|
image.decoding = 'async';
|
||||||
|
|
||||||
// Create a container for the image.
|
// Create a container for the image.
|
||||||
const content = createElement('div', null, null, [image]);
|
const content = createElement('div', null, null, [image]);
|
||||||
|
|
||||||
// Initialize the Bootstrap Popper instance.
|
// Initialize the Bootstrap Popper instance.
|
||||||
new Popover(element, {
|
new Popover(element, {
|
||||||
// Attach this custom class to the popover so that it styling can be controlled via CSS.
|
// Attach this custom class to the popover so that its styling
|
||||||
|
// can be controlled via CSS.
|
||||||
customClass: 'image-preview-popover',
|
customClass: 'image-preview-popover',
|
||||||
trigger: 'hover',
|
trigger: 'hover',
|
||||||
html: true,
|
html: true,
|
||||||
|
|||||||
@@ -89,6 +89,29 @@ img.plugin-icon {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Image preview popover (rendered for <a.image-preview> by initImagePreview())
|
||||||
|
.image-preview-popover {
|
||||||
|
--bs-popover-max-width: clamp(240px, 25vw, 640px);
|
||||||
|
|
||||||
|
.popover-header {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.popover-body {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
img {
|
||||||
|
display: block;
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: clamp(160px, 33vh, 640px);
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
body[data-bs-theme=dark] {
|
body[data-bs-theme=dark] {
|
||||||
// Assuming icon is black/white line art, invert it and tone down brightness
|
// Assuming icon is black/white line art, invert it and tone down brightness
|
||||||
img.plugin-icon {
|
img.plugin-icon {
|
||||||
|
|||||||
@@ -5,6 +5,16 @@
|
|||||||
font-variant-ligatures: none;
|
font-variant-ligatures: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Remove when Tabler releases fix for https://github.com/tabler/tabler/issues/2271
|
||||||
|
// and NetBox upgrades to that version. Fix merged to Tabler dev branch in PR #2548.
|
||||||
|
:root,
|
||||||
|
:host {
|
||||||
|
@include media-breakpoint-up(lg) {
|
||||||
|
margin-left: 0;
|
||||||
|
scrollbar-gutter: stable;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Restore default foreground & background colors for <pre> blocks
|
// Restore default foreground & background colors for <pre> blocks
|
||||||
pre {
|
pre {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
<div class="col col-md-12">
|
<div class="col col-md-12">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2 class="card-header">{% trans "Configuration Data" %}</h2>
|
<h2 class="card-header">{% trans "Configuration Data" %}</h2>
|
||||||
{% include 'core/inc/config_data.html' with config=object.data %}
|
{% include 'core/inc/config_data.html' %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
|
|||||||
@@ -95,7 +95,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th scope="row" class="ps-3">{% trans "Custom validators" %}</th>
|
<th scope="row" class="ps-3">{% trans "Custom validators" %}</th>
|
||||||
{% if config.CUSTOM_VALIDATORS %}
|
{% if config.CUSTOM_VALIDATORS %}
|
||||||
<td><pre>{{ config.CUSTOM_VALIDATORS }}</pre></td>
|
<td><pre class="p-0">{{ config.CUSTOM_VALIDATORS }}</pre></td>
|
||||||
{% else %}
|
{% else %}
|
||||||
<td>{{ ''|placeholder }}</td>
|
<td>{{ ''|placeholder }}</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -103,7 +103,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th scope="row" class="border-0 ps-3">{% trans "Protection rules" %}</th>
|
<th scope="row" class="border-0 ps-3">{% trans "Protection rules" %}</th>
|
||||||
{% if config.PROTECTION_RULES %}
|
{% if config.PROTECTION_RULES %}
|
||||||
<td class="border-0"><pre>{{ config.PROTECTION_RULES }}</pre></td>
|
<td class="border-0"><pre class="p-0">{{ config.PROTECTION_RULES }}</pre></td>
|
||||||
{% else %}
|
{% else %}
|
||||||
<td class="border-0">{{ ''|placeholder }}</td>
|
<td class="border-0">{{ ''|placeholder }}</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -116,7 +116,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th scope="row" class="border-0 ps-3">{% trans "Default preferences" %}</th>
|
<th scope="row" class="border-0 ps-3">{% trans "Default preferences" %}</th>
|
||||||
{% if config.DEFAULT_USER_PREFERENCES %}
|
{% if config.DEFAULT_USER_PREFERENCES %}
|
||||||
<td class="border-0"><pre>{{ config.DEFAULT_USER_PREFERENCES|json }}</pre></td>
|
<td class="border-0"><pre class="p-0">{{ config.DEFAULT_USER_PREFERENCES }}</pre></td>
|
||||||
{% else %}
|
{% else %}
|
||||||
<td class="border-0">{{ ''|placeholder }}</td>
|
<td class="border-0">{{ ''|placeholder }}</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
{% load helpers %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2 class="card-header">{% trans "Resources" %}</h2>
|
||||||
|
<table class="table table-hover attr-table">
|
||||||
|
<tr>
|
||||||
|
<th scope="row"><i class="mdi mdi-gauge"></i> {% trans "Virtual CPUs" %}</th>
|
||||||
|
<td>{{ object.vcpus|placeholder }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row"><i class="mdi mdi-chip"></i> {% trans "Memory" %}</th>
|
||||||
|
<td>
|
||||||
|
{% if object.memory %}
|
||||||
|
<span title={{ object.memory }}>{{ object.memory|humanize_ram_megabytes }}</span>
|
||||||
|
{% else %}
|
||||||
|
{{ ''|placeholder }}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<i class="mdi mdi-harddisk"></i> {% trans "Disk Space" %}
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
{% if object.disk %}
|
||||||
|
{{ object.disk|humanize_disk_megabytes }}
|
||||||
|
{% else %}
|
||||||
|
{{ ''|placeholder }}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
@@ -1,199 +1 @@
|
|||||||
{% extends 'virtualization/virtualmachine/base.html' %}
|
{% extends 'virtualization/virtualmachine/base.html' %}
|
||||||
{% load buttons %}
|
|
||||||
{% load static %}
|
|
||||||
{% load helpers %}
|
|
||||||
{% load plugins %}
|
|
||||||
{% load i18n %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="row my-3">
|
|
||||||
<div class="col col-12 col-md-6">
|
|
||||||
<div class="card">
|
|
||||||
<h2 class="card-header">{% trans "Virtual Machine" %}</h2>
|
|
||||||
<table class="table table-hover attr-table">
|
|
||||||
<tr>
|
|
||||||
<th scope="row">{% trans "Name" %}</th>
|
|
||||||
<td>{{ object }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th scope="row">{% trans "Status" %}</th>
|
|
||||||
<td>{% badge object.get_status_display bg_color=object.get_status_color %}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th scope="row">{% trans "Start on boot" %}</th>
|
|
||||||
<td>{% badge object.get_start_on_boot_display bg_color=object.get_start_on_boot_color %}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th scope="row">{% trans "Role" %}</th>
|
|
||||||
<td>{{ object.role|linkify|placeholder }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th scope="row">{% trans "Platform" %}</th>
|
|
||||||
<td>{{ object.platform|linkify|placeholder }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th scope="row">{% trans "Description" %}</th>
|
|
||||||
<td>{{ object.description|placeholder }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th scope="row">{% trans "Serial Number" %}</th>
|
|
||||||
<td>{{ object.serial|placeholder }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th scope="row">{% trans "Tenant" %}</th>
|
|
||||||
<td>
|
|
||||||
{% if object.tenant.group %}
|
|
||||||
{{ object.tenant.group|linkify }} /
|
|
||||||
{% endif %}
|
|
||||||
{{ object.tenant|linkify|placeholder }}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th scope="row">{% trans "Config Template" %}</th>
|
|
||||||
<td>{{ object.config_template|linkify|placeholder }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th scope="row">{% trans "Primary IPv4" %}</th>
|
|
||||||
<td>
|
|
||||||
{% if object.primary_ip4 %}
|
|
||||||
<a href="{% url 'ipam:ipaddress' pk=object.primary_ip4.pk %}" id="primary_ip4">{{ object.primary_ip4.address.ip }}</a>
|
|
||||||
{% if object.primary_ip4.nat_inside %}
|
|
||||||
({% trans "NAT for" %} <a href="{{ object.primary_ip4.nat_inside.get_absolute_url }}">{{ object.primary_ip4.nat_inside.address.ip }}</a>)
|
|
||||||
{% elif object.primary_ip4.nat_outside.exists %}
|
|
||||||
({% trans "NAT" %}: {% for nat in object.primary_ip4.nat_outside.all %}<a href="{{ nat.get_absolute_url }}">{{ nat.address.ip }}</a>{% if not forloop.last %}, {% endif %}{% endfor %})
|
|
||||||
{% endif %}
|
|
||||||
{% copy_content "primary_ip4" %}
|
|
||||||
{% else %}
|
|
||||||
{{ ''|placeholder }}
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th scope="row">{% trans "Primary IPv6" %}</th>
|
|
||||||
<td>
|
|
||||||
{% if object.primary_ip6 %}
|
|
||||||
<a href="{% url 'ipam:ipaddress' pk=object.primary_ip6.pk %}" id="primary_ip6">{{ object.primary_ip6.address.ip }}</a>
|
|
||||||
{% if object.primary_ip6.nat_inside %}
|
|
||||||
({% trans "NAT for" %} <a href="{{ object.primary_ip6.nat_inside.get_absolute_url }}">{{ object.primary_ip6.nat_inside.address.ip }}</a>)
|
|
||||||
{% elif object.primary_ip6.nat_outside.exists %}
|
|
||||||
({% trans "NAT" %}: {% for nat in object.primary_ip6.nat_outside.all %}<a href="{{ nat.get_absolute_url }}">{{ nat.address.ip }}</a>{% if not forloop.last %}, {% endif %}{% endfor %})
|
|
||||||
{% endif %}
|
|
||||||
{% copy_content "primary_ip6" %}
|
|
||||||
{% else %}
|
|
||||||
{{ ''|placeholder }}
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
{% include 'inc/panels/custom_fields.html' %}
|
|
||||||
{% include 'inc/panels/tags.html' %}
|
|
||||||
{% include 'inc/panels/comments.html' %}
|
|
||||||
{% plugin_left_page object %}
|
|
||||||
</div>
|
|
||||||
<div class="col col-12 col-md-6">
|
|
||||||
<div class="card">
|
|
||||||
<h2 class="card-header">{% trans "Cluster" %}</h2>
|
|
||||||
<table class="table table-hover attr-table">
|
|
||||||
<tr>
|
|
||||||
<th scope="row">{% trans "Site" %}</th>
|
|
||||||
<td>
|
|
||||||
{{ object.site|linkify|placeholder }}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th scope="row">{% trans "Cluster" %}</th>
|
|
||||||
<td>
|
|
||||||
{% if object.cluster.group %}
|
|
||||||
{{ object.cluster.group|linkify }} /
|
|
||||||
{% endif %}
|
|
||||||
{{ object.cluster|linkify|placeholder }}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th scope="row">{% trans "Cluster Type" %}</th>
|
|
||||||
<td>
|
|
||||||
{{ object.cluster.type|linkify|placeholder }}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th scope="row">{% trans "Device" %}</th>
|
|
||||||
<td>
|
|
||||||
{{ object.device|linkify|placeholder }}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<div class="card">
|
|
||||||
<h2 class="card-header">{% trans "Resources" %}</h2>
|
|
||||||
<table class="table table-hover attr-table">
|
|
||||||
<tr>
|
|
||||||
<th scope="row"><i class="mdi mdi-gauge"></i> {% trans "Virtual CPUs" %}</th>
|
|
||||||
<td>{{ object.vcpus|placeholder }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th scope="row"><i class="mdi mdi-chip"></i> {% trans "Memory" %}</th>
|
|
||||||
<td>
|
|
||||||
{% if object.memory %}
|
|
||||||
<span title={{ object.memory }}>{{ object.memory|humanize_ram_megabytes }}</span>
|
|
||||||
{% else %}
|
|
||||||
{{ ''|placeholder }}
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th scope="row">
|
|
||||||
<i class="mdi mdi-harddisk"></i> {% trans "Disk Space" %}
|
|
||||||
</th>
|
|
||||||
<td>
|
|
||||||
{% if object.disk %}
|
|
||||||
{{ object.disk|humanize_disk_megabytes }}
|
|
||||||
{% else %}
|
|
||||||
{{ ''|placeholder }}
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<div class="card">
|
|
||||||
<h2 class="card-header">
|
|
||||||
{% trans "Application Services" %}
|
|
||||||
{% if perms.ipam.add_service %}
|
|
||||||
<div class="card-actions">
|
|
||||||
<a href="{% url 'ipam:service_add' %}?parent_object_type={{ object|content_type_id }}&parent={{ object.pk }}" class="btn btn-ghost-primary btn-sm">
|
|
||||||
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> {% trans "Add an application service" %}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</h2>
|
|
||||||
{% htmx_table 'ipam:service_list' virtual_machine_id=object.pk %}
|
|
||||||
</div>
|
|
||||||
{% include 'inc/panels/image_attachments.html' %}
|
|
||||||
{% plugin_right_page object %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col col-md-12">
|
|
||||||
<div class="card">
|
|
||||||
<h2 class="card-header">
|
|
||||||
{% trans "Virtual Disks" %}
|
|
||||||
{% if perms.virtualization.add_virtualdisk %}
|
|
||||||
<div class="card-actions">
|
|
||||||
<a href="{% url 'virtualization:virtualdisk_add' %}?device={{ object.device.pk }}&virtual_machine={{ object.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-ghost-primary btn-sm">
|
|
||||||
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> {% trans "Add Virtual Disk" %}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</h2>
|
|
||||||
{% htmx_table 'virtualization:virtualdisk_list' virtual_machine_id=object.pk %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col col-md-12">
|
|
||||||
{% plugin_full_width_page object %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{% load i18n %}
|
||||||
|
<a href="{{ value.get_absolute_url }}"{% if name %} id="attr_{{ name }}"{% endif %}>{{ value.address.ip }}</a>
|
||||||
|
{% if value.nat_inside %}
|
||||||
|
({% trans "NAT for" %} <a href="{{ value.nat_inside.get_absolute_url }}">{{ value.nat_inside.address.ip }}</a>)
|
||||||
|
{% elif value.nat_outside.exists %}
|
||||||
|
({% trans "NAT" %}: {% for nat in value.nat_outside.all %}<a href="{{ nat.get_absolute_url }}">{{ nat.address.ip }}</a>{% if not forloop.last %}, {% endif %}{% endfor %})
|
||||||
|
{% endif %}
|
||||||
|
<a class="btn btn-sm btn-primary copy-content" data-clipboard-target="#attr_{{ name }}" title="{% trans "Copy to clipboard" %}">
|
||||||
|
<i class="mdi mdi-content-copy"></i>
|
||||||
|
</a>
|
||||||
@@ -78,8 +78,8 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th scope="row">{% trans "MAC Address" %}</th>
|
<th scope="row">{% trans "MAC Address" %}</th>
|
||||||
<td>
|
<td>
|
||||||
{% if object.mac_address %}
|
{% if object.primary_mac_address %}
|
||||||
<span class="font-monospace">{{ object.mac_address }}</span>
|
<span class="font-monospace">{{ object.primary_mac_address|linkify }}</span>
|
||||||
<span class="badge text-bg-primary">{% trans "Primary" %}</span>
|
<span class="badge text-bg-primary">{% trans "Primary" %}</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ ''|placeholder }}
|
{{ ''|placeholder }}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -24,6 +24,7 @@ class TokenTable(NetBoxTable):
|
|||||||
token = columns.TemplateColumn(
|
token = columns.TemplateColumn(
|
||||||
verbose_name=_('token'),
|
verbose_name=_('token'),
|
||||||
template_code=TOKEN,
|
template_code=TOKEN,
|
||||||
|
orderable=False,
|
||||||
)
|
)
|
||||||
enabled = columns.BooleanColumn(
|
enabled = columns.BooleanColumn(
|
||||||
verbose_name=_('Enabled')
|
verbose_name=_('Enabled')
|
||||||
|
|||||||
24
netbox/users/tests/test_tables.py
Normal file
24
netbox/users/tests/test_tables.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
from django.test import RequestFactory, tag, TestCase
|
||||||
|
|
||||||
|
from users.models import Token
|
||||||
|
from users.tables import TokenTable
|
||||||
|
|
||||||
|
|
||||||
|
class TokenTableTest(TestCase):
|
||||||
|
@tag('regression')
|
||||||
|
def test_every_orderable_field_does_not_throw_exception(self):
|
||||||
|
tokens = Token.objects.all()
|
||||||
|
disallowed = {'actions'}
|
||||||
|
|
||||||
|
orderable_columns = [
|
||||||
|
column.name for column in TokenTable(tokens).columns
|
||||||
|
if column.orderable and column.name not in disallowed
|
||||||
|
]
|
||||||
|
fake_request = RequestFactory().get("/")
|
||||||
|
|
||||||
|
for col in orderable_columns:
|
||||||
|
for direction in ('-', ''):
|
||||||
|
with self.subTest(col=col, direction=direction):
|
||||||
|
table = TokenTable(tokens)
|
||||||
|
table.order_by = f'{direction}{col}'
|
||||||
|
table.as_html(fake_request)
|
||||||
0
netbox/virtualization/ui/__init__.py
Normal file
0
netbox/virtualization/ui/__init__.py
Normal file
34
netbox/virtualization/ui/panels.py
Normal file
34
netbox/virtualization/ui/panels.py
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from netbox.ui import attrs, panels
|
||||||
|
|
||||||
|
|
||||||
|
class VirtualMachinePanel(panels.ObjectAttributesPanel):
|
||||||
|
name = attrs.TextAttr('name')
|
||||||
|
status = attrs.ChoiceAttr('status')
|
||||||
|
start_on_boot = attrs.ChoiceAttr('start_on_boot')
|
||||||
|
role = attrs.RelatedObjectAttr('role', linkify=True)
|
||||||
|
platform = attrs.NestedObjectAttr('platform', linkify=True, max_depth=3)
|
||||||
|
description = attrs.TextAttr('description')
|
||||||
|
serial = attrs.TextAttr('serial', label=_('Serial number'), style='font-monospace', copy_button=True)
|
||||||
|
tenant = attrs.RelatedObjectAttr('tenant', linkify=True, grouped_by='group')
|
||||||
|
config_template = attrs.RelatedObjectAttr('config_template', linkify=True)
|
||||||
|
primary_ip4 = attrs.TemplatedAttr(
|
||||||
|
'primary_ip4',
|
||||||
|
label=_('Primary IPv4'),
|
||||||
|
template_name='virtualization/virtualmachine/attrs/ipaddress.html',
|
||||||
|
)
|
||||||
|
primary_ip6 = attrs.TemplatedAttr(
|
||||||
|
'primary_ip6',
|
||||||
|
label=_('Primary IPv6'),
|
||||||
|
template_name='virtualization/virtualmachine/attrs/ipaddress.html',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class VirtualMachineClusterPanel(panels.ObjectAttributesPanel):
|
||||||
|
title = _('Cluster')
|
||||||
|
|
||||||
|
site = attrs.RelatedObjectAttr('site', linkify=True, grouped_by='group')
|
||||||
|
cluster = attrs.RelatedObjectAttr('cluster', linkify=True)
|
||||||
|
cluster_type = attrs.RelatedObjectAttr('cluster.type', linkify=True)
|
||||||
|
device = attrs.RelatedObjectAttr('device', linkify=True)
|
||||||
@@ -10,12 +10,15 @@ from dcim.filtersets import DeviceFilterSet
|
|||||||
from dcim.forms import DeviceFilterForm
|
from dcim.forms import DeviceFilterForm
|
||||||
from dcim.models import Device
|
from dcim.models import Device
|
||||||
from dcim.tables import DeviceTable
|
from dcim.tables import DeviceTable
|
||||||
|
from extras.ui.panels import CustomFieldsPanel, ImageAttachmentsPanel, TagsPanel
|
||||||
from extras.views import ObjectConfigContextView, ObjectRenderConfigView
|
from extras.views import ObjectConfigContextView, ObjectRenderConfigView
|
||||||
from ipam.models import IPAddress, VLANGroup
|
from ipam.models import IPAddress, VLANGroup
|
||||||
from ipam.tables import InterfaceVLANTable, VLANTranslationRuleTable
|
from ipam.tables import InterfaceVLANTable, VLANTranslationRuleTable
|
||||||
from netbox.object_actions import (
|
from netbox.object_actions import (
|
||||||
AddObject, BulkDelete, BulkEdit, BulkExport, BulkImport, BulkRename, DeleteObject, EditObject,
|
AddObject, BulkDelete, BulkEdit, BulkExport, BulkImport, BulkRename, DeleteObject, EditObject,
|
||||||
)
|
)
|
||||||
|
from netbox.ui import actions, layout
|
||||||
|
from netbox.ui.panels import CommentsPanel, ObjectsTablePanel, TemplatePanel
|
||||||
from netbox.views import generic
|
from netbox.views import generic
|
||||||
from utilities.query import count_related
|
from utilities.query import count_related
|
||||||
from utilities.query_functions import CollateAsChar
|
from utilities.query_functions import CollateAsChar
|
||||||
@@ -23,6 +26,7 @@ from utilities.views import GetRelatedModelsMixin, ViewTab, register_model_view
|
|||||||
from . import filtersets, forms, tables
|
from . import filtersets, forms, tables
|
||||||
from .models import *
|
from .models import *
|
||||||
from .object_actions import BulkAddComponents
|
from .object_actions import BulkAddComponents
|
||||||
|
from .ui import panels
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -336,6 +340,7 @@ class ClusterAddDevicesView(generic.ObjectEditView):
|
|||||||
# Virtual machines
|
# Virtual machines
|
||||||
#
|
#
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(VirtualMachine, 'list', path='', detail=False)
|
@register_model_view(VirtualMachine, 'list', path='', detail=False)
|
||||||
class VirtualMachineListView(generic.ObjectListView):
|
class VirtualMachineListView(generic.ObjectListView):
|
||||||
queryset = VirtualMachine.objects.prefetch_related('primary_ip4', 'primary_ip6')
|
queryset = VirtualMachine.objects.prefetch_related('primary_ip4', 'primary_ip6')
|
||||||
@@ -348,6 +353,44 @@ class VirtualMachineListView(generic.ObjectListView):
|
|||||||
@register_model_view(VirtualMachine)
|
@register_model_view(VirtualMachine)
|
||||||
class VirtualMachineView(generic.ObjectView):
|
class VirtualMachineView(generic.ObjectView):
|
||||||
queryset = VirtualMachine.objects.all()
|
queryset = VirtualMachine.objects.all()
|
||||||
|
layout = layout.SimpleLayout(
|
||||||
|
left_panels=[
|
||||||
|
panels.VirtualMachinePanel(),
|
||||||
|
CustomFieldsPanel(),
|
||||||
|
TagsPanel(),
|
||||||
|
CommentsPanel(),
|
||||||
|
],
|
||||||
|
right_panels=[
|
||||||
|
panels.VirtualMachineClusterPanel(),
|
||||||
|
TemplatePanel('virtualization/panels/virtual_machine_resources.html'),
|
||||||
|
ObjectsTablePanel(
|
||||||
|
model='ipam.Service',
|
||||||
|
title=_('Application Services'),
|
||||||
|
filters={'virtual_machine_id': lambda ctx: ctx['object'].pk},
|
||||||
|
actions=[
|
||||||
|
actions.AddObject(
|
||||||
|
'ipam.Service',
|
||||||
|
url_params={
|
||||||
|
'parent_object_type': lambda ctx: ContentType.objects.get_for_model(ctx['object']).pk,
|
||||||
|
'parent': lambda ctx: ctx['object'].pk,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
ImageAttachmentsPanel(),
|
||||||
|
],
|
||||||
|
bottom_panels=[
|
||||||
|
ObjectsTablePanel(
|
||||||
|
model='virtualization.VirtualDisk',
|
||||||
|
filters={'virtual_machine_id': lambda ctx: ctx['object'].pk},
|
||||||
|
actions=[
|
||||||
|
actions.AddObject(
|
||||||
|
'virtualization.VirtualDisk', url_params={'virtual_machine': lambda ctx: ctx['object'].pk}
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(VirtualMachine, 'interfaces')
|
@register_model_view(VirtualMachine, 'interfaces')
|
||||||
|
|||||||
Reference in New Issue
Block a user