Merge pull request #21496 from netbox-community/20923-convert-virtualization-views-to-new-ui-layout

Closes #20923: Migrate Virtualization object views to declarative layouts
This commit is contained in:
bctiemann
2026-02-20 09:26:56 -05:00
committed by GitHub
16 changed files with 329 additions and 370 deletions

View File

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

@@ -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)