Repository navigation
feat(vms): the virtual machine page, loaded on demand - #31
Conversation
The page behind the Virtual Machines card, for the opt-in virt module (CasaOS-VirtManagement): the list of VMs in the user's order, and for the one selected its state, actions, console (noVNC), resources, disks, CD-ROM, network and USB devices; creating a VM from an ISO or a disk image, deleting one, and the default storage folder. State changes come from the message bus (vm:* events, now declared in eventProperties.spec.js). Everything stays in src/components/VirtualMachines and is imported only by the page, so a box without the module never downloads it nor noVNC. English and Italian strings; French to follow.
It was the last item of the More menu, easy to miss. The button opens the same dialog, which asks before anything is deleted. The More menu shows only when there is something in it.
b67a848 to
eb85e52
Compare
inkly
left a comment
There was a problem hiding this comment.
Thanks Vinz, this is a big piece of work and it is clean where it matters: it is written for Vue 3 (no lifecycle hook or API of Vue 2; beforeUnmount, emits, modelValue throughout; the noVNC connection and the socket handlers are released on unmount, I checked by mounting it), noVNC stays out of the main bundle, and the build and the 73 VM specs pass. I read it with a few independent reviewers who ran it; here is what I would like changed before it merges, then what can wait.
Before merging (I will push these seven as separate commits on feat/vm-page so that you do not have to; tell me if you would rather do them yourself)
- Stored XSS in the toasts. Buefy 3.1.0 renders a toast
messagewithv-html, and the VM title and the service's error text go into it unescaped (VmPage.vuewherevm:create-errorand the other bus errors are shown,toastError,VmConsoleModal.vue). A VM titled<img src=x onerror=...>, or an import whose zip entry or qcow2 backing file has such a name (the service echoes those in its errors), runs script in the dashboard origin, which holds the JWT. Please escape the interpolated text in the three places (a three-lineescapeHtml, or thevue-dompurify-htmlthe repository already ships). Double-brace interpolation andb-messageare already safe. DownloadModal.vue: closing the modal while a poll request is in flight leaves the 1 s polling running for good (beforeUnmountonly clears the timer that exists at that moment). Aclosedflag checked after eachawaitbefore arming the timer is enough.VmCreateModal.vue: a create answered 409 (created, did not start) leaves the dialog open with Create enabled, so a retry makes a second VM and disk.PathPicker.vue: a default storage folder under/mediagives anisofolder whose images the service then refuses (400, not under/DATA).VmPage.vue: the state of the VMs only comes from bus events, with no refetch when the socket reconnects, so one missed event leaves a VM in the wrong state until the page is reopened.- Force off / Force reset run at once, one click, next to Restart: please ask for a confirmation.
- One Escape closes every stacked dialog (Buefy has no modal stack): Escape in a picker also drops the create form. Open the picker and the download dialog without
escape, or let the lower one ignore it while a child is open.
Can be follow-ups (I will open issues for them if you prefer): the console toolbar at phone width (its only Close button goes off-screen), keyboard use of the VM list and the path picker, role=button rows without tabindex, status colours that fail WCAG AA, VmDetail form copied once from the prop (phantom unsaved edits), a VNC password prompt (credentialsrequired) that leaves the console on Connecting, ws: true on the dev proxy, VmPage importing VmConsoleModal statically (the noVNC chunk loads when the page opens, not when a console opens), specs that mount a component (cleanup on unmount, the polling), the two Italian keys missing, and a line in the NOTICE for noVNC (MPL-2.0).
I will add the French strings myself once this is in (I have them ready). Nothing here is a reason to doubt the design.
…own as HTML) Buefy puts the message of a toast in the page as HTML. The title of a VM, which the message bus carries to every open dashboard, and the errors of the service, which quote file names, went into it as they were: a title with a tag in it ran in the dashboard. They are escaped now, in the page's toasts and in the console's. The specs fire the handlers with a hostile title on the real Buefy, and one reads the sources for any message of a toast or a dialog that is neither escaped nor a fixed text.
The dialog asks for the list every second while something downloads, and set the next ask after the answer came. Closed while a request was on its way, it asked again and again for as long as the download ran, an hour for a big ISO, against a dialog nobody saw. Start and Remove, which look at the list when they are done, did the same when closed before that. A flag set when the dialog goes is checked before a request and before the next ask is armed.
The service answers 409 when it made the VM and its disk and then could not start it, for instance for want of memory. The dialog stayed open with Create enabled, so a second click made a second VM and a second disk. It closes now, the list is read again so that the VM shows, and a toast says why it did not start. Any other error keeps the dialog open with the reason, as before. One string added, in English and Italian.
The default storage folder can be a folder under /media, and its iso folder then holds the images and the downloads. The service takes images from /DATA only, so creating a VM from one, or putting one in the CD-ROM drive, answered 400. Until the service accepts the storage roots, the suggestions of the create dialog, the CD-ROM list and the Use button of a download keep to /DATA, and the settings and the download dialogs say why when the folder is somewhere else. The disc already in the drive stays in its list. One string added, in English and Italian.
The state of the VMs came from the bus events alone. The bus does not replay what it sent while the browser was away, after a sleep or a restart of the gateway, so a VM that went off meanwhile stayed Running until the page was reopened, and its buttons answered 409. The list is read again each time the socket connects, which includes each time it connects again, unless the page is still loading its first one or has no libvirt to ask. An answer from the service about a VM could also come back after a later one, or after the VM was deleted, and put the old state, or the deleted VM, back in the list. Each ask for a VM is counted, and a delete counts as one: only the newest answer is taken.
Both ran on one click, in the row of Shut down and Restart: a misclick on Restart's neighbour pulled the plug on a running guest, which loses what it has not saved and can damage its disk. Delete asked, these did not. They ask now, in the dialog the snapshots use, with Cancel in focus and the VM's name in the title. Shut down and Restart stay at one click. Four strings added, in English and Italian.
Buefy has no stack of modals: each one that takes Escape hears the key, so a single press in the picker also closed the create form under it and the page under that, and what was typed in was lost. The same went for the delete dialog and the confirmations over the page. Escape now closes the modal on top, the ones that open nothing: the pickers, the download dialog, the delete dialog and the confirmations. The page, the create form and the settings open others and close from their own buttons.
…utton One Escape no longer closes the dialogs under the one on top, but it did more than that: the page, the create form and the settings took no Escape at all, and the close of the page was a span with a click handler. Focus is trapped in the page, so a keyboard could not leave it. The page, the create form and the settings take Escape again, and only while no other modal is open above them, whoever opened that one: the picker, the download dialog, a confirmation, the console, which keeps Escape for the VM. The ones that open nothing keep Buefy's own Escape. The close of the page and the close of the console are buttons now, with a name, so that Tab reaches them and Enter and Space work.
Only the single-VM answers were guarded. The whole list, read at each connection of the message bus and after a create that did not start, could still come after a later event: a VM removed meanwhile came back, and one that went off showed Running again. Two reads answering out of order let the older one win. Only the newest read is taken now. A VM asked for, made or dropped since the read began is kept as the page has it, and the rest comes from the list. The read that follows a connection is nobody's request: when it fails, the page stays as it was without a toast of a raw English error, and the read is tried once more five seconds later. A read the person started still says why it failed.
The bus checks a token and nothing more: any signed-in user can publish to it, and the page took the id of a VM from an event and put it in the path of a request, with the person's token. An id like ../../../v1/users/ current made the browser ask for another route, and the answer was pushed in the list as if it were a VM. A state it carried, such as constructor, made the console fail to render. An id from the bus is used only when it is a UUID or belongs to a VM the page has; the end of a creation, whose VM the page may not have yet, takes any name. Every id, name and action that goes into a route is encoded, only an object with an id of its own is put in the list, and a state the service has no name for is ignored, and the label of a state is looked up on the table itself, not on what every object inherits. What the page remembers of the VMs it asked about no longer grows with each id it is told of: past two hundred, the ones of VMs it does not have go, oldest first.
The dialog kept the VM as it was at the click. If the guest stopped while the dialog was open, the page showed it off and the dialog still offered Force off: Confirm sent the action, the service answered 409, and a toast said the domain was not running. A VM deleted meanwhile ended in a 404. Confirm reads the VM as the page has it then, and sends nothing, with no word, when its state no longer allows the action. The dialog closes when its VM leaves the list, whether an event or a new read of the list took it away.
…order The download dialog spec counted the timers of the fake clock, which includes the one-shot timer Vue arms for its devtools on the first app of a file: the spec passed only when an earlier one had spent it, and failed alone or in some shuffled orders. It now asks only what matters: that the dialog asks for nothing more once closed. The folder passes under shuffled orders for eight seeds. The guard on what Buefy shows as HTML read the sources with a regular expression, and missed the notification and modal content, a toast or snackbar given a bare string or a variable, options on the next line, files that are not components, and an escape that covered only a part of the message. It reads the code now, in the components and in the other files of the folder, and a message has to be a call of escape on the whole, or the translation of a fixed key. The guard is tested on the forms it must refuse and the ones it must let through. The spec of a creation that did not start types a hostile title, so that escaping the server's message alone no longer passes.
The settings dialog warned that images cannot be used from a folder that is not under /DATA when the folder was empty, which is how it looks while the host has not said where the storage is. The service's own default is under /DATA, so the warning stood next to an empty field for nothing. It shows only for a folder that is set and is somewhere else.
A browser takes "." and ".." out of a path, also when written as %2e, so no encoding keeps such an id to one segment: a message on the bus naming ".." made the page ask for the route above the VMs. The routes now refuse an id made of dots, and the end of a creation naming one is ignored.
A power action, a confirmed Force off or Force reset, or a change saved from the detail put the answer of the service in the page as soon as it came. The answer can be older than a read that landed meanwhile, and a VM deleted meanwhile came back, or had its console opened. The count of the VM is now taken before the call is made, and the answer is kept only if the count has not moved. The console opens on the VM as the page has it, and only if the page still has it.
The rule that takes a button's own look off the close buttons of the page and of the console outranks the global .close-button:active, so pressing one showed nothing. The rule now sets the background at rest, on hover and when pressed.
|
I pushed the fixes for the review to Two things you should know:
I will add the French strings after this is in. Thank you for the page and for the quick turnaround on the card. |
inkly
left a comment
There was a problem hiding this comment.
The review points are fixed (see the commits and the note above); CI and the attribution check are green. Merging.
Second of the two UI PRs agreed in ReCasaOS/CasaOS#18, on top of #25.
The page behind the Virtual Machines card, for the opt-in
virtmodule (CasaOS-VirtManagement):vm:*property keys are declared ineventProperties.spec.jsEverything stays in
src/components/VirtualMachines/and is reached only through the asyncVmPagefrom #25. noVNC is not in the main bundle, so a box without the module never downloads it.Strings: en_US + it_IT (158 keys). @inkly: French whenever you like, as agreed.
Tested against the module v0.1.0 on a Mint box: list, detail, start, console.
Lint: 0 errors. VM unit tests pass. The remaining failures in
src/components/Appsare the existing Node 26 localStorage issue, untouched here.