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 "Resources" %}

+ + + + + + + + + + + + + +
{% 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 "Virtual Machine" %}

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
{% 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 "Cluster" %}

- - - - - - - - - - - - - - - - - -
{% 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 "Resources" %}

- - - - - - - - - - - - - -
{% 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 %} -
-
-
-

- {% trans "Application Services" %} - {% if perms.ipam.add_service %} - - {% endif %} -

- {% htmx_table 'ipam:service_list' virtual_machine_id=object.pk %} -
- {% include 'inc/panels/image_attachments.html' %} - {% plugin_right_page object %} -
-
- -
-
-
-

- {% trans "Virtual Disks" %} - {% if perms.virtualization.add_virtualdisk %} - - {% endif %} -

- {% 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')