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
This commit is contained in:
Martin Hauser
2026-02-20 14:58:20 +01:00
parent 20fee95a9a
commit cc47afc401
16 changed files with 329 additions and 370 deletions

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)