1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-11 12:46:49 +00:00
webvirtcloud/instances/templates/allinstances.html
catborise 52b721e6df feat(instances): find the VMs using an IP address or network
The Instances page's filters and the address search become one control:
the state list, the search box and a "Find IP" button. "Find IP" opens a
dialog (with the address typed in the search box, if any). An IP address
or a network (CIDR) finds the running VMs whose network cards use it,
read live from libvirt within 5 seconds: libvirt's DHCP leases, the host's
ARP table and the guest agent. Only addresses on the VM's own NICs count,
so interfaces inside the guest such as docker0 never match. Each match
shows the VM, its MAC, where the address came from, and a possible-
conflict mark when two VMs report one address. A coverage line says how
many running VMs were checked, how many had no connected guest agent (a
static address on them cannot be seen), which agents could not be read,
which hosts were not reached or not finished in time, and when it was
read.

The hosts are read in parallel: leases and ARP first, then each guest
agent on its own thread, so an agent that never answers holds back only
its own VM; libvirt waits for an agent without limit, so a search starts
only while its threads fit in 16 search threads in the worker.
Administrators search every compute, VMs the panel has not registered
yet included, and see the hosts; other users find only their own VMs and
see no hosts or host errors.
2026-10-24 22:22:50 +03:00

144 lines
7.6 KiB
HTML

{% extends "base.html" %}
{% load i18n %}
{% load static %}
{% block title %}{% trans "Instances" %}{% endblock %}
{% block page_heading %}{% trans "Instances" %}{% endblock page_heading %}
{% block page_heading_extra %}
{% if request.user.is_superuser %}
{% include 'create_inst_block.html' %}
{% endif %}
{# the list's filters (state, text) and the address search, as one control #}
<div class="float-end search">
<div class="input-group">
<select id="state-filter" class="form-select flex-grow-0 w-auto" aria-label="{% trans 'State' %}">
<option value="">{% trans 'All states' %}</option>
{% for state in states %}
<option>{{ state }}</option>
{% endfor %}
</select>
<input id="filter" class="form-control" type="text" placeholder="{% trans 'Search' %}" aria-label="{% trans 'Search' %}">
<button type="button" class="btn btn-outline-secondary" id="find-address-open"
data-bs-toggle="modal" data-bs-target="#find-address">{% trans "Find IP" %}</button>
</div>
</div>
{% endblock page_heading_extra %}
{% block content %}
{% if show_hosts %}
{% for compute in computes %}
{% if compute.status is not True %}
<div class="alert alert-danger alert-dismissible fade show" role="alert">
{% trans 'Problem occurred with host' %} {{ compute.name }}: {{ compute.connection_error }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endif %}
{% endfor %}
{% elif own_hosts_down %}
<div class="alert alert-warning alert-dismissible fade show" role="alert">
{% trans 'A host of some of your virtual machines cannot be reached right now.' %}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endif %}
<div class="col-lg-12">
{% if app_settings.VIEW_INSTANCES_LIST_STYLE == 'grouped' and request.user.is_superuser %}
{% include 'allinstances_index_grouped.html' %}
{% else %}
{% include 'allinstances_index_nongrouped.html' %}
{% endif %}
</div>
<div class="modal fade" id="find-address" tabindex="-1" aria-labelledby="find-address-title" aria-hidden="true"
data-url="{% url 'instances:find_address' %}" data-failed="{% trans "The search did not go through. Try again." %}"
data-none="{% trans "No running VM reports this address." %}"
data-unregistered="{% trans "(not registered in the panel)" %}" data-conflict="{% trans "possible conflict" %}"
data-checked="{% trans "Running VMs checked: %(n)s." %}"
data-no-agent="{% trans "%(n)s had no connected guest agent; a static address on them cannot be seen." %}"
data-unread="{% trans "Guest agents not read (no answer in time, or an error): %(n)s." %}"
data-hosts-down="{% trans "Not reached: %(hosts)s." %}"
data-hosts-unfinished="{% trans "Not finished in time: %(hosts)s." %}"
data-read-at="{% trans "Read at %(time)s." %}"
data-some-unchecked="{% trans "Some of your VMs could not be checked." %}">
<div class="modal-dialog modal-lg modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title h5" id="find-address-title">{% trans "Find the VMs using an IP address" %}</h2>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{% trans "Close" %}"></button>
</div>
<div class="modal-body">
<form id="find-address-form" class="d-flex gap-2 mb-3">
<label class="visually-hidden" for="find-address-q">{% trans "IP address or network" %}</label>
<input type="text" class="form-control" name="q" id="find-address-q" required
placeholder="192.0.2.10 {% trans "or" %} 192.0.2.0/24">
<button type="submit" class="btn btn-primary">{% trans "Find" %}</button>
</form>
<div class="alert alert-danger d-none" role="alert" id="find-address-error"></div>
<div class="table-responsive">
<table class="table table-sm d-none" id="find-address-results">
<thead>
<tr>
<th scope="col">{% trans "Address" %}</th>
<th scope="col">{% trans "VM" %}</th>
<th scope="col">{% trans "MAC" %}</th>
<th scope="col">{% trans "Source" %}</th>
{% if show_hosts %}<th scope="col">{% trans "Host" %}</th>{% endif %}
</tr>
</thead>
<tbody></tbody>
</table>
</div>
<p class="small text-body-secondary mb-0" id="find-address-coverage"></p>
</div>
</div>
</div>
</div>
{% endblock content %}
{% block script %}
<script src="{% static 'js/instance_filter.js' %}"></script>
<script src="{% static 'js/find_address.js' %}"></script>
{% if request.user.is_superuser %}
<script>
// The new-VM dialog's CPU bars: each compute's load over one second,
// read when the dialog opens, two computes at a time (each read holds
// a worker for that second). Answers of an earlier opening are ignored.
const addInstance = document.getElementById("AddInstance");
let cpuOpening = 0, cpuQueue = [], cpuReading = 0;
function setCpuUsage(cell, percent, text) {
const meter = cell.querySelector(".progress");
cell.querySelector(".progress-bar").style.width = (percent || 0) + "%";
if (percent === null) {
meter.removeAttribute("aria-valuenow");
} else {
meter.setAttribute("aria-valuenow", percent);
}
cell.querySelector(".cpu-usage-value").textContent = text;
}
function readCpuUsage() {
while (cpuReading < 2 && cpuQueue.length) {
const cell = cpuQueue.shift(), opening = cpuOpening;
const show = (percent, text) => { if (opening === cpuOpening) setCpuUsage(cell, percent, text); };
cpuReading++;
$.getJSON(cell.dataset.url)
.done((data) => {
const usage = typeof data.cpudata === "number" ? Math.round(data.cpudata) : null;
show(usage, usage === null ? "—" : usage + "%");
})
.fail(() => show(null, "—"))
.always(() => { cpuReading--; readCpuUsage(); });
}
}
addInstance.addEventListener("show.bs.modal", function () {
cpuOpening++;
cpuQueue = Array.from(addInstance.querySelectorAll(".cpu-usage"));
cpuQueue.forEach((cell) => setCpuUsage(cell, null, "…"));
readCpuUsage();
});
addInstance.addEventListener("hide.bs.modal", function () {
cpuOpening++;
cpuQueue = [];
});
</script>
{% endif %}
{% endblock script %}