mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-08 11:16:47 +00:00
- A Host devices tab on the VM page lists the VM's USB host devices, each once: how it is chosen (its address, or any device of its model), the device the running VM holds and whether its next start has it; its other host devices (PCI, SCSI, mediated) are listed for the XML editor. - Superusers add a USB device of the host and take it back, on a running VM too. The add form is loaded when the tab opens, as its choices read every VM of the host; a hub, a device the host has its own disks or network interfaces on, or one another VM names is not offered, with the reason, and refused on the server under the compute's claim. A device is chosen by model when the host has one of it, else by its address. - A VM without a USB controller is refused; a device is taken from each definition as that definition describes it. - A clone gets none of the VM's host devices (the clone form says so), and a VM with host devices is not migrated. A snapshot revert is refused when it would give the VM a device another VM names or the host uses. - A NIC that is a host device is no longer edited or removed from the Network tab: the XML editor changes it.
468 lines
20 KiB
HTML
468 lines
20 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 text-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 text-center active" id="power-tab" aria-controls="power" data-bs-toggle="pill" data-bs-target="#power" type="button" role="tab" aria-selected="true">
|
|
<span class="d-block py-2" aria-hidden="true">{% bs_icon 'power' size='2em'%} </span>
|
|
{% trans "Power" %}
|
|
</button>
|
|
</li>
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link text-center" id="access-tab" aria-controls="access" data-bs-toggle="pill" data-bs-target="#access" type="button" role="tab" aria-selected="false">
|
|
<span class="d-block py-2" aria-hidden="true">{% bs_icon 'lock' size='2em'%} </span>
|
|
{% trans "Access" %}
|
|
</button>
|
|
</li>
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link text-center" id="resize-tab" aria-controls="resize" data-bs-toggle="pill" data-bs-target="#resize" type="button" role="tab" aria-selected="false">
|
|
<span class="d-block py-2" 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 text-center" id="snapshots-tab" aria-controls="snapshots" data-bs-toggle="pill" data-bs-target="#snapshots" type="button" role="tab" aria-selected="false">
|
|
<span class="d-block py-2" 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 text-center" id="settings-tab" aria-controls="settings" data-bs-toggle="pill" data-bs-target="#settings" type="button" role="tab" aria-selected="false">
|
|
<span class="d-block py-2" 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 text-center" id="graphics-tab" aria-controls="graphics" data-bs-toggle="pill" data-bs-target="#graphics" type="button" role="tab" aria-selected="false">
|
|
<span class="d-block py-2" 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 text-center" id="undefine-tab" aria-controls="undefine" data-bs-toggle="pill" data-bs-target="#undefine" type="button" role="tab" aria-selected="false">
|
|
<span class="d-block py-2" 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) {
|
|
// moved, not rebuilt: the checkbox keeps its handler
|
|
$('#b_order label').eq(newPos).before(label);
|
|
}
|
|
});
|
|
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);
|
|
}
|
|
});
|
|
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.toggleClass("d-none");
|
|
// 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>
|
|
// the Host devices tab's add form: loaded once, when the tab first opens
|
|
var hostUsb = document.getElementById("host-usb");
|
|
if (hostUsb) {
|
|
document.querySelector('[data-bs-target="#hostdevs"]').addEventListener("shown.bs.tab", function () {
|
|
if (hostUsb.dataset.loaded) return;
|
|
hostUsb.dataset.loaded = "true";
|
|
fetch(hostUsb.dataset.url).then(function (response) {
|
|
if (!response.ok) throw new Error(response.statusText);
|
|
return response.text();
|
|
}).then(function (html) {
|
|
hostUsb.innerHTML = html;
|
|
}).catch(function () {
|
|
hostUsb.textContent = "{{ _("The host's USB devices could not be read.")|escapejs }}";
|
|
});
|
|
});
|
|
}
|
|
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 %}
|