Compare commits

..

1 Commits

Author SHA1 Message Date
Martin Hauser
6362048788 refactor(virtualization): Port to declarative layout
Add declarative layout panels for Cluster, Cluster Group, Cluster Type,
Virtual Disk, and VM Interface, including addressing, VLAN assignment,
and FHRP group handling.

Expand the declarative layout primitives:
- add GFK attribute rendering support
- add panel for rendering context-provided tables
- update templates to support new panels/attrs

Closes #20923
2026-02-19 23:36:03 +01:00
16 changed files with 339 additions and 386 deletions

37
netbox/ipam/ui/panels.py Normal file
View File

@@ -0,0 +1,37 @@
from django.contrib.contenttypes.models import ContentType
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from netbox.ui import actions, panels
class FHRPGroupAssignmentsPanel(panels.ObjectPanel):
"""
A panel which lists all FHRP group assignments for a given object.
"""
template_name = 'ipam/panels/fhrp_groups.html'
title = _('FHRP Groups')
actions = [
actions.AddObject(
'ipam.FHRPGroup',
url_params={
'return_url': lambda ctx: reverse(
'ipam:fhrpgroupassignment_add',
query={
'interface_type': ContentType.objects.get_for_model(ctx['object']).pk,
'interface_id': ctx['object'].pk,
},
),
},
label=_('Create Group'),
),
actions.AddObject(
'ipam.FHRPGroupAssignment',
url_params={
'interface_type': lambda ctx: ContentType.objects.get_for_model(ctx['object']).pk,
'interface_id': lambda ctx: ctx['object'].pk,
},
label=_('Assign Group'),
),
]

View File

@@ -10,6 +10,7 @@ __all__ = (
'BooleanAttr',
'ColorAttr',
'ChoiceAttr',
'GenericForeignKeyAttr',
'GPSCoordinatesAttr',
'ImageAttr',
'NestedObjectAttr',
@@ -279,6 +280,32 @@ class NestedObjectAttr(ObjectAttribute):
}
class GenericForeignKeyAttr(ObjectAttribute):
"""
An attribute representing a related generic relation object.
This attribute is similar to `RelatedObjectAttr` but uses the
ContentType of the related object to be displayed alongside the value.
Parameters:
linkify (bool): If True, the rendered value will be hyperlinked
to the related object's detail view
"""
template_name = 'ui/attrs/generic_object.html'
def __init__(self, *args, linkify=None, **kwargs):
super().__init__(*args, **kwargs)
self.linkify = linkify
def get_context(self, obj, context):
value = self.get_value(obj)
content_type = value._meta.verbose_name
return {
'content_type': content_type,
'linkify': self.linkify,
}
class AddressAttr(ObjectAttribute):
"""
A physical or mailing address.

View File

@@ -12,6 +12,7 @@ from utilities.views import get_viewname
__all__ = (
'CommentsPanel',
'ContextTablePanel',
'JSONPanel',
'NestedGroupObjectPanel',
'ObjectAttributesPanel',
@@ -339,3 +340,42 @@ class PluginContentPanel(Panel):
def render(self, context):
obj = context.get('object')
return _get_registered_content(obj, self.method, context)
class ContextTablePanel(ObjectPanel):
"""
A panel which renders a django-tables2/NetBoxTable instance provided
via the view's extra context.
This is useful when you already have a fully constructed table
(custom queryset, special columns, no list view) and just want to
render it inside a declarative layout panel.
Parameters:
table (str | callable): Either the context key holding the table
(e.g. "vlan_table") or a callable which accepts the template
context and returns a table instance.
"""
template_name = 'ui/panels/context_table.html'
def __init__(self, table, **kwargs):
super().__init__(**kwargs)
self.table = table
def _resolve_table(self, context):
if callable(self.table):
return self.table(context)
return context.get(self.table)
def get_context(self, context):
table = self._resolve_table(context)
return {
**super().get_context(context),
'table': table,
}
def render(self, context):
table = self._resolve_table(context)
if table is None:
return ''
return super().render(context)

View File

@@ -0,0 +1,47 @@
{% extends "ui/panels/_base.html" %}
{% load perms %}
{% load i18n %}
{% block panel_content %}
<table class="table table-hover attr-table">
<thead>
<tr class="border-bottom">
<th>{% trans "Group" %}</th>
<th>{% trans "Protocol" %}</th>
<th>{% trans "Virtual IPs" %}</th>
<th>{% trans "Priority" %}</th>
<th></th>
</tr>
</thead>
<tbody>
{% for assignment in object.fhrp_group_assignments.all %}
<tr>
<td>{{ assignment.group|linkify:"group_id" }}</td>
<td>{{ assignment.group.get_protocol_display }}</td>
<td>
{% for ipaddress in assignment.group.ip_addresses.all %}
{{ ipaddress|linkify }}{% if not forloop.last %}<br />{% endif %}
{% endfor %}
</td>
<td>{{ assignment.priority }}</td>
<td class="text-end d-print-none">
{% if request.user|can_change:assignment %}
<a href="{% url 'ipam:fhrpgroupassignment_edit' pk=assignment.pk %}?return_url={{ object.get_absolute_url }}" class="btn btn-sm btn-warning" title="{% trans "Edit" %}">
<i class="mdi mdi-pencil" aria-hidden="true"></i>
</a>
{% endif %}
{% if request.user|can_delete:assignment %}
<a href="{% url 'ipam:fhrpgroupassignment_delete' pk=assignment.pk %}?return_url={{ object.get_absolute_url }}" class="btn btn-sm btn-danger" title="{% trans "Delete" %}">
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i>
</a>
{% endif %}
</td>
</tr>
{% empty %}
<tr>
<td colspan="5" class="text-muted">{% trans "None" %}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock panel_content %}

View File

@@ -0,0 +1,3 @@
<span{% if name %} id="attr_{{ name }}"{% endif %}>
{% if linkify %}{{ value|linkify }}{% else %}{{ value }}{% endif %}{% if content_type %} ({{ content_type }}){% endif %}
</span>

View File

@@ -0,0 +1,6 @@
{% extends "ui/panels/_base.html" %}
{% load render_table from django_tables2 %}
{% block panel_content %}
{% render_table table 'inc/table.html' %}
{% endblock panel_content %}

View File

@@ -1,95 +1 @@
{% extends 'virtualization/cluster/base.html' %}
{% load helpers %}
{% load plugins %}
{% load i18n %}
{% block content %}
<div class="row">
<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 "Name" %}</th>
<td>{{ object.name }}</td>
</tr>
<tr>
<th scope="row">{% trans "Type" %}</th>
<td>{{ object.type|linkify }}</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 "Description" %}</th>
<td>{{ object.description|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Group" %}</th>
<td>{{ object.group|linkify|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 "Scope" %}</th>
{% if object.scope %}
<td>{{ object.scope|linkify }} ({% trans object.scope_type.name %})</td>
{% else %}
<td>{{ ''|placeholder }}</td>
{% endif %}
</tr>
</table>
</div>
{% 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 "Allocated Resources" %}</h2>
<table class="table table-hover attr-table">
<tr>
<th scope="row"><i class="mdi mdi-gauge"></i> {% trans "Virtual CPUs" %}</th>
<td>{{ vcpus_sum|placeholder }}</td>
</tr>
<tr>
<th scope="row"><i class="mdi mdi-chip"></i> {% trans "Memory" %}</th>
<td>
{% if memory_sum %}
<span title={{ memory_sum }}>{{ memory_sum|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 disk_sum %}
{{ disk_sum|humanize_disk_megabytes }}
{% else %}
{{ ''|placeholder }}
{% endif %}
</td>
</tr>
</table>
</div>
{% include 'inc/panels/related_objects.html' %}
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/tags.html' %}
{% plugin_right_page object %}
</div>
</div>
<div class="row">
<div class="col col-md-12">
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}

View File

@@ -1,7 +1,4 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% load plugins %}
{% load render_table from django_tables2 %}
{% load i18n %}
{% block extra_controls %}
@@ -11,36 +8,3 @@
</a>
{% endif %}
{% endblock extra_controls %}
{% block content %}
<div class="row mb-3">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Cluster Group" %}</h2>
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Name" %}</th>
<td>{{ object.name }}</td>
</tr>
<tr>
<th scope="row">{% trans "Description" %}</th>
<td>{{ object.description|placeholder }}</td>
</tr>
</table>
</div>
{% include 'inc/panels/tags.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-12 col-md-6">
{% include 'inc/panels/related_objects.html' %}
{% include 'inc/panels/comments.html' %}
{% include 'inc/panels/custom_fields.html' %}
{% plugin_right_page object %}
</div>
</div>
<div class="row">
<div class="col col-md-12">
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}

View File

@@ -1,7 +1,4 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% load plugins %}
{% load render_table from django_tables2 %}
{% load i18n %}
{% block extra_controls %}
@@ -11,42 +8,3 @@
</a>
{% endif %}
{% endblock extra_controls %}
{% block content %}
<div class="row mb-3">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Cluster Type" %}</h2>
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Name" %}</th>
<td>{{ object.name }}</td>
</tr>
<tr>
<th scope="row">{% trans "Description" %}</th>
<td>{{ object.description|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Clusters" %}</th>
<td>
<a href="{% url 'virtualization:cluster_list' %}?type_id={{ object.pk }}">{{ object.clusters.count }}</a>
</td>
</tr>
</table>
</div>
{% include 'inc/panels/tags.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-12 col-md-6">
{% include 'inc/panels/related_objects.html' %}
{% include 'inc/panels/comments.html' %}
{% include 'inc/panels/custom_fields.html' %}
{% plugin_right_page object %}
</div>
</div>
<div class="row">
<div class="col col-md-12">
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,34 @@
{% load helpers %}
{% load i18n %}
<div class="card">
<h2 class="card-header">{% trans "Allocated Resources" %}</h2>
<table class="table table-hover attr-table">
<tr>
<th scope="row"><i class="mdi mdi-gauge"></i> {% trans "Virtual CPUs" %}</th>
<td>{{ vcpus_sum|placeholder }}</td>
</tr>
<tr>
<th scope="row"><i class="mdi mdi-chip"></i> {% trans "Memory" %}</th>
<td>
{% if memory_sum %}
<span title={{ memory_sum }}>{{ memory_sum|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 disk_sum %}
{{ disk_sum|humanize_disk_megabytes }}
{% else %}
{{ ''|placeholder }}
{% endif %}
</td>
</tr>
</table>
</div>

View File

@@ -10,48 +10,3 @@
<a href="{% url 'virtualization:virtualmachine_disks' pk=object.virtual_machine.pk %}">{{ object.virtual_machine }}</a>
</li>
{% endblock %}
{% block content %}
<div class="row mb-3">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Virtual Disk" %}</h2>
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Virtual Machine" %}</th>
<td>{{ object.virtual_machine|linkify }}</td>
</tr>
<tr>
<th scope="row">{% trans "Name" %}</th>
<td>{{ object.name }}</td>
</tr>
<tr>
<th scope="row"><i class="mdi mdi-harddisk"></i> {% trans "Size" %}</th>
<td>
{% if object.size %}
{{ object.size|humanize_disk_megabytes }}
{% else %}
{{ ''|placeholder }}
{% endif %}
</td>
</tr>
<tr>
<th scope="row">{% trans "Description" %}</th>
<td>{{ object.description|placeholder }}</td>
</tr>
</table>
</div>
{% include 'inc/panels/tags.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-12 col-md-6">
{% include 'inc/panels/custom_fields.html' %}
{% plugin_right_page object %}
</div>
</div>
<div class="row">
<div class="col col-md-12">
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,2 @@
{% load helpers %}
{{ value|humanize_disk_megabytes }}

View File

@@ -1,8 +1,4 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% load plugins %}
{% load render_table from django_tables2 %}
{% load i18n %}
{% block breadcrumbs %}
{{ block.super }}
@@ -10,151 +6,3 @@
<a href="{% url 'virtualization:virtualmachine_interfaces' pk=object.virtual_machine.pk %}">{{ object.virtual_machine }}</a>
</li>
{% endblock %}
{% block content %}
<div class="row mb-3">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Interface" %}</h2>
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Virtual Machine" %}</th>
<td>{{ object.virtual_machine|linkify }}</td>
</tr>
<tr>
<th scope="row">{% trans "Name" %}</th>
<td>{{ object.name }}</td>
</tr>
<tr>
<th scope="row">{% trans "Enabled" %}</th>
<td>
{% if object.enabled %}
<span class="text-success"><i class="mdi mdi-check-bold"></i></span>
{% else %}
<span class="text-danger"><i class="mdi mdi-close"></i></span>
{% endif %}
</td>
</tr>
<tr>
<th scope="row">{% trans "Parent" %}</th>
<td>{{ object.parent|linkify|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Bridge" %}</th>
<td>{{ object.bridge|linkify|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Description" %}</th>
<td>{{ object.description|placeholder }} </td>
</tr>
<tr>
<th scope="row">{% trans "MTU" %}</th>
<td>{{ object.mtu|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "802.1Q Mode" %}</th>
<td>{{ object.get_mode_display|placeholder }}</td>
</tr>
{% if object.mode == 'q-in-q' %}
<tr>
<th scope="row">{% trans "Q-in-Q SVLAN" %}</th>
<td>{{ object.qinq_svlan|linkify|placeholder }}</td>
</tr>
{% endif %}
<tr>
<th scope="row">{% trans "Tunnel" %}</th>
<td>{{ object.tunnel_termination.tunnel|linkify|placeholder }}</td>
</tr>
</table>
</div>
{% include 'inc/panels/tags.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-12 col-md-6">
{% include 'inc/panels/custom_fields.html' %}
<div class="card">
<h2 class="card-header">{% trans "Addressing" %}</h2>
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "MAC Address" %}</th>
<td>
{% if object.primary_mac_address %}
<span class="font-monospace">{{ object.primary_mac_address|linkify }}</span>
<span class="badge text-bg-primary">{% trans "Primary" %}</span>
{% else %}
{{ ''|placeholder }}
{% endif %}
</td>
</tr>
<tr>
<th scope="row">{% trans "VRF" %}</th>
<td>{{ object.vrf|linkify|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "VLAN Translation" %}</th>
<td>{{ object.vlan_translation_policy|linkify|placeholder }}</td>
</tr>
</table>
</div>
{% include 'ipam/inc/panels/fhrp_groups.html' %}
{% plugin_right_page object %}
</div>
</div>
<div class="row mb-3">
<div class="col col-md-12">
<div class="card">
<h2 class="card-header">
{% trans "IP Addresses" %}
{% if perms.ipam.add_ipaddress %}
<div class="card-actions">
<a href="{% url 'ipam:ipaddress_add' %}?virtual_machine={{ object.virtual_machine.pk }}&vminterface={{ 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 IP Address" %}
</a>
</div>
{% endif %}
</h2>
{% htmx_table 'ipam:ipaddress_list' vminterface_id=object.pk %}
</div>
</div>
</div>
<div class="row mb-3">
<div class="col col-md-12">
<div class="card">
<h2 class="card-header">
{% trans "MAC Addresses" %}
{% if perms.ipam.add_macaddress %}
<div class="card-actions">
<a href="{% url 'dcim:macaddress_add' %}?virtual_machine={{ object.device.pk }}&vminterface={{ 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 MAC Address" %}
</a>
</div>
{% endif %}
</h2>
{% htmx_table 'dcim:macaddress_list' vminterface_id=object.pk %}
</div>
</div>
</div>
<div class="row mb-3">
<div class="col col-md-12">
{% include 'inc/panel_table.html' with table=vlan_table heading="VLANs" %}
</div>
</div>
{% if object.vlan_translation_policy %}
<div class="row mb-3">
<div class="col col-md-12">
{% include 'inc/panel_table.html' with table=vlan_translation_table heading="VLAN Translation" %}
</div>
</div>
{% endif %}
<div class="row mb-3">
<div class="col col-md-12">
{% include 'inc/panel_table.html' with table=child_interfaces_table heading="Child Interfaces" %}
</div>
</div>
<div class="row">
<div class="col col-md-12">
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-02-20 05:22+0000\n"
"POT-Creation-Date: 2026-02-19 05:26+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -472,7 +472,7 @@ msgstr ""
#: netbox/dcim/tables/devicetypes.py:214 netbox/dcim/tables/devicetypes.py:255
#: netbox/dcim/tables/devicetypes.py:274 netbox/dcim/tables/racks.py:30
#: netbox/extras/forms/bulk_edit.py:306 netbox/extras/tables/tables.py:552
#: netbox/netbox/ui/attrs.py:192 netbox/templates/circuits/circuittype.html:30
#: netbox/netbox/ui/attrs.py:190 netbox/templates/circuits/circuittype.html:30
#: netbox/templates/circuits/virtualcircuittype.html:30
#: netbox/templates/dcim/cable.html:44 netbox/templates/dcim/devicerole.html:38
#: netbox/templates/dcim/frontport.html:40
@@ -2129,7 +2129,7 @@ msgstr ""
msgid "Warning"
msgstr ""
#: netbox/core/constants.py:33 netbox/netbox/tables/columns.py:589
#: netbox/core/constants.py:33 netbox/netbox/tables/columns.py:590
#: netbox/templates/core/job.html:26
msgid "Error"
msgstr ""
@@ -4249,7 +4249,7 @@ msgstr ""
#: netbox/extras/forms/model_forms.py:691
#: netbox/extras/forms/model_forms.py:743 netbox/extras/ui/panels.py:69
#: netbox/netbox/forms/bulk_import.py:27 netbox/netbox/forms/mixins.py:131
#: netbox/netbox/tables/columns.py:495
#: netbox/netbox/tables/columns.py:496
#: netbox/templates/circuits/inc/circuit_termination.html:29
#: netbox/templates/generic/bulk_edit.html:78
#: netbox/templates/inc/panels/tags.html:5
@@ -12782,15 +12782,15 @@ msgstr ""
msgid "Chinese"
msgstr ""
#: netbox/netbox/tables/columns.py:183
#: netbox/netbox/tables/columns.py:184
msgid "Select all"
msgstr ""
#: netbox/netbox/tables/columns.py:196
#: netbox/netbox/tables/columns.py:197
msgid "Toggle all"
msgstr ""
#: netbox/netbox/tables/columns.py:316
#: netbox/netbox/tables/columns.py:317
#: netbox/templates/inc/table_controls_htmx.html:35
msgid "Toggle Dropdown"
msgstr ""
@@ -12817,13 +12817,7 @@ msgstr ""
msgid "Copy"
msgstr ""
#: netbox/netbox/ui/attrs.py:211
#, python-brace-format
msgid ""
"Invalid decoding option: {decoding}! Must be one of {image_decoding_choices}"
msgstr ""
#: netbox/netbox/ui/attrs.py:316
#: netbox/netbox/ui/attrs.py:286
msgid "GPS coordinates"
msgstr ""
@@ -16641,12 +16635,12 @@ msgstr ""
msgid "Required column header \"{header}\" not found."
msgstr ""
#: netbox/utilities/forms/widgets/apiselect.py:132
#: netbox/utilities/forms/widgets/apiselect.py:133
#, python-brace-format
msgid "Missing required value for dynamic query param: '{dynamic_params}'"
msgstr ""
#: netbox/utilities/forms/widgets/apiselect.py:149
#: netbox/utilities/forms/widgets/apiselect.py:150
#, python-brace-format
msgid "Missing required value for static query param: '{static_params}'"
msgstr ""

View File

@@ -3,6 +3,16 @@ from django.utils.translation import gettext_lazy as _
from netbox.ui import attrs, panels
class ClusterPanel(panels.ObjectAttributesPanel):
name = attrs.TextAttr('name')
type = attrs.RelatedObjectAttr('type', linkify=True)
status = attrs.ChoiceAttr('status')
description = attrs.TextAttr('description')
group = attrs.RelatedObjectAttr('group', linkify=True)
tenant = attrs.RelatedObjectAttr('tenant', linkify=True, grouped_by='group')
scope = attrs.GenericForeignKeyAttr('scope', linkify=True)
class VirtualMachinePanel(panels.ObjectAttributesPanel):
name = attrs.TextAttr('name')
status = attrs.ChoiceAttr('status')
@@ -32,3 +42,35 @@ class VirtualMachineClusterPanel(panels.ObjectAttributesPanel):
cluster = attrs.RelatedObjectAttr('cluster', linkify=True)
cluster_type = attrs.RelatedObjectAttr('cluster.type', linkify=True)
device = attrs.RelatedObjectAttr('device', linkify=True)
class VirtualDiskPanel(panels.ObjectAttributesPanel):
virtual_machine = attrs.RelatedObjectAttr('virtual_machine', linkify=True, label=_('Virtual Machine'))
name = attrs.TextAttr('name')
size = attrs.TemplatedAttr('size', template_name='virtualization/virtualdisk/attrs/size.html')
description = attrs.TextAttr('description')
class VMInterfacePanel(panels.ObjectAttributesPanel):
virtual_machine = attrs.RelatedObjectAttr('virtual_machine', linkify=True, label=_('Virtual Machine'))
name = attrs.TextAttr('name')
enabled = attrs.BooleanAttr('enabled')
parent = attrs.RelatedObjectAttr('parent_interface', linkify=True)
bridge = attrs.RelatedObjectAttr('bridge', linkify=True)
description = attrs.TextAttr('description')
mtu = attrs.TextAttr('mtu', label=_('MTU'))
mode = attrs.ChoiceAttr('mode', label=_('802.1Q Mode'))
qinq_svlan = attrs.RelatedObjectAttr('qinq_svlan', linkify=True, label=_('Q-in-Q SVLAN'))
tunnel_termination = attrs.RelatedObjectAttr('tunnel_termination.tunnel', linkify=True, label=_('Tunnel'))
class VMInterfaceAddressingPanel(panels.ObjectAttributesPanel):
title = _('Addressing')
primary_mac_address = attrs.TextAttr(
'primary_mac_address', label=_('MAC Address'), style='font-monospace', copy_button=True
)
vrf = attrs.RelatedObjectAttr('vrf', linkify=True, label=_('VRF'))
vlan_translation_policy = attrs.RelatedObjectAttr(
'vlan_translation_policy', linkify=True, label=_('VLAN Translation')
)

View File

@@ -14,6 +14,7 @@ from extras.ui.panels import CustomFieldsPanel, ImageAttachmentsPanel, TagsPanel
from extras.views import ObjectConfigContextView, ObjectRenderConfigView
from ipam.models import IPAddress, VLANGroup
from ipam.tables import InterfaceVLANTable, VLANTranslationRuleTable
from ipam.ui.panels import FHRPGroupAssignmentsPanel
from netbox.object_actions import (
AddObject,
BulkDelete,
@@ -25,7 +26,14 @@ from netbox.object_actions import (
EditObject,
)
from netbox.ui import actions, layout
from netbox.ui.panels import CommentsPanel, ObjectsTablePanel, TemplatePanel
from netbox.ui.panels import (
CommentsPanel,
ContextTablePanel,
ObjectsTablePanel,
OrganizationalObjectPanel,
RelatedObjectsPanel,
TemplatePanel,
)
from netbox.views import generic
from utilities.query import count_related
from utilities.query_functions import CollateAsChar
@@ -54,6 +62,17 @@ class ClusterTypeListView(generic.ObjectListView):
@register_model_view(ClusterType)
class ClusterTypeView(GetRelatedModelsMixin, generic.ObjectView):
queryset = ClusterType.objects.all()
layout = layout.SimpleLayout(
left_panels=[
OrganizationalObjectPanel(),
TagsPanel(),
],
right_panels=[
RelatedObjectsPanel(),
CustomFieldsPanel(),
CommentsPanel(),
],
)
def get_extra_context(self, request, instance):
return {
@@ -121,6 +140,17 @@ class ClusterGroupListView(generic.ObjectListView):
@register_model_view(ClusterGroup)
class ClusterGroupView(GetRelatedModelsMixin, generic.ObjectView):
queryset = ClusterGroup.objects.all()
layout = layout.SimpleLayout(
left_panels=[
OrganizationalObjectPanel(),
TagsPanel(),
],
right_panels=[
RelatedObjectsPanel(),
CustomFieldsPanel(),
CommentsPanel(),
],
)
def get_extra_context(self, request, instance):
return {
@@ -202,6 +232,18 @@ class ClusterListView(generic.ObjectListView):
@register_model_view(Cluster)
class ClusterView(GetRelatedModelsMixin, generic.ObjectView):
queryset = Cluster.objects.all()
layout = layout.SimpleLayout(
left_panels=[
panels.ClusterPanel(),
CommentsPanel(),
],
right_panels=[
TemplatePanel('virtualization/panels/cluster_resources.html'),
RelatedObjectsPanel(),
CustomFieldsPanel(),
TagsPanel(),
],
)
def get_extra_context(self, request, instance):
return {
@@ -507,6 +549,7 @@ class VirtualMachineBulkDeleteView(generic.BulkDeleteView):
# VM interfaces
#
@register_model_view(VMInterface, 'list', path='', detail=False)
class VMInterfaceListView(generic.ObjectListView):
queryset = VMInterface.objects.all()
@@ -518,6 +561,44 @@ class VMInterfaceListView(generic.ObjectListView):
@register_model_view(VMInterface)
class VMInterfaceView(generic.ObjectView):
queryset = VMInterface.objects.all()
layout = layout.SimpleLayout(
left_panels=[
panels.VMInterfacePanel(),
TagsPanel(),
],
right_panels=[
CustomFieldsPanel(),
panels.VMInterfaceAddressingPanel(),
FHRPGroupAssignmentsPanel(),
],
bottom_panels=[
ObjectsTablePanel(
model='ipam.IPaddress',
filters={'vminterface_id': lambda ctx: ctx['object'].pk},
actions=[
actions.AddObject(
'ipam.IPaddress',
url_params={
'virtual_machine': lambda ctx: ctx['object'].virtual_machine.pk,
'vminterface': lambda ctx: ctx['object'].pk,
},
),
],
),
ObjectsTablePanel(
model='dcim.MACAddress',
filters={'vminterface_id': lambda ctx: ctx['object'].pk},
actions=[
actions.AddObject(
'dcim.MACAddress', url_params={'vminterface': lambda ctx: ctx['object'].pk}
),
],
),
ContextTablePanel('vlan_table', title=_('Assigned VLANs')),
ContextTablePanel('vlan_translation_table', title=_('VLAN Translation')),
ContextTablePanel('child_interfaces_table', title=_('Child Interfaces')),
],
)
def get_extra_context(self, request, instance):
@@ -623,6 +704,15 @@ class VirtualDiskListView(generic.ObjectListView):
@register_model_view(VirtualDisk)
class VirtualDiskView(generic.ObjectView):
queryset = VirtualDisk.objects.all()
layout = layout.SimpleLayout(
left_panels=[
panels.VirtualDiskPanel(),
TagsPanel(),
],
right_panels=[
CustomFieldsPanel(),
],
)
@register_model_view(VirtualDisk, 'add', detail=False)