diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py
index f920a0bb3..a516bd950 100644
--- a/netbox/dcim/views.py
+++ b/netbox/dcim/views.py
@@ -13,7 +13,6 @@ from django.utils.translation import gettext_lazy as _
from django.views.generic import View
from circuits.models import Circuit, CircuitTermination
-from dcim.ui import panels
from extras.ui.panels import CustomFieldsPanel, ImageAttachmentsPanel, TagsPanel
from extras.views import ObjectConfigContextView, ObjectRenderConfigView
from ipam.models import ASN, IPAddress, Prefix, VLAN, VLANGroup
@@ -44,6 +43,7 @@ from .choices import DeviceFaceChoices, InterfaceModeChoices
from .models import *
from .models.device_components import PortMapping
from .object_actions import BulkAddComponents, BulkDisconnect
+from .ui import panels
CABLE_TERMINATION_TYPES = {
'dcim.consoleport': ConsolePort,
diff --git a/netbox/templates/virtualization/panels/virtual_machine_resources.html b/netbox/templates/virtualization/panels/virtual_machine_resources.html
new file mode 100644
index 000000000..b0ad7c07e
--- /dev/null
+++ b/netbox/templates/virtualization/panels/virtual_machine_resources.html
@@ -0,0 +1,34 @@
+{% load helpers %}
+{% load i18n %}
+
+
+
+
+
+ | {% trans "Virtual CPUs" %} |
+ {{ object.vcpus|placeholder }} |
+
+
+ | {% trans "Memory" %} |
+
+ {% if object.memory %}
+ {{ object.memory|humanize_ram_megabytes }}
+ {% else %}
+ {{ ''|placeholder }}
+ {% endif %}
+ |
+
+
+ |
+ {% trans "Disk Space" %}
+ |
+
+ {% if object.disk %}
+ {{ object.disk|humanize_disk_megabytes }}
+ {% else %}
+ {{ ''|placeholder }}
+ {% endif %}
+ |
+
+
+
diff --git a/netbox/templates/virtualization/virtualmachine.html b/netbox/templates/virtualization/virtualmachine.html
index 1ee566eb0..42b17b0ba 100644
--- a/netbox/templates/virtualization/virtualmachine.html
+++ b/netbox/templates/virtualization/virtualmachine.html
@@ -1,199 +1 @@
{% extends 'virtualization/virtualmachine/base.html' %}
-{% load buttons %}
-{% load static %}
-{% load helpers %}
-{% load plugins %}
-{% load i18n %}
-
-{% block content %}
-
-
-
-
-
-
- | {% trans "Name" %} |
- {{ object }} |
-
-
- | {% trans "Status" %} |
- {% badge object.get_status_display bg_color=object.get_status_color %} |
-
-
- | {% trans "Start on boot" %} |
- {% badge object.get_start_on_boot_display bg_color=object.get_start_on_boot_color %} |
-
-
- | {% trans "Role" %} |
- {{ object.role|linkify|placeholder }} |
-
-
- | {% trans "Platform" %} |
- {{ object.platform|linkify|placeholder }} |
-
-
- | {% trans "Description" %} |
- {{ object.description|placeholder }} |
-
-
- | {% trans "Serial Number" %} |
- {{ object.serial|placeholder }} |
-
-
- | {% trans "Tenant" %} |
-
- {% if object.tenant.group %}
- {{ object.tenant.group|linkify }} /
- {% endif %}
- {{ object.tenant|linkify|placeholder }}
- |
-
-
- | {% trans "Config Template" %} |
- {{ object.config_template|linkify|placeholder }} |
-
-
- | {% trans "Primary IPv4" %} |
-
- {% if object.primary_ip4 %}
- {{ object.primary_ip4.address.ip }}
- {% if object.primary_ip4.nat_inside %}
- ({% trans "NAT for" %} {{ object.primary_ip4.nat_inside.address.ip }})
- {% elif object.primary_ip4.nat_outside.exists %}
- ({% trans "NAT" %}: {% for nat in object.primary_ip4.nat_outside.all %}{{ nat.address.ip }}{% if not forloop.last %}, {% endif %}{% endfor %})
- {% endif %}
- {% copy_content "primary_ip4" %}
- {% else %}
- {{ ''|placeholder }}
- {% endif %}
- |
-
-
- | {% trans "Primary IPv6" %} |
-
- {% if object.primary_ip6 %}
- {{ object.primary_ip6.address.ip }}
- {% if object.primary_ip6.nat_inside %}
- ({% trans "NAT for" %} {{ object.primary_ip6.nat_inside.address.ip }})
- {% elif object.primary_ip6.nat_outside.exists %}
- ({% trans "NAT" %}: {% for nat in object.primary_ip6.nat_outside.all %}{{ nat.address.ip }}{% if not forloop.last %}, {% endif %}{% endfor %})
- {% endif %}
- {% copy_content "primary_ip6" %}
- {% else %}
- {{ ''|placeholder }}
- {% endif %}
- |
-
-
-
- {% include 'inc/panels/custom_fields.html' %}
- {% include 'inc/panels/tags.html' %}
- {% include 'inc/panels/comments.html' %}
- {% plugin_left_page object %}
-
-
-
-
-
-
- | {% trans "Site" %} |
-
- {{ object.site|linkify|placeholder }}
- |
-
-
- | {% trans "Cluster" %} |
-
- {% if object.cluster.group %}
- {{ object.cluster.group|linkify }} /
- {% endif %}
- {{ object.cluster|linkify|placeholder }}
- |
-
-
- | {% trans "Cluster Type" %} |
-
- {{ object.cluster.type|linkify|placeholder }}
- |
-
-
- | {% trans "Device" %} |
-
- {{ object.device|linkify|placeholder }}
- |
-
-
-
-
-
-
-
- | {% trans "Virtual CPUs" %} |
- {{ object.vcpus|placeholder }} |
-
-
- | {% trans "Memory" %} |
-
- {% if object.memory %}
- {{ object.memory|humanize_ram_megabytes }}
- {% else %}
- {{ ''|placeholder }}
- {% endif %}
- |
-
-
- |
- {% trans "Disk Space" %}
- |
-
- {% if object.disk %}
- {{ object.disk|humanize_disk_megabytes }}
- {% else %}
- {{ ''|placeholder }}
- {% endif %}
- |
-
-
-
-
-
- {% htmx_table 'ipam:service_list' virtual_machine_id=object.pk %}
-
- {% include 'inc/panels/image_attachments.html' %}
- {% plugin_right_page object %}
-
-
-
-
-
-
-
- {% htmx_table 'virtualization:virtualdisk_list' virtual_machine_id=object.pk %}
-
-
-
-
-
-
- {% plugin_full_width_page object %}
-
-
-{% endblock %}
diff --git a/netbox/templates/virtualization/virtualmachine/attrs/ipaddress.html b/netbox/templates/virtualization/virtualmachine/attrs/ipaddress.html
new file mode 100644
index 000000000..7b4345657
--- /dev/null
+++ b/netbox/templates/virtualization/virtualmachine/attrs/ipaddress.html
@@ -0,0 +1,10 @@
+{% load i18n %}
+{{ value.address.ip }}
+{% if value.nat_inside %}
+ ({% trans "NAT for" %} {{ value.nat_inside.address.ip }})
+{% elif value.nat_outside.exists %}
+ ({% trans "NAT" %}: {% for nat in value.nat_outside.all %}{{ nat.address.ip }}{% if not forloop.last %}, {% endif %}{% endfor %})
+{% endif %}
+
+
+
diff --git a/netbox/virtualization/ui/__init__.py b/netbox/virtualization/ui/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/netbox/virtualization/ui/panels.py b/netbox/virtualization/ui/panels.py
new file mode 100644
index 000000000..bff967cb6
--- /dev/null
+++ b/netbox/virtualization/ui/panels.py
@@ -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)
diff --git a/netbox/virtualization/views.py b/netbox/virtualization/views.py
index b7aca4d73..6def3f5a1 100644
--- a/netbox/virtualization/views.py
+++ b/netbox/virtualization/views.py
@@ -10,12 +10,15 @@ from dcim.filtersets import DeviceFilterSet
from dcim.forms import DeviceFilterForm
from dcim.models import Device
from dcim.tables import DeviceTable
+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 netbox.object_actions import (
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 utilities.query import count_related
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 .models import *
from .object_actions import BulkAddComponents
+from .ui import panels
#
@@ -336,6 +340,7 @@ class ClusterAddDevicesView(generic.ObjectEditView):
# Virtual machines
#
+
@register_model_view(VirtualMachine, 'list', path='', detail=False)
class VirtualMachineListView(generic.ObjectListView):
queryset = VirtualMachine.objects.prefetch_related('primary_ip4', 'primary_ip6')
@@ -348,6 +353,44 @@ class VirtualMachineListView(generic.ObjectListView):
@register_model_view(VirtualMachine)
class VirtualMachineView(generic.ObjectView):
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')