1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-08 19:26:48 +00:00
Commit graph

194 commits

Author SHA1 Message Date
catborise
9cf2d55b6e docs(readme): the cloud-init datasource gives keys to the image's default user
The datasource puts the SSH keys of the VM's users in ssh_authorized_keys,
which cloud-init gives to the image's default user, not to root.
2026-10-25 21:33:41 +03:00
catborise
f004552148 feat(instances): a new VM can get a cloud-init seed with its hostname and SSH keys
Cloud images read their first-boot settings with cloud-init. The panel
served them only over HTTP, which a VM reaches only with the network
and a reverse DNS name for its address. A new VM can now carry them
itself: Cloud-init seed (Advanced) on the create form, or cloud_init in
the API.

The job writes a NoCloud seed, an ISO labelled cidata, into the disk's
directory pool as cidata-<uuid>.iso and puts it in the VM's CD-ROM
drive. Its meta-data holds the VM's UUID as instance-id and its name as
hostname, written as JSON so a VM named 123 or yes keeps a string
hostname; its user-data is what the HTTP datasource serves, with the
creating user's SSH keys. The ISO is built with pycdlib and written
through a libvirt stream, as the NVRAM copy is, now one helper for both.

A seed needs a CD-ROM bus and a directory pool, else the create is
refused. A failed create deletes the seed with the new disk, recovery
names one left behind, and deleting the VM with its disks deletes its
seed, found in the pools by name even when ejected. A clone's drive is
left empty: the seed is the source VM's identity.
2026-10-25 19:57:15 +03:00
catborise
75fcc6f6c0 feat(instances): a running VM gets its root password and SSH key through its guest agent
The Access tab's Root Password and SSH Keys worked only on a shut-off
VM, through gstfsd on the compute. A running VM whose QEMU guest agent
is connected now gets them through the agent: guest-set-user-password
with the SHA-512 crypt hash, and guest-ssh-add-authorized-keys adding
the key to root's keys. A shut-off VM still goes through gstfsd; any
other VM is refused with "Shut the VM down, or start its QEMU guest
agent, and try again", and the buttons are disabled.

Each is one agent command with a 10 s timeout. libvirt's own calls for
them (setUserPassword, authorizedSSHKeysSet) wait for a hung agent
indefinitely, holding a worker and the VM's lock, and the domain's
agent timeout would change every other agent call of the VM too.

The agent's or libvirt's error is shown as is through error_text, so
users who do not see the hosts get no connection details, and the
attempt is logged as failed. The README names the SELinux boolean a
RHEL-family guest needs before its agent may add SSH keys.
2026-10-25 15:37:37 +03:00
catborise
1c60696ea1 feat(accounts): API tokens for scripts and tools
The REST API took only the login session, so a script could not use it
without a browser. A user now makes API tokens on the profile page (API
Tokens tab): a name, and an expiry of 30, 90 or 365 days or none. The
token is shown once, on a page that is not cached, and then kept only as
a SHA-512 digest; the list shows its name, first characters, creation
and expiry, and a token is deleted there. Tools send it as
"Authorization: Token wvc_...", and act as its user, with that user's
permissions. Making and deleting a token is logged, without the token.

A password alone (HTTP Basic) still does not reach the API, as it would
skip the OTP second factor: a token is made in a session that passed the
login and its OTP. The token class comes first, so a refused request gets
401 with its challenge instead of 403.

django-rest-knox 5.1.0 provides the tokens (accounts.ApiToken adds the
name); settings.py takes "knox", KNOX_TOKEN_MODEL and REST_KNOX from the
template.
2026-10-24 23:14:27 +03:00
catborise
9ec665cbca docs: the serial console step starts compose like the other steps
The step ran `docker compose -p webvirtcloud up -d`. The other Docker
steps let compose name the project after the directory, so in a clone
under another directory name this command started a second deployment
with its own volumes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-24 10:49:51 +03:00
catborise
ab5ed59ea1 feat(instances): a new VM is made on one form in three steps
The create page had three tabs (flavor, custom, template), each with its
own copy of the same create form; every flavor rendered one more copy in
a dialog, and fixes had to be made in each copy. All of them posted the
same form to the same job, differing only in where the disk comes from.

The page is now one form in sections: name and size, disk, networks and
an Advanced section. The disk section offers three sources as radio
cards (a new disk, existing volumes, a copy of a template); only the
chosen source's fields are shown, and the others are disabled, so they
are neither sent nor validated. A template's copy goes into the
template's pool unless another is picked. Flavors are presets that fill
vCPU, memory and disk size and pick a new disk; they are added and
deleted in a dialog outside the form. Every source uses the
per-interface network rows, so a new disk no longer requires a MAC:
libvirt gives an interface without one its address, as it did for the
other sources. A refused form comes back with its source, volumes,
template and networks, and the focus on its first error. Without a
storage pool or a network the page shows why instead of the form, and a
host that cannot be read is a message, not a 500.

The host dialog and the machine page share the form's look: hosts are
rows with their CPU and memory use, the machine page is a card with
Cancel/Next and the XML way is a collapsible section. All three steps
show the same step indicator, done steps linking back.

The create job refuses more than one disk source (the page sends one; a
crafted request could send two, and the job would have used the first).
A field with help text is described by its help and its error.

Frontend review fixes: the presets and the disclosure buttons show a
focus ring for keyboard users; a disclosure spans its card and its
chevron turns while open; a preset says it picks a new disk; numeric
fields bring a numeric keyboard and MAC fields no autocorrection; the
machine page's Next waits for a machine type. Tom Select's control
keeps the text colour of the select it replaces: its own fixed colour
made typed text invisible on dark Bootswatch themes, on every page that
uses it.
2026-10-23 23:26:28 +03:00
catborise
5b8793ad4c feat(logs): changes to networks, storage, interfaces, secrets and filters are logged
The Logs page now records every change an admin makes to a compute's
libvirt objects, with the compute's name in the host column:

- networks: add, start, stop, delete, autostart, DHCP range, fixed
  addresses, QoS and XML edits;
- storage pools: add, start, stop, delete, autostart, from web and
  API; volumes: add and delete (web and API), clone and ISO upload;
- host interfaces: add, start, stop, delete;
- secrets: add, set value (never the value), delete;
- network filters: add, clone, delete, XML edits, rules and
  references.

A refused or failed change is a failed row with its reason (libvirt
text through safe_text). The volume clone and upload jobs write their
row when they end; a refused submission is a failed row at once. A
volume row names the volume libvirt created (a dir pool adds .qcow2 or
.img). Volume create or delete through the API on an inactive pool
(409) is a failed row.

Network filters: the rows read "Network filter f1: add" instead of
"f1 network filter is created", and their host is the compute's name
instead of its hostname. A duplicate name or UUID no longer raises a
bare exception, and a filter XML that cannot be parsed no longer ends
in UnboundLocalError.
2026-10-23 13:30:21 +03:00
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
8c0fbff8f7 feat(logs): admin changes and logins are recorded on the Logs page
The Logs page shows who added, changed or deleted a compute, from web
or API, and every login, failed login, lockout and logout, with the
client address and a Result column (Done, Failed, Unknown for older
rows). A failed change is recorded with its reason and the error still
reaches the user; a refused one is recorded too.

- logs.audit.write saves each row in its own savepoint and never
  raises, so a log write cannot break or undo the action it records.
  The audit() context manager writes one row when the change ends.
- Logs gains success and ip; user, host and instance are wider and
  date is indexed. The page orders by date then id.
- A compute edit names the changed fields; an edit that changes
  nothing writes no row, and an empty password field is no change.
- A compute's VMs leaving the panel get a system row each: owners
  kept on the Removed VMs page, moved to another compute, restored,
  left without owners, or the removed record expired.
- Job rows carry the result. A job's log entry that cannot be
  computed is logged and skipped instead of rolling back the finish
  (test_service.py changes this on purpose).
- Logins are recorded by signal receivers; CustomLoginView is removed
  and the plain LoginView is used. A wrong OTP code is a failed login,
  counted by django-axes toward the lockout.

Panel records and host-side objects follow in the next layers.
2026-10-23 10:30:48 +03:00
catborise
2dee5ba41c fix(computes): a deleted compute's VMs keep their owners
Deleting a compute removed its VMs from the panel together with their
owners, while the VMs kept running on the host: added again, the host's
VMs came back with no owners. A compute's delete now treats each VM as
one that left its compute: its owners follow the same UUID on another
compute or wait in a tombstone for the retention period. One function,
forget(), does this for the refresh and for the delete; the API's
compute delete goes through the same model delete.

The delete holds the compute exclusive and locks its row before its
VMs', as the reconcile does: a reconcile, VM operation or job on the
compute makes it busy instead of losing owners to a concurrent restore
or deadlocking with it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 13:24:22 +03:00
catborise
8e23138ef7 test: the suite runs with manage.py test --parallel
Django clones a SQLite test database by copying its main file, while
migrate's writes were still in the WAL file, so every clone had no
tables. The project test runner writes the WAL back before cloning.
Its pool workers are no longer daemonic, so the operations process
tests can start their own workers. tblib carries the traceback of a
failure out of a worker instead of ending the run.

The suite takes 35 s on 8 CPUs instead of 133 s.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-21 09:01:40 +03:00
catborise
6f44461c28 feat(security): every page carries a Content-Security-Policy that needs no template change
A small middleware sends object-src 'none', base-uri 'self', form-action
'self' and frame-ancestors 'none': no plugin content, no injected <base>
redirecting relative URLs, no form posting to another origin, no framing.
script-src and style-src are left out: the templates still hold inline
scripts, event handlers and javascript: URLs, which an enforced script
policy would block.

The middleware is added to the settings template, which the update steps
and each Docker container rebuild settings.py from.
2026-10-19 15:49:39 +03:00
catborise
75559629e4 docs(tests): the live suite also runs over a socket compute, inside compute1
A socket compute is the panel's own qemu:///system, so the live tests for
it have to run on the libvirt host. Two more lines in the Vagrant block
copy the committed HEAD into compute1, install the dev requirements in a
venv with the distro's libvirt bindings, and run the suite there with
TEST_LIBVIRT_TYPE=4 and the LVM, iSCSI and NFS tests enabled.
2026-10-19 14:36:58 +03:00
catborise
c8b0cb3a4a fix(storages): pool sources take real device and export paths, a leading special character is refused
The source check of a new logical or NFS pool accepted only letters,
digits and "/", which refused paths such as /dev/mapper/vg0-data,
/dev/disk/by-id/wwn-..., /dev/disk/by-path/pci-0000:00:1f.2-ata-1 or
/export/vm_images. The target check let any first character but "-"
through, so a quote, "<" or "&" reached the pool XML and libvirt failed
on it.

Target and source now share one expression: letters, digits, "/", "_",
".", ":" and "-" (not first, not right after "/"), still in linear time.
The Vagrant computes' NFS export becomes /srv/wvc-nfs, created through the
page in the live pool test.
2026-10-19 12:10:33 +03:00
catborise
baeb023da2 fix(storages): a new NFS pool starts when its mount point does not exist
create_storage_netfs started the pool without a build, so a netfs pool
whose target directory was missing failed to mount ("mount point ... does
not exist"): the page showed the error and the pool stayed defined and
inactive. It now starts with VIR_STORAGE_POOL_CREATE_WITH_BUILD, as the
dir and logical pools do, which makes the mount point.

The Vagrant computes export /srv/wvcnfs to 127.0.0.1, and the live pool
test creates an NFS pool on it at a missing mount point when
TEST_LIBVIRT_NFS_EXPORT is set.
2026-10-19 11:58:06 +03:00
catborise
5c68050ce0 build: LDAP login's packages are optional
django-auth-ldap, python-ldap and pyasn1 move to
conf/requirements-ldap.txt. Installs without LDAP no longer need the
openldap and sasl headers that pip builds python-ldap with; the
installer and the README's system packages drop them, and the README's
LDAP section installs them with that file. The settings already work
without the module.

The Docker image and dev/requirements.txt keep LDAP, so CI still builds
and audits python-ldap.
2026-10-18 22:50:11 +03:00
catborise
41468c700d build: the themes ship compiled, libsass is no longer installed
A theme change compiled Bootswatch with libsass (34 MB, upstream
deprecated) into two tracked files, which updates, rollbacks and new
Docker containers then reset. The 27 themes now ship as
static/css/themes/<theme>.min.css, built by "manage.py build_themes";
the Settings page only saves the theme name and every page links that
file (zephyr when no settings are in context, as on the 500 page).

libsass moves to dev/requirements.txt; a test checks that each shipped
file is its theme compiled from dev/scss. The "Theme SASS Path" setting
is removed, and the theme default "flaty", a typo that never applied,
becomes zephyr, the theme those installs showed. The update and
rollback steps restore an older version's rewritten theme files only
where they are modified.
2026-10-18 22:36:29 +03:00
catborise
28a73519d7 fix(interfaces): deleting a bridge keeps the devices it held
With the netcf backend (RHEL/AlmaLinux 8), undefining a bridge also
removes its members' configuration: the device came back without any
after a reboot. Like virsh iface-unbridge, each member is now defined
again on its own after the bridge is undefined, with the bridge's start
mode and, when it is the only member, its addresses. A bond keeps its
mode and devices.

Checked on AlmaLinux 8 (libvirt 8.0, netcf): an ethernet and a bond
member get their ifcfg back, and the bond comes up with the address
after a reboot.
2026-10-18 22:01:18 +03:00
catborise
7526e9ab32 dev: the Vagrant computes accept TLS logins, and the live suite runs over TLS too
vagrant up makes a throwaway CA and a client certificate in
.vagrant/tls/.pki/libvirt with the host's openssl (Vagrant puts its own
first on PATH). Each compute signs its server certificate for its
address, asks for SASL over TLS like over TCP, and listens on 16514.

libvirt takes a client certificate from $HOME/.pki/libvirt, so the
README's second run uses HOME=.vagrant/tls with TEST_LIBVIRT_TYPE=3 and
leaves the user's own certificates alone.
2026-10-18 21:32:52 +03:00
catborise
8d175bbd54 test: a LUN of an iSCSI pool attaches live as a block disk
The Vagrant computes serve a 1 GiB LUN on 127.0.0.1 (targetcli). A live
test, run with TEST_LIBVIRT_ISCSI_TARGET, starts a transient iSCSI pool
on it and attaches the LUN from the VM page to a stopped and a running
VM: both definitions get a raw block disk on its /dev/disk/by-path link.
2026-10-18 21:32:52 +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
a83b46d65d dev: Vagrant provides two disposable AlmaLinux computes for the live tests
The Vagrantfile (Ubuntu 18.04, supervisor, a missing requirements file)
is replaced: two AlmaLinux 10 VMs with nested KVM, set up by the
bootstrap, with a SASL user for TCP and the host user's SSH key for
root, so the live suite, migration included, runs without real hosts.
The README's Running tests says what the host needs and how to run it.
2026-10-18 11:53:06 +03:00
catborise
99570669c5 fix(install): RHEL steps enable CRB, openSUSE steps warn about nginx's default server, the compute bootstrap detects openSUSE Leap
The RHEL manual install failed at libvirt-devel, which is in CRB; the
installer already enables it. openSUSE's nginx.conf has its own port-80
server for localhost, which the RHEL steps already warn about. The
bootstrap skipped /etc/os-release as a symlink, which on openSUSE Leap
is the only release file, so it found no distribution.
2026-10-18 00:05:42 +03:00
catborise
42359ca2e2 build: computes run gstfsd as a systemd unit instead of under supervisor
The compute bootstrap installs gstfsd and webvirt-gstfsd.service in one
helper and no longer installs or restarts supervisor; the README's
compute step does the same, a remote panel's bind goes into a drop-in,
and a compute moving from supervisor removes its gstfsd program.
2026-10-17 23:42:41 +03:00
catborise
837c5468ec build: systemd runs the panel on hosts instead of supervisor
The installer and the manual installation install four systemd units
(webvirt-cloud, webvirt-novncd, webvirt-socketiod, webvirt-worker) that
run as the web server's user with absolute paths and read
/etc/default/webvirtcloud. gunicorn, socketiod and the worker stop with
KillMode=mixed, so they wind down their own children. supervisor, its
installer code and the contributed systemd files go; the README gets
the update, rollback and a one-time move from supervisor, and the
installer refuses to run over a supervisor install. Docker keeps runit;
novncd's runit script is the one-line form of the others.
2026-10-17 23:18:26 +03:00
catborise
fbef55fe2d docs: updates set aside migrations an older installer generated
Installers before September 2025 ran makemigrations, which left
untracked migration files: one has the name of a tracked accounts
migration (git refuses to check out over it, and once applied it makes
migrate skip the tracked one), the others compete with tracked
migrations. The update now moves them into the backup, marks that
accounts migration unapplied when it was generated locally, and the
rollback puts them back.
2026-10-17 22:35:29 +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
d37aa42392 feat(console): a working serial console
The xterm console never connected: socketiod expected a different token
format, its worker had no compute credentials, one global console was
shared by every client and its output broadcast to all of them, and the
Docker image did not run socketiod at all.

- console/tokens.py: one authorize() for noVNC and socketiod; the signed
  token is the only credential, checked at connect time.
- socketiod: one pty worker per client (consolecallback with the
  instance id), one session per VM, a session cap, input and resize
  checked; the parent never opens libvirt. The engine.io same-origin
  check is on.
- consolecallback: opens the console with the compute's credentials and
  exits with a message when it cannot.
- Docker runs socketiod as a runit service; nginx proxies /socket.io/
  with Host and the scheme for the origin check. Supervisor and systemd
  start it as well; WEBVIRTCLOUD_SERIAL_CONSOLE=1 enables it.
- Tests: token and session unit tests, socketiod as a process, and a
  live test that reads SeaBIOS from a wvc-test VM on the TCP and SSH
  hosts.
2026-10-17 19:43:09 +03:00
catborise
d3e4ef9132 feat(instances): a shut-off UEFI VM can be migrated
The move copies the VM's NVRAM file to the destination with libvirt
streams (unless both hosts share the NVRAM directory) and removes the
copy if the move fails; the source's file goes with the existing
cleanup. The undefine keeps the file, as libvirt refuses to undefine a
VM with NVRAM otherwise. A VM with TPM state is still refused.
2026-10-16 21:05:23 +03:00
catborise
c93bcc5d04 docs: a running VM can be cloned 2026-10-16 16:52:38 +03:00
catborise
4629e0557f feat(storages): the upload dialog closes once an upload starts
The job panel follows an ISO upload, so the dialog closes when the first
chunk is accepted and its progress bar goes; the dialog opens again with
the error if the upload stops. The browser still sends the file, so the
page asks before it is left while an upload runs. The storage page no
longer includes the please-wait dialog, which nothing on it opened.
2026-10-16 12:16:35 +03:00
catborise
06f03d65fc docs: an ISO upload is a job 2026-10-16 11:53:59 +03:00
catborise
41e1fcd4b9 docs: migration runs as a job 2026-10-16 07:57:02 +03:00
catborise
149c14f1bb docs: snapshot create and revert run as jobs
Live tests that expected the snapshot or clone result at once now call the
backend or run the queued job.
2026-10-15 14:21:32 +03:00
catborise
9d3d218fbb docs: clone and create run as jobs 2026-10-15 09:28:43 +03:00
catborise
b59f30138a build: run the job worker in every install type 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
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
08cad04468 fix(instances): deleting a VM's data takes its snapshot files too
Deleting a VM with its data deleted only the images its disks pointed
at. With external snapshots that is the newest overlay: the image the
snapshots were taken on, the older overlays and the memory files stayed
on disk, with nothing referring to them once the VM and its snapshot
metadata were gone.

The files the VM's snapshots were taken on, down each disk's backing
chain, the overlays of snapshots kept off the chain after a revert, and
the snapshots' memory files are now the VM's own too. A template the
VM grew from ends the chain and stays, as does an overlay of a disk
detached since. The backing references of the VM's own volumes no
longer count as another domain's use, and a file another VM uses keeps
the files below it.

A backing file that cannot be read (other than one in no pool) stops the
split before anything is deleted, instead of ending the chain there.
2026-10-13 09:03:05 +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
c55f52f025 test: each app keeps its tests in a tests package
131 test modules sat next to the code they test; instances alone had
51 of them beside views.py and utils.py. They move into a
tests/ package per app, and the JavaScript checks into
webvirtcloud/tests/js. Each non-empty tests.py becomes tests/test_views.py;
the empty generated stubs are removed.

Imports of the app's own modules become absolute, paths built from
__file__ go one level up, the live helper is instances.tests.livetest,
and coverage omits */tests/*. The suite discovers the same 960 tests as
before.
2026-10-12 15:17:30 +03:00
catborise
0906838193 fix(nginx): browsers revalidate static files
Static URLs carry no version, but nginx served them with expires max, so
after an update or a theme change browsers kept the old CSS and JS. They
now revalidate each load (304 when unchanged). CI checks the header, the
304 and that a changed file is sent again.
2026-10-12 08:52:41 +03:00
catborise
d4f04cbf1e build(deps): Bootstrap and Bootswatch 5.3.8
Bootstrap SCSS, the JS bundle and the Bootswatch themes go from 5.3.3 to
5.3.8 (bug fixes only), and wvc-main.min.css is recompiled from zephyr.
Bootswatch now gives table text the body color, and Lux drops card and
modal header borders.

Tests compile every theme with the pinned libsass and check that the
shipped CSS is zephyr from these sources. The update and rollback steps
put back the two files a theme change rewrites, or git refuses to pull
over them.
2026-10-12 08:44:31 +03:00
catborise
72681ab54e build(deps): the OTP e-mail draws its QR code with segno
django-qr-code was used for one thing: the QR code in the OTP e-mail,
as an inline SVG and a PNG. It brought pydantic, pydantic-core,
annotated-types and typing-inspection (about 10 MB) and, from 5.0 on,
Pillow. segno, which it used underneath, now draws both images with the
same options (error correction M, 4-module border, 1.8 mm SVG modules,
12 px PNG modules); the SVG, the PNG and the alt text are byte for byte
what django-qr-code 4.2 produced. account.html loaded the tag library
without using it.
2026-10-12 07:24:56 +03:00
catborise
3be3b4b2cc fix(install): openSUSE installs get python-ldap and pyasn1 without known flaws
The installer's venv sees the system site packages, and pip keeps a
system package that meets a requirement. On openSUSE Leap 15.6 the LDAP
login thus ran on the distro's python-ldap 3.4.4 (CVE-2025-61911, a
filter-escaping bypass, and CVE-2025-61912) and pyasn1 0.5.0
(CVE-2026-30922 and others), which django-auth-ldap loads through
ldap.controls. python-ldap>=3.4.5 and pyasn1>=0.6.4 make pip install
fixed copies in the venv. python-ldap builds there, so openldap2-devel
joins the openSUSE package lists (installer, manual and development
setup) and the update steps. Checked in a fresh Leap 15.6 container
with the installer's package list: 3.4.8 and 0.6.4 from the venv,
ldap.controls imports.
2026-10-11 22:45:52 +03:00
catborise
87ffed66b9 fix(instances): attach a disk, NIC or guest agent to both definitions in one call
On an active VM a device was attached in two calls, running VM first.
When the persistent attach was refused, for example because a disk target
or an agent channel was already pending there, the device stayed in the
running VM only and was gone at the next start. A NIC added without a
MAC got a different generated MAC in each definition. One call with
AFFECT_LIVE|AFFECT_CONFIG lets QEMU check the persistent copy before the
hotplug and keeps one MAC. Detach and update still go per definition,
each from its own XML.
2026-10-11 20:12:24 +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
c1f6620c4b feat(instances): a running VM's settings tab says most settings apply at the next start
On an active VM most edits in the settings tab change the persistent
definition only. The tab now says so above its panes.
2026-10-11 17:47:44 +03:00