mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-08 11:16:47 +00:00
feat(instances): a template's page is a template's
A template's page was a VM's: it opened on Power, with only a note there that a template is not started, while its clone form waited in a section of Settings, next to a console, access, snapshots and stats that a VM that never runs has no use for. A template's page now carries a Template badge and a Clone button, opens on the clone form, and leaves out Power, Access, Snapshot, Stats, the console settings, host devices and the guest agent and address lines; Resize, Settings and Destroy stay for whoever may change it. A page that no section opens shows its first.
This commit is contained in:
parent
8a809443a4
commit
dbc0115c06
3 changed files with 53 additions and 19 deletions
|
|
@ -20,17 +20,22 @@
|
|||
<h1 class="h4 mb-0" id="vm-name">{{ instance.name }}</h1>
|
||||
{% vm_state instance.status as state %}
|
||||
<span class="badge text-bg-{{ state.1 }}">{{ state.0 }}</span>
|
||||
{% if instance.is_template %}<span class="badge text-bg-light border">{% trans "Template" %}</span>{% endif %}
|
||||
</div>
|
||||
{% if instance.title %}<p class="text-body-secondary mb-1">{{ instance.title }}</p>{% endif %}
|
||||
{% include 'instances/host_link.html' %}
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
{% if instance.is_template and can_clone %}
|
||||
<button type="button" class="btn btn-sm btn-primary" onclick="show_tab('#settings'); show_tab('#clone');">{% bs_icon 'copy' %} {% trans "Clone" %}</button>
|
||||
{% endif %}
|
||||
{% if instance.guest_agent_ready %}
|
||||
<button type="button" class="btn btn-sm btn-secondary" onclick="get_osinfo();">{% bs_icon 'info-circle' %} {% trans "OS details" %}</button>
|
||||
{% endif %}
|
||||
<a class="btn btn-sm btn-secondary" href="{% url 'instances:instance' instance.id %}">{% bs_icon 'arrow-clockwise' %} {% trans "Refresh" %}</a>
|
||||
</div>
|
||||
</div>
|
||||
{% if not instance.is_template %}
|
||||
<div class="d-flex flex-wrap column-gap-3 row-gap-1 small mt-2">
|
||||
{% if not instance.guest_agent %}
|
||||
<span>{% bs_icon 'dash-circle' %} {% trans "Guest agent off" %}</span>
|
||||
|
|
@ -41,6 +46,7 @@
|
|||
{% endif %}
|
||||
<span>{% bs_icon 'camera' %} {% blocktrans count counter=instance.snapshots|length %}{{ counter }} snapshot{% plural %}{{ counter }} snapshots{% endblocktrans %}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
<dl class="row g-2 small mb-0 mt-1">
|
||||
<div class="col-6 col-lg-2">
|
||||
<dt class="fw-normal text-body-secondary">{% trans "CPU" %}</dt>
|
||||
|
|
@ -54,24 +60,29 @@
|
|||
<dt class="fw-normal text-body-secondary">{% trans "Disks" %}</dt>
|
||||
<dd class="mb-0">{% for disk in instance.disks %}{{ disk.dev }} {{ disk.size|filesizeformat }}{% if not forloop.last %} · {% endif %}{% empty %}{% trans "None" %}{% endfor %}</dd>
|
||||
</div>
|
||||
{% if not instance.is_template %}
|
||||
<div class="col-6 col-lg-3">
|
||||
<dt class="fw-normal text-body-secondary">{% trans "IPv4" %}</dt>
|
||||
<dd class="mb-0 font-monospace">{{ ipv4_addresses|join:", "|default:"—" }}</dd>
|
||||
</div>
|
||||
{% endif %}
|
||||
</dl>
|
||||
</div>
|
||||
<div class="card-footer p-2">
|
||||
<ul class="nav nav-pills flex-wrap flex-lg-nowrap gap-1 small" id="navbtn" role="tablist" aria-label="{% trans "Instance actions" %}">
|
||||
{# a template is cloned, not run: no power, access, snapshots or stats #}
|
||||
{% if not instance.is_template %}
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link text-nowrap active" id="power-tab" aria-controls="power" data-bs-toggle="pill" data-bs-target="#power" type="button" role="tab" aria-selected="true">{% bs_icon 'power' %} {% trans "Power" %}</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link text-nowrap" id="access-tab" aria-controls="access" data-bs-toggle="pill" data-bs-target="#access" type="button" role="tab" aria-selected="false">{% bs_icon 'lock' %} {% trans "Access" %}</button>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link text-nowrap" id="resize-tab" aria-controls="resize" data-bs-toggle="pill" data-bs-target="#resize" type="button" role="tab" aria-selected="false">{% bs_icon 'arrows-angle-expand' %} {% trans "Resize" %}</button>
|
||||
</li>
|
||||
{% if can_snapshot %}
|
||||
{% if can_snapshot and not instance.is_template %}
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link text-nowrap" id="snapshots-tab" aria-controls="snapshots" data-bs-toggle="pill" data-bs-target="#snapshots" type="button" role="tab" aria-selected="false">{% bs_icon 'camera' %} {% trans "Snapshot" %}</button>
|
||||
</li>
|
||||
|
|
@ -81,9 +92,11 @@
|
|||
<button class="nav-link text-nowrap" id="settings-tab" aria-controls="settings" data-bs-toggle="pill" data-bs-target="#settings" type="button" role="tab" aria-selected="false">{% bs_icon 'gear-wide-connected' %} {% trans "Settings" %}</button>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if not instance.is_template %}
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link text-nowrap" id="graphics-tab" aria-controls="graphics" data-bs-toggle="pill" data-bs-target="#graphics" type="button" role="tab" aria-selected="false">{% bs_icon 'bar-chart-line' %} {% trans "Stats" %}</button>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link text-nowrap" id="undefine-tab" aria-controls="undefine" data-bs-toggle="pill" data-bs-target="#undefine" type="button" role="tab" aria-selected="false">{% bs_icon 'trash' %} {% trans "Destroy" %}</button>
|
||||
</li>
|
||||
|
|
@ -95,12 +108,14 @@
|
|||
<div id="mainTabMenu">
|
||||
<!-- Tab panes -->
|
||||
<div class="tab-content" id=mainTabMenuContent>
|
||||
{% if not instance.is_template %}
|
||||
{% include 'instances/power_tab.html' %}
|
||||
{% include 'instances/access_tab.html' %}
|
||||
{% endif %}
|
||||
{% include 'instances/resize_tab.html' %}
|
||||
{% if can_snapshot %}{% include 'instances/snapshots_tab.html' %}{% endif %}
|
||||
{% if can_snapshot and not instance.is_template %}{% include 'instances/snapshots_tab.html' %}{% endif %}
|
||||
{% if show_settings %}{% include 'instances/settings_tab.html' %}{% endif %}
|
||||
{% include 'instances/stats_tab.html' %}
|
||||
{% if not instance.is_template %}{% include 'instances/stats_tab.html' %}{% endif %}
|
||||
{% include 'instances/destroy_tab.html' %}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -248,6 +263,7 @@
|
|||
<script src="{% static 'js/chart.umd.min.js' %}"></script>
|
||||
<script src="{% static 'js/charts.js' %}"></script>
|
||||
<script src="{% static 'js/poll.js' %}"></script>
|
||||
{% if not instance.is_template %}
|
||||
<script>
|
||||
{% trans "average over" as average_over %}
|
||||
{% trans "In" as rx_label %}{% trans "Out" as tx_label %}{% trans "Read" as rd_label %}{% trans "Write" as wr_label %}
|
||||
|
|
@ -331,6 +347,7 @@
|
|||
}, 0);
|
||||
});
|
||||
</script>
|
||||
{% endif %}
|
||||
<script>
|
||||
backgroundJobRunning = false;
|
||||
// not "status": that is window.status, which the browser may not keep
|
||||
|
|
@ -366,7 +383,13 @@
|
|||
}
|
||||
// after the whole page is parsed: load_osinfo is defined further down
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
restoreInstanceTab(document, location.hash, show_tab, load_osinfo);
|
||||
// a template opens on its clone form
|
||||
restoreInstanceTab(document, location.hash || "{% if instance.is_template and can_clone %}#clone{% endif %}", show_tab, load_osinfo);
|
||||
// none open (a template has no Power tab): the first section the page has
|
||||
if (!document.querySelector("#navbtn .nav-link.active")) {
|
||||
var first = document.querySelector("#navbtn .nav-link");
|
||||
if (first) { show_tab(first.getAttribute("data-bs-target")); }
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<script>
|
||||
|
|
|
|||
|
|
@ -248,17 +248,33 @@ class TemplatePageControlsTestCase(PageControls, TestCase):
|
|||
self.assertEqual(self.controls(viewer, status=5), {"clone"})
|
||||
|
||||
def test_only_staff_owners_change_a_template(self):
|
||||
# its access (root password, SSH key) is for a VM that runs: not on a template's page
|
||||
owner = self.user("tpl_plain_changer", owner={"is_change": True})
|
||||
staff = self.user("tpl_staff_owner", staff=True, owner={"is_change": True})
|
||||
self.assertEqual(self.controls(owner, status=5) & CHANGE, set())
|
||||
self.assertEqual(self.controls(staff, status=5) & CHANGE, CHANGE)
|
||||
self.assertEqual(self.controls(staff, status=5) & CHANGE, {"resize", "options"})
|
||||
|
||||
def test_console_settings_of_a_template_are_for_staff_owners(self):
|
||||
def test_a_template_s_page_has_no_console_settings_or_snapshots(self):
|
||||
# a template is cloned, not run
|
||||
flags = {"is_change": True, "is_vnc": True}
|
||||
owner = self.user("tpl_vnc_owner", owner=flags)
|
||||
staff = self.user("tpl_vnc_staff", staff=True, owner=flags)
|
||||
self.assertNotIn("console settings", self.controls(owner, status=5))
|
||||
self.assertIn("console settings", self.controls(staff, status=5))
|
||||
perms = ("clone_instances", "snapshot_instances")
|
||||
for user in (self.user("tpl_vnc_owner", perms=perms, owner=flags),
|
||||
self.user("tpl_vnc_staff", staff=True, perms=perms, owner=flags)):
|
||||
with self.subTest(user=user.username):
|
||||
controls = self.controls(user, status=5)
|
||||
self.assertNotIn("console settings", controls)
|
||||
self.assertNotIn("snapshots", controls)
|
||||
|
||||
def test_a_template_s_page_opens_on_its_clone_form(self):
|
||||
viewer = self.user("tpl_cloner", perms=("view_instances", "clone_instances"))
|
||||
self.controls(viewer, status=5)
|
||||
doc = lxml_html.fromstring(self.last_page)
|
||||
self.assertEqual(doc.xpath("string(//section[@aria-labelledby='vm-name']//span[@class='badge text-bg-light border'])"),
|
||||
"Template")
|
||||
self.assertTrue(doc.xpath("//button[contains(@onclick, \"show_tab('#clone')\")]"))
|
||||
self.assertIn('location.hash || "#clone"', self.last_page)
|
||||
for pane in ("power", "access", "graphics", "vncsettings", "hostdevs"):
|
||||
self.assertEqual(doc.xpath("//*[@id=$p]", p=pane), [], pane)
|
||||
|
||||
def test_only_staff_owners_destroy_a_template(self):
|
||||
owner = self.user("tpl_deleter", owner={"is_delete": True})
|
||||
|
|
@ -266,13 +282,6 @@ class TemplatePageControlsTestCase(PageControls, TestCase):
|
|||
self.assertNotIn("destroy", self.controls(owner, status=5))
|
||||
self.assertIn("destroy", self.controls(staff, status=5))
|
||||
|
||||
def test_snapshots_of_a_template_are_for_staff_owners(self):
|
||||
perms = ("clone_instances", "snapshot_instances")
|
||||
owner = self.user("tpl_changer", perms=perms, owner={"is_change": True})
|
||||
staff = self.user("tpl_staff_changer", staff=True, perms=perms, owner={"is_change": True})
|
||||
self.assertNotIn("snapshots", self.controls(owner, status=5))
|
||||
self.assertIn("snapshots", self.controls(staff, status=5))
|
||||
|
||||
|
||||
class TemplateSnapshotRuleTestCase(PageControls, TestCase):
|
||||
"""The backend side of the template rule: only staff owners (and
|
||||
|
|
|
|||
|
|
@ -273,7 +273,8 @@ def _instance_page(request, pk, instance, compute):
|
|||
keymaps = settings.QEMU_KEYMAPS
|
||||
# The console form renders the VNC password, so only users who may manage
|
||||
# console settings get the form (and the password) at all.
|
||||
can_manage_console = utils.can_manage_console(request.user, instance)
|
||||
# a template is cloned, not run: no console settings or host devices on its page
|
||||
can_manage_console = utils.can_manage_console(request.user, instance) and not instance.is_template
|
||||
console_form = None
|
||||
if can_manage_console:
|
||||
console_form = ConsoleForm(instance=instance, is_superuser=request.user.is_superuser)
|
||||
|
|
@ -350,7 +351,8 @@ def _instance_page(request, pk, instance, compute):
|
|||
for row in pci_hostdevs:
|
||||
row["name"] = names.get(row["address"], "")
|
||||
# owners see the devices their VM has; only superusers change them
|
||||
show_hostdevs = request.user.is_superuser or bool(usb_hostdevs or pci_hostdevs or other_hostdevs)
|
||||
show_hostdevs = not instance.is_template and (
|
||||
request.user.is_superuser or bool(usb_hostdevs or pci_hostdevs or other_hostdevs))
|
||||
show_settings = show_settings or show_hostdevs
|
||||
|
||||
# what keeps the Migrate tab from migrating the VM to any destination
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue