Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CHANGELOG.fr.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,21 @@ et le projet suit le [versionnage sémantique](https://semver.org/lang/fr/).

## [Non publié]

## [0.1.84] - 2026-08-24

### Ajouté

- **`doctor` contrôle désormais le pool de stockage Incus**
(linux-dsoxlab-training#54). Un utilisateur a signalé que sur un
provisionnement Incus raté, « seules les résolutions liées à KVM sont
proposées ». Le symptôme avait raison et l'hypothèse la plus évidente avait
tort : `_check_incus` **propose bien** `incus admin init` — mais seulement
quand `incus list` échoue en le disant. Or `incus list` **réussit** sur une
installation jamais initialisée : elle rend simplement une liste vide. Le
contrôle passait au vert, et la branche Incus de `doctor` n'ajoutait que
l'outil ISO, là où la branche KVM contrôle son pool depuis longtemps. Le
template crée le réseau mais écrit `pool = "default"` en dur sans le créer.

## [0.1.83] - 2026-08-24

### Modifié
Expand Down
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.1.84] - 2026-08-24

### Added

- **`doctor` now checks the Incus storage pool**
(linux-dsoxlab-training#54). A user reported that on a failed Incus
provisioning, "only the KVM-related fixes are offered". The symptom was right
and the obvious hypothesis wrong: `_check_incus` *does* offer `incus admin
init` — but only when `incus list` fails saying so. `incus list` **succeeds**
on a never-initialised install: it simply returns an empty list. The check
passed green, and the Incus branch of `doctor` only added the ISO tool, where
the KVM branch has checked its storage pool for a long time. The template
creates the network but hard-codes `pool = "default"` without creating it.

## [0.1.83] - 2026-08-24

### Changed
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "dsoxlab"
version = "0.1.83"
version = "0.1.84"
description = "Turn declarative exercises into reproducible, runnable and verifiable lab environments"
readme = "README.md"
requires-python = ">=3.11"
Expand Down
6 changes: 6 additions & 0 deletions src/dsoxlab/i18n/strings/en.py
Original file line number Diff line number Diff line change
Expand Up @@ -919,6 +919,12 @@
"check_resources": "RAM / disk resources",

"detail_shell_always": "always available",
"check_incus_pool": "Incus pool",
"detail_incus_pool_absent":
"pool '{pool}' does not exist: incus was never initialised, and "
"'provision' will fail",
"detail_incus_pool_muet":
"cannot list incus pools: state unknown",
"detail_incus_missing": "not found",
"detail_incus_ok": "client {version}, daemon ok",
"detail_incus_daemon_down": "client {version}, daemon inactive",
Expand Down
6 changes: 6 additions & 0 deletions src/dsoxlab/i18n/strings/fr.py
Original file line number Diff line number Diff line change
Expand Up @@ -929,6 +929,12 @@
"check_resources": "Ressources RAM / disque",

"detail_shell_always": "toujours disponible",
"check_incus_pool": "Pool Incus",
"detail_incus_pool_absent":
"le pool « {pool} » n'existe pas : incus n'a jamais été initialisé, "
"et « provision » échouera",
"detail_incus_pool_muet":
"impossible de lister les pools incus : état inconnu",
"detail_incus_missing": "introuvable",
"detail_incus_ok": "client {version}, daemon ok",
"detail_incus_daemon_down": "client {version}, daemon inactif",
Expand Down
40 changes: 40 additions & 0 deletions src/dsoxlab/services/doctor.py
Original file line number Diff line number Diff line change
Expand Up @@ -882,6 +882,43 @@ def explique_echec_provision(message: str) -> tuple[str, str] | None:
return None


#: Le pool que le template Incus écrit en dur dans ses volumes. Il n'est
#: **pas** créé par Terraform, contrairement au réseau : `incus admin init` est
#: le seul geste qui le pose.
_POOL_INCUS = "default"


def _check_incus_pool() -> Check:
"""Le pool de stockage d'Incus existe-t-il ?

`incus list` réussit sur une installation jamais initialisée — elle rend
simplement une liste vide — si bien que `_check_incus` passait au vert et
que rien n'annonçait l'échec de `provision`. Le template crée le réseau
mais **suppose** le pool : il l'écrit en dur dans chaque volume.

C'est le pendant exact de `_check_libvirt_pool`, qui existait déjà côté
KVM. Son absence côté Incus est ce qui faisait dire à un utilisateur que
« seules les résolutions liées à KVM sont proposées ».
"""
sonde = _sonder(["incus", "storage", "list", "--format", "csv"])
if sonde is None or sonde.returncode != 0:
# Ne rien pouvoir mesurer n'est ni un pool présent ni un pool absent.
return _check("incus_pool", False, _("detail_incus_pool_muet"),
forced_state=STATE_UNKNOWN)

pools = [
ligne.split(",")[0]
for ligne in sonde.stdout.splitlines() if ligne.strip()
]
if _POOL_INCUS in pools:
return _check("incus_pool", True, _POOL_INCUS)
return _check(
"incus_pool", False,
_("detail_incus_pool_absent", pool=_POOL_INCUS),
fix=_fix(["sudo", "incus", "admin", "init", "--auto"]),
)


def _check_iso_tool() -> Check:
"""Incus fabrique le CD-ROM ``agent:config`` sur l'hôte.

Expand Down Expand Up @@ -1180,6 +1217,9 @@ def collect_checks(root: Path, repo_meta: RepoMetadata | None) -> DoctorReport:
or "default")
report.required.append(_check_libvirt_pool(pool))
elif active == "incus" and hypervisors["incus"].ok:
# Symétrie avec la branche kvm juste au-dessus : elle contrôle son
# pool de stockage, celle-ci ne contrôlait que l'outil ISO.
report.required.append(_check_incus_pool())
report.required.append(_check_iso_tool())

report.required.append(_check_labs(root, labs, compter_fichiers_labs(root)))
Expand Down
171 changes: 171 additions & 0 deletions tests/test_incus_pool.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
"""Le pool Incus est diagnostiqué comme celui de libvirt (linux-dsoxlab-training#54).

Un utilisateur a signalé que sur un provisionnement Incus raté faute d'avoir
joué `incus admin init --auto`, « seules les résolutions liées à KVM sont
proposées ». La vérification a donné raison au symptôme et tort à l'hypothèse
la plus évidente : `_check_incus` **propose bien** `incus admin init` — mais
seulement quand `incus list` échoue en le disant.

Or `incus list` **réussit** sur une installation jamais initialisée : elle rend
simplement une liste vide. Le contrôle passait donc au vert, et le seul contrôle
supplémentaire de la branche Incus était l'outil ISO — là où la branche KVM
contrôle son pool de stockage depuis longtemps.

Le template le suppose sans le créer : il crée bien le réseau
(`resource "incus_network" "lab"`) mais écrit `pool = "default"` en dur dans
chaque volume. Sans ce pool, `provision` échoue.
"""

from __future__ import annotations

from pathlib import Path
from typing import Any

import pytest

from dsoxlab.services import doctor


class _Sortie:
"""Ce que `_sonder` rend : un CompletedProcess, ou None s'il n'a pas répondu."""

def __init__(self, stdout: str = "", returncode: int = 0) -> None:
self.stdout = stdout
self.stderr = ""
self.returncode = returncode


def _sonde(monkeypatch: pytest.MonkeyPatch, resultat: Any) -> None:
monkeypatch.setattr(doctor, "_sonder", lambda *a, **k: resultat)


# ── Les trois états, comme pour le pool libvirt ─────────────────────────────

def test_un_pool_present_passe_au_vert(monkeypatch: pytest.MonkeyPatch) -> None:
_sonde(monkeypatch, _Sortie("default,zfs,,14,CREATED\n"))

controle = doctor._check_incus_pool()

assert controle.ok is True
assert "default" in controle.detail


def test_un_pool_absent_propose_l_initialisation(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Le cas de l'issue : incus répond, mais n'a aucun pool."""
_sonde(monkeypatch, _Sortie(""))

controle = doctor._check_incus_pool()

assert controle.ok is False
assert controle.fix is not None
assert ("sudo", "incus", "admin", "init", "--auto") in controle.fix.commands


def test_une_sonde_muette_ne_tranche_pas(monkeypatch: pytest.MonkeyPatch) -> None:
"""Ne pas pouvoir lister n'est ni un pool présent ni un pool absent.

Proposer `incus admin init` sur une machine dont on ignore l'état
réinitialiserait peut-être une installation qui fonctionne.
"""
_sonde(monkeypatch, None)

controle = doctor._check_incus_pool()

assert controle.state == doctor.STATE_UNKNOWN
assert controle.fix is None


def test_un_autre_pool_ne_suffit_pas(monkeypatch: pytest.MonkeyPatch) -> None:
"""Le template écrit `default` en dur : c'est celui-là qu'il faut.

Un pool nommé autrement existe sans que le provisionnement fonctionne.
"""
_sonde(monkeypatch, _Sortie("autre-pool,dir,,3,CREATED\n"))

assert doctor._check_incus_pool().ok is False


# ── Le classement : Incus rattrape la symétrie qui lui manquait ─────────────

def test_le_pool_incus_est_requis_quand_incus_est_actif(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch,
) -> None:
"""C'est l'absence de ce contrôle qui a produit l'issue."""
monkeypatch.setattr(doctor, "_check_incus",
lambda: doctor._check("incus", True, "7.2"))
monkeypatch.setattr(doctor, "_check_kvm",
lambda: doctor._check("kvm", False, "absent"))
_sonde(monkeypatch, _Sortie("default,zfs,,14,CREATED\n"))

(tmp_path / "meta.yml").write_text(
"repo:\n id: essai\n category: essai\n"
"infra:\n provider: incus\n hosts:\n - name: h.lab\n",
encoding="utf-8")
base = tmp_path / "labs" / "l1"
base.mkdir(parents=True)
(base / "lab.yaml").write_text(
"id: l1\ntitle: T\nlevel: l1\nskills: [s]\ndistros: [any]\n"
"doc_url: https://example.org/\n"
"runtime:\n type: vm\n targets:\n - name: c\n host: h.lab\n",
encoding="utf-8")

from dsoxlab.discovery.scanner import read_repo_metadata

rapport = doctor.collect_checks(tmp_path, read_repo_metadata(tmp_path))

assert "incus_pool" in [c.key for c in rapport.required]


def test_le_pool_incus_ne_gene_pas_un_depot_kvm(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Un dépôt qui provisionne en KVM n'a pas à voir de contrôle Incus.

C'est la règle du dépôt appliquée aux providers : jamais de rouge pour un
composant que ce catalogue n'utilise pas.
"""
monkeypatch.setattr(doctor, "_check_kvm",
lambda: doctor._check("kvm", True, "ok"))
monkeypatch.setattr(doctor, "_check_libvirt_pool",
lambda pool: doctor._check("libvirt_pool", True, pool))

(tmp_path / "meta.yml").write_text(
"repo:\n id: essai\n category: essai\n"
"infra:\n provider: kvm\n hosts:\n - name: h.lab\n",
encoding="utf-8")
base = tmp_path / "labs" / "l1"
base.mkdir(parents=True)
(base / "lab.yaml").write_text(
"id: l1\ntitle: T\nlevel: l1\nskills: [s]\ndistros: [any]\n"
"doc_url: https://example.org/\n"
"runtime:\n type: vm\n targets:\n - name: c\n host: h.lab\n",
encoding="utf-8")

from dsoxlab.discovery.scanner import read_repo_metadata

rapport = doctor.collect_checks(tmp_path, read_repo_metadata(tmp_path))

assert "incus_pool" not in [c.key for c in rapport.required]


# ── Ce que le template suppose, et qui justifie le contrôle ────────────────

def test_le_template_suppose_le_pool_sans_le_creer() -> None:
"""Si un jour Terraform crée le pool, ce contrôle n'a plus lieu d'être.

Le test le dira : il échouera quand la ressource apparaîtra, et personne
n'aura à se souvenir que ce diagnostic existait pour cette raison.
"""
import dsoxlab

main = (Path(dsoxlab.__file__).resolve().parent / "templates" / "terraform"
/ "incus" / "main.tf").read_text(encoding="utf-8")

assert 'pool = "default"' in main or '"pool" = "default"' in main, (
"le template ne référence plus le pool en dur"
)
assert 'resource "incus_storage_pool"' not in main, (
"le template crée désormais le pool : le contrôle doctor est caduc"
)
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading