Skip to content

v0.7.0 — the three bugs a v0.6.3 user reported, and four things they asked for#4

Merged
chicohaager merged 3 commits into
mainfrom
release/v0.7.0
Jul 23, 2026
Merged

v0.7.0 — the three bugs a v0.6.3 user reported, and four things they asked for#4
chicohaager merged 3 commits into
mainfrom
release/v0.7.0

Conversation

@chicohaager

@chicohaager chicohaager commented Jul 23, 2026

Copy link
Copy Markdown
Owner

Community feedback on v0.6.3 (forum thread), verified against a live v0.6.3 on ZimaOS 1.7.0-beta1 before fixing and re-verified in the browser afterwards.

Reported bugs

Snapshot names containing a space could be created and then never removed. Create and delete used different validators. Measured on the running v0.6.3:

DELETE .../snapshot/<vm>/nospace-x    -> 500  virsh: "no domain snapshot with matching name"
DELETE .../snapshot/<vm>/with space   -> 400  "invalid snapshot name"

Revert had the same gate, so those snapshots were stuck entirely. Delete and revert now resolve the name against virsh snapshot-list instead of filtering characters — snapshots already stranded on v0.6.3 become removable with no migration step. Overlay filenames are derived separately, so a snapshot named before Windows update does not put spaces into a --diskspec file= path.

Every VM showed 100 % memory. It divided balloon.current by balloon.maximum, but balloon.current is the balloon's target size — with an uninflated balloon those are the same number. The daemon now enables the guest's balloon statistics on demand (dommemstat --period 5 --live, rate limited, self-healing after a VM restart) and the card shows used · available-to-apps · total. Where a guest reports nothing it says "Memory (allocated)" and shows no percentage rather than inventing one.

The snapshot storage target vanished for a shut-off VM. Correct behaviour — such a VM gets an internal snapshot inside its own qcow2 — but hiding the row read as "broken". It stays, disabled, and explains itself.

Found while fixing

The external target directory did not follow the VM picker. With VM B selected the field still held VM A's path, so B's overlay files would have been written into A's directory.

Requested features

Power controls on the Autostart tab (start · shut down · reboot · resume · confirmed force-off), warning when the VM's watchdog would restart it anyway. Backup and snapshot target directories remembered on the appliance rather than in one browser. A short in-page history sparkline per metric.

Not included: the interactive folder picker. It needs an endpoint that enumerates the host filesystem through the gateway, and that surface deserves its own pass.

Unprompted

The PCIe tab listed 41 devices on a ZimaCube, 23 of them bridges, plus entries with no IOMMU group. Both are hidden behind a toggle now — 41 down to 17. The IOMMU-group rule only applies when at least one device has a group, so a host with IOMMU disabled in firmware still sees its devices and is told why none qualify.

build.sh stamps ?v= from VERSION plus a per-asset hash. The hand-maintained ?v=0.6.3 had gone stale, so the daemon reported 0.7.0 while browsers kept running 0.6.3's app.js.

Verified

On 192.168.1.147 (ZimaOS 1.7.0-beta1), through the UI rather than the API: snapshot zvme test 070 created and deleted via the form and the Delete button; storage row visible and annotated for a shut-off VM; memory reading 1 % — 40.6 MB used · 3.5 GB avail. to apps · 3.6 GB total where it read 100 % — 4.0 GB / 4.0 GB; five sparklines drawing; PCIe list 41 -> 17 with the toggle restoring all 41. go vet, go test and the build are green.

Note on the open PR

This branch touches internal/handlers, internal/virsh and the frontend, and adds internal/settings. #1 adds internal/vnc/{store,vnc}.go — paths now occupied by the VNC work that landed in v0.5.1 — and its own /api/vnc/ handler, so it already conflicts with main independently of this branch.

🤖 Generated with Claude Code


Added after review: VNC reachability (the idea behind #1)

The listen address and port are now settable in the existing VNC tab, as a second "Reachability" card. Four things differ from #1:

  • No console goes onto all interfaces without a password. feat: add VNC port exposure and settings UI #1 validates only that the address is one of two strings. Every path here that sets 0.0.0.0 ends with a password on the console. Tightening to 127.0.0.1 is deliberately allowed without one — restricting a console is protective in itself.
  • The <listen> child element is rewritten, not just the attribute. libvirt keeps the address in both places and honours the child; editing only the attribute reports success and changes nothing.
  • The address is pinned and reconciled like the password, because ZVM regenerates <graphics> with listen='::' on every re-save.
  • Three states, not two — a changed address shows "restart required" with both the configured and the live value, since the running qemu keeps the socket it started with.

Verified by measuring the socket rather than the config, and from the LAN rather than from the host:

restricted VM untouched VMs
qemu -vnc 127.0.0.1:0 [::]:1, [::]:2
ss VNC 127.0.0.1:5900 *:5901, *:5902
ss websocket 127.0.0.1:5700 *:5701, *:5702
TCP probe from another machine refused reachable

libvirt applies the address to ZVM's websocket port too, so both surfaces close together — worth checking before calling anything "local only". The guard was exercised through the button and again straight at the API, bypassing the button, and refused both times.

#1 cannot merge as it stands regardless of this branch: it adds internal/vnc/{store,vnc,vnc_test}.go as new files, and those paths have been occupied since v0.5.1.

chicohaager and others added 3 commits July 23, 2026 08:46
…sked for

Snapshot names containing a space could be created and then never removed.
Create and delete used different validators: create only refused / \ and .,
delete ran the name through validName(), which rejects spaces. Measured
against a live v0.6.3 — "nospace-x" reaches virsh and comes back 500 "no
domain snapshot with matching name", "with space" never gets there and comes
back 400 "invalid snapshot name". Revert had the same gate.

Delete and revert no longer apply a character allowlist. They resolve the
name against virsh snapshot-list and pass through what libvirt actually has,
which cannot strand a name we failed to anticipate and hands virsh a string
virsh gave us. Snapshots already stranded on v0.6.3 become deletable with no
migration. Create accepts single spaces; overlay filenames are derived
separately (fileStem) so a snapshot named "before Windows update" does not
put spaces into a --diskspec file= path.

Every VM showed 100 % memory. balloon.current is the balloon's target size,
not consumption — with an uninflated balloon it equals balloon.maximum, so
"4.0 GB / 4.0 GB" was two names for the same number. The in-guest figures
exist only once libvirt is collecting them, which nothing was doing. The
daemon now arms dommemstat --period 5 --live on demand (live only, rate
limited, self-healing after a VM restart) and reports balloon.available,
unused, usable and rss. The card shows used, available-to-apps and total
side by side; deliberately not a second "used" derived from usable, because
Linux subtracts the low watermark from MemAvailable and it can sit below
MemFree (measured here: 3.47 GiB usable vs 3.59 GiB unused of 3.63 GiB), so
that figure would exceed the first and read as a contradiction. Where a guest
reports nothing, the card says "Memory (allocated)" and shows no percentage
rather than inventing one.

The snapshot storage target vanished for a shut-off VM. That was correct —
such a VM gets an internal snapshot inside its own qcow2 and there is no
target — but hiding the row said "broken", not "not applicable". The row
stays, disabled, and explains itself.

Requested and added: power controls on the Autostart tab (start, ACPI shut
down, reboot, resume, confirmed force-off), which warn when the VM's watchdog
would restart it within 30 s; backup and snapshot target directories
remembered on the appliance in settings.json rather than in one browser; and
a short in-page history sparkline per metric.

Not added: the interactive folder picker. It needs an endpoint that
enumerates the host filesystem over the gateway, and that surface deserves
its own pass rather than a bugfix release.

Also, unprompted: the PCIe tab listed 41 devices on a ZimaCube of which 23
were bridges that can never be passed through, plus entries with no IOMMU
group, which VFIO requires. Both are hidden behind a toggle now — 41 down to
17 here. The IOMMU-group rule only applies when at least one device has a
group, so a host with IOMMU disabled in firmware still sees its devices and
gets told why none of them qualify.

And the reason this took two deploys to verify: index.html carried a
hand-maintained ?v=0.6.3 on its script and stylesheet. Bumping VERSION did
not touch it, so the daemon reported 0.7.0 while every browser kept running
0.6.3's app.js — and I read that stale copy as evidence my fix had not
landed. build.sh now stamps the URL from VERSION plus a hash of each asset,
so it changes whenever the bytes do; a version alone would not have caught
the second iteration of the same release.

Verified on 192.168.1.147 (ZimaOS 1.7.0-beta1) through the browser UI, not
just the API: snapshot "zvme test 070" created and deleted through the form
and the Delete button; the storage row visible and annotated for a shut-off
VM; memory reading 1 % — 40.6 MB used / 3.5 GB avail. to apps / 3.6 GB total
where it read 100 % — 4.0 GB / 4.0 GB an hour earlier; five sparklines
drawing; PCIe list 41 -> 17 with the toggle restoring all 41.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tering

Spotted while re-shooting the README screenshots: with Fedora selected, the
snapshot tab still showed /DATA/AppData/zima-vm-extras/snapshots/bde975b9 —
the Mint VM looked at a moment earlier. renderSnapshotsForCurrentVM only
filled the field `if (!extDirInput.value)`, so once anything was in it the
per-VM subdirectory never changed again. An external snapshot of one VM would
have written its overlay and memory files into another VM's directory. Not
destructive, since the filenames carry the snapshot name, but wrong and
confusing to clean up.

The field now recomposes when it still holds a value this code put there, and
is left alone when the operator typed their own path. Picking a storage target
recomposes for the currently selected VM too, without stealing focus, since
that path also ends in the VM name.

PCIe: a ZimaCube lists 41 devices, 23 of them bridges that isPassable()
already refused, plus entries with no IOMMU group, which VFIO requires. They
drowned the ones that can actually be passed. Both classes are hidden behind a
"Show bridges & non-passable" toggle — 41 down to 17 here. Pinned devices stay
visible regardless. The IOMMU-group rule only applies when at least one device
has a group: on a host with IOMMU disabled in firmware, nothing has one, and
filtering on that alone would have emptied the list and hidden the actual
cause. That case now says so instead.

Metrics memory shows both figures that exist rather than one: used
(available - unused), the guest's own MemAvailable, and the total. No second
"used" is derived from usable — Linux subtracts the low watermark from
MemAvailable, so it can sit below MemFree (3.47 vs 3.59 GiB of 3.63 on the
test guest) and the derived figure would exceed the first one.

Screenshots: dashboard.png re-shot for v0.7.0 — the old one predates the power
controls and still showed v0.6.3 — and metrics.png and pcie-passthrough.png
added, since neither tab had an image and both changed in this release.
usb-passthrough.png, vnc-security.png and tpm-secure-boot.png are unchanged
apart from the version badge in the header and were left as they are.

build.sh now stamps ?v= from VERSION plus a hash of each asset. A version
alone was not enough: two builds of 0.7.0 with different app.js produced the
same URL, so a browser that had already loaded the page kept the old script.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adopts the idea behind #1 — making the console's listen address and port
settable — into the existing VNC tab rather than alongside it. #1 adds its own
internal/vnc package and /api/vnc/ handler, paths that the VNC work in v0.5.1
already occupies, so it cannot merge as it stands.

Four things are different from that PR, all of them following from what this
tab is for.

No console goes onto all interfaces without a password. #1 validates only that
listen_address is one of two strings. Here every path that sets 0.0.0.0 ends
with a password on the console; asking for it without one is refused with "a
console on all interfaces needs a password". The reverse is deliberately
allowed: restricting a console to 127.0.0.1 is protective in itself and needs
no password, so tightening is never gated behind remembering one.

The <listen> child element is rewritten, not just the attribute. libvirt keeps
the address in both places and honours the child; editing only the attribute
would have reported success and changed nothing. A child of type='network' is
replaced wholesale rather than patched, since it would otherwise keep
overriding the address. This is the same class of trap as the --security-info
masking documented at the top of virsh/vnc.go.

The address is pinned and reconciled like the password. ZVM regenerates
<graphics> with listen='::' on every re-save, which would quietly put a
restricted console back on the LAN.

The card has three states, not two. `virsh define` writes the persistent
config and the running qemu keeps the socket it started with, so a changed
address shows "restart required" with both the configured and the live address
instead of a green "local only" over a console still on the LAN.

One bug caught while building: an entry that pins only an address has no
password, and the reconciler would have called SetVNCPassword(vm, "") — after
which the config still has no password, so the next pass would repair it
again, once a minute, forever. The password repair is now skipped for such
entries, with a test.

Verified on 192.168.1.147 by measuring the socket rather than the config, and
from the LAN rather than from the host. After restricting one VM and starting
it: qemu gets `-vnc 127.0.0.1:0` where the untouched VMs have `[::]:1` and
`[::]:2`; `ss` shows 127.0.0.1:5900 against *:5901 and *:5902; and a TCP probe
from another machine reaches 5901 and 5902 but not 5900. libvirt applies the
address to ZVM's websocket port as well — 127.0.0.1:5700 against *:5701 and
*:5702 — so both surfaces close together, which was worth checking before
calling anything "local only". The guard was exercised through the button and
again straight at the API, bypassing the button, and refused both times.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@chicohaager chicohaager reopened this Jul 23, 2026
@chicohaager
chicohaager merged commit 299b5cc into main Jul 23, 2026
@chicohaager
chicohaager deleted the release/v0.7.0 branch July 23, 2026 08:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant