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>
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>
Every change to a filter (a rule or reference removed or added, the XML
edited) undefined it and defined the new XML. libvirt refuses to
undefine a filter a running VM uses, so none of these worked on such a
filter; on an unused one a refused definition was undone by defining
the old XML without its UUID, so the filter came back as another one.
The filter is now defined again with its own UUID, which libvirt does
in place, while VMs use it too; XML it cannot parse or apply to the
running VMs is refused and reported. The XML editor shows the UUID.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
On a running network the change did nothing, yet the page said the
range was changed; a network without a range or an unknown address
family ended in a server error. Each is refused now with the reason:
stop the network first, or the network has no such range.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Stopping a network cut the running VMs on it off, deleting one or a
secret kept the VMs that name it from starting, and stopping a pool
took the disks of running VMs away, all without a word. Each is now
refused with the VMs (and, for a secret, the pools) that need it:
stopping while a running VM uses it, deleting while a VM's definition
names it. A disk given by its file path does not need its pool's
definition. The API's pool stop is refused the same way.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The upload view sent the error of an unexpected failure to the browser
but left nothing in the server log, so whoever runs the server had no
trace of it. It is logged with its traceback now; the superuser still
sees the message.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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>
In the VM lists, Force Off and Power Cycle ran on a single click,
though both cut the VM off as a power failure does. Each button now
asks first, naming the VM and saying that what the guest has not
written to disk is lost.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The login page showed its sign-in card to a user who was already signed
in, with the navbar squeezed beside it. Both login views, with and
without OTP, now send that user to the page in next, or to the default
page when next is missing or points elsewhere. Every permission check
answers 403 instead of redirecting to the login, so this cannot loop.
The page test renders the login page signed out.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Column offsets without a breakpoint applied below it, where the column
is already full width, so the change password, compute, user, VM
create and VM settings forms ran past the right edge on a phone. Each
offset now starts at its column's breakpoint. The interface and network
detail lists were rows directly inside rows and sit in a column now;
the compute form loses a Bootstrap 3 thumbnail padding that broke its
gutters, and the add-rule dialog's hint is no longer cut off. The page
test checks both grid rules on every page.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Twenty tables had no .table-responsive wrapper, so on a phone the user
list, compute list, VM lists, network and network filter pages scrolled
sideways as a whole. Each table now scrolls inside its box, as the
other ten already did. The page test walks the network page too, and
checks that every table on every page has the wrapper.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
CI once found a test worker still running 15 s after SIGTERM, and the
failure said nothing more. The worker children now dump their threads'
stacks on SIGUSR1, and tearDown sends it to a child that is still alive
before killing it, so the next such failure names the place.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The pool workers were made non-daemonic so the operations process
tests could start workers, but a terminated pool (Ctrl-C, --failfast)
left those workers running. The test runner now keeps the test cases
tagged "processes" out of the pool and runs them in the main process
after it. A WAL that cannot be written back before cloning now stops
the run instead of giving clones without tables. One CI leg runs the
suite with --parallel.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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>
No page or script called it, and it answered with the disk device
types for both videos and buses. The create view keeps reading the
domain capabilities through wvmConnect.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The pool, network and interface cards had a solid green or red header
with light text, and status words (Connected, ON, OFF, the DRBD state)
were in the theme's green or red text colour. axe found them below the
WCAG AA 4.5:1 contrast on the live theme (4.29:1, 3.52:1), on the
default zephyr theme, whose green is light (2.5:1), and on dark themes
(darkly 1.18:1).
The card headers, and the status words as badges, are now the theme's
pale status background with its dark status text (bg-*-subtle with
text-*-emphasis). Bootstrap derives the pair from each theme's palette,
and it does not depend on the page's background: the dark Bootswatch
themes keep light-mode emphasis colours, which on their own would be
dark text on a dark page. The card border keeps the status colour.
The usage list of the create-secret dialog came from the map that names
the usage types of existing secrets, so it offered none and tls too; the
form takes ceph, volume and iscsi only, and picking none or tls always
failed with "Select a valid choice". The dialog now lists the form's own
choices.
A network's QoS form whose rate was not a whole number (abc, 1.5) made
wvmNetwork.set_qos raise a ValueError that the view did not catch: the
request ended in a server error. The page's pattern stops this in a
browser, but the server must refuse it too. set_qos now says "QoS rates
must be whole numbers", and the view reports that, as it does a libvirt
error, and goes back to the network's page.
The create-network form only checked the characters of its subnets, so
192.0.2.0/33 or 999.0.2.0/24 passed and network_size raised a
ValueError: the user got a server error instead of a message. So did a
subnet too small for the gateway, DHCP range and broadcast address that
network_size derives (0.0.0.0/32, 255.255.255.255/32), and 192.0.2.0/31
or a bare address gave an inverted DHCP range.
Each subnet field now parses its value as a network of its IP version
and refuses it with a field error otherwise. An IPv4 prefix must be /30
or shorter; the IPv6 prefix must be /64, which the view checked after
the form and now the form does.
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.
The edit dialog of a secret was filled with its current value, so every
readable secret's value was in the secrets page's HTML. A private
secret, or one with no value yet, cannot be read: its field held the
text "None", which is valid base64, and saving the dialog unchanged set
the secret to the three bytes it decodes to. The dialog now only writes
a new value: the field starts empty, is a password field and is
required. The page no longer reads secret values, and get_secret_value,
used only for that, is gone.
Three selects of the VM create page had no accessible name. TomSelect
points the label of a select it enhances at its own input, so the
network select lost its label and the image select never had one; the
template select sits beside the template pool's label with none of its
own. Each now has an aria-label (Images, Networks, Template), which
TomSelect also copies to its input for the image select.
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.
The bus list of a disk on the VM create page came from the host's disk
device types: choosing "disk" for an added image offered cdrom, disk and
lun as its buses. The disk branch of get_disk_buses now returns the
host's disk bus types, the list the page starts each disk with.
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.
The custom create form shows a MAC per network (random, or typed in),
but had no mac field: the script wrote the joined list into every
name="mac" input of the page, the other six-plus forms' own MAC fields,
so a flavor form then sent one MAC per custom network and was refused,
and the custom VM got other MACs from libvirt. The custom form gets its
own hidden mac field and the script writes only there. The field took
17 characters, one MAC, so the API also refused a VM with two NICs and
their MACs; it takes 255. The serializer's test sent no submission_key,
so its check for more MACs than networks never ran; it does now, next
to the case of two networks with two MACs.
Changing a disk's device type in the custom create form asked for its
buses at /archs/undefined/machines/undefined/...: the script read the
architecture and machine from a select[name=arch] and #machine-control
the page does not have. The request failed (500) and the bus list kept
the disk's buses, so a CD-ROM could be put on virtio. The script takes
both from the page's own URL.
After a refused submission the focus stayed on the page body: a keyboard
or screen reader user had to find the error. base.html now focuses the
first visible invalid field (the inline errors of the page forms), else
the first error message at the top of the page (the modal forms, and
pages whose host does not answer). The messages sit in a #messages
container so the script finds only them.
An accessibility scan of the pages on a real host (the earlier scans ran
on libvirt's test driver) found unnamed elements the test driver never
renders: the network page's add fixed address buttons and their dialog
fields, the XML box of each nwfilter's details dialog and the convert
checkbox of the volume clone dialog. The buttons get a title, the labels
a for= and the fields an id, the XML box an aria-label.
get_secret_value returned bytes, so the edit dialog showed b'cHc=' (the
bytes' repr) for the base64 value cHc=. Saving the dialog as shown fed
"b'cHc='" to b64decode, which dropped the quotes and the b and stored
other bytes: the secret silently changed (pw became m\xc1\xdc). The value
is now base64 text, and set_secret_value decodes with validate=True, so
a value that is not base64 is refused with a message instead. The field
had an invalid pattern ([a-zA-Z0-9]$+, ignored by browsers) and a 45
character limit; it takes base64 of any length.
create_storage_ceph and create_storage_netfs wrote the pool, host, user,
secret and format values into the pool XML unescaped and unchecked: the
page's patterns were the only guard, and a direct POST could break or
extend the XML. The values now go through util.xml_escape, as the
network code does. The page's host fields stopped at 20 characters, so
nfs01.storage.example.com could not be typed: they take a host name's
253; the Ceph pool and user fields lose the limit the server never had.
The MAC field's pattern ([0-9/:]+) had no hex letters, so the browser
refused nearly every MAC address (52:54:00🆎cd:ef) and no IPv4 fixed
address could be added from the page. The host name field of both
dialogs ([a-zA-Z0-9_]+) refused names such as web-01 or db.lan. The MAC
takes hex digits and ":", the name letters, digits, "_", "." and "-";
the server already escapes both into the network XML.
A user with the permission (and a superuser, who holds every permission)
gets the console password filled in, so noVNC does not ask for it; a
user without it must be asked, and the page must not hold the password.
The test covers both console views for an owner without and with the
permission and for a superuser.
The console settings form lost its password, keymap and listen address
fields long ago (47009d4), but its scripts stayed: show_console(), which
nothing calls, change handlers for #console_passwd_gen, #console_passwd_clear
and #console_keymap_clear, and two blocks that preselected
#console_select_keymap and #console_select_listener_address. No template
defines any of these ids, so the code matched nothing. The instance list
drops a commented-out line that read #compute_select.
An accessibility scan with every modal opened found 49 fields without a
name: the labels of the instance create forms (custom, template and each
flavor's modal), the interface, network, secret and storage pool modals
did not point at their fields. Each label now has a for= and its field an
id, prefixed per form (and per flavor), so a click on the label focuses
the field too. The "Advanced" collapse buttons and the memory and
processing bars are named.
The VCPU config select had id="vcpu_mode" in each of these forms, nine
times on the create page; the unique-ID test did not see it as the test
driver reports no CPU modes. The id goes (nothing used it), and the test
now renders that branch.
create_secret wrote the usage value into the secret XML with an f-string,
unchecked: a quote, "<" or "&" broke the XML or added to it. The page's
pattern ([a-z0-9. ]+) was the only check and refused real values: a
volume secret's path ("/") and an iSCSI IQN ("-", ":"). The XML is now
built with lxml, so the value is text whatever it holds, and the input
keeps only required and the form's 100 characters; libvirt reports a
value it does not take.
A live test creates a volume, an iSCSI and a Ceph secret with real
values through the page.
The create pool modals kept their own pattern attributes, older and
stricter than the server's check: a logical source took only lower-case
letters, digits and "/", dir and NFS paths no "." or ":", and the NFS
fields stopped at 40 characters (the form allows 100). The browser
refused /dev/mapper/vg0-data, /dev/disk/by-id/wwn-..., /srv/vm.images or
/export/vm_images.v2 before the server saw them. The path fields keep
"required" and leave the check to the server, which shows its message.
An accessibility scan (axe-core, WCAG 2 A/AA) of every page found 87
elements without a name. Icon-only buttons and links (the navbar's
administration menu on every page, the add/create buttons, the flavor
delete and back buttons) get a title, which is also their tooltip; the
labels of the language, architecture, chipset and filter selects point
at them; the memory bar is named. The shared delete confirmation page
had an empty <title> and heading, as no view passes "title": it says
"Delete <object>". The power-off form's aria-label was misspelled.
The grouped list opens one tbody per compute, so with no compute the
sortable table had none, and sortable.js threw on page load ("Cannot read
properties of undefined (reading 'rows')"). An empty tbody is rendered
in that case.
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.
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.
A script built the console page's error alert as HTML text, with a broken
class attribute (role="alert" ended up in it), a close button without a
handler on a danger alert that never fades, and placed it above the
viewport-high console container, cutting off its bottom. The template now
renders the message (autoescaped) as a dismissible Bootstrap alert inside
the container, under the navbar; the log_* functions had no other caller.
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.
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.
create_storage built only logical pools, so a dir pool whose target path
was missing failed to start ("cannot open directory"): the page showed the
libvirt error and the pool stayed defined, inactive and without autostart.
The pool is now started with VIR_STORAGE_POOL_CREATE_WITH_BUILD, which
makes the directory, or for a logical pool the volume group as the
separate build call did.
A live test creates a dir pool at a missing path on the storages page;
the LVM live test still creates its logical pool the same way.
console-base.html had no doctype, so the noVNC and serial console pages
rendered in quirks mode, where body and #main_container got their height
from the viewport. The console area overflowed the window: the full noVNC
page scrolled by the navbar height, the lite page's canvas and the serial
terminal ran below the bottom edge, hiding the last rows.
With the doctype, #main_container is a viewport-high flex column and the
console area (the noVNC container or the terminal) takes the space below
the navbar. Without that, the lite page's canvas collapsed to 0x0.
Rendered through every URL on libvirt's test driver, the pages had
duplicate IDs and markup browsers repaired silently:
- Each flavor modal's "Advanced" button toggled #flav-add-collapse, the
first modal's section; every modal now has its own. The flavor and
template copies of the custom tab's control IDs, and the template
tab's unused images input, are gone.
- The VM page's action icons and the XML editors were styled by
repeated IDs; they use classes.
- Modal forms opened inside modal-body and closed after modal-footer;
they wrap both. Stray end tags and duplicate attributes are removed,
paragraphs no longer hold modals or rows, table heads have a row, and
the create page closes its row and column.
- html lang is the active language instead of "en".
A test walks every URL as a superuser and fails on a repeated ID. The
console page still has no doctype: its full-height layout relies on
quirks mode and needs its own check.
The floor stayed at 50% while CI measures 77% on every leg; it is now
76%. The Python 3.12 / SQLite leg uploads the HTML report as the
coverage-html artifact, also when the tests fail.
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.