Releases: stephrobert/dsoxlab
Release list
v0.2.5
Fixed
-
/dev/kvmexisted and could not be opened. The node is there on a
GitHub runner — the job checks it first, andkvm-okagrees — but it belongs
to thekvmgroup, and therunneraccount is not in it. QEMU then fails
with Qemu failed to start, a message that names neither the file nor the
right, and Packer halts twenty-three seconds in. A udev rule now makes the
node accessible, and the job opens it rather than assuming the rule
worked: a check that cannot look never concludes green. -
The build is no longer silent about its own failure.
PACKER_LOG=1now
writes to a file, and the last two hundred lines are printed only when the
build fails. The verbose log is unreadable when everything works and
indispensable when QEMU says nothing useful — which is exactly the situation
that cost this release.
v0.2.4
Fixed
- The appliance build got one step further, and stopped on the step before.
With/mntfinally writable, Packer refused to start: Output directory
'/mnt/appliance/out' already exists. It must not exist. That refusal is a
feature — it is how Packer guarantees it never writes over a previous
build — and the preparation step was creating that very directory. It now
creates only the parent and the temporary directory, and leavesout/to
Packer. Two failures in a row at the same place say something worth keeping:
a workflow that has never run is a workflow that does not work, and the only
way to know is to run it.
v0.2.3
Fixed
-
The appliance workflow had never run, and died four hundredths of a second
in.mkdir: cannot create directory '/mnt/appliance': Permission denied—
on a GitHub runner/mntbelongs to root, and that is where the disk space
is (~70 GB against ~14 on/). No local build could show it, since the
directory exists and is writable on a developer's machine. A dedicated step
now creates it withsudoand hands it to the runner. -
The image is published on every release, not only on minor ones. The old
rule saved bandwidth and cost more than it saved: the documentation announced
an appliance the latest release did not carry, and the reader had to work out
which earlier release did. "The latest release carries the image" is the only
sentence that needs no caveat, and nothing accumulates since the retention
step keeps the last two sets. The two workflows now start from the same tag,
so the upload waits for the Release to exist instead of assuming it — and
says which workflow to look at if it never does.
Changed
-
The documentation is a path now, not a pile of pages. Read end to end
against the code, it had breaks that no single page could reveal:learner.mdpromisedrunright after cloning a catalog, while 66 of the
Linux catalog's 86 labs arevmlabs: thatrunexits 2, theprovision
it suggests exits 1 for want of an SSH key, and neitherinstructor bootstrap, norstart, nor the word Terraform appeared anywhere on the
page. It now names what avmlab asks of a machine, and putsstartfirst
— the command that plays the whole order and announces each step with the
command that replays it alone.catalog-author.mdlisted a lab's files without saying what goes in them.
The scale thatvalidate-structureenforces (### … (20 pts)against a
N tasks, M pointsheader, and one point per test), the format of
hints.yaml, and whereconftest.pylives — pytest runs from the catalog
root — were documented nowhere.The README reached neither the index nor
exit-codes.md,
machine-output.mdandinfra-only.md: three reference pages unreachable
from the front door.The SSH key was described as something a catalog ships. It is not: published
catalogs ignore the wholessh/directory, andprovisionrefuses to start
without the private half. It is generated per clone, byinstructor bootstrap, learners included.contract-v1.mdcontradicted itself nine lines apart on whether
repo.categoryis required. -
docs/appliance.mdis a step-by-step, from installing VirtualBox to the
first lab, with the digest check, the memory to give the machine depending on
the host's, what the first boot looks like in three acts, and a
troubleshooting table. Both READMEs open on the two ways in, each with its
own four-step path.
Added
- Credit where it is due: the appliance is @cedric-ribier's idea, and it
exists because he had already built one end to end and documented it in
#91. The one shipped here
is directly inspired by his.
v0.2.2
Added
-
The appliance: a ready-to-play VM, built by the CI (issue #91, moved to the
0.3.0 milestone and now started). For Windows and macOS, whereuv tool install dsoxlabis not an option. On Linux it stays the wrong answer, and the README
says so: downloading half a gigabyte to avoid one command makes no sense.packer/holds the recipe —qemubuilder, automated Debian 13 install, four
provisioning scripts — and.github/workflows/appliance.ymlbuilds it on a
GitHub-hosted runner, with no self-hosted machine: Linux runners expose
/dev/kvm, so QEMU is accelerated. That choice also removesovftool
(proprietary) and VirtualBox from the chain: the OVA is derived from the qcow2
withqemu-imgandtaralone.The image pins nothing. The first boot installs the latest dsoxlab, and the
hypervisors only if the host exposes nested virtualization — checked live, never
assumed. Which is why it is rebuilt on minor tags only: republishing half a
gigabyte for every patch would cost a lot and change nothing.Measured, not estimated: 461 MiB for the qcow2 and 446 MiB for the OVA,
built in 4 min 50 s, against a budget the workflow enforces at 800 MiB. A
comparable appliance built withvirtualbox-isoweighs 979 MiB. -
The README now presents both ways in, side by side: install the tool, or
download the appliance. It also says which one is right on Linux, rather than
selling them as equals. -
Only the last two sets of images are kept on the Releases. Roughly 900 MB
per minor version, forever, for images that pin no dsoxlab version and
install the latest at first boot: an old one offers no reproducibility, only
weight. Two rather than one, so a broken image has a fallback. The Releases,
their changelog and the Python distributions are untouched. -
The OVA is offered for VMware as well as VirtualBox, and the workflow now
holds it to that promise: the OVF is validated against the DMTF schema at
every build. VirtualBox imports almost anything, VMware checks — and without
a Broadcom licence, conformity to the specification is the only honest way to
keep the claim. The appliance page says plainly what was tested and what was
not, and that Apple Silicon is out of reach: both images are x86-64,
GitHub's arm64 runners expose no/dev/kvmto build another, and nested
virtualization on those Macs only exists from the M3.
Fixed
-
Three defects in that recipe, each caught by a measurement rather than a
reading. They are worth recording because they are the kind that ship
silently:fstrimreturns success without freeing anything when the build disk is
attached withdiscard=ignore, which is Packer's default. The guest announced
"520.4 MiB trimmed" while the qcow2 kept every byte: 1110 MiB of artifact
against 597 for the control. So the disk is now attachedunmap, with
detect_zeroes=unmapso that zeroing costs nothing — and the never-triggered
|| ddfallback is gone, along with the 17 GiB it wrote when it did fire.vm_namehad no extension, and Packer adds none: the*.qcow2globs of the
budget check, the checksums and the upload would never have seen the file. The
first local build produceddsoxlab-appliance-dev, which proved it.The size check tested the 2 GB hard limit, so it would have waved through a
1.1 GB image the day the trim regressed. It is now a budget of 800 MiB, with
qemu-img infoandcheckprinted, and an error that says where to look. -
lsb_releaseno longer exists once thestandardtask is dropped from the
preseed, and20-outils.shused it for the HashiCorp repository. Read from
/etc/os-releaseinstead — without this the build would have failed outright. -
Ten more defects, found by playing the appliance rather than reading the
recipe. Each was invisible to every check that came before, and the first one
alone made the image useless to the very people it is built for:The network did not survive a change of hypervisor. Debian's installer
freezes the interface name it saw at install time —enp0s2under the QEMU that
builds the image — and that name is derived from the card's PCI position. Under
VirtualBox the card isenp0s17, so the configuration applied to nothing: the
imported appliance came up withenp0s17 DOWN, aresolv.confwithout a single
nameserver, and a first boot that failed on "Temporary failure in name
resolution". Nothing was installed — no dsoxlab, no hypervisors, no desktop. The
image now configures the network withsystemd-networkd, whose[Match]
describes the card (en*,eth*) instead of naming it.The first boot marked itself done even when everything had failed, so the
machine had no way left to catch up. It now reports what failed, keeps no
marker, and starts over at the next boot. It also waits for a name to actually
resolve before it begins:network-online.targetwas reached with no DHCP lease
taken, which is how the failure above got its eight-second timeout.Root inherited the student's
HOMEduring the build (sudo -E), so
terraform versionandansible-playbook --versioncreated a root-owned
~/.ansibleand~/.terraform.d. Ansible then refused to start for the learner
and dsoxlab reportedrc=5, Stats: {}— a symptom nothing connected to the
cause. Fixed withsudo -H, plus achownbelt.ovmfandqemu-utilswere missing: both are mere recommendations of
qemu-kvmon Debian, so--no-install-recommendsdropped them. Without the
first, libvirt exposes no EFI firmware and everyprovisionstops dead; without
the second, it cannot create a qcow2 volume. Found by provisioning for real from
inside the appliance — and the first was named in 2.3 seconds by the guard added
for issue #234.No libvirt
defaultpool and no group membership: a fresh Debian defines
neither, sodoctor --strictexited 9 andstudentcould not open/dev/kvm.
Both are now set up at first boot, which is also why it reboots at the end.The desktop had no X server. Same pattern as
ovmfandqemu-utils, for
the third time:xserver-xorgis a recommendation ofxfce4andlightdm,
never a dependency, so--no-install-recommendsdropped it./usr/bin/Xorg
did not exist,lightdmwasfailed, and the machine came up on a console
despitegraphical.target— with nothing tying that to a missing package. The
server and its video drivers (vmwarefor the VMSVGA controller VirtualBox and
VMware present,vesaandfbdevas fallbacks) are now named explicitly.~/.ssh/configdid not include what dsoxlab writes.provisiondrops a
~/.ssh/config.d/<catalog>.confand warns, at every run, that nothing reads it.
TheIncludeis now placed at the top of the file, where OpenSSH requires it.startcalled an unusable infrastructure "already provisioned". It
decided from the state alone, so an address was enough. The sequence that
followed was absurd:provisionexited 8 on "some hosts did not answer: the
infrastructure exists, but it is not usable as it stands", and thestart
relaunched right after — relaunched because nothing answered — announced
"already provisioned, nothing to build again", then failed further down on a
raw AnsibleUNREACHABLEthat nothing tied to the cause. It now probes port
22 as well: an address is not a machine. Replayingprovisioncosts nothing,
since it resumes without recreating anything, and it waits.The console was flooded with
AF_VSOCKerrors, one per systemd reload, so
a dozen red lines in five seconds while packages installed. Nothing was broken,
but nothing said so either.systemd-ssh-generatoris now masked, the
documented way, with a symlink to/dev/null.
v0.2.1
Added
-
A
Thankssection in both READMEs. Three people outside the project have
reported defects with the diagnosis, and often the fix. The section names what
each report changed rather than listing handles: four releases come from
@cedric-ribier (nominal disk sizes, libvirt 8,
the Incus agent on AlmaLinux with a configurable host wait, and nested
virtualization named), theconftest.pythat silently blocked everyshelllab
from @Gedd18, and two labs whose starting state was
unusable from @VictorVare.A credit that says what it fixed is worth more than a list, and it tells the next
reader what kind of report is useful.
v0.2.0
Productization. This version adds no code: it marks a milestone, and its
thesis was written when the milestone opened —
a framework is a tool its author knows how to use; a product is a tool a
stranger can install, understand and diagnose.
Seventy-one issues went into it. What changed is not a feature count, it is that
the path from "I found this repository" to "I played a lab" no longer has an
implicit step, and that nothing green claims what it has not measured.
What the milestone actually delivered
Installing and starting. uv tool install dsoxlab is what the README shows
first, dsoxlab demo gives a first lab with no hypervisor, no catalog to clone
and no container, and dsoxlab start plays a lab end to end while announcing
each step with the command that replays it alone — the sequence is content, not
plumbing to hide.
Diagnosing. doctor sorts what this repository requires from what merely
informs it, and a check has three outcomes, never two: ok, failed, and
unknown when the probe itself did not complete. --strict turns that into exit
codes 9 and 10, kept apart because one gets repaired and the other gets
measured again. dsoxlab support produces an anonymised report, and --issue
files it where it belongs.
Refusing to pretend. That is the through line of the whole milestone. A
destroy that left machines up exited 0; a provision that lost hosts announced
success; a validator said "every lab is valid" about a catalog it had not fully
read; a required check painted a healthy install red over nominal disk sizes. Each
of those now has a code, a message, and a test — and several of those tests were
verified by being made to fail.
Contract and integration. schema_version: 1 freezes the v1 contract,
meta.schema.json and lab.schema.json are published, --json is documented
field by field, and the exit codes have a page of their own stating, for each one,
the gesture it calls for.
The acceptance criterion, played rather than asserted
A user who has never seen the repository plays a lab end to end following only
the README, with no hypervisor.
Done, from the published package, in an isolated HOME, typing exactly what the
README shows: install, demo, course, run, challenge, hint, check — three
tests green, 80/100, which is the maximum the brief allows since it deliberately
invites taking the hint that costs 20 points.
What was deliberately left out
Ready-made VM images (#91) move to 0.3.0. Not for lack of merit — a contributor
has built a working appliance — but because their cost is recurrent: build,
hosting, provenance, security updates, and compatibility with every hypervisor
release. What 0.2.0 kept from that issue is the part that serves it: doctor now
names missing nested virtualization instead of sending its reader into a BIOS a
virtual machine does not have.
v0.1.101
Added
-
dsoxlab start [<id>]: the sequence, played and shown (issue #79). To
begin a lab you had to know in which order to chain commands, none of which says
it assumes another: the firstrunof avmlab on absent infrastructure fails,
and the learner has to guess thatprovisionwas missing.provisiondoes not
even appear in the README's sequence.This command had been revised before being written, and the revision governs
its shape. The argument, from the issue: a command that implicitly chains
context, dependencies, provisioning, services, preparation and session makes
failures more opaque — when it breaks, the user must guess which of the six
steps did. That is the opposite of what this milestone spent its time doing. And
for a teaching tool, the sequence is content: seeing infrastructure get
provisioned, then a lab prepared, then a session open is part of what a learner
came for, and what they will do without dsoxlab one day.So
startdoes not swallow the sequence, it states it. Each step is
announced with the single command that replays it alone:Starting l2-swap-management — 4 steps, each announced with its own command. ▶ 1/4 · active context (dsoxlab use l2) ▶ 2/4 · prerequisites (dsoxlab doctor) 16 required checks, all green ▶ 3/4 · infrastructure (dsoxlab provision) ▶ 4/4 · preparation and session (dsoxlab run l2-swap-management)On failure it names the step that broke and the command that retries just that
one, attempts nothing beyond it, and returns that step's own exit code —
never a code invented forstart. That is the issue's non-negotiable criterion,
and two tests hold it: each was verified by being made to fail.What it shortens is the typing and the order to remember, never the
understanding. Every unit command keeps working exactly as before.Details that took a decision:
- a step that does not exist for this lab is absent — a
shelllab has no
infrastructure, and "3/4 · infrastructure — skipped" would be noise. A step
that exists but is already done stays announced and says so. The first
trial run went from 3 steps to 2 on the second invocation, because the context
was already set: a total that moves is confusing, and it was hidingdsoxlab use, which the learner needs to know; startis idempotent: it reads the Terraform state and skips provisioning
when the declared hosts already have addresses. It does not probe over SSH — a
powered-off machine is still provisioned, and "does it answer" isstatus's
question;- without an id, it takes the lab
nextsuggests, computed by the same service.
Verified in the open on both runtimes, which is what the acceptance criteria
demand: ashelllab from the demonstration catalog, and avmlab of the Linux
catalog with no infrastructure at all — three VMs brought up in 38 seconds,
setup.yamlplayed, session opened, then destroyed.That real run caught a defect no unit test could:
_infra_prete()readlabenv
at the root of the inventory instead ofall.children.labenv, so it always
answered "needs provisioning" and the second run rebuilt an infrastructure that
was already up. Fixed, with the three tests that were missing — they no longer
simulate the function they measure. - a step that does not exist for this lab is absent — a
Changed
- The body of
provisionis now a function without the lock
(provisionner()).runtakes the write lock itself and releases it before
opening the interactive session — otherwise thedsoxlab checkthe learner types
in that sub-shell would be refused by their own session.starttherefore takes
the lock for the infrastructure step only, releases it, then delegates torun.
Holding it throughout would have madestartexit 7 on its own lock, which
would be quite something for a command whose purpose is to remove surprises. The
provisioncommand itself is unchanged.
v0.1.100
Fixed
-
A network name too long for the kernel is now refused before any work, and
named (issue #214). The Linux kernel rejects an interface name longer than 15
characters (IFNAMSIZis 16, terminator included). The packaged kvm template
computes the bridge name asvirbr-+ the network name with everylab-
removed, sonetwork: lab-kubernetesyieldsvirbr-kubernetes— sixteen
characters. Terraform failed with:Error: Network Start Failed Network defined but failed to start: error creating bridge interface virbr-kubernetes: Numerical result out of rangeThat message names neither the bridge, nor the limit, nor the field that
produced it — and the offending name appears nowhere inmeta.yml, since
it is derived. Worse, the failure landed after the base image had been
downloaded, so every attempt cost a minute.provisionnow refuses beforeterraform init, so before downloading a
provider or an image, exits2(IMPOSSIBLE), names the computed bridge with
its length, and states the length the network name must fit in.doctorreports
the same check, in the required table, as soon as the repository declares hosts
with a local provider — a check that probes nothing and only counts characters,
so it always has an answer.The two derivation rules now live in Python, in
infra/pont.py, where a check
can read them:kvmcomputes the name,incususes the network name verbatim
(so the limit applies with nolab-margin), andoutscalecreates no
interface on this machine. A declaredbridge_nameoverride wins, exactly as
the template reads it — otherwise the check would refuse a catalog that had
already worked around the problem.A test confronts the Python rule with the HCL expression actually written in
the template. Two definitions of "what the bridge is called" would drift, and
the drift would stay invisible until the next long-named catalog. That test is
also what caught a wrong assumption of mine: HCL'sreplaceremoves the
occurrence wherever it sits, somon-lab-reseaugivesvirbr-mon-reseau, not
virbr-monreseau.The remediation states the target length rather than how many characters to
drop: a suggested truncation is not a name anyone wants, and a target lets the
author pick one that means something.docs/contract-v1.*now documents the
limit per provider, which is where an author looks.
v0.1.99
Changed
-
The prerequisites now name the tool the very next line uses (issue #202).
The README said "Requires Python 3.11+. Nothing to clone, nothing to build",
and three lines below installed withuv tool install dsoxlab. The learner
documentation went further: "Python 3.11 or newer, and that is the whole
prerequisite." The reader this page is written for is precisely the one who has
neitheruvnorpipx— someone discovering the tool. A README whose stated
purpose is to remove surprises hid one in its first promise. Both are now named,
with a link touv's installer, in English and in French. -
CONTRIBUTING.mdno longer describes a file that has not existed for
twenty-five versions, nor demands the opposite of what the repository does
(issue #196). Two claims were false:src/dsoxlab/cli.pywas named three times as "the only entry point". It has
been a package of 17 modules since 0.1.72. The cost was measured: an
external review of the repository recommended splittingcli.pyinto command
groups — work finished twelve versions earlier — because it read this page
instead of the code.The page also stated that "issues, pull requests, code comments and commit
messages are written in English", while the code is commented in French across
several thousand lines, and so are the commits and the issues. The working
language is now written as it is: French inside, and bilingual or English
for everything that leaves the repository — translated strings through_()in
EN and FR, paired documentation files, and the log andsupportreport in
English because they are compared between machines and published. A contribution
written in English remains welcome, and the page says so.A governance rule the repository contradicts is worse than no rule: it makes
people write falsehoods with confidence.
v0.1.98
Fixed
-
The
supportreport is now written in English, whateverDSOXLAB_LANGsays
(issue #227). It was French, hardcoded:### Environnement,### Outils externes,_Aucune trace enregistrée._,présent, version illisible, and row
labels taken straight from the document keys, which are French too. An
DSOXLAB_LANG=ensession therefore got a French report — and since 0.1.86,
--issuedrops it intobug_report.yml, whose every label is English.This is exactly the reasoning that put the log in English in 0.1.83, with one
more condition: this report is published. It gets searched word for word,
compared between machines with different locales, it already carries an English
log, and it now lands in an English form. What is published does not follow the
locale of whoever produced it. The coherence gained in 0.1.83 stopped halfway
through the file.What made it less trivial than it looks: the row labels are the document
keys, and that document issupport --json, a contract for programs. Renaming
systemetosystemto fix a display would have broken consumers for a reason
unrelated to the defect. So the two are now separate — the keys stay exactly as
they were, and a translation table gives the rendering its English labels. A key
absent from that table renders as-is, deliberately: tool names come from the
system, and a new field is better shown raw than hidden.The words dsoxlab supplies as values moved too, because they travel in the
JSON as well:aucun→none,présent, version illisible→present, version unreadable,inconnu→unknownfor a shell it cannot name.Nine tests hold this, sharing the French-word list of
test_journal_en_anglais.py— one definition of "this word only exists in
French" for both rules. Each guard was verified by being made to fail: a section
title put back in French, the absent-value word put back in French, and a
document key renamed all turn the suite red.