1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-08 11:16:47 +00:00
webvirtcloud/instances/templates/instance.html
catborise 60f12cefa1 feat(instances): a VM's access in one strip, its console password kept to its rule
The Access tab hid the console, the root password, the SSH key and the
remote viewer behind four sub-tabs. The owner chose a compact layout
from three rendered options: the state, a strip with the console and
the remote viewer, then two rows for root's password and key, each
saying why it waits when it does.

Codex reviewed access end to end; these are fixed:

- console.vv carries the console password only for users with the
  passwordless console permission, as the console page passes it; it
  gave it to every user who may open the console.
- The root password field is a labelled password input, not plain text
  with a 24-character limit.
- Without a saved public key the key row is disabled and points to the
  profile, instead of offering "None".
- A paused VM's console opens, as the console view allows.
- The remote viewer address and console.vv show only for a VNC or SPICE
  console that listens beyond the host's loopback; a viewer on the
  user's computer cannot reach the others, and a serial console gave a
  pty://…:None address.
2026-10-30 20:39:40 +03:00

440 lines
21 KiB
HTML

{% extends "base.html" %}
{% load static %}
{% load vm_states %}
{% load i18n %}
{% load operations %}
{% load bootstrap_icons %}
{% block title %}{% trans "Instance" %} - {{ instance.name }}{% endblock %}
{% block page_header %}{% endblock page_header %}{# the card below has the heading #}
{% block content %}
{% include 'pleasewaitdialog.html' %}
{# the VM at a glance in one card: who and where it is, what it has, and its sections as the card's footer #}
<section class="card mb-3" aria-labelledby="vm-name">
<div class="card-body p-3">
<div class="d-flex flex-wrap justify-content-between align-items-start gap-2">
<div>
<div class="d-flex align-items-center gap-2">
<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>
</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.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>
<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>
{% elif instance.guest_agent_ready %}
<span><span class="text-success">{% bs_icon 'check-circle' %}</span> {% trans "Guest agent connected" %}</span>
{% else %}
<span><span class="text-warning">{% bs_icon 'exclamation-circle' %}</span> {% trans "Guest agent not connected" %}</span>
{% endif %}
<span>{% bs_icon 'camera' %} {% blocktrans count counter=instance.snapshots|length %}{{ counter }} snapshot{% plural %}{{ counter }} snapshots{% endblocktrans %}</span>
</div>
<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>
<dd class="mb-0">{% firstof instance.cur_vcpu instance.vcpu %} {% trans "of" %} {{ instance.vcpu }} {% trans "vCPU" %}</dd>
</div>
<div class="col-6 col-lg-2">
<dt class="fw-normal text-body-secondary">{% trans "Memory" %}</dt>
<dd class="mb-0">{{ instance.cur_memory }} {% trans "MB" %}</dd>
</div>
<div class="col-6 col-lg-5">
<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>
<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>
</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" %}">
<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>
<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 %}
<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>
{% endif %}
{% if show_settings %}
<li class="nav-item" role="presentation">
<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 %}
<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>
<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>
</ul>
</div>
</section>
<div id="job-panel" aria-live="polite"></div>
<div id="mainTabMenu">
<!-- Tab panes -->
<div class="tab-content" id=mainTabMenuContent>
{% include 'instances/power_tab.html' %}
{% include 'instances/access_tab.html' %}
{% include 'instances/resize_tab.html' %}
{% if can_snapshot %}{% include 'instances/snapshots_tab.html' %}{% endif %}
{% if show_settings %}{% include 'instances/settings_tab.html' %}{% endif %}
{% include 'instances/stats_tab.html' %}
{% include 'instances/destroy_tab.html' %}
</div>
</div>
{% endblock %}
{% block script %}
<script src="{% static 'js/ace/ace.js' %}" type="text/javascript" charset="utf-8"></script>
<script>
function get_volumes(compute_id, pool) {
get_vol_url = "/computes/" + compute_id + "/storage/" + pool + "/volumes";
$.getJSON(get_vol_url, function (data) {
$("#vols").find('option').remove();
$("#vols").removeAttr("disabled");
$.each(data['vols'], function(i, item) {
$("#vols").append($("<option>").val(item).text(item));
})
});
var sto_drop = document.getElementById('select_storage');
sto_drop.value = pool;
sto_drop.textContent = pool;
var caret = document.createElement("span");
caret.className = "caret";
sto_drop.appendChild(caret);
var sto_input = document.getElementById('selected_storage');
sto_input.setAttribute('value', pool);
//sto_input.innerHTML = pool;
}
</script>
<script src="{% static 'js/xml_editor.js' %}"></script>
<script>
function random_mac(net) {
$.getJSON("{% url 'instances:random_mac_address' %}", function (data) {
$('input[name="' + net + '"]').val(data['mac']);
});
}
</script>
<script>
function guess_mac_address(src_elem, net) {
new_vname = $(src_elem).val();
guess_mac_address_url = "{% url 'instances:guess_mac_address' 1 %}".replace(1, new_vname);
$.getJSON(guess_mac_address_url, function(data) {
$('input[name="clone-net-mac-'+net+'"]').val(data['mac']);
});
}
</script>
<script>
function guess_clone_name() {
$.getJSON("{% url 'instances:guess_clone_name' %}", function(data) {
// a free name of the DHCP file, if there is one
if (!data.name) {
$('#clone-name-help').text("{{ _("The DHCP file has no free name to suggest.")|escapejs }}");
return;
}
$('#clone-name-help').text('');
guessed_name = data['name'].split(".")[0];
$('#clone_name').val(guessed_name);
update_clone_disk_name(guessed_name);
guess_mac_address('#clone_name', 0);
});
}
</script>
<script>
function update_clone_disk_name(new_vname) {
vname = '{{ instance.name|escapejs }}';
{% for disk in instance.config_disks %}
disk_name = '{{ disk.image|escapejs }}';
disk_dot = disk_name.split('.');
disk_dot_suffix = disk_dot[disk_dot.length-1];
if (disk_name.startsWith(vname)) {
image = disk_name.replace(vname, new_vname);
} else if (disk_name.lastIndexOf('.') > -1 && disk_dot_suffix.length <= 7) {
disk_dot.pop();
disk_name_only = disk_dot.join('-');
image = new_vname + "-" + disk_name_only + "." + disk_dot_suffix
} else if (new_vname != disk_name) {
image = new_vname
} else {
image = new_vname + '-clone';
}
$('#disk_name-{{ disk.dev|escapejs }}').val(image);
{% endfor %}
}
</script>
<script>
$('#clone_name').on('input', function () {
update_clone_disk_name($(this).val());
});
$(document).ready(function () {
// get video model or fall back to default
let video_model = "{{ instance.config_video_model }}";
if (video_model != '') {
$("#video_model_select option[value='" + video_model + "']").prop('selected', true);
}
});
{% if can_open_console and vdi_reachable %}
$(document).ready(function () {
// set vdi url
$.get("{% url 'vdi_url' compute.id instance.name %}", function(data) {
$("#vdi_url_input").attr("value", data);
$("#vdi_url").attr("href", data);
});
});
{% endif %}
{% if request.user.is_superuser %}
$(document).ready(function () {
// every NIC of the clone a new address: one the VM has would clash with it
$('input[name^="clone-net-mac-"]').each(function () { random_mac(this.name); });
random_mac('add-net-mac');
update_clone_disk_name($('#clone_name').val());
});
{% endif %}
</script>
<script>
$(document).ready(function(){
$('[data-bs-toggle="popover"]').popover({
placement : 'top'
});
});
</script>
<script>
// the boot order: the checked devices, top to bottom, as "disk:vda,cdrom:sda"
function set_orderlist() {
var order = $('#b_order input:checked').map(function () { return this.value; }).get();
$('#bootorder').val(order.join(','));
}
$(function () {
set_orderlist();
$('#b_order').on('change', 'input', set_orderlist);
$('#boot_order_up').on('click', function () {
$('#b_order label input:checked').each(function () {
var label = $(this).parent();
var newPos = label.index() - 1;
if (newPos > -1) {
$('#b_order label').eq(newPos).before(label);
}
});
set_orderlist();
});
$('#boot_order_down').on('click', function () {
var countOptions = $('#b_order label').length;
var countSelected = $('#b_order label input:checked').length;
$('#b_order label input:checked').each(function () {
var label = $(this).parent();
var newPos = label.index() + countSelected;
if (newPos < countOptions) {
$('#b_order label').eq(newPos).after(label);
}
});
set_orderlist();
});
});
</script>
<script src="{% static 'js/chart.umd.min.js' %}"></script>
<script src="{% static 'js/charts.js' %}"></script>
<script src="{% static 'js/poll.js' %}"></script>
<script>
{% trans "average over" as average_over %}
var stats_tab = document.querySelector('#graphics-tab');
var stats_poll = null;
// the seconds each CPU point averages
var cpuWindows = [];
// the usage sample of the previous answer: the next one averages since then
var statsSample = null;
// The charts are polled while the Stats tab is shown; they are made once.
stats_tab.addEventListener('hidden.bs.tab', function (event) {
stats_poll.stop();
});
stats_tab.addEventListener('shown.bs.tab', function (event) {
if (stats_poll !== null) {
stats_poll.start();
return;
}
var cpuChart = lineChart(document.getElementById("cpuChart"), "%", ["Usage"], {
max: 100,
tooltip: function (context, text) {
var span = cpuWindows[context.dataIndex];
return span ? text + ' ({{ average_over|escapejs }} ' + span + ' s)' : text;
},
});
var memChart = lineChart(document.getElementById("memChart"), "MB", ["Usage (RSS)"]);
var diskChart = {};
{% for disk in instance.disks %}
diskChart['{{ disk.dev|escapejs }}'] = lineChart(document.getElementById("blk{{ disk.dev|escapejs }}Chart"), "MB/s", ["Read", "Write"]);
{% endfor %}
var netChart = {};
{% for net in instance.networks %}
netChart[{{ forloop.counter0 }}] = lineChart(document.getElementById("netEth{{ forloop.counter0 }}Chart"), "Mbit/s", ["Inbound", "Outbound"]);
{% endfor %}
// the first point at once, then every 5 s like the host graph
stats_poll = pollWhileVisible(5000, function () {
return $.getJSON("{% url 'instances:stats' instance.id %}", statsSample ? {sample: statsSample} : {})
.fail(function () {
statsSample = null;
});
}, function graph_usage(data) {
statsSample = data.sample || null;
cpuWindows.push(data.window);
if (cpuWindows.length > CHART_POINTS) {
cpuWindows.shift();
}
addPoint(cpuChart, data.timeline, [data.cpudata]);
// a VM whose memory could not be read gives total 0: a gap, same axis
if (data.memdata.total) {
memChart.options.scales.y.max = Math.round(data.memdata.total / 1024);
addPoint(memChart, data.timeline, [Math.round(data.memdata.used / 1024)]);
} else {
addPoint(memChart, data.timeline, [null]);
}
// a disk or NIC added after the page loaded has no chart until a reload
function push(chart, values) {
if (chart) {
addPoint(chart, data.timeline, values);
}
}
data.blkdata.forEach(function (disk) { push(diskChart[disk.dev], disk.data); });
data.netdata.forEach(function (nic) { push(netChart[nic.dev], nic.data); });
}, 0);
});
</script>
<script>
backgroundJobRunning = false;
// not "status": that is window.status, which the browser may not keep
var vm_status = {{ instance.status|lower }};
var status_poll = pollWhileVisible(5000, function () {
return $.getJSON("{% url 'instances:status' instance.id %}");
}, function get_status(data) {
// an XML edit not saved yet stays: the reload waits until it is saved or undone
if (data['status'] != vm_status && !backgroundJobRunning && !(window.xmlEditUnsaved && window.xmlEditUnsaved())) {
window.location.reload()
}
});
// Stop getting status info before delete instance
$('#delete_form').submit(function(){
status_poll.stop();
return true;
});
</script>
{% job_panel_script job_resource %}
<script src="{% static 'js/instance_tabs.js' %}"></script>
<script>
// Open the tab whose control targets "#<pane id>"
function show_tab(target) {
var controls = document.querySelectorAll('[data-bs-target], a[data-bs-toggle][href]');
for (var i = 0; i < controls.length; i++) {
var c = controls[i];
if ((c.getAttribute('data-bs-target') || c.getAttribute('href')) === target) {
bootstrap.Tab.getOrCreateInstance(c).show();
return;
}
}
}
// after the whole page is parsed: load_osinfo is defined further down
document.addEventListener('DOMContentLoaded', function () {
restoreInstanceTab(document, location.hash, show_tab, load_osinfo);
});
</script>
<script>
// the Host devices tab's add forms: loaded once, when the tab first opens
var hostDevices = document.getElementById("host-devices");
if (hostDevices) {
document.querySelector('[data-bs-target="#hostdevs"]').addEventListener("shown.bs.tab", function () {
if (hostDevices.dataset.loaded) return;
hostDevices.dataset.loaded = "true";
fetch(hostDevices.dataset.url).then(function (response) {
if (!response.ok) throw new Error(response.statusText);
return response.text();
}).then(function (html) {
hostDevices.innerHTML = html;
}).catch(function () {
hostDevices.textContent = "{{ _("The host's devices could not be read.")|escapejs }}";
});
});
}
// the Migrate tab's disks: loaded for the chosen destination when the tab opens or the choice changes
// the form is not sent until the disks of the chosen destination are shown
var migratePlan = document.getElementById("migrate-plan");
var migrateCompute = document.getElementById("migrate_compute");
var migrateLoading = null;
function loadMigratePlan() {
if (!migrateCompute.value) return;
if (migrateLoading) migrateLoading.abort();
var loading = migrateLoading = new AbortController();
var submit = migratePlan.closest("form").querySelector('button[name="migrate"]');
submit.disabled = true;
migratePlan.textContent = "{{ _("Checking the disks…")|escapejs }}";
fetch(migratePlan.dataset.url + "?compute_id=" + encodeURIComponent(migrateCompute.value),
{signal: loading.signal}).then(function (response) {
if (!response.ok) throw new Error(response.statusText);
return response.text();
}).then(function (html) {
migratePlan.innerHTML = html;
// a destination that did not answer, or a disk that cannot go there
submit.disabled = migratePlan.querySelector("[data-refused]") !== null;
}).catch(function () {
if (loading.signal.aborted) return; // another destination was chosen
migratePlan.textContent = "{{ _("The disks could not be checked.")|escapejs }}";
submit.disabled = false; // the migration checks them again
});
}
if (migratePlan && migrateCompute) {
document.querySelector('[data-bs-target="#migrate"]').addEventListener("shown.bs.tab", function () {
if (!migratePlan.dataset.loaded) {
migratePlan.dataset.loaded = "true";
loadMigratePlan();
}
});
migrateCompute.addEventListener("change", loadMigratePlan);
}
function get_osinfo() {
show_tab('#settings');
show_tab('#osinfo');
load_osinfo();
}
function load_osinfo() {
$.getJSON("{% url 'instances:osinfo' instance.id %}", function (data) {
$('#osinfo-error').addClass('d-none');
$('#oshostname').text(data['host-name'] || '');
$('#osname').text(data.id || '');
$('#osprettyname').text(data['pretty-name'] || '');
$('#oskernelrelease').text(data['kernel-release'] || '');
$('#oskernelversion').text(data['kernel-version'] || '');
$('#osversion').text(data.version || '');
$('#ostimezone').text(data.zone ? data.zone + " / " + data.offset : '');
}).fail(function (xhr) {
var error = xhr.responseJSON && xhr.responseJSON.error;
$('#osinfo-error').text(error || "{% trans 'The guest agent did not answer' %}").removeClass('d-none');
});
}
</script>
{% endblock %}