A stopped service on the far side showed up only as a wall of « channel N:
open failed: connect failed: Connection refused », one line per browser
request, saying nothing about WHICH end refused. The tunnel itself was fine;
there was simply nothing to reach. Diagnosing it took three commands.
The port is now probed first, and the answer is plain:
⚠ Rien n'écoute sur le port 8069 de test-vm_02+erplibre-ubuntu-2404
Démarrer le service là-bas, ou continuer quand même.
Continuer quand même ? (o/N)
The probe opens a real TCP connection to « localhost:<port> » from the remote
host rather than reading its listening table. That is exactly what the tunnel
will do — same host resolution, same IPv4/IPv6 choice — so it cannot say open
where the tunnel would fail. It also needs no ss or netstat, which minimal
images lack.
Three outcomes, three behaviours: listening goes straight through, closed
warns and asks (default no), and an inconclusive probe — unreachable host, no
bash — says so and continues rather than blocking on its own uncertainty.
Verified against the real VM: port 22 open, port 9999 closed, an unknown host
inconclusive, and port 8069 correctly reported closed after the Odoo service
had been stopped — the very case that prompted this. Then at flow level:
refusing aborts without opening anything, forcing opens the tunnel anyway, and
an inconclusive probe still opens it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reaching a VM's Odoo from the workstation browser meant remembering the
-L syntax and which side « localhost » refers to. Entry [3] of the Deploy
menu asks for a host, a remote port (8069 by default) and a local one, then
holds the tunnel open.
Nothing has to be said about jumps: the ProxyJump already in ~/.ssh/config
applies on its own, which is what makes a NESTED VM reachable — its address
means nothing from here, only from its parent.
Two guards, both from getting it wrong by hand:
· a local port already in use is reported before ssh fails on it;
· a local port that differs from the remote one gets a warning, because
Odoo redirects using web.base.url and would send the browser to an
address that does not exist locally. With matching ports and the usual
web.base.url = http://localhost:8069, there is nothing to adjust.
The host list is read from ~/.ssh/config by a small shared helper: it expands
a Host line carrying several names and drops the wildcard patterns, which are
rules rather than machines.
Verified against a config holding « Host * », a plain host and a two-name
line: the three real names listed in order, selection by number and by name,
8069/8069 by default, 9072:localhost:8072 warning about web.base.url,
matching ports staying silent, an empty host cancelling without running
anything, and the busy-port probe answering correctly on a socket bound then
released.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A nested VM was written as « Host erplibre-ubuntu-2404
test-vm_02+erplibre-ubuntu-2404 » — two patterns on one Host line. Valid ssh
syntax, but the short name only repeats the tail of the chain and buys
nothing, so it is gone. Nested hosts now carry the chained name alone.
Existing configs repair themselves: replacing a block drops any whose name
list intersects the new one, so the old two-name line is removed as a whole
and rewritten with the single chained name — no stale short entry left behind.
Verified on a config holding exactly the reported line, alongside an unrelated
host that must survive.
The naming rule is simpler too: a chain cannot collide with another machine,
so there is no case left where a short name has to be preferred or avoided.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The message claimed virt-manager rewrites its settings on exit and had to be
restarted. Reading the 5.1.0 source shows the opposite for the part that
matters: connection.py registers listen_perconn on /pretty-name and its
callback emits state-changed, so a running virt-manager picks a renamed
connection up immediately. Only the connection LIST is read at startup, so a
restart is needed to see a newly added one — which is what the message now
says.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A nested QEMU host appeared in virt-manager under its bare name, so nothing
said which machine it lived inside. It now carries the same « parent+child »
chain the ssh aliases use, at two levels of defence:
- the connection URI uses the CHAINED alias, so even a virt-manager that
ignores custom labels shows the nesting in the host part;
- pretty-name is set explicitly, which is the label virt-manager actually
displays.
pretty-name lives in a RELOCATABLE GSettings schema,
org.virt-manager.virt-manager.connection, one path per connection. The path is
built with no escaping at all: virt-manager just deletes every « / » from the
URI and uses the rest verbatim (virtManager/config.py, _make_perconn_key), so
qemu+ssh://erplibre@a+b/system becomes conns/qemu+ssh:erplibre@a+bsystem/.
That was verified for real rather than assumed: virt-manager is not installed
here, so its gschema was fetched from the distribution package, compiled into
a temporary schema dir, and the set/get/reset round trip run against it with a
genuinely chained URI — colons, @ and + all survive the dconf path. The key
was reset afterwards; nothing was left in dconf.
Making the URI carry the chain needs the chained name to resolve, so a nested
host is now written as « Host <short> <parent+child> » — ONE block, two names,
since ssh accepts several patterns on a Host line. The short name stays for
typing and is still only kept when free; the chain is always there. Checked
with ssh -G: both names yield the same HostName and ProxyJump, and a third
level chains as a+b+c. libvirt was checked too — it hands the URI host to ssh
untouched, + included.
Replacing a block by name meant a regex that could not see a Host line
carrying several names, which would have left the same name defined twice —
ssh honours the first, so an update would silently not apply. Removal now
parses the file into blocks and drops any whose name list intersects the new
one. Verified on a config with a global directive, a Match block and a
multi-name Host: only the targeted block goes, rewriting twice is idempotent,
and an unknown name leaves the file byte-identical.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Every reachable machine ended up in virt-manager, not just the ones hosting
VMs. The probe ran « for n in $(sudo virsh list --all --name 2>/dev/null) »:
with no virsh, the command substitution is empty, the loop body never runs and
the snippet exits 0 with no output — indistinguishable from « QEMU is here,
it just has no VM ». Both read as a libvirt host.
The probe now states it outright, « LIBVIRT<TAB>yes|no » as its first line, so
the two cases separate: a machine WITHOUT QEMU is skipped, a machine with QEMU
and no VM is still offered — that is where one would create some.
Verified against the real snippet: this host answers « yes » plus its two VMs,
and with virsh out of PATH it answers « no ». Then on a simulated fleet, only
vm-avec-qemu and vm-qemu-sans-vm are proposed; the plain dev VM and the
unreachable one are not.
Also: the ~/.ssh/config blocks were missing IdentityFile. Every entry now
names the private key it needs — the one cloud-init injected, or the one just
deployed — with IdentitiesOnly yes beside it. Without that flag IdentityFile
ADDS to the agent's identities instead of replacing them, and a slightly full
agent hits « Too many authentication failures » before reaching the right key.
The .pub suffix is stripped: IdentityFile wants the private half.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Writing ~/.ssh/config only helps if the key is accepted at the other end.
Both SSH options now offer to create an ed25519 key when none exists — the
same choice deploy_qemu.ensure_ssh_key makes, so created and adopted VMs share
one key — and to push it with ssh-copy-id.
Hosts that already accept the key are skipped, tested with
PasswordAuthentication=no: without it ssh would fall back to the password and
every host would look like it already had the key. ssh-copy-id runs on the
real terminal rather than through captured output, otherwise its password
prompt would be invisible.
In the recursive walk the key is deployed BEFORE probing each level, not at
the end. The probe uses BatchMode, so an un-keyed machine answers nothing and
the level below it stays invisible — deploying afterwards would find only the
first level.
virt-manager, when installed, gets the machines that actually run libvirt
added to its connection list, so their nested VMs can be driven from the local
GUI. The URI uses the SSH ALIAS rather than the raw IP: qemu+ssh goes through
the ssh binary, hence ~/.ssh/config, so the alias already carries both the
address and the ProxyJump — a bare IP could not reach a nested VM at all.
Connections live in GSettings, not a file. The list is READ first and written
back merged, so nothing already configured is lost, and a failed read (no
schema, no virt-manager) simply means the whole feature stays silent — no
prompt, no write. virt-manager rewrites its settings when it exits, so a
warning says to restart it.
Verified: key generated with 0600 on the private half and reused on the second
call; ssh-copy-id issued only for hosts that need it; the recursive walk
deploying level by level before each probe; GSettings merge keeping existing
URIs and skipping the write when nothing is missing; « @as [] », a populated
list and a missing schema all parsed correctly.
Caveat: virt-manager is not installed on this machine, so the absent path was
exercised for real and the write path only against a stubbed gsettings.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
~/.ssh/config entries were only ever written while deploying a VM. A fleet
that already exists — or one whose DHCP leases have moved — had no way to
refresh them. Entry [13] of the QEMU menu does it on demand:
[1] update ~/.ssh/config for the local VMs
[2] add the nested VMs through ProxyJump, recursively
The second one matters because of the « ERPLibre Deployment (+ QEMU + dev) »
profile: a VM built that way hosts VMs of its own, on its own private network.
Those are not reachable from this host at all — only from their parent. So
each level is written with a ProxyJump to the level above, and OpenSSH chains
the hops on its own. « ssh erplibre-fedora-42 » then works from here even
though the address only means something two machines away.
The recursion probes over « ssh <alias> », i.e. through the block just
written, so the parent's own ProxyJump applies automatically and one probe
works identically at any depth. One SSH connection per MACHINE, not per VM: a
single snippet returns every « name<TAB>ip » pair, falling back to the guest
agent when the dnsmasq lease is missing. Passwordless sudo is a given here —
the cloud-init config grants it (deploy_qemu.py:1175).
A nested VM keeps its short name, which is what one wants to type, and is only
prefixed with its parent on collision — so discovering a machine that already
exists elsewhere never overwrites the other one's entry. Depth defaults to 2
(host, VM, nested VM) and already-seen aliases are skipped, which is what
stops a cycle: a child that reports its own grandparent.
Verified on a simulated two-level fleet including a deliberate cycle: the
ProxyJump chain is correct at each level, the colliding name is prefixed, the
parent block is not overwritten, and only one probe per machine is issued. The
remote snippet itself was run for real on this host — valid POSIX sh, two VMs
with their addresses. A VM without an IP is skipped rather than written with
an empty HostName.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Uninstalling before the 13->14 bump asked for 15 modules; 12 of them no longer
exist in the 13.0 addons path. check_addons_exist.py rejects the command on
the first missing name, so the uninstall aborted whole — and the 3 modules
that WERE there stayed installed, blocking the bump for a reason that had
nothing to do with them.
The list is now split before the script is called, using check_addons_exist
which was already there and simply never consulted at this point. « Missing »
means the addons path has no code for it, not that the database lacks it —
the distinction is the whole point, since Odoo cannot uninstall a module whose
code is gone.
When some are missing, a choice is offered rather than a failure:
[1] uninstall the present ones, skip the missing (default)
[2] try the whole list anyway (it will fail)
[3] uninstall nothing, continue
Only what is really uninstalled is subtracted from the per-version module
list, so a module left in place stays counted as installed — which it is. The
missing names are written to the progression comments, so what was skipped is
still on record afterwards.
Verified on the exact list from the failure: 3 present and 12 missing,
option 1 issuing a command with only the 3 and leaving the 12 counted as
installed, option 2 sending all 15, option 3 running nothing.
Also swapped entries 4 and 5 of the Deploy menu — QEMU/KVM now sits at [4],
NTFY at [5].
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The migration now opens with the same choice the QEMU deployment does — TUI
or line-by-line prompts — settled in advance by TODO > Configuration if you
want it to be. Choosing the TUI loads the saved progression and asks the very
same first question: where does this migration stand, and where do we resume?
prompt_resume was one function that rendered, read and decided at once, so a
second view would have meant a second copy of the decision. It is now three:
resume_context() the progression -> plain data (file, database, target,
steps with their icon and detail, version bumps)
print_resume() renders it on the terminal
apply_resume_answer() answer -> (progression, changed)
The TUI returns the SAME answer strings as the prompt — c, n, r, q, 0..4,
4.<version> — so apply_resume_answer stays the only place that decides what a
choice means. Neither view can drift into describing the migration
differently, because both render the same context.
In the TUI the steps are a table and the version bumps a list: Enter on either
replays from there, which is what « [0-4] » and « [4.N] » meant in text. The
cursor opens on the first unfinished step and on the first unmigrated version
— where it stopped is where one usually wants to act.
Both views gain « q », quit without doing anything. A TUI needs Escape to do
something sane, and an escape hatch present in only one of the two views is
exactly the kind of divergence this split exists to prevent. execute_odoo_
upgrade returns immediately on it, writing nothing.
Verified on a 12->18 progression with steps 0-3 done and 2 of 6 bumps
migrated: identical context feeding both views, Enter on step 2 giving « 2 »,
Enter on the 15 bump giving « 4.15 », the c/n/r/q/Escape shortcuts, and every
answer producing the same progression through both paths — « 2 » leaving only
the state of steps 0 and 1, « 4.15 » resetting the clone list from the third
bump on so the half-migrated intermediate database gets rebuilt. « q » checked
to leave the progression file byte-identical.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Deploying a VM meant answering a dozen questions in a row, never seeing the
whole of one's choices, and starting over to revisit an answer. The first
question is now which interface to use — TUI form or the classic prompts —
defaulting to whatever TODO > Configuration says.
The form shows every setting at once with a plan that recomputes on each
change: names, resources, and the two collisions marked as they arise (a
defined VM is skipped, an orphan qcow2 will make deploy_qemu fail). F2 edits
one VM, F3 previews the commands, F5 deploys, F6/F7/F8 select all / main
versions / none.
Function keys rather than ctrl+letter: ctrl+p is Textual's command palette and
silently swallowed the shortcut, and a bare letter is eaten by whichever input
has focus.
Both interfaces go through _qemu_deploy_parts_for, so the same choices produce
the same command by construction — and a test now drives the CLI prompts and
the form to the same state and compares the argv, which is what keeps them
from drifting.
Nothing privileged or networked runs inside the form. Every virsh call in this
codebase goes through sudo, and a password prompt while Textual owns the
terminal would wreck the display; the domain list and the remote branches are
fetched before the app starts and arrive as plain data.
The progress view is optional (preference: CLI output stays the default, since
it is the easiest to copy from). It gives one collapsible block per VM,
expanded while running, folded on success — and left OPEN on failure, which is
the part worth reading. « c » copies the selected log, « C » all of them.
On copying from a TUI over SSH: copy_to_clipboard emits OSC 52, which the
LOCAL terminal emulator interprets, so it does reach the workstation's
clipboard. Two caveats are handled: the payload is capped at 100 kB keeping
the TAIL (some terminals truncate long OSC 52), and the notification says a
compatible terminal is required — macOS Terminal.app has none, tmux needs
set-clipboard on.
Verified: pure logic (profiles, per-VM overrides surviving a profile change,
totals, statuses) by direct calls; the form headless via run_test — default
selection, F6/F7/F8, profile switch, and the orphan guard demanding a second
F5; parity CLI/TUI on identical argv; the progress view on three fake jobs,
with the failure staying open and the clipboard filled. The semaphore bounding
concurrency was measured: four 0.35 s jobs take 1.63 s at 1 and 0.58 s at 4 —
without it, « 4 in parallel » launched every VM at once.
Also removed a duplicated @staticmethod on _qemu_install_dir, harmless since
Python 3.10 but misleading.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
_qemu_deploy was ~400 lines mixing a dozen prompts with the parallel
deployment, IP resolution, ~/.ssh/config and the ERPLibre install. A second
interface could not be added to it without duplicating all of that — and the
repository already shows where duplication leads: _qemu_choose_cli_browser
(todo.py) and Monitor._choose_browser (qemu_install_monitor.py) are the same
function twice, and they have already drifted apart.
The function now has three parts around a plain dict, the SPEC:
_qemu_collect_vms_cli arch, catalog, resources, names -> the VM list
_qemu_collect_options_cli SSH key, install, parallelism -> the spec
_qemu_run_spec consumes a spec, asks nothing
_qemu_deploy_parts_for is the single point every command goes through, so two
interfaces producing the same spec necessarily produce the same command —
which makes their divergence testable rather than a matter of discipline.
Pure, I/O-free helpers come out of the body: _qemu_catalog_entries (the flat
distro × version × arch list), _qemu_arches_for, _qemu_make_vm,
_qemu_split_existing and _qemu_orphan_disks. The last two matter beyond
tidiness — the form must recompute collisions on every keystroke, and every
virsh call in this file goes through sudo. Existence is now resolved with ONE
virsh list --all --name (_qemu_list_domains, already present) instead of one
sudo per VM, and the orphan-disk scan needs no privileges at all.
VMs travel as dicts rather than 6-tuples plus a parallel names list. The
tuple-based resource prompts are left untouched and converted at the boundary.
No behaviour change intended, and verified as such: replaying identical
scripted answers against a worktree pinned at the previous commit produces
byte-identical output — including the granular selection spanning three
architectures — once the repository path and the instantaneous free-RAM
reading are normalised.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Nothing in the CLI could be remembered from one session to the next except the
language, which lives in env_var.sh with its own ad-hoc parser. Entry [6]
Configuration, below Telemetry, now groups the settings that belong to the
USER rather than to the repository.
todo_prefs.py stores them in ~/.erplibre/todo_prefs.json — the same place and
the same best-effort shape as todo_telemetry.py, since both are per-user and
per-machine, and neither must ever prevent the CLI from starting. A DEFAULTS
table gives every known key its fallback, so a missing or corrupt file simply
reads as defaults.
The two keys added here prepare the QEMU deploy form: which interface to use
(ask / TUI / classic) and what to display while deploying (CLI output or TUI).
They are declared once in _PREF_CHOICES — the screen, the current-value label
and the editor all derive from that single table, so adding a preference is
one entry, not three edits.
Language keeps its own mechanism: it is read before the preferences file
exists and is consumed by shell scripts too.
Verified against a temporary HOME: defaults returned with no file on disk, a
change persisted and reflected in the menu, and reset falling back to
defaults.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Resources per VM offered x1..x4 only — anything else meant deploying with the
wrong sizing and resizing afterwards. « [5] Custom » now asks vCPU, RAM and
disk once for the whole fleet, and vCPU joins disk and RAM in the per-VM
customisation. Presets 1, 2, 4, 6, 8, 16, 24, 32.
The two places ask the SAME three questions, so they are one definition each
(_qemu_ask_disk / _qemu_ask_ram / _qemu_ask_cpu) instead of two copies that
would drift. vCPU is no longer a single global value: it travels per VM
through the whole chain, down to --vcpus.
Overcommit is allowed on an explicit choice — KVM permits more vCPU than
cores — and only warned about. The x1..x4 path still caps at the host core
count: that one is an automatic computation, not a decision.
Three prompts default to yes (install ERPLibre, ~/.ssh/config, deploy): they
are what one wants nearly every time, and typing « o » on each was noise.
Flipping a destructive-by-omission default demanded the guards below.
Before deploying, a final review lists everything that will change — VMs to
create with their effective disk (ERPLibre's +5 G included), VMs left
untouched, install profile and branch, SSH key, ~/.ssh/config, parallelism.
Answering no asks for confirmation rather than dropping every answer given
over a dozen prompts.
Name collisions are now reported BEFORE the wait, with their two very
different consequences: an already-defined VM is skipped and nothing is
overwritten, while a qcow2 left behind by a deleted VM makes deploy_qemu fail,
since it refuses to overwrite without --force. Continuing requires an explicit
yes; the default is no.
Also translated two strings of this flow that had no entry and stayed in
English (« Resolving VM IPs… », « no IP »).
Verified by driving the prompts with scripted answers: custom profile applied
and left intact when blank, per-VM override, 32 vCPU on 28 cores warned but
accepted, review with and without ERPLibre install, no → no → deploy, no →
yes → cancel, and collisions defaulting to no. Rendering checked in fr and en.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The install history was only surfaced as a « ~5m avg (3) » suffix next to a
distro. Entry [12] now shows what that history actually contains: totals and
success rate, period covered, median/min/max and cumulated duration, then a
breakdown by distribution, version and architecture, plus the current VMs and
the disk they occupy. « [r] » erases the history after confirmation.
record_duration() was only called on success, so no success rate could ever be
computed. Failures are now recorded with ok=False. They are counted separately
and EXCLUDED from the averages and the ETA: how long a failed install ran says
nothing about how long a successful one takes. Entries written before the flag
existed have no « ok » key and are read as successes, which they were.
Aggregation lives in qemu_install_monitor.py as pure functions (stats_summary,
stats_by, all_runs, reset_stats), display in todo.py — the split the file
already follows.
Disk presets extended to 400G, 600G, 800G, 1T, 1.5T and 2T. The parser only
understood G, so « 1T » would have been rejected: sizes are now normalised
through _qemu_parse_disk (1 T = 1024 G, decimal comma accepted), since the rest
of the chain reasons in gigabytes.
Verified with a synthetic history of 9 runs including 2 failures: the rate,
the per-group failure counts and the reset all behave; a group with no success
shows « — » rather than a misleading « ~0s ».
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The presets stopped at 32768 MB; current virtualization hosts go well beyond
that. The series now doubles up to 262144 MB (256G).
The prompt is in MB while people think in GB, so each preset carries its
equivalent: « [g] 65536 (64G) ». With nine of them the line no longer fits, so
suggestions are laid out five per row. Disk presets keep the plain format.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Both prompts expected a raw number, so common values had to be retyped every
time. They now list suggestions:
[a] 20G [b] 40G [c] 60G [d] 80G [e] 120G [f] 200G
New disk size in G, blank = keep (20G):
Letters start at « a » so they can never collide with a value typed directly:
anything starting with a digit is read as the value itself, and blank still
keeps the current one. A letter outside the range is rejected instead of being
silently taken for a size.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
On a rolling distro every fresh development VM was born unable to create VMs.
« make install_os » runs a full system upgrade, the kernel package is replaced
and /lib/modules/<running kernel> disappears. modprobe bridge then fails and
libvirt cannot create virbr0, so the « default » network stays inactive and
virt-install dies on « network 'default' is not active » -- with every package
correctly installed. Measured twice on a freshly created Arch VM: booted on
7.1.3-arch1-3 at 05:44, upgraded to 7.1.5.arch1-2 at 05:46.
Only a reboot fixes it, and one is enough: the default network is already
flagged autostart, so libvirt brings it up by itself once the modules match.
--setup-host therefore gains --reboot-if-needed, used by the deployment
profile. The reboot is scheduled through systemd-run --on-active=5 rather than
issued immediately, otherwise it would kill the installer's SSH session and the
orchestrator would report a failure for a VM that actually succeeded. Without
the flag the behaviour is unchanged: explain and exit 1, which is what a
workstation wants.
Also fix « Error setting up logfile: No write access to
/var/tmp/erplibre-virtinst/virt-manager »: that path was shared, so a first run
under sudo created it as root and later non-root runs could not write. It is
now per-UID.
Verified on the Arch VM that failed: without the flag it exits 1 with the
diagnosis; with it, the reboot is scheduled and the command still returns 0;
after the reboot the kernel and modules match, « default » is active on its own
and virbr0 exists. The cache directory is created as erplibre:erplibre 0700.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The « ERPLibre Deployment (+ QEMU + dev) » profile installed packages with a
one-liner ending in « || true » and hiding stderr, so a broken host looked
installed. Reproduced on erplibre-arch-latest: every binary was present, yet
virt-install failed and « virsh list --all » could not reach any hypervisor.
Three distinct causes, all unhandled:
- The user was never added to the libvirt group. Without it a non-root libvirt
client falls back to qemu:///session, where the « default » network does not
exist, so « --network network=default » fails while everything looks
installed. Being in the group grants the RIGHT to reach qemu:///system but
does NOT change the default URI, so virt-install and virsh now pass
« --connect qemu:///system » explicitly (new LIBVIRT_URI constant).
- dnsmasq was missing: ensure_tools only installed DAEMON_PACKAGES when the
daemon was absent, and libvirt was already there. setup_host now forces them.
- On a rolling distro, « make install_os » upgrades the kernel and the package
manager removes /lib/modules/<running kernel>. modprobe bridge then fails and
libvirt cannot create virbr0 (« Unable to create bridge virbr0: Package not
installed »). No package fixes that, only a reboot: it is now diagnosed and
reported instead of surfacing as an unreadable virsh error.
The profile now calls « deploy_qemu.py --setup-host », which reuses the
existing ensure_* chain, so package names stay defined in one place
(TOOL_PACKAGES / DAEMON_PACKAGES) and keep working for apt, dnf, pacman,
zypper and brew. It fails loudly instead of « || true ».
Verified on the Arch VM that failed: setup-host reports the stale kernel and
exits 1; after a reboot it installs dnsmasq, joins libvirt/kvm, starts the
default network, and reports « Hôte prêt » (exit 0, idempotent on rerun).
The generated command now reads « virt-install --connect qemu:///system ».
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Measured on erplibre-ubuntu-2404: unattended-upgrades fired in the middle of
an Odoo 12->13 migration and restarted the PostgreSQL cluster three times
(« received fast shutdown request »). OpenUpgrade lost its connection and the
intermediate database was left half migrated.
On a development VM the ERPLibre installer now turns off unattended-upgrades
and the apt-daily timers, and drops an apt.conf.d snippet so they stay off
across reboots. dnf-automatic gets the same treatment on Fedora. It runs right
after the cloud-init wait and before the apt-get calls, so apt-daily can no
longer grab the lock between the two either -- the same contention that made
« apt-get update » fail during deployment.
Production VMs are left untouched: automatic security updates must stay on
there. The switch is the existing dev/prod answer, already threaded down to
_qemu_erplibre_remote_cmd.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Le montage sshfs de « Configurer sshfs » ajoute désormais
« -o follow_symlinks » : sshfs résout les symlinks côté serveur. Sans ça,
git échoue sur les worktrees google-repo d'ERPLibre (leur .git est une
chaîne de symlinks relatifs profonds vers .repo/projects et project-objects)
-> « erreur à la lecture de .git », git status/commit impossibles sur le
montage.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
L'entrée « Update - Update all developed staging source code » quitte le
menu Execute (renumérotation 9->8 … 15->14, dispatch elif mis à jour) et
rejoint le sous-menu « Code - Outil pour développeur » (dernière entrée,
appelle prompt_execute_update). Le libellé/i18n (🔃) est réutilisé.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
« Renommer quelles VM ? » devient « Modifier quelles VM ? » : pour chaque
VM choisie, on peut changer le NOM, la taille de DISQUE et la RAM (vide =
garder). _qemu_customize_names -> _qemu_customize_vms renvoie (names,
selected) avec les valeurs mises à jour.
Le multiplicateur de ressources est désormais CUIT dans `selected` (RAM
finale) et le nombre de vCPU fixé une fois (uniforme) -> un override de RAM
par VM est une valeur ABSOLUE, sans ambiguïté avec le multiplicateur. La
fonction eff_res disparaît (plan, dry-run et déploiement lisent les valeurs
finales de `selected`).
Validé : modif VM 1 (nom/disque/RAM) appliquée, VM 2 inchangée.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
« SELinuxContext=unconfined_u:unconfined_r:unconfined_t:s0 » ne débloque PAS
l'exécution : la transition init_t -> unconfined_t est refusée par la
politique -> le service échouait toujours en 203/EXEC (« Permission denied »
sur run.sh, contexte user_home_t) sur Fedora.
En DEV (VM jetable, « SELinux relâché ») on passe désormais SELinux en
PERMISSIF (setenforce 0 + persistance dans /etc/selinux/config) si actif ->
le service exécute run.sh/venv sous /home sans blocage.
PROD reste confiné (/opt/erplibre hors user_home_t + restorecon) — à
peaufiner quand on reviendra sur la prod.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Le fix précédent (DPkg::Lock::Timeout) ne suffisait pas : cette option NE
couvre PAS le verrou /var/lib/apt/lists/lock de « apt-get update ». Au 1er
boot, cloud-init/apt-daily tient ce verrou -> update échouait AUSSITÔT
-> lists vides -> « Unable to locate package git » (exit 100), toujours sur
debian-12.
On RÉESSAIE désormais « apt-get update » jusqu'à libération du verrou (et
lists peuplées), borné à ~5 min (30×10s), avant l'install. Une fois update
OK, git/make s'installent normalement.
Note : le remote_cmd est construit côté HÔTE -> relancer le CLI todo pour
que la nouvelle commande soit envoyée aux VM.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Au déploiement (« Déployer une ou plusieurs VM »), après le choix de la
branche, on demande l'ENVIRONNEMENT cible (dev par défaut / prod).
- DEV : ERPLibre dans ~/git/erplibre ; service systemd unconfined si
SELinux actif (comportement inchangé).
- PROD : ERPLibre dans /opt/erplibre (sudo git clone + chown à
l'utilisateur) ; service systemd CONFINÉ par SELinux (pas d'unconfined)
+ restorecon des contextes -> hors user_home_t, un service peut exécuter
le contenu sans lever le confinement.
Le drapeau `prod` est propagé : _qemu_deploy -> _qemu_install_erplibre_
monitored/_vm -> _qemu_erplibre_remote_cmd -> _qemu_odoo_service_cmd.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Corrections issues de la revue complète des 13 VM (6 en échec réel) :
- APT lock (debian-12) : au 1er boot, cloud-init/apt-daily tient le verrou
et « apt-get install » échouait aussitôt. Ajout de
« -o DPkg::Lock::Timeout=600 » (update + install) -> attend le verrou.
- wkhtmltopdf Debian 13 « trixie » (debian-13) : on prenait le build
« bullseye » (dépend de libssl1.1, absent de trixie) -> gdebi échouait.
Désormais bookworm pour bookworm/trixie/+. ET l'échec gdebi est NON
bloquant (avertissement au lieu d'exit 1 : wkhtmltopdf est optionnel,
sinon tout install_os avortait et Odoo n'était jamais installé).
- SELinux 203/EXEC (fedora-41/43/44) : un service système ne peut pas
exécuter run.sh/venv sous /home (contexte user_home_t). Ajout
conditionnel de « SELinuxContext=unconfined_u:unconfined_r:unconfined_t:s0 »
au service (uniquement si getenforce != Disabled).
- poetry status 1 (ubuntu-2604) : « -q » masquait la cause. À l'échec, on
rejoue « poetry install -v » pour capturer l'erreur dans le log.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
À l'agrandissement, on indique désormais l'espace libre de l'hôte et la
taille virtuelle MAX « soutenable » ≈ (taille réelle actuelle + libre
hôte) — affichée AVANT la saisie pour guider le choix. Si la cible la
dépasse, avertissement NON bloquant : le qcow2 est creux, donc OK tant que
la VM ne remplit pas, mais au-delà l'hôte tomberait à court d'espace.
Ex. : réel 115G + libre 30G -> max soutenable ~145G ; viser 190G affiche
« dépasse de ~45G — surallocation ». L'opération n'est pas bloquée.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sur un hôte en français, « virsh domstate » renvoie « en cours d'exécution »
au lieu de « running ». Le test « state == "running" » échouait donc :
l'agrandissement à chaud tombait dans la branche « qemu-img resize » (au
lieu de « virsh blockresize ») sur une VM ALLUMÉE -> « Failed to get write
lock ».
On force désormais LC_ALL=C / LANG=C (via _qemu_c_env) sur tous les outils
dont on PARSE la sortie, pour avoir l'anglais quelle que soit la locale :
virsh domstate/domname/dominfo/domblklist/list, sgdisk -i, dumpe2fs,
resize2fs -P (todo.py) et virsh list --all (dashboard). Cela répare aussi,
en locale fr, la détection d'état (pause/éteinte), l'analyse des infos
avancées (CPU/RAM) et le parsing de la réduction (partition/GPT).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Trois améliorations autour de la sauvegarde de disque à la réduction :
- La sauvegarde .bak n'est plus systématique : on DEMANDE (défaut OUI)
« Sauvegarder le disque avant réduction ? ». Si non, avertissement (un
échec pourrait casser le disque, pas de restauration possible).
- À la FIN, après avoir proposé de démarrer la VM (donc après test manuel
possible), on propose d'EFFACER la sauvegarde (défaut NON -> on la garde
par prudence).
- « Nettoyer QEMU » détecte désormais les sauvegardes *.qcow2.bak (libellé
« sauvegarde de disque (redim.) ») et permet de les effacer.
_qemu_shrink_revert gère l'absence de sauvegarde (avertit de lancer fsck
au lieu de restaurer). Validé de bout en bout sur image jetable.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
« Partition à réduire introuvable » sur une vraie VM : _qemu_root_part
parsait lsblk en positionnel et EXIGEAIT 4 colonnes. Juste après le connect
nbd, le FSTYPE n'est pas encore en cache -> colonne VIDE -> 3 tokens ->
toutes les partitions étaient ignorées. (Le test initial passait car le FS
avait eu le temps d'être détecté.)
- lsblk -P (paires clé="valeur") : robuste aux colonnes vides ; on repère
la partition par TYPE="part" et on sonde le FSTYPE via blkid si absent.
- _qemu_nbd_connect attend l'APPARITION des sous-périphériques nbdNpM
(jusqu'à ~15 s) avant de rendre la main.
- partprobe silencieux (capture) -> plus de spam « Invalid argument during
seek » pendant la réparation GPT.
Validé sur image jetable au layout cloud (p1 root + p14 bios + p15 ESP),
détection immédiate après connect : 25G -> 15G, GPT « No problems found »,
partitions préservées, fsck propre.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
La réduction « sûre » via virt-resize ne marchait pas : (1) j'utilisais
« virt-filesystems -b » (option INEXISTANTE -> aucune partition détectée ->
« abandon »), et (2) libguestfs est inutilisable ici (aucun vmlinuz dans
/boot -> l'appliance supermin échoue). La VM restait donc à 25G.
Réécriture SANS libguestfs, avec des outils de base présents et éprouvés
(qemu-nbd, e2fsck, resize2fs, sgdisk, parted/partprobe) :
1. copie .bak AVANT toute modification ;
2. nbd + détection de la racine (plus grosse partition, ext seulement) ;
3. e2fsck -> resize2fs (FS) -> sgdisk réécrit la partition en PRÉSERVANT
type/UUID/nom (PARTUUID intact) -> qemu-img --shrink (conteneur) ->
sgdisk -e (GPT de secours) -> fsck final ;
4. en cas d'échec à N'IMPORTE quelle étape : restauration depuis .bak
-> corruption impossible.
Validé de bout en bout sur une image jetable (GPT + ext4, racine à offset
élevé, 800M de données) : 25G -> 15G, GPT « No problems found », fsck
propre, UUID préservé, données intactes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Si le dashboard se ferme (bug, sortie), on peut désormais le ROUVRIR sur un
run passé pour reprendre l'analyse. Nouvelle entrée [11] du menu QEMU :
« 📈 Rouvrir le suivi d'installation (dernier run / historique) ».
- mon.list_install_runs() : liste les runs (~/.erplibre/qemu-install/*/
session.json) triés du plus récent au plus ancien.
- _qemu_reopen_monitor : affiche l'historique (VM par run), choix (défaut =
le dernier), puis run_monitor(session.json) rouvre le dashboard.
- « Lister les images » passe de [11] à [12] ; config -> 13+.
Validé : 20 runs listés, dashboard reconstruit (titre + 5 VM) depuis un
session.json passé.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
« Suivi interactif (dashboard) ? » répondait NON par défaut (réponse vide).
Le défaut est désormais OUI : nouveau helper _is_yes_default_yes (vide = oui)
et libellé « (O/n, défaut : oui) » / « (Y/n, default: yes) ».
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
virt-resize/virt-filesystems (requis pour la réduction SÛRE de disque) sont
des binaires SYSTÈME (OCaml), pas des paquets Python : ils ne peuvent pas
vivre dans .venv.erplibre. On les ajoute donc au jeu de paquets système du
profil « ERPLibre Déploiement » (à côté de qemu/libvirt/virtinst) :
apt libguestfs-tools · dnf guestfs-tools · pacman libguestfs.
Ainsi tout hôte provisionné avec ce profil peut réduire un disque sans
risque. (La commande de redimensionnement propose déjà d'installer
libguestfs à la demande si absent.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Réduire avec « qemu-img resize --shrink » tronquait le conteneur qcow2 SANS
réduire le FS/partition/GPT invités : partition racine tronquée + GPT de
secours perdue -> dracut-initqueue en échec, OS non bootable.
Désormais la réduction passe par virt-resize (libguestfs) qui réduit
proprement système de fichiers + partition + GPT :
- _qemu_safe_shrink : écrit dans une NOUVELLE image (qemu-img create +
virt-resize --shrink <partition>), et ne remplace l'originale (mv) QUE si
virt-resize réussit. En cas d'échec/refus, le disque d'origine reste
INTACT (impossible de corrompre). Sauvegarde conservée en .bak.
- _qemu_largest_partition : détecte la partition racine (la plus grosse) via
virt-filesystems.
- _qemu_install_libguestfs : propose d'installer libguestfs-tools si absent ;
sinon on ABANDONNE (on ne tronque jamais).
- _qemu_offer_start extrait (redémarrage après extinction).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Après un shrink, « Démarrer la VM » faisait « virsh start <id> » : une fois
la VM éteinte, l'ID numérique disparaît -> « failed to get domain 32 ».
On résout désormais le NOM canonique dès le début de _qemu_resize_disk
(VM encore allumée, ID résoluble) et on l'utilise partout, y compris pour
le redémarrage final. Plus de dépendance à l'ID une fois la VM arrêtée.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Le choix du navigateur propose désormais « [i] Installer un autre
navigateur » en plus des navigateurs déjà installés. L'option ouvre le
sous-menu d'installation (choix w3m/lynx/links/elinks, commande adaptée à
l'OS, validation), même s'il existe déjà un navigateur.
Le flux d'installation est extrait dans _qemu_install_cli_browser
(réutilisé quand aucun navigateur n'est présent ET via l'option [i]).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Le navigateur ne faisait qu'imprimer sans réagir au clavier : la commande
passait par exec_command_live, qui exécute avec stdout=PIPE (et sans stdin
terminal) -> un navigateur texte (w3m/elinks) n'a pas de TTY interactif.
On lance désormais le navigateur avec os.system(), qui hérite du vrai
terminal (stdin/stdout/stderr) — même principe que le suivi d'installation
(TUI) qui l'appelle dans self.suspend(). Ici, en CLI simple, aucun suspend
n'est nécessaire.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Nouvelle entrée [10] dans « Gérer » du menu QEMU/KVM : 🧪 Tester une VM.
Elle liste les VM, demande laquelle, résout son IP puis ouvre
http://IP:8069 (Odoo) dans un navigateur web EN LIGNE DE COMMANDE choisi
par l'utilisateur.
- _qemu_test_vm : résolution d'IP (_qemu_vm_ip), lancement du navigateur,
message d'aide si la page ne s'affiche pas (Odoo pas démarré / réseau).
- _qemu_choose_cli_browser : liste les navigateurs CLI installés et laisse
choisir ; si aucun, propose d'en installer un (réutilise CLI_BROWSERS /
INSTALLABLE_BROWSERS / browser_install_command de qemu_install_monitor).
- « Lister les images » passe de [10] à [11] ; les entrées de config
suivent à 12+ (branche else inchangée via la liste `real`).
Validé : rendu du menu, choix du navigateur ([2]->elinks, vide->w3m).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Suivi d'installation (qemu_install_monitor.py) :
- Table VM/État LISIBLE : largeurs de colonnes fixes (VM 26, ⚠ 4, État 12,
Durée 7, Disque 8) -> l'État n'est plus tronqué à 3-4 caractères
(« ❌ effacée », « ⏸ en pause » lisibles) ; table défilable (overflow-x,
height 1fr) pour les longs noms et les gros parcs.
- « w » web : offre désormais la LISTE des navigateurs CLI installés
(_choose_browser) pour choisir lequel utiliser, + option [i] installer.
Déploiement (deploy_qemu.py) :
- guest-exec AUTORISÉ : on vide la liste de blocage de qemu-ga
(block-rpcs/blacklist vides — pas allow-rpcs qui est une liste BLANCHE et
casserait les autres RPC), + neutralise /etc/sysconfig/qemu-ga (Fedora).
Installation (todo.py) :
- Profils AVEC Odoo (install_odoo*) UNIQUEMENT : Odoo est enregistré comme
service systemd (erplibre.service, inspiré de script/systemd/
install_daemon.sh) puis enable --now. Pas pour ERPLibre seul / mobile /
Déploiement.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Provisioning (deploy_qemu.py) :
- qemu-guest-agent installé + activé dans le runcmd cloud-init (APRÈS
sshd, « || true » : ne bloque pas le boot si le réseau est lent).
- Canal virtio org.qemu.guest_agent.0 ajouté à virt-install : virsh peut
piloter la VM SANS réseau.
- Extension du FS invité (todo.py) : nouveau tier AGENT INVITÉ
(_qemu_guest_exec via qemu-agent-command guest-exec) entre SSH et la
console série -> étend le FS même sans IP.
Suivi d'installation (qemu_install_monitor.py) :
- Détection d'erreurs dans le log à la complétion (succès OU échec) en
réutilisant la logique de script/test/run_parallel_test.py (sous-chaîne
error/warning + listes d'ignore). Nouvelle colonne « ⚠ » À GAUCHE d'État
(⚠N erreurs / ⚡N avert. / ✓ propre).
- Sommaire de stats EN CHIFFRES (📊 total · ✅ · ❌ · ⏳ · ⏸ · 🗑 · ⚠ · ⚡) ;
CLIC pour déplier le détail (VM en erreur + durées).
- Boutons « p » Pause tout (virsh suspend des VM running) et « o »
Reprendre tout (virsh resume) — les logs continuent (offsets conservés).
Validé headless : succès-avec-erreur -> ⚠1, stats/détail, pause du parc.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Trois améliorations QEMU :
- Extension du FS invité robuste : résolution d'IP avec BATTEMENT
(_qemu_resolve_ips, parallèle, boot émulé lent) au lieu d'un timeout
court ; en cas d'absence d'IP ou d'échec SSH, repli sur la CONSOLE
SÉRIE avec la commande growpart/resize prête à coller (login
erplibre/erplibre). Commande factorisée dans _GROW_FS_REMOTE.
- Temps estimés PAR VERSION : nouveau mon.avg_by_version(distro, version)
et _qemu_stat_avg("version", v, distro) -> suffixe « · ~46s moy (1) »
dans les listes de versions (prompt simple + granulaire).
- i18n : « Versions for » n'avait AUCUNE traduction (restait en anglais).
Ajout FR « Versions des » (+ « Version des ») et capitalisation de la
distro -> « Versions des Ubuntu : ».
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Deux points :
- Fil d'Ariane depuis la télémétrie : une commande lancée DEPUIS le TUI de
télémétrie ne passait par aucun menu, donc son chemin n'était jamais
affiché. On imprime désormais « 📍 TODO › … › <commande> » (dernier
segment traduit + icône) avant l'exécution, et on enregistre le chemin.
- Arrêt de VM (réduction disque) par SIGNAL : virsh shutdown --mode
acpi,agent (bouton ACPI puis agent invité) au lieu d'un arrêt implicite.
Pendant l'attente, on affiche un compte à rebours du timeout
(« ⏳ arrêt en cours… NNN s restantes ») et le délai max au départ.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Après « virsh list --all », le menu propose désormais :
[1] Infos avancées (vCPU, RAM, disque)
[2] Changer l'état d'une ou plusieurs VM
[Entrée] Rien
Changement d'état (_qemu_change_state) :
- Saisie d'une liste de VM séparée par des virgules (noms ou ID, résolus
via _qemu_domname et validés contre les VM existantes).
- Choix de l'état cible : Ouvrir (start) ou Fermer (shutdown).
- DOUBLE validation (« Appliquer : … ? » puis « Confirmer pour de vrai ? »)
avant d'exécuter virsh start/shutdown sur chaque VM.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Si la VM a dû être éteinte pour la réduction, on le NOTE (« La VM a été
éteinte pour le redimensionnement. ») puis on demande (o/N) si on veut la
redémarrer (virsh start sur le nom canonique). Si la VM était déjà éteinte
ou n'a pas eu besoin de l'être (agrandissement à chaud), on ne demande
rien : drapeau was_shut_down positionné uniquement après un arrêt effectif.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Avant, réduire le disque d'une VM allumée affichait seulement « Éteignez
la VM » puis abandonnait. Désormais on DEMANDE (o/N) si on veut l'éteindre
et réessayer :
- _qemu_shutdown_wait : arrêt ACPI gracieux (virsh shutdown), attente
jusqu'à « shut off » (timeout 120s), puis propose un arrêt forcé
(virsh destroy) si l'arrêt traîne.
- _qemu_domname : résout un ID numérique en nom canonique — l'ID
disparaît une fois la VM éteinte, le polling doit utiliser le nom.
Validé : domname(14) -> erplibre-ubuntu-2004.
- Tous les nouveaux prompts affichent la valeur par défaut (o/N).
Une fois éteinte, la réduction (qemu-img resize --shrink) se poursuit
automatiquement après confirmation du risque.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Après « sudo virsh list --all », le menu propose désormais (o/N) un
tableau détaillé par VM : état, vCPU, RAM allouée (Max memory), taille
disque virtuelle et réelle (qemu-img info -U, lit même VM allumée), plus
l'espace total/libre/utilisé du stockage des images (shutil.disk_usage).
- _qemu_list_vms(ask_advanced=False) : seul le menu [4] prompte ;
les appels internes (IP, console, resize, delete) restent inchangés.
- Helpers _qemu_dominfo (vcpu + Max memory) et _qemu_disk_sizes
(virtuel + réel, -U). Validé en réel : 8 vCPU / 8.0G / 30.0G / 2.5G.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
qemu-img info échouait (« Failed to get shared write lock ») sur une VM
running car libvirt tient le lock d'écriture : la taille virtuelle lue
tombait à 0.0 G, et « -10G » donnait alors 0-10 = -10 → « Taille invalide ».
- Ajout de -U (--force-share) aux deux appels qemu-img info (affichage +
_qemu_disk_virtual_bytes) : lecture seule sûre même VM allumée.
- Garde-fou : si la taille reste illisible (0), on abandonne avec un
message clair au lieu de calculer une cible négative.
Le redimensionnement lui-même était déjà correct (virsh blockresize à
chaud si running, qemu-img resize si éteinte) — pas besoin d'éteindre la
VM pour AGRANDIR. Validé : lecture -U renvoie 30.0 G sur une VM running.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
L'historique d'installation est désormais enregistré dans un fichier DÉDIÉ
.venv.erplibre/qemu_install_stats.json (repli ~/.erplibre) : chaque run garde
distro + version + architecture + durée + horodatage (500 derniers).
- record_duration(distro, version, arch, secs) enregistre le run (appelé par
le dashboard à la complétion d'une VM).
- Menus de sélection enrichis : chaque architecture et chaque distribution
affiche la DURÉE MOYENNE d'install historique « · ~5m moy (3) » quand la
donnée existe. La DERNIÈRE install (distro version [arch] — durée) est
rappelée en tête du déploiement.
- eta_reference lit désormais les runs (médiane par arch, repli global).
Nouveaux helpers : avg_by_arch, avg_by_distro, last_run.
Validé : enregistrement + moyennes (amd64 ~5m (2), ubuntu ~13m (3)),
dernière install affichée, distros sans données masquées.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>