1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-08 19:26:48 +00:00
webvirtcloud/instances/templates/instance.html
catborise 42a1783178 fix(instances): the Resize tab changes only what was asked
Disk sizes were pre-filled rounded to whole GiB, so a 3.5 GiB disk showed
4 and was grown, for good, whenever another disk was resized; an empty
field or a disk outside a pool was a 500. A disk's field is now empty with
its size as the placeholder: an empty field leaves the disk alone, a
larger whole number grows it, anything else is refused with a message.

vCPUs change only on a shut-off VM, also on a direct request: a running
VM got them at its next start, past the quota counted against its live
count. Current vCPUs or memory above the maximum are refused instead of
failing (and dropping vCPU hot-plug) or being capped by libvirt. A running
VM's maximum memory select is disabled, since its maximum is kept, and a
custom memory value is sent only while its box is ticked.
2026-10-26 13:49:37 +03:00

453 lines
19 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_heading %}
{{ instance.name }}{% if instance.title %} ({{ instance.title }}){% endif %}
{% endblock page_heading %}
{% block content %}
{% include 'pleasewaitdialog.html' %}
<div>
<div>
{% vm_state instance.status as state %}
<span class="badge bg-{{ state.1 }}">{{ state.0 }}</span>
|
<span
{%if instance.guest_agent %}
{% if instance.guest_agent_ready %}
class="badge bg-success"
title="{% trans "Guest Agent Enabled & Connected" %}"
{% else %}
class="badge bg-warning"
title="{% trans "Guest Agent Enabled but not Connected" %}"
{% endif %}
{% else %}
class="badge bg-danger"
title="{% trans "Guest Agent Not Enabled & Not Connected" %}"
{% endif %}>{% bs_icon 'broadcast' size='1em' %}
</span>
|
{% if instance.snapshots %}
{% bs_icon 'camera'%} <i title="There are {{ instance.snapshots|length }} snapshot(s)"></i>
|
{% endif %}
{% if instance.cur_vcpu %}
{{ instance.cur_vcpu }} {% trans "VCPU" %}
{% else %}
{{ instance.vcpu }} {% trans "VCPU" %}
{% endif %}
|
{{ instance.cur_memory }} {% trans "MB" %} {% trans "RAM" %}
|
{% for disk in instance.disks %}
{{ disk.size|filesizeformat }} {% trans "Disk" %} |
{% endfor %}
{% for net in instance.networks %}
{% for ipv4 in net.ipv4 %}
{{ ipv4 }} |
{% endfor %}
{% endfor %}
{% if instance.guest_agent_ready %}
<a class="link-warning" title="{% trans 'Show Instance OS details' %}" onclick="get_osinfo();">
<span>{% bs_icon 'info-circle' %} </span>
</a>
{% endif %}
<a class="link-success" href="{% url 'instances:instance' instance.id %}" title="{% trans 'Refresh instance info' %}">
<span>{% bs_icon 'repeat'%} </span>
</a>
</div>
<hr>
</div>
<div id="job-panel" aria-live="polite"></div>
<div id="mainTabMenu">
<!-- Nav tabs -->
<ul class="nav nav-pills nav-fill" id="navbtn" role="tablist" aria-label="Instance actions">
<li class="nav-item" role="presentation">
<button class="nav-link action-button active" id="power-tab" aria-controls="power" data-bs-toggle="pill" data-bs-target="#power" type="button" role="tab" aria-selected="true">
<span class="action-block" aria-hidden="true">{% bs_icon 'power' size='2em'%} </span>
{% trans "Power" %}
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link action-button" id="access-tab" aria-controls="access" data-bs-toggle="pill" data-bs-target="#access" type="button" role="tab" aria-selected="false">
<span class="action-block" aria-hidden="true">{% bs_icon 'lock' size='2em'%} </span>
{% trans "Access" %}
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link action-button" id="resize-tab" aria-controls="resize" data-bs-toggle="pill" data-bs-target="#resize" type="button" role="tab" aria-selected="false">
<span class="action-block" aria-hidden="true">{% bs_icon 'arrows-angle-expand' size='2em'%} </span>
{% trans "Resize" %}
</button>
</li>
{% if can_snapshot %}
<li class="nav-item" role="presentation">
<button class="nav-link action-button" id="snapshots-tab" aria-controls="snapshots" data-bs-toggle="pill" data-bs-target="#snapshots" type="button" role="tab" aria-selected="false">
<span class="action-block" aria-hidden="true">{% bs_icon 'camera' size='2em'%} </span>
{% trans "Snapshot" %}
</button>
</li>
{% endif %}
{% if show_settings %}
<li class="nav-item" role="presentation">
<button class="nav-link action-button" id="settings-tab" aria-controls="settings" data-bs-toggle="pill" data-bs-target="#settings" type="button" role="tab" aria-selected="false">
<span class="action-block" aria-hidden="true">{% bs_icon 'gear-wide-connected' size='2em'%} </span>
{% trans "Settings" %}
</button>
</li>
{% endif %}
<li class="nav-item" role="presentation">
<button class="nav-link action-button" id="graphics-tab" aria-controls="graphics" data-bs-toggle="pill" data-bs-target="#graphics" type="button" role="tab" aria-selected="false">
<span class="action-block" aria-hidden="true">{% bs_icon 'bar-chart-line' size='2em'%} </span>
{% trans "Stats" %}
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link action-button" id="undefine-tab" aria-controls="undefine" data-bs-toggle="pill" data-bs-target="#undefine" type="button" role="tab" aria-selected="false">
<span class="action-block" aria-hidden="true">{% bs_icon 'trash' size='2em'%} </span>
{% trans "Destroy" %}
</button>
</li>
</ul>
<!-- 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>
{% if app_settings.VIEW_INSTANCE_DETAIL_BOTTOM_BAR == 'True' %}
{% include 'bottom_bar.html' %}
{% endif %}
{% 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) {
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 }}';
{% for disk in instance.disks %}
disk_name = '{{ disk.image }}';
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 }}').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 %}
$(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 () {
random_mac('clone-net-mac-0');
random_mac('add-net-mac');
update_clone_disk_name($('#clone_name').val());
});
{% else %}
$('#select_clone_name').on('change', function () {
update_clone_disk_name($(this).val());
guess_mac_address('#select_clone_name', 0);
});
$(document).ready(function () {
update_clone_disk_name($('#select_clone_name').val());
guess_mac_address('#select_clone_name', 0);
});
{% endif %}
</script>
<script>
$(document).ready(function(){
$('[data-bs-toggle="popover"]').popover({
placement : 'top'
});
});
</script>
<script>
function set_orderlist(obj){
var result = '';
$('#b_order label input:checked').each(function () {
if (result != '') result += ',';
result += $(this).val();
});
obj.val(result);
}
$(document).ready(function () {
{# Boot Order Arragements #}
jQuery.fn.multiselect = function() {
$(this).each(function() {
var checkboxes = $(this).find("input:checkbox");
checkboxes.each(function() {
var checkbox = $(this);
// Highlight pre-selected checkboxes
if (checkbox.prop("checked"))
checkbox.parent().addClass("multiselect-on");
// Highlight checkboxes that the user selects
checkbox.click(function() {
if (checkbox.prop("checked"))
checkbox.parent().addClass("multiselect-on");
else
checkbox.parent().removeClass("multiselect-on");
});
});
});
};
$(function() {
$(".multiselect").multiselect();
});
$('#boot_order_up').bind('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><input type='checkbox' value='"+$(this).val()+"' name='"+$(this).val()+"' checked>"+$(this).parent().text()+"</label>");
label.remove();
}
$(".multiselect").multiselect();
});
set_orderlist($("#bootorder"));
});
$('#boot_order_down').bind('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><input type='checkbox' value='"+$(this).val()+"' name='"+$(this).val()+"' checked>"+$(this).parent().text()+"</label>");
label.remove();
}
$(".multiselect").multiselect();
});
set_orderlist($("#bootorder"));
});
});
</script>
<script>
$(function () {
$('.js-custom__checkbox').change(function () {
var container = $(this).closest('.js-custom__container');
var toggles = container.find('.js-custom__toggle');
toggles.toggle();
// a custom value is sent only while its box is ticked
container.find('input.js-custom__toggle').prop('disabled', !this.checked);
});
});
</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) {
if (data['status'] != vm_status && !backgroundJobRunning) {
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>
function get_osinfo() {
show_tab('#settings');
show_tab('#osinfo');
load_osinfo();
}
function load_osinfo() {
$.getJSON("{% url 'instances:osinfo' instance.id %}", function (data) {
$.each(data, function() {
$('#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.offset);
})
});
}
</script>
{% endblock %}