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.
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>
On the machine step, "Or define it from XML" lost the definition when it
was refused: a taken name re-rendered the page with an empty, collapsed
editor; malformed XML went on to libvirt, and libvirt's refusal sent the
browser back to an empty page with the reason at the top.
All three refusals now render the page with the section open, the
submitted XML in the editor (and in the field it posts, so it can be sent
again unchanged), and the reason above the editor, which takes the focus.
Fixes#174
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The create page's flavor dialog posted its forms the normal way, and the
server sent a fresh page back: the VM's name, sizes, disk choices and MAC
edits were lost, and a refused flavor left the page for a bare form.
The dialog's forms are now posted from the page. The flavor views answer
such a post with the presets and the dialog body, rendered from the same
includes as the page, and the script swaps them in: a refused flavor shows
its errors in the dialog, and the focus stays in it. A post without the
script, or one whose request fails, still goes the normal way.
Fixes#173
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The create form and the API took 0 or a negative number for vCPUs,
memory and a new disk's size: the API queued the job (202) and it
failed later in libvirt; the page did the same, its formnovalidate
skipping the browser's min. The create model's fields now refuse
values below 1, so the form shows the error beside its field and the
API answers 400 naming it.
The form also checks the MAC addresses as the API and the job do (one
per NIC at most, each a MAC address), so a bad one is an error on the
form instead of a job that fails.
Closes#175
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.
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.
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.