Skip to content

Releases: stephrobert/dsoxlab

v0.2.5

Choose a tag to compare

@github-actions github-actions released this 26 Sep 09:50
c4bdb3a

Fixed

  • /dev/kvm existed and could not be opened. The node is there on a
    GitHub runner — the job checks it first, and kvm-ok agrees — but it belongs
    to the kvm group, and the runner account 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=1 now
    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

Choose a tag to compare

@github-actions github-actions released this 26 Sep 09:34
d5540e4

Fixed

  • The appliance build got one step further, and stopped on the step before.
    With /mnt finally 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 leaves out/ 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

Choose a tag to compare

@github-actions github-actions released this 26 Sep 09:26
201724d

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 /mnt belongs 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 with sudo and 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.md promised run right after cloning a catalog, while 66 of the
    Linux catalog's 86 labs
    are vm labs: that run exits 2, the provision
    it suggests exits 1 for want of an SSH key, and neither instructor bootstrap, nor start, nor the word Terraform appeared anywhere on the
    page. It now names what a vm lab asks of a machine, and puts start first
    — the command that plays the whole order and announces each step with the
    command that replays it alone.

    catalog-author.md listed a lab's files without saying what goes in them.
    The scale that validate-structure enforces (### … (20 pts) against a
    N tasks, M points header, and one point per test), the format of
    hints.yaml, and where conftest.py lives — pytest runs from the catalog
    root — were documented nowhere.

    The README reached neither the index nor exit-codes.md,
    machine-output.md and infra-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 whole ssh/ directory, and provision refuses to start
    without the private half. It is generated per clone, by instructor bootstrap, learners included.

    contract-v1.md contradicted itself nine lines apart on whether
    repo.category is required.

  • docs/appliance.md is 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

Choose a tag to compare

@github-actions github-actions released this 26 Sep 08:51
206d336

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, where uv tool install dsoxlab is 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 — qemu builder, automated Debian 13 install, four
    provisioning scripts — and .github/workflows/appliance.yml builds it on a
    GitHub-hosted runner
    , with no self-hosted machine: Linux runners expose
    /dev/kvm, so QEMU is accelerated. That choice also removes ovftool
    (proprietary) and VirtualBox from the chain: the OVA is derived from the qcow2
    with qemu-img and tar alone.

    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 with virtualbox-iso weighs 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/kvm to 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:

    fstrim returns success without freeing anything when the build disk is
    attached with discard=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 attached unmap, with
    detect_zeroes=unmap so that zeroing costs nothing — and the never-triggered
    || dd fallback is gone, along with the 17 GiB it wrote when it did fire.

    vm_name had no extension, and Packer adds none: the *.qcow2 globs of the
    budget check, the checksums and the upload would never have seen the file. The
    first local build produced dsoxlab-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 info and check printed, and an error that says where to look.

  • lsb_release no longer exists once the standard task is dropped from the
    preseed, and 20-outils.sh used it for the HashiCorp repository. Read from
    /etc/os-release instead — 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 — enp0s2 under the QEMU that
    builds the image — and that name is derived from the card's PCI position. Under
    VirtualBox the card is enp0s17, so the configuration applied to nothing: the
    imported appliance came up with enp0s17 DOWN, a resolv.conf without 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 with systemd-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.target was reached with no DHCP lease
    taken, which is how the failure above got its eight-second timeout.

    Root inherited the student's HOME during the build (sudo -E), so
    terraform version and ansible-playbook --version created a root-owned
    ~/.ansible and ~/.terraform.d. Ansible then refused to start for the learner
    and dsoxlab reported rc=5, Stats: {} — a symptom nothing connected to the
    cause. Fixed with sudo -H, plus a chown belt.

    ovmf and qemu-utils were missing: both are mere recommendations of
    qemu-kvm on Debian, so --no-install-recommends dropped them. Without the
    first, libvirt exposes no EFI firmware and every provision stops 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 default pool and no group membership: a fresh Debian defines
    neither, so doctor --strict exited 9 and student could 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 ovmf and qemu-utils, for
    the third time: xserver-xorg is a recommendation of xfce4 and lightdm,
    never a dependency, so --no-install-recommends dropped it. /usr/bin/Xorg
    did not exist, lightdm was failed, and the machine came up on a console
    despite graphical.target — with nothing tying that to a missing package. The
    server and its video drivers (vmware for the VMSVGA controller VirtualBox and
    VMware present, vesa and fbdev as fallbacks) are now named explicitly.

    ~/.ssh/config did not include what dsoxlab writes. provision drops a
    ~/.ssh/config.d/<catalog>.conf and warns, at every run, that nothing reads it.
    The Include is now placed at the top of the file, where OpenSSH requires it.

    start called an unusable infrastructure "already provisioned". It
    decided from the state alone, so an address was enough. The sequence that
    followed was absurd: provision exited 8 on "some hosts did not answer: the
    infrastructure exists, but it is not usable as it stands", and the start
    relaunched right after — relaunched because nothing answered — announced
    "already provisioned, nothing to build again", then failed further down on a
    raw Ansible UNREACHABLE that nothing tied to the cause. It now probes port
    22 as well: an address is not a machine. Replaying provision costs nothing,
    since it resumes without recreating anything, and it waits.

    The console was flooded with AF_VSOCK errors, 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-generator is now masked, the
    documented way, with a symlink to /dev/null.

v0.2.1

Choose a tag to compare

@github-actions github-actions released this 25 Sep 21:47
5de62c2

Added

  • A Thanks section 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), the conftest.py that silently blocked every shell lab
    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

Choose a tag to compare

@github-actions github-actions released this 25 Sep 20:44
16ca262

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

Choose a tag to compare

@github-actions github-actions released this 25 Sep 20:30
4b5bc02

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 first run of a vm lab on absent infrastructure fails,
    and the learner has to guess that provision was missing. provision does 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 start does 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 for start. 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 shell lab 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 hiding dsoxlab use, which the learner needs to know;
    • start is 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" is status's
      question;
    • without an id, it takes the lab next suggests, computed by the same service.

    Verified in the open on both runtimes, which is what the acceptance criteria
    demand: a shell lab from the demonstration catalog, and a vm lab of the Linux
    catalog with no infrastructure at all — three VMs brought up in 38 seconds,
    setup.yaml played, session opened, then destroyed.

    That real run caught a defect no unit test could: _infra_prete() read labenv
    at the root of the inventory instead of all.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.

Changed

  • The body of provision is now a function without the lock
    (provisionner()). run takes the write lock itself and releases it before
    opening the interactive session — otherwise the dsoxlab check the learner types
    in that sub-shell would be refused by their own session. start therefore takes
    the lock for the infrastructure step only, releases it, then delegates to run.
    Holding it throughout would have made start exit 7 on its own lock, which
    would be quite something for a command whose purpose is to remove surprises. The
    provision command itself is unchanged.

v0.1.100

Choose a tag to compare

@github-actions github-actions released this 25 Sep 19:41
88c5232

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 (IFNAMSIZ is 16, terminator included). The packaged kvm template
    computes the bridge name as virbr- + the network name with every lab-
    removed, so network: lab-kubernetes yields virbr-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 range
    

    That message names neither the bridge, nor the limit, nor the field that
    produced it — and the offending name appears nowhere in meta.yml, since
    it is derived. Worse, the failure landed after the base image had been
    downloaded, so every attempt cost a minute.

    provision now refuses before terraform init, so before downloading a
    provider or an image, exits 2 (IMPOSSIBLE), names the computed bridge with
    its length, and states the length the network name must fit in. doctor reports
    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: kvm computes the name, incus uses the network name verbatim
    (so the limit applies with no lab- margin), and outscale creates no
    interface on this machine. A declared bridge_name override 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's replace removes the
    occurrence wherever it sits, so mon-lab-reseau gives virbr-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

Choose a tag to compare

@github-actions github-actions released this 25 Sep 19:17
f6c17c4

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 with uv 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
    neither uv nor pipx — 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 to uv's installer, in English and in French.

  • CONTRIBUTING.md no 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.py was 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 splitting cli.py into 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 and support report 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

Choose a tag to compare

@github-actions github-actions released this 25 Sep 18:54
06ecaa4

Fixed

  • The support report is now written in English, whatever DSOXLAB_LANG says
    (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=en session therefore got a French report — and since 0.1.86,
    --issue drops it into bug_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 is support --json, a contract for programs. Renaming
    systeme to system to 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 → unknown for 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.