1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-09 11:46:49 +00:00
Commit graph

324 commits

Author SHA1 Message Date
catborise
e04adec87b feat(logs): changes to users, groups, owners, flavors and settings are logged
The Logs page now records the panel's own records too: users (add,
change with the changed fields, delete, block, unblock, an admin's
password set, a user's own password change including the first-login
one, own profile edits), groups with their members and permissions,
VM owners from the accounts pages and from the VM page, OTP QR code
mails, flavors from web and API, app settings and the theme,
removed-VM records and SSH keys. No row carries a password or a key.

- A user is created in one transaction: when a VM named in
  NEW_USER_DEFAULT_INSTANCES is missing, no half-made user stays.
- The OTP mail checks the email address before it creates a device;
  without one it writes a failed row and makes no device.
- The VM page's owner rows read "VM web-1 owner bob: add" and
  "...: remove" instead of "Add owner: bob" and "Delete owner: 17"; a
  second owner refused by the single-owner setting is a failed row.
  Owner rows keep the VM's host and name, so the VM's log tab shows
  them.
- A setting or theme posted with its current value is no change and
  writes no row; the old free-text setting rows are gone.

Host-side objects (networks, storage, interfaces, secrets, filters)
follow in the next layer.
2026-10-23 11:10:24 +03:00
catborise
fbe93d9aee fix(templates): each delete page says what deleting its object does
The shared delete page asked only whether to delete the object. Each
view now gives its consequence: a deleted compute's VMs keep running
but the panel forgets them and their owners, a deleted user's VMs
stay, a group's members keep their accounts, a VM's owner loses only
the access, a removed VM's record no longer brings its owners back,
an SSH key stays on the VMs that have it, a flavor only goes as a
size template.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 11:15:00 +03:00
catborise
2144b3fe20 fix(templates): each confirmation says what its action does
Twenty-five prompts asked only "Are you sure?", and on the storage page
one handler asked the same before every button, Start and Enable
included. Each destructive button now names its object and says what
happens: a stopped network cuts running VMs off, a deleted pool keeps
its files, a deleted volume loses its data, a reverted snapshot loses
the changes made since. Saves no longer ask, and disabled buttons lose
prompts they could not show. The VM QoS messages said to restart the
network where the VM has to be powered off and on.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-21 21:25:45 +03:00
catborise
90c148d287 fix: a form libvirt refuses goes back to its page with the reason
A libvirt error that a view left to ExceptionMiddleware showed the "500
Internal Server Error" page, which blames the server, at the form's
action URL; its only way on was the browser's Back. Adding a NIC, a new
or existing disk, editing a disk, setting a NIC's QoS and creating a
volume end there when libvirt refuses, as do the other VM actions.

For a form (any method but GET or HEAD) the middleware now goes back to
the page the form was on, with the error in the messages, as it already
does for a busy compute; a page that cannot be shown keeps the error
page. same_origin_referer moves from the instances views to the
middleware module, next to error_text, for both to use.
2026-10-20 22:37:40 +03:00
catborise
96141c3ef7 fix(instances): a refused custom VM form comes back with its disks and networks
A refused custom create form came back with its image pool unselected
and its disk and network lists empty: every disk's device and bus and
every MAC had to be chosen again. Adding a disk or a network also reset
the rows already there: each addition rebuilt all rows from an HTML
string, so the earlier disks fell back to the default device and bus
and every NIC got a new random MAC, a typed one included.

The view sends the submitted pool, disks (volume, device, bus) and
networks (name, MAC) back with json_script, built field by field from
the POST (submitted_custom_lists). The rows are cloned from two
<template> elements and filled with textContent and listeners; one
function, sync_custom_lists, derives the images, networks and mac
fields and the device<i>/bus<i> names from the rows in order. Adding or
removing a row leaves the others as they are, and a random MAC that
arrives after one was typed is dropped. On load the page selects the
pool, waits for its volumes, rebuilds each disk with its device, waits
for that device's bus list and selects its bus, rebuilds the networks
with their MACs, and then focuses the first error. A submitted value the
loaded lists no longer have stays selected. The bus and volume loaders
return their requests so that the page can wait on them.
2026-10-20 21:24:36 +03:00
catborise
dba41f97f3 fix(instances): a refused VM create form comes back with what was entered
When the server refused a VM create submission (a field error, a busy
resource, a reused submission key, an existing VM name, a missing MAC
for a new disk), the page came back on its first tab with the flavor
dialog closed and every entered value gone: the hand-written fields
showed fixed defaults, and the errors sat in a block at the top of the
page, outside #messages and tied to no field.

The view builds one NewVMForm per create form on the page (custom,
template, one per flavor; create_forms), each with its own defaults; a
hidden form_id, which is also the prefix of the form's field ids, names
the submitted one, and it comes back bound. Every field renders from its
own form and is marked by two small template tags (form_fields:
error_attrs, field_error) when refused; a summary at the top of the
form lists all of its errors, also those of fields it keeps hidden. The
submitted tab or flavor dialog, marked data-form-id, reopens with any
"Advanced" section that holds an error; a reopened dialog shows the
messages too; the template form gets its pool, template and storage
back. base.html's error focus is a function that walks the places an
error can be, and a page can call it once it is set up, as the create
page does. form_id changes nothing in how a POST is processed; one of
an unknown form (a flavor deleted since) has its errors in the
messages. The default video now applies to every create form.

The custom form's disks and networks are not restored yet.
2026-10-20 08:12:25 +03:00
catborise
c46212bb38 fix(instances): a disk on an LVM pool is not resized, the page says so instead of failing
libvirt's logical pool cannot change a volume's capacity, so every
resize of such a disk ended in a 500. Disks are resized one at a time;
VIR_ERR_NO_SUPPORT becomes a message naming the disk, and the other
disks still grow.

A live test creates an LVM pool from the storage page on a spare disk
(TEST_LIBVIRT_LVM_DEVICE; the Vagrant computes get /dev/vdb), attaches
new and existing volumes to stopped and running VMs as block disks, and
checks the refused resize in every state.
2026-10-18 21:10:06 +03:00
catborise
0f9bbd3e86 feat(instances): choose VNC or the serial console on the VM page
Settings > Console gets a Console type field. Serial removes the VM's
graphics device (adding a pty console when there is none); VNC adds the
graphics device new VMs get, on localhost, and then applies the form's
VNC fields, so an administrator can open it to all interfaces for a TCP
compute. Serial is offered only while SERIAL_CONSOLE_ENABLED is on.
2026-10-17 19:48:58 +03:00
catborise
31cbe60aa4 feat(instances): migration runs as a job 2026-10-16 07:57:02 +03:00
catborise
8d2e88f4bb feat(instances): snapshot revert runs as a job 2026-10-15 14:21:32 +03:00
catborise
6f81043438 feat(instances): snapshot create runs as a job 2026-10-15 14:21:32 +03:00
catborise
6f0c730071 feat(instances): VM create runs as a job, also in the API 2026-10-15 09:28:43 +03:00
catborise
fbf82da5a9 feat(instances): VM clone runs as a job 2026-10-15 09:28:43 +03:00
catborise
49232c6e1d feat(storages): volume clone runs as a job 2026-10-15 09:28:43 +03:00
catborise
0fb0eb2bac feat(operations): jobs page 2026-10-14 23:27:57 +03:00
catborise
9c7943a0c8 feat(operations): job panel on the VM page 2026-10-14 23:27:57 +03:00
catborise
079e2ca5ca feat(instances): external snapshot delete runs as a job 2026-10-14 23:27:57 +03:00
catborise
68812ddbdf refactor(computes): the lock helpers take claims instead of file locks 2026-10-14 23:27:57 +03:00
catborise
5f8f7a8643 fix(instances): a snapshot needs free space in the VM's storage pools
A disk-only snapshot was taken without any space check, though its
overlays grow as the guest writes and a later delete merges them down;
a full pool pauses the guest and can leave the snapshot undeletable.
Only a memory snapshot was checked, for the memory alone.

Before a snapshot, each storage pool with a disk of the VM must have
free a share of those disks' virtual size (10%, at least 1 GiB), and
the pool of the memory file the VM's memory on top. Both values are on
the Settings page (Snapshot Free Percent, Snapshot Free Min GiB; 0
turns a rule off, an invalid value keeps the default). The pools are
refreshed first, and a failed refresh or an unreadable volume refuses
the snapshot instead of skipping the check; a disk in no pool is not
checked. Nothing is reserved: later writes can still fill a pool.
2026-10-13 13:27:39 +03:00
catborise
f766890825 feat(instances): external snapshots can freeze the guest file systems
A disk-only snapshot of a running VM is crash-consistent: data the
guest has not written yet is missing from it. With a connected QEMU
guest agent, the snapshot form now offers to freeze the guest's file
systems while the disks are snapshotted (libvirt's QUIESCE flag); the
box is checked by default and can be cleared. It is offered only for a
running VM whose agent is connected, and not together with memory:
libvirt accepts quiesce only for disk-only snapshots, and a memory
snapshot keeps the whole guest state anyway. A request that does not
fit is refused before libvirt is called.
2026-10-13 10:55:24 +03:00
catborise
097a91592c fix(instances): a snapshot file another VM uses is not called left behind
After a snapshot revert or delete, files libvirt could not remove are
named in a warning that says nothing uses them and asks to remove them
by hand. A file another VM on the same host uses (as a disk, a backing
file or a snapshot file) is no longer named: removing it would break
that VM. The warning now says no VM on this host uses the files.
2026-10-13 09:21:51 +03:00
catborise
c970f8e853 fix(storages): keep volumes a VM still needs
libvirt deletes any volume it is asked to. The storage page and the API
deleted a VM's disk, the base image of another volume or the file a
snapshot was taken on without a check, breaking the VM or its
snapshots.

wvmStorage.del_volume now refuses a volume a domain on the compute uses
as a disk or a backing file of one, a file of a domain's snapshots, or
the backing file of another volume. The check is the one destroy
already used, moved to wvmConnect and extended to snapshot files, so
destroy also keeps the files of another VM's snapshots. The storage
page shows the refusal; the API answers 400 with it, as for other
refusals. Deleting a disk from a VM refuses after the detach when the
VM's own snapshots need the volume, and says the volume was kept.

Network disks count by their source name, pool volume disks by their
volume's path. A volume whose metadata cannot be read stops the check
instead of counting as unused; only one removed meanwhile is skipped.
The check and the delete hold the compute lock, so no VM operation
attaches the volume in between; a busy compute is a message on the page
and 409 in the API.
2026-10-13 09:13:02 +03:00
catborise
3fd422db7e fix(instances): name snapshot files libvirt could not remove
When libvirt cannot remove a snapshot file it merged or discarded, it
only logs a warning and the delete or revert still succeeds. On NFS it
removes them as the qemu user, so an image directory the qemu user may
not write to left every merged overlay on disk without a word.

Revert and delete now compare the files in use before and after and
show a warning naming the ones still on disk. Only a snapshot's own
files (overlays, memory) and overlays on a snapshot point count, and a
disk the revert detached is never named, so a kept disk is not offered
for removal.
2026-10-12 21:41:20 +03:00
catborise
ff1508b3c2 feat(instances): external snapshots use libvirt's snapshot API
Create, revert and delete are one libvirt call each. The pflash-to-rom
loader rewrite, the hand-written block commit and the s1./s2. names are
gone, so UEFI VMs can take external snapshots, a revert keeps disks added
later, and deleting an older snapshot keeps the newer one.

Create refuses what libvirt could not revert or delete later: internal
snapshots on the VM, a saved state, a running snapshot job, disks that
are not files, CD-ROM media, a branch after a revert, and memory without
a running VM or room in the first disk's pool. A disk-only revert keeps
the VM running, paused or shut off; one with memory resumes it as saved.
A revert in the same second as the previous one, which libvirt fails
after stopping the VM, is retried once.
A snapshot the old code took of a UEFI VM saved the loader as rom; its
revert is refused (deleting it still works).

The snapshots tab lists internal and external snapshots separately, can
save memory too, shows running snapshot jobs, and the revert confirmation
names the disks it detaches and that NVRAM is not restored.
2026-10-12 15:25:44 +03:00
catborise
e924f3e7a0 fix(instances): a new disk on a running VM does not take a pending disk's name
The device name of a new disk or CD-ROM was chosen from the running VM
only. A name used only in the persistent definition, such as a disk
renamed by a pending bus change, could be chosen again: the disk was
attached to the running VM, the persistent attach was refused, and the
disk was gone after the next start. Names either definition uses are now
taken.
2026-10-11 17:58:19 +03:00
catborise
d0bd2ac32b fix(instances): a running VM's boot order, video and console forms show the pending settings
On an active VM the boot order editor, the video model select and the
console form showed the running VM: a pending boot order, video model or
console setting looked unset, and the boot order editor offered devices
of the running VM only. Choosing one of those cleared the pending order,
since the setter dropped the old order and skipped devices it could not
find. They now read the persistent definition (get_bootmenu,
get_bootorder, get_media_devices, get_video_model and the console
getters take config=), and set_bootorder refuses a device that is not in
it before it changes anything.
2026-10-11 14:56:51 +03:00
catborise
8d2c9b1217 fix(instances): a running VM's disk edit keeps the other pending disk changes
On a running VM a disk edit changes the definition for the next start,
but the disk list and its edit form showed the running VM's disks. The
form posts every field, so an edit wrote the running values back over
earlier pending changes (cache, io, serial, ...), and a disk only in the
next definition could not be edited. The list now shows the persistent
disks. A volume is deleted only when both definitions use it at that
device, since the list may now show another source than the running VM.
2026-10-10 22:53:43 +03:00
catborise
251ddbbec1 fix(instances): a paused VM's memory is resized as a running VM's
Only a running VM counted as active, so a paused one (or one blocked,
PM-suspended or shutting down) had its maximum and current memory written
for the next boot and nothing applied live. Every active VM now has its
current memory changed live and in its definition and keeps its maximum;
a paused guest takes the new size once resumed. The quota check follows
the same rule. Checked on a real guest: paused, shrunk, resumed, then
grown back.
2026-10-10 09:52:56 +03:00
catborise
f1e83455e4 refactor(instances): one way to reach a VM, its console and its devices
The .vv console file is built from the VM found by its UUID, with the
console page's getters, instead of looking the VM up by name five times;
a console without a listen address or password now gives 127.0.0.1 and
an empty password instead of "None". Removed methods nothing calls: the
by-name getters and power methods of wvmInstances, managed save, the vCPU
mode getter (it read the wrong attribute), the old NIC editor and its
bridge lookup. CD-ROM media and NIC hotplug use _definitions() like the
other device edits, the disk editor takes the bus prefix from
util.vol_dev_type, and an unreadable disk is logged, not printed.
2026-10-09 18:13:32 +03:00
catborise
11fe2409f9 fix(instances): device edits fit every definition and device type
A volume of an LVM or iSCSI pool can be attached: by its own path, with
the format its volume reports (raw when the pool knows none). Detaching
a disk takes each definition's own description of it, so a disk only in
the persistent definition goes too. Every NIC type can be a boot device;
bridge and direct NICs were dropped from the boot order. The video model
is a value, never parsed as XML. QoS changes clear the page's read cache.

The NIC model "default" leaves the choice to libvirt instead of writing
model "default", and rt18139 is rtl8139: a VM with either did not start
(checked on a RHEL 10 host). A migration corrects the default NIC type
setting.
2026-10-09 18:13:23 +03:00
catborise
24231813d7 perf(instances): the VM stats no longer wait a second per poll
Each stats answer carries a signed sample of the counters, bound to the VM,
its compute and UUID. The next poll sends it back and the rates are the
averages since then; a sample from another run of the VM (domain ID),
another host CPU count or device list, or older than 30 s falls back to
one second as before. Windows are measured on the monotonic clock.

Now that a poll is cheap, the Stats tab polls every 5 s like the host
graph, the first time as soon as it opens (pollWhileVisible takes the
delay of the first request).

A counter the host reports as unsupported (-1) or that went back gives no
value instead of 0 or a jump; a NIC without a target keeps its chart, a
pool volume disk is measured, and a disk or NIC attached after the page
loaded no longer stops the other charts. Disk rates are MB/s (they were
MiB/s labelled Mb/s), network Mbit/s; the CPU tooltip names the window.
2026-10-09 11:59:41 +03:00
catborise
439f24041e perf(instances): the VM page reads each disk, the state and host info once
Measured on a VM with two disks, a CD-ROM and a NIC: 70 libvirt calls per
page, now 43. Within the page's cached_reads block the VM's state, the
host's info and domain capabilities, and each disk path's volume are read
once; the running and the persistent disk lists share a path's volume.
The ISO list takes the active pools in one call instead of looking each up
by name; it still refreshes them, so an image copied in by hand shows up.

The opt-in DRBD status waits at most 10 s for the compute's ssh, then
leaves the status unknown instead of holding the page.
2026-10-09 08:36:04 +03:00
catborise
cbc8b22f98 fix(instances): a clone's quota and automatic disk names follow the disks it copies
A clone copies the disks of the VM's persistent definition, but its disk
quota and the disk names chosen with automatic naming came from the
running VM's disks, which differ after a disk is attached or detached on
the running VM only.
2026-10-08 08:50:21 +03:00
catborise
9fdb039977 fix(instances): snapshot and console settings requests without the permission get 403
Without instances.snapshot_instances (snapshots) or is_vnc (console
settings) the request was accepted and silently did nothing, although
the documented answer for a missing right is 403. delete_external_snapshot
also asked libvirt for the VM state before checking the permission.
2026-10-08 08:50:21 +03:00
catborise
70bed79c60 fix(instances): a template is not power-cycled on, and deleted only by superusers and staff owners
Power cycle force-stopped and started a template, which power on refuses.
A non-staff owner with is_delete could delete a template that the
template rule otherwise leaves to superusers and staff owners;
get_instance(perm_type="delete") now refuses it, and the page offers
Destroy by the same rule.
2026-10-08 08:50:21 +03:00
catborise
e481b24240 fix(instances): an operation on a VM a migration moved meanwhile is retried, not run on the new host
A VM operation (or destroy and clone, which hold the whole compute) waits
for its compute's lock while a migration holds it. When the migration
moved the VM's record to the destination, the waiting operation went on
there holding only the source compute's lock, unordered with a
reconciliation, migration or disk attach on the destination. After taking
the locks it now checks the VM is still on the locked compute and
otherwise answers busy, so the retry locks the right one. Clone also
checks again under the lock whether the user may clone the VM, and
checks the quota and the clone's settings against the VM as it is then:
its template mark, size or the user's rights may have changed meanwhile.
2026-10-08 08:50:21 +03:00
catborise
5ee13f305d fix(gstfsd): find the guest OS by inspection, answer once, and stop a change at its deadline
gstfsd mounted every partition it listed: a root on LVM was never found,
a disk with two OS roots got the change twice and two concatenated
replies, and no root, an error or a request longer than 1024 bytes ended
the connection without a reply. It now lets libguestfs inspect the guest,
mounts its one OS the way the guest does (a broken filesystem off the
changed path is left out), and sends exactly one reply, keeping the first
error. The root entry is found on any line of /etc/shadow and must be
unique; a key needs no shadow file. A request is at most 64 KiB. Each
change runs in a worker process with a directly started appliance; both
are killed when the request is not done 100 s after gstfsd accepted it
(GSTFSD_DEADLINE may only lower that), before the panel's 120 s timeout
releases the VM, or when the panel's connection ends, and the reply waits
until they are gone, so nothing touches the disk once the panel has given
up. A worker that dies says why. The panel
connects over IPv4 or IPv6, sends the whole request and reads the reply
until gstfsd closes.
2026-10-08 08:50:21 +03:00
catborise
4dff1008cc fix(instances): only superusers and staff owners change a template VM
The documented rule held only for snapshots: an owner with is_change could
still resize a template, set its root password and SSH keys, change its
options and console settings. get_instance(perm_type="change") now
refuses a template to owners who are not staff, which covers every change
view and the API; the per-view snapshot checks it replaces are removed,
and the page offers those controls by the same rule. Viewing, the console,
power (a no-op on a template) and cloning are unchanged.
2026-10-07 21:04:23 +03:00
catborise
bf1f122984 fix(instances): the VM page offers only the controls the user may use
The page showed controls the backend then refused: power to global
viewers, the console, virt-viewer and VDI controls and the root password
and SSH key forms to viewers, resize to staff, per-vCPU hotplug, video
model and guest agent to non-superusers, clone and options by the clone
permission alone, snapshots without change permission. The page now asks
the views' own rules (owner, is_change, clone and snapshot permissions,
the template rules). OS info, opened from the header, is there for every
viewer; the XML editor and tab restoring after a redirect no longer throw
when a pane is absent; Settings opens on the first section the user has.
The authorization matrix also covers a template VM, the template clone
rule and the template snapshot rule.
2026-10-07 20:42:10 +03:00
catborise
80e11f5eba fix(instances): missing or malformed mutation form fields are reported, not a 500
Several instance mutation views crashed with a bare HTTP 500 when a form field
the UI always sends was absent or malformed (a direct or crafted request);
ExceptionMiddleware turns only libvirt and timeout errors into 4xx, so these
reached a 500. Fixed:

- add_network parsed a None network source ("'NoneType' has no attribute
  'split'"); it now requires a selected network.
- set_qos read an unbound local 'mac' when no field name contained "mac", and
  passed an invalid direction or non-numeric rate to the proxy (ValueError); it
  now requires a MAC, an inbound/outbound direction and whole-number rates.
- unset_qos passed an empty direction into the proxy's XPath (XPathEvalError);
  it now requires a MAC and a valid direction.
- resizevm_cpu and resize_memory ran int() over an empty or non-numeric value;
  they now validate with int() (not str.isdigit(), which accepts "²" that
  int() rejects).
- add_public_key passed a missing/non-numeric sshkeyid into an integer-id
  lookup (ValueError); it now requires a numeric id.

Each reports the problem and returns to the VM page without calling libvirt or
logging, as change_network already does.

Tests: each view with a field missing, non-numeric or a unicode digit returns
302 with an error message, writes no log, and does not attempt the mutation.
The tests fail on the previous code.
2026-10-07 10:38:41 +03:00
catborise
17016e90cd fix(computes): a compute's hostname and login cannot inject a connection URI or ssh option
A compute's hostname and login go unquoted into the libvirt connection URI
(qemu+ssh://login@host/system, qemu+tcp://host/system, qemu+tls://...) and,
for an SSH compute, into the ssh command line of the DRBD status view. They
were barely validated: validate_hostname used re.match with a negated class,
so it caught only a leading special character, and login was never checked.
A hostname like "192.0.2.1/system?command=/path" injected a libvirt URI
parameter (verified: connecting ran the named command through the ssh
wrapper), and TLS/SSH parameters such as no_verify, pkipath, socket or
netcat could be added the same way; a login like "-oProxyCommand=..." became
an ssh option. The DRF ComputeSerializer applied neither check, and
validate_name tested a string literal instead of its argument, so it never
raised.

validate_hostname now accepts only an IPv4 address or a host name with
fullmatch (no scheme, port, path, query, whitespace or IPv6 literal; IPv6 is
left out until the URI building brackets it). validate_login accepts an empty
value (TCP falls back to the server's authentication, the socket transport
ignores the login) or a plain name with no leading "-". Both sit on the
Compute model fields, so the forms and the DRF serializer (which copies model
validators onto its fields) reject the values; a migration records the
metadata. validate_name checks its argument. drbd_status validates the login
and hostname before building login@host and ends ssh option parsing with
"--"; an existing or bypassed bad row is skipped rather than run.

Tests: hostname/login validators accept real values and reject URI and
option injection; the SSH form and the serializer (create, and an empty TCP
login) enforce them; drbd_status skips an injecting target. Checked the real
test computes still validate. The new tests fail on the previous code.
2026-10-06 22:10:19 +03:00
catborise
1016f745c2 fix(instances): a clone stays on the host of the VM it was made from
With "VM Clone Auto Migrate" on, a new clone was migrated to a random
compute, whatever storage, CPU or network that host had, and a failed
migration showed as a failed clone although the clone existed.

The setting and its code are removed; migration appsettings.0015
deletes the setting. A clone can be migrated afterwards like any VM.
The README's update notes list the migration changes: the migration
address, storage pools for the disk check, and this setting.
2026-10-06 15:58:35 +03:00
catborise
5061cc7f2c fix(instances): a migration runs in the mode the VM's state allows, and is logged as it ran
moveto read the VM state for each flag and dropped options that did
not fit it, so a request could run as something else than asked; the
log then named the mode from the form ("live" or "offline"), not the
one used. The API did not log at all and answered "instance migrate is
started" after the migration had finished.

- moveto reads the state once. A running VM migrates live, or without
  live paused for the transfer (non-live); a paused VM stays paused; a
  shut-off VM moves offline. Live for a shut-off VM, offline for a
  running or paused one, and any other state are refused before
  anything changes. unsafe, auto converge (live only) and compressed
  apply to a running VM.
- migrate_instance logs the outcome for the VM page and the API: the
  mode and options used, or the error. The API answers
  "instance is migrated (<mode>)" and accepts requests that leave out
  an optional option.
- Post copy is gone from the form, the view and the API: only the
  flag was set, and without migrateStartPostCopy, which the app never
  called, the migration never switched to post copy.
2026-10-06 15:26:49 +03:00
catborise
5a523befbd feat(computes): a compute names the address migrations reach it on
Migration is client-driven: libvirt sends the VM's memory to the
destination's own host name unless a migration URI is given, and the
app never gave one. Where the hosts cannot resolve each other's names
(or the names point at a management network) every migration failed
with "Unable to resolve address '<host name>' service '49152'".

A compute now has an optional migration address (an IP address or a
host name, no scheme or port), editable in the compute form and the
API. A VM migrating to that compute is sent to tcp://<address>
(bracketed for IPv6). Empty, the default for existing computes, keeps
the previous behavior. The compute's host name is not reused, because
for SSH computes it can be an ssh_config alias.

The migration log entry names the address when one is set.
2026-10-06 15:05:40 +03:00
catborise
25b3671c33 fix(instances): a migrated VM's source definition is always removed
Migration left the VM defined on the source host unless "Delete
original" was ticked (VIR_MIGRATE_UNDEFINE_SOURCE). With the shared
storage migration needs, that definition points at the disks the
destination VM is running on: the VM showed on both hosts and could be
started (or autostarted) a second time against the same disks; only a
configured lock manager stops that.

moveto now always passes VIR_MIGRATE_UNDEFINE_SOURCE with
VIR_MIGRATE_PERSIST_DEST. The option is gone from the form, the view,
the API (MigrateSerializer.xml_del) and the clone auto-migration. It
removes the definition, not the disk files.
2026-10-06 13:58:59 +03:00
catborise
802e90c79f fix: connection errors on VM actions name the host only to administrators
When a VM action failed because its host could not be reached (the
connection could not be opened, or it dropped during the call), the
error page showed libvirt's connection error to any user: it names the
host and how it is reached, e.g. "ssh: connect to host 10.x port 22".
The lists and the VM page already keep that to administrators.

- Opening a connection now fails with ConnectionFailed (a subclass of
  OperationError). A dropped connection is recognized by its error
  domain and code (RPC/remote/SSH layer with a transport code, measured:
  a keepalive timeout is VIR_FROM_RPC/VIR_ERR_INTERNAL_ERROR); an
  operation the daemon does not support (VIR_ERR_NO_SUPPORT) is not
  treated as one.
- ExceptionMiddleware (pages and API) and the views that report the
  error themselves (snapshot, revert, disk deletion, clone) show it to
  superusers and view_instances users, and to everyone else "The host
  of this virtual machine cannot be reached right now." Errors of the
  operation itself ("domain is not running") are shown as before.

Checked on the test setup: powering on a VM of an unreachable compute
shows the owner the generic text and the administrator the error. The
tests fail on the previous code.
2026-10-06 10:08:48 +03:00
catborise
1fb74d6378 fix(instances): turning a template back into a VM is checked against the quota
Templates do not count towards a user's quota and cannot be started.
Staff owners may mark and unmark their VMs as templates, so a staff
user could mark a VM as a template, use the freed quota elsewhere, and
unmark it again: the VM came back without any check, over the quota.

For users with a quota (everyone but superusers), turning a template
back into a VM now passes check_user_quota with the VM's size (one VM,
its vCPUs, memory and disks). If the quota is reached or cannot be
verified, it stays a template and the user is told why. Marking a VM as
a template frees quota and is not checked. The view takes the
per-user quota lock like the other quota-checked changes.

The existing template-flag test gives its mocked VM a size. The new
tests fail on the previous code.
2026-10-05 22:19:34 +03:00
catborise
f5276655fd perf(instances): the VM page reads each kind of domain XML once
The VM page's getters read the domain XML on their own: one load made
86 libvirt calls for a real VM, 29 of them for the same three XML
documents (live, live with secrets, persistent) and 6 for the host
capabilities. Each call is a round trip to the host.

wvmInstance.cached_reads() is a block in which _XMLDesc (per flags
value) and get_cap_xml are read from libvirt once; it ends with the
block, even on an error, does not nest, and _defineXML drops it.
Outside a block every read goes to libvirt as before. Only the VM page
view, which does not change the VM, uses it, around its rendering.
Views that change the VM keep reading fresh XML: most changes (devices,
QoS, memory) do not go through _defineXML, and several views read,
change and define more than once in one request.

Measured on the test host: 86 -> 55 calls (XMLDesc 29 -> 3,
getCapabilities 6 -> 1) for one VM, 82 -> 54 for another; the page took
0.23-0.25 s before, 0.14-0.15 s after. The HTML of both pages is
identical before and after (CSRF tokens aside). A test renders the page
on libvirt's test driver through the real getters: 25 XML reads before,
one per kind now.
2026-10-05 21:24:07 +03:00
catborise
5bb3a545a0 perf(instances): VM statistics sample all devices in one second
Each open VM page asks for its statistics every 10 seconds. For a
running VM the stats view measured the CPU, then each disk, then each
NIC, sleeping one second for each. With two disks and a NIC a request
held a sync gunicorn worker for over 4 s, so a few dozen open VM pages
could keep every worker busy.

wvmInstance.usage() replaces cpu_usage, disk_usage and net_usage, the
stats view's only callers: it reads all counters, sleeps one second
once and reads them again. Values and units are unchanged (CPU percent
of the host, disk bytes, network bits per second); compared against the
previous methods on the same counters, the results are identical.

Measured on the test host for a running VM with two disks and a NIC:
the stats request took 4.1-4.3 s before, 1.1-1.3 s now. The new test
fails on the previous code.
2026-10-05 15:27:55 +03:00
catborise
f3929b8e8f fix(instances): only administrators choose where a console listens
A VM owner who may manage the console (is_change and is_vnc) could set
its listen address to all interfaces and remove its password: a VNC
console reachable from the network without a password. Checked on a
test host: the owner's form changed the VM's listen address to 0.0.0.0
and then removed the password.

- The listen address field is disabled for users who are not
  superusers; a disabled field ignores posted values. They can still
  set or change the password and the keymap.
- A console whose persistent definition does not bind it to loopback
  (or a Unix socket) only loses its password through an administrator.
  A binding the XML does not pin down counts as not local: listen type
  network, or no address (the host's default in qemu.conf).

On the test host, the owner's form now leaves the listen address alone
and is refused when it removes the password of a console listening on
0.0.0.0; with the console on localhost the password can be removed.
The new tests fail on the previous code.
2026-10-05 15:07:31 +03:00