A job stored its error and its result's warning as English text, much
of it built with the values in it (volume paths, a disk, libvirt's
error), so no catalog could translate it, and the job pages and panel
showed English whatever the language.
A job now stores them as messages: our English text, marked for
translation, and its values; libvirt's own text is a message without
values and is shown as it is. The jobs API and pages put them in the
reader's language. OperationError keeps its text and values apart for
the same reason, so a refusal raised while a job is submitted is shown
translated too. Messages one after another still read as sentences.
Errors stored before are kept as one message each, shown as they were.
The job panel on VM and storage pages wrote its buttons and notes in
English (Cancel, Open, Refresh, Dismiss, Finishing..., Files not
removed, Worker unavailable, Cancel failed) and its state badge as the
state's code, and the jobs API gave the phase untranslated. The panel
now takes its texts and the state labels from the page's settings, and
the API gives the phase in the viewer's language, as it gives the
label; the job pages show that phase.
A job that cannot be cancelled and ran past its time limit had
"(over its time limit)" written into its phase, which then matched no
translation. The phase stays as the job sets it, and the pages say the
job is past its limit.
The VM page's and the Settings page's views marked their messages with
gettext_noop, so every message they gave (a refused resize, a saved
setting, a QoS change, a busy VM) showed in English whatever the
language, though the catalogs translate them. They translate in the
request's language now; their module-level texts lazily. The Settings
page's group titles stay English in the view, as the page links them
by their slugs and translates them itself.
Two values went into translated messages as English codes: a job's
state ("... is running on this resource") and a QoS direction ("Remove
the inbound limit ..."). They are named as the pages name them.
A step towards a Content-Security-Policy without inline scripts. The
scripts every page or many pages share now load from static files: the
error focus of base.html, the please-wait dialog, the sign-in forms'
password toggle and busy button, the jobs pages' reload while a job
runs, and the start of the job panel and of the VM list's state poll,
whose values come as json_script data. Cancel links go back through a
data-history-back attribute instead of a javascript: URL. The IE8
html5shiv/respond.js block, X-UA-Compatible and shrink-to-fit are gone:
no supported browser reads them.
A VM made from a template came out at the template's size and shut off,
and was then resized and started by hand. The Clone tab now asks for the
clone's vCPUs and memory, filled in with the VM's, and whether to start
it. After the copy, before the clone is recorded, it is resized with the
Resize tab's own functions and started; a step that fails is a warning
of the job, and the clone stays. The quota counts the larger of the size
asked and the VM's, so a resize that fails leaves the clone at a size it
allowed.
When the job ends the page opens the clone's, not the cloned VM's again:
a template's page, shut off, read as a clone that had not started. An
outcome kept for a warning links to it.
A VM list showed what it read when it was loaded: a VM stopped from
inside its guest or with virsh, or made or removed elsewhere, showed only
after a reload, unlike the VM page, which reloads when its VM's state
changes. The lists of all VMs and of one host's VMs now poll the states
of their VMs, at once and then every 5 s as the VM page does, and reload
when an answer differs from the one before. The states come from one
libvirt call per host; a user who does not see hosts gets only their own
VMs. The page in use (a dialog, a field, an unsaved XML edit, a job
outcome shown) holds the reload until it is free; that check is shared
with the job panel.
Since VM creation, clones, migrations and uploads run as jobs, the page
came back before they ended and the job panel then showed the outcome
with a Refresh button: a new VM, for one, showed only after a click. A
job of the page that succeeds without a warning now reloads the page,
which shows what it changed. The outcome box stays, with Refresh, for a
job that failed or warned, and when a reload would lose something: a
dialog open, a field in use, an XML edit not saved, or another job still
running, shown or awaited. A remembered job is taken when the page
renders, so the reload does not see it again.
The Jobs list sits in a card: the count and the filters in its header,
the job with its phase, its progress and its date in one cell, the state
at the row's end.
The job page is one card: resource, user and state on top, then the
phase, Cancel and the progress, the error and the results, and a
timeline that says how long the job waited and ran. Resolving an
uncertain job folds into its own card.
A user who is not a superuser chooses the clone's name from the free
names the clone form lists, or gets the first one with automatic names,
but the server took any posted name. It now refuses a name outside the
list, and with automatic names it takes the first free one whatever is
posted. A superuser still names a clone freely.
The panel checks VM names on every compute, but a job held a name only
on its compute, so two clones from VMs on two hosts could take the same
free name at once; a name claim now holds the name everywhere. A clone
job whose name a VM found on a host took while it was queued stops
before it copies anything.
Job dates showed no year and no time zone, unlike the Logs page; they now
read 2026-10-13 23:04:00 under "Created (time zone)". The host filter
matched only a whole host name and the user filter only an exact username;
both now ignore case and take the start of a host or any part of a name.
From the Codex and agy review of the VM audit log before v3.2.0:
- Resolving an uncertain migration wrote its row without the VM's UUID,
because the migrate job writes no entries of its own; the resolution now
takes the UUID from the job's intent, so the VM's Logs tab shows it.
- A console keymap or listen address the VM could not take (no graphics
device, or a console on a socket) was logged as done although nothing
changed; it now writes no row, as an edit that changes nothing.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Operations on a VM now write their rows the way the admin audit does: one
row per operation, done or failed with a safe reason, with the user, IP and
host. Each row names the VM ("VM web-1: delete snapshot snap1") and carries
its UUID in the new Logs.instance_uuid column.
- audit_vm and failed_vm wrap audit() and failed() for a VM. A failure the
view turns into a message is caught outside the block, so its failed row
is written; busy claims, permission and quota refusals and invalid input
write nothing. No password, key or XML goes into a row.
- All VM page operations move from addlogmsg to audit_vm, and addlogmsg
goes. QoS changes and defining a VM from XML are logged now; a gstfsd
error reply (root password, SSH key) is a failed row, not a done one.
- Partial results are rows of their own: the VM delete, then each disk
delete; the disk detach, then the volume delete; the migration, then a
failed autostart restore, NVRAM removal or record on the target.
- Migration rows use the compute name as the host column, as every other
row does.
- VM job rows (create, clone, snapshots) and their resolutions carry the
VM's UUID; snapshot job intents now record it. The owner rows of the VM
page and the accounts pages, and the ownership system rows, carry it too.
- The Logs tab loads vm_logs/<pk>/ and shows the rows with the VM's UUID,
so a rename keeps the history, two VMs with one name are told apart and
a VM on two computes is one VM. It gains a Result column. Rows from
before this change have no UUID and stay on the Logs page only.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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.
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>
libvirt's backup copies the disks of a running or paused VM from one
instant into new volumes while it keeps running; the clone is defined
shut off as before. The VM is held for the copy, the copy can be
cancelled, and recovery reads the job's records and libvirt.
Request jobs (an upload's chunks) run in gunicorn: their kind is admitted
running with the request as its worker, never claimed or recovered by the
worker, cancelled in the request, and asked by every housekeeping pass to
end its jobs that went idle. The panel can follow a job the page just
started. A job a request runs does not wait for the worker, so it does not
raise the worker-unavailable warning on the Jobs page or in the panel.
Only the request middleware loaded the Settings page values, so in the
worker process app_settings stayed empty: a VM create job failed reading
the default volume format, and the snapshot free-space check fell back to
its defaults. The worker now loads them before each job.
The list and the detail page were rendered once, so a job that finished
kept showing running until the page was reloaded by hand. While a shown
job is queued, running or cancelling, the page now reloads every five
seconds (not while hidden or while a form field has focus).