1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-09 11:46:49 +00:00
webvirtcloud/instances/templates/instance.html
catborise 731d14efd7 fix(instances): pages poll one request at a time, and not while hidden
The VM page polls the VM state every 5 s and, while its Stats tab is
open, the statistics every 10 s; the compute overview polls the host
graph every 5 s. All used setInterval:

- A new request went out on schedule while the previous one was still
  open. When a host goes silent, a stats request waits up to 30-45 s,
  so each open page held several sync workers at once.
- Each time the Stats tab was shown, the page made new charts and a
  new interval without stopping the old ones, and polling went on on
  the other tabs of the page.
- Hidden pages kept polling at full rate.

Measured in a browser against a test server (old code / new): Stats tab
shown 4 times, 31 s on it: 12 / 2 requests; 31 s on another tab of the
page: 12 / 0; page hidden for 31 s: 15 / 0, and the new code asks once
right after the page is shown again; with 25 s answers, at most 15 / 1
requests open at once.

static/js/poll.js repeats a request while the page is visible: the
next one 5 or 10 s after the previous answer, none while hidden, one at
once when shown again. The Stats tab makes its charts once and polls
only while shown. The answer handler runs after the poller's own
bookkeeping, so an error in it (a chart for a disk added meanwhile)
does not stop the polling. A Node test of poll.js runs with the test
suite when node is installed.
2026-10-05 16:55:50 +03:00

743 lines
29 KiB
HTML

{% extends "base.html" %}
{% load static %}
{% load i18n %}
{% 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>
{% if instance.status == 5 %}
<span class="badge bg-danger">{% trans "Off" %}</span>
{% endif %}
{% if instance.status == 1 %}
<span class="badge bg-success">{% trans "Active" %}</span>
{% endif %}
{% if instance.status == 3 %}
<span class="badge bg-warning">{% trans "Suspend" %}</span>
{% endif %}
|
<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="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 id="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 id="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 id="action-block" aria-hidden="true">{% bs_icon 'arrows-angle-expand' size='2em'%} </span>
{% trans "Resize" %}
</button>
</li>
{% if allow_admin_or_not_template and 'instances.snapshot_instances' in perms %}
<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 id="action-block" aria-hidden="true">{% bs_icon 'camera' size='2em'%} </span>
{% trans "Snapshot" %}
</button>
</li>
{% endif %}
<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 id="action-block" aria-hidden="true">{% bs_icon 'gear-wide-connected' size='2em'%} </span>
{% trans "Settings" %}
</button>
</li>
<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 id="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 id="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' %}
{% include 'instances/snapshots_tab.html' %}
{% include 'instances/settings_tab.html' %}
{% 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>
var editor = ace.edit("editor");
editor.getSession().setMode("ace/mode/xml");
var input = $('input[name="inst_xml"]');
editor.getSession().on("change", function () {
input.val(editor.getSession().getValue());
});
</script>
<script>
function random_mac(net) {
$.getJSON("{% url 'instances:random_mac_address' %}", function (data) {
$('input[name="' + net + '"]').val(data['mac']);
});
}
</script>
<script>
function show_console() {
if ($('#console_show_pass').attr('type') == 'password') {
$('#console_show_pass').attr('type', 'text');
} else {
$('#console_show_pass').attr('type', 'password');
}
}
</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>
$(document).on('change', '#console_passwd_gen', function () {
if ($(this).prop('checked')) {
$('#console_passwd_manual').hide();
$('#console_passwd_clear').prop('checked', false);
} else {
$('#console_passwd_manual').show();
}
});
$(document).on('change', '#console_passwd_clear', function () {
if ($(this).prop('checked')) {
$('#console_passwd_manual').hide();
$('#console_passwd_gen').prop('checked', false);
} else {
$('#console_passwd_manual').show();
}
});
$(document).on('change', '#console_keymap_clear', function () {
if ($(this).prop('checked')) {
$('#console_keymap_selection').hide();
} else {
$('#console_keymap_selection').show();
}
});
$('#clone_name').on('input', function () {
update_clone_disk_name($(this).val());
});
$(document).ready(function () {
// set current console keymap or fall back to default
var keymap = "{{ instance.console_keymap }}";
if (keymap != '') {
$("#console_select_keymap option[value='" + keymap + "']").prop('selected', true);
}
});
$(document).ready(function () {
// set current console listen address or fall back to default
var console_listener_address = "{{ instance.console_listener_address }}";
if (console_listener_address != '') {
$("#console_select_listener_address option[value='" + console_listener_address + "']").prop('selected', true);
}
});
$(document).ready(function () {
// get video model or fall back to default
let video_model = "{{ instance.video_model }}";
if (video_model != '') {
$("#video_model_select option[value='" + video_model + "']").prop('selected', true);
}
});
$(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);
});
});
{% 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();
});
});
</script>
<script src="{% static 'js/Chart.bundle.min.js' %}"></script>
<script src="{% static 'js/poll.js' %}"></script>
<script>
var stats_tab = document.querySelector('#graphics-tab');
var stats_poll = 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 cpu_ctx = $("#cpuChart").get(0).getContext("2d");
var cpuChart = new Chart(cpu_ctx, {
type: 'line',
data: {
datasets : [{
backgroundColor: "rgba(44,127,184,0.5)",
label: "Usage"
}]
},
options: {
responsive: true,
legend: {
display: false
},
scales: {
xAxes:[{
offset: false,
ticks: {
beginAtZero: false,
autoSkip: true,
maxTicksLimit: 10,
maxRotation: 0,
minRotation: 0,
stepSize: 10,
},
}],
yAxes: [{
ticks: {
suggestedMax: 100,
suggestedMin: 0,
stepSize: 20,
callback: function(value, index, values) {
return value + ' %';
}
},
}],
},
tooltips: {
callbacks: {
label: function (tooltipItem, chart) {
var label = chart.datasets[tooltipItem.datasetIndex].label || '';
if (label) {
label += ': ';
}
return label += tooltipItem.yLabel + ' %';
}
}
}
}
});
var mem_ctx = $("#memChart").get(0).getContext("2d");
var memChart = new Chart(mem_ctx, {
type: 'line',
data: {
datasets : [{
label: "Usage"
}]
},
options: {
responsive: true,
legend: {
display: false
},
scales: {
xAxes:[{
offset: false,
ticks: {
beginAtZero: false,
autoSkip: true,
maxTicksLimit: 10,
maxRotation: 0,
minRotation: 0,
stepSize: 10,
},
}],
yAxes: [{
ticks: {
suggestedMax: 100,
suggestedMin: 0,
stepSize: 20,
callback: function(value, index, values) {
return value + ' MB';
}
},
}],
},
tooltips: {
callbacks: {
label: function (tooltipItem, chart) {
var label = chart.datasets[tooltipItem.datasetIndex].label || '';
if (label) {
label += '(RSS): ';
}
return label += tooltipItem.yLabel + ' MB';
}
}
}
}
});
var diskChart = {};
{% for disk in instance.disks %}
var disk_ctx_{{ disk.dev }} = $("#blk{{ disk.dev }}Chart").get(0).getContext("2d");
diskChart['{{ disk.dev }}'] = new Chart(disk_ctx_{{ disk.dev }}, {
type: 'line',
data: {
datasets : [{
backgroundColor: "rgba(127,205,187,0.5)",
label: "Read"
},
{
backgroundColor: "rgba(44,127,184,0.5)",
label: "Write"
}]
},
options: {
responsive: true,
legend: {
display: false
},
scales: {
xAxes:[{
offset: false,
ticks: {
beginAtZero: false,
autoSkip: true,
maxTicksLimit: 10,
maxRotation: 0,
minRotation: 0,
stepSize: 10,
},
}],
yAxes: [{
ticks: {
suggestedmax: 100,
suggestedMin: 0,
callback: function(value, index, values) {
return value + ' Mb/s';
}
},
}],
},
tooltips: {
callbacks: {
label: function (tooltipItem, chart) {
var label = chart.datasets[tooltipItem.datasetIndex].label || '';
if (label) {
label += ': ';
}
return label += tooltipItem.yLabel + ' Mb/s';
}
}
}
}
});
{% endfor %}
var netChart = {};
{% for net in instance.networks %}
var net_ctx_{{ forloop.counter0 }} = $("#netEth{{ forloop.counter0 }}Chart").get(0).getContext("2d");
netChart[{{ forloop.counter0 }}] = new Chart(net_ctx_{{ forloop.counter0 }}, {
type: 'line',
data: {
datasets : [
{
backgroundColor: "rgba(127,205,187,0.5)",
label: "Inbound"
},
{
backgroundColor: "rgba(44,127,184,0.5)",
label: "Outbound"
}]
},
options: {
responsive: true,
legend: {
display: false
},
scales: {
xAxes:[{
offset: false,
ticks: {
beginAtZero: false,
autoSkip: true,
maxTicksLimit: 10,
maxRotation: 0,
minRotation: 0,
stepSize: 10,
},
}],
yAxes: [{
ticks: {
suggestedMax: 100,
suggestedMin: 0,
callback: function(value, index, values) {
return value + ' Mbps';
}
},
}],
},
tooltips: {
callbacks: {
label: function (tooltipItem, chart) {
var label = chart.datasets[tooltipItem.datasetIndex].label || '';
if (label) {
label += ': ';
}
return label += tooltipItem.yLabel + ' Mbps';
}
}
}
}
});
{% endfor %}
stats_poll = pollWhileVisible(10000, function () {
return $.getJSON("{% url 'instances:stats' instance.id %}");
}, function graph_usage(data) {
cpuChart.data.labels.push(data.timeline);
cpuChart.data.datasets[0].data.push(data.cpudata);
if (cpuChart.data.datasets[0].data.length > 10){
cpuChart.data.labels.shift();
cpuChart.data.datasets[0].data.shift();
}
cpuChart.update();
memChart.data.labels.push(data.timeline);
memChart.options.scales.yAxes[0].ticks.max = parseInt(data.memdata.total / 1024);
memChart.options.scales.yAxes[0].ticks.stepSize = parseInt(data.memdata.total / (1024 * 5));
memChart.data.datasets[0].data.push(data.memdata.used / 1024);
if (memChart.data.datasets[0].data.length > 10){
memChart.data.labels.shift();
memChart.data.datasets[0].data.shift();
}
memChart.update();
for (let j = 0; j < data.blkdata.length; j++) {
diskChart[data.blkdata[j].dev].data.labels.push(data.timeline);
diskChart[data.blkdata[j].dev].data.datasets[0].data.push(data.blkdata[j].data[0]);
diskChart[data.blkdata[j].dev].data.datasets[1].data.push(data.blkdata[j].data[1]);
if (diskChart[data.blkdata[j].dev].data.datasets[0].data.length > 10){
diskChart[data.blkdata[j].dev].data.labels.shift();
diskChart[data.blkdata[j].dev].data.datasets[0].data.shift();
diskChart[data.blkdata[j].dev].data.datasets[1].data.shift();
}
diskChart[data.blkdata[j].dev].update();
}
for (let j = 0; j < data.netdata.length; j++) {
netChart[data.netdata[j].dev].data.labels.push(data.timeline);
netChart[data.netdata[j].dev].data.datasets[0].data.push(data.netdata[j].data[0]);
netChart[data.netdata[j].dev].data.datasets[1].data.push(data.netdata[j].data[1]);
if (netChart[data.netdata[j].dev].data.datasets[0].data.length > 10){
netChart[data.netdata[j].dev].data.labels.shift();
netChart[data.netdata[j].dev].data.datasets[0].data.shift();
netChart[data.netdata[j].dev].data.datasets[1].data.shift();
}
netChart[data.netdata[j].dev].update();
}
});
});
</script>
<script>
backgroundJobRunning = false;
var status = {{ instance.status|lower }};
var status_poll = pollWhileVisible(5000, function () {
return $.getJSON("{% url 'instances:status' instance.id %}");
}, function get_status(data) {
if (data['status'] != status && !backgroundJobRunning) {
window.location.reload()
}
});
// Stop getting status info before delete instance
$('#delete_form').submit(function(){
status_poll.stop();
return true;
});
</script>
<script>
var hash = location.hash;
if (~$.inArray(hash, ['#poweron', '#poweroff', '#powercycle', '#suspend', '#resume'])) {
var btnsect = $('#navbtn>li>a');
$(btnsect).each(function () {
if ($(this).attr('href') === '#power') {
$(this).trigger('click');
}
});
}
if (~$.inArray(hash, ['#resize', "#resizevm_cpu", "#resizevm_mem", "#resizevm_disk"])) {
var btnsect = $('#navbtn>li>a');
$(btnsect).each(function () {
if ($(this).attr('href') === '#resize') {
$(this).trigger('click');
}
});
}
if (~$.inArray(hash, ['#osinfo', '#boot_opt', "#disks", '#network', '#clone', '#xmledit', '#vncsettings', '#migrate', '#options', '#users'])) {
var btnsect = $('#navbtn>li>a');
$(btnsect).each(function () {
if ($(this).attr('href') === '#settings') {
$(this).trigger('click');
}
});
var btn = $('#settings>div>ul>li>a');
$(btn).each(function () {
if ($(this).attr('href') === hash) {
$(this).trigger('click');
}
});
}
if (~$.inArray(hash, ['#takesnapshot', "#takeextsnapshot", "#managesnapshot"])) {
var btnsect = $('#navbtn>li>a');
$(btnsect).each(function () {
if ($(this).attr('href') === '#snapshots') {
$(this).trigger('click');
}
});
var btn = $('#snapshots>div>ul>li>a');
$(btn).each(function () {
if ($(this).attr('href') === hash) {
$(this).trigger('click');
}
});
}
</script>
<script>
function get_osinfo() {
document.querySelector("#settings-tab").click();
document.querySelector("#osinfo-tab").click();
$.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 %}