Aller au contenu
English
English
medium

Créer un cours dsoxlab et ses challenges de A à Z

Read this page in English

35 min de lecture

Un cours dsoxlab est un dépôt git avec un meta.yml à la racine et un lab.yaml par lab sous labs/ : rien d'autre ne le lie au moteur, aucune dépendance à installer, aucun plugin à écrire. Cette leçon suit la création d'un catalogue de bout en bout, du squelette posé par dsoxlab new jusqu'aux tests qui prouvent la réussite, en passant par la mission et son barème, les indices et leur coût, et le validateur qui refuse un lab avant qu'un apprenant ne le subisse. Elle décrit le contrat de la version 0.2.5 et s'adresse à qui a déjà joué un lab et veut en écrire.

  • Partir d'une capacité démontrable, jamais d'un chapitre à couvrir.
  • Poser un catalogue et un lab conformes avec dsoxlab new.
  • Déclarer le contrat dans meta.yml et lab.yaml, et savoir ce que chacun impose.
  • Rédiger le cours, le scénario, la mission notée et les indices payants.
  • Prouver la réussite par des tests pytest sur l'état du système.
  • Valider avec validate-structure, dans l'éditeur et en CI.

Un lab existe pour faire démontrer une capacité, c'est-à-dire un résultat observable sur un système : créer un utilisateur avec accès SSH par clé uniquement, étendre un volume logique et prouver que le montage survit au redémarrage, corriger un refus SELinux sans désactiver SELinux. Un lab par chapitre serait absurde, personne ne monte une machine virtuelle pour apprendre cut, et si la capacité ne se formule pas avec un verbe et un résultat vérifiable, le lab n'est pas prêt à être écrit.

Le runtime n'est pas un choix de confort : il est imposé par le sujet. Ce qui touche sshd, systemd, un pare-feu, SELinux, le démarrage ou le stockage persistant se prouve dans une machine virtuelle, parce qu'un conteneur n'a ni vrai cycle de démarrage ni politique SELinux propre. Ce qui touche des fichiers, du texte, des permissions ou un outil qui tourne sur le poste, comme Terraform, se prouve en shell, et ne coûte presque rien.

Enfin, la validation prouve, elle ne fait pas confiance. Vérifier qu'une commande a été tapée est interdit : l'apprenant peut taper la bonne commande et rater l'objectif. On vérifie l'état du système, et dès que le sujet le justifie, la persistance après redémarrage : une règle de pare-feu sans --permanent, un montage absent de /etc/fstab, un service sans enable. C'est le piège qui fait échouer les candidats RHCSA, et le test doit le prendre en défaut.

Un catalogue est un dépôt ordinaire, et retirer dsoxlab doit laisser ses labs jouables à la main, avec ansible-playbook setup.yaml puis pytest. C'est le test de non-couplage, et c'est lui qui garde le moteur neutre vis-à-vis du domaine. Voici ce que porte un lab complet :

ma-formation/
├── meta.yml # catalogue : identité, topologie, ordre des sections
├── meta.fr.yml # optionnel : titres et descriptions en français
├── conftest.py # optionnel, mais indispensable dès qu'un test lit un hôte
├── ssh/id_ed25519.pub # produite par instructor bootstrap, jamais commitée
└── labs/
└── mon-domaine/l1/premier-lab/
├── lab.yaml # obligatoire
├── README.md # obligatoire : le cours
├── scenario.md # obligatoire : la situation
├── setup.yaml # obligatoire pour runtime vm (Ansible)
├── cleanup.yaml # obligatoire pour runtime vm (Ansible)
├── fixtures/ # optionnel, pour runtime shell
└── challenge/
├── README.md # la mission affichée par dsoxlab challenge
├── hints.yaml # optionnel : les indices et leur coût
└── tests/
└── test_functional.py # obligatoire, nom exact

challenge/tests/test_functional.py est le seul nom de fichier de test que le validateur exige ; rien n'interdit d'en poser d'autres à côté, pytest collecte le répertoire. Le zéro-bash est imposé : cleanup.sh, runtime/kvm.sh, runtime/shell.sh et Makefile sont refusés dans un répertoire de lab. La préparation est déclarative, dans lab.yaml, ou Ansible, dans setup.yaml.

Deux commandes créent des fichiers conformes sans retouche, et c'est le bon point de départ, parce qu'un lab.yaml écrit de mémoire oublie toujours un champ. new catalog pose meta.yml, labs/, un .gitignore et ssh/ ; new lab pose un lab découvert dès le prochain list-labs.

Fenêtre de terminal
dsoxlab new catalog ma-formation --in ~/Projets # le catalogue
cd ~/Projets/ma-formation
dsoxlab new lab premier-lab --runtime shell # ou --runtime vm
dsoxlab list-labs # le lab est déjà découvert

new lab écrit lab.yaml, README.md, scenario.md et challenge/tests/test_functional.py, plus setup.yaml et cleanup.yaml pour un runtime vm. Il n'écrit pas le fichier challenge/README.md, la mission, ni conftest.py : ces deux fichiers sont à vous, et la suite dit ce qu'on y met.

Seul repo.id est obligatoire. repo.category est exigé par le validateur dès qu'un lab.yaml existe, parce qu'il devient la section par défaut de chaque lab, et qu'un lab sans section resterait introuvable par list-labs --section. Le bloc infra: n'est requis que par les labs vm, et la leçon suivante le détaille ; un catalogue sans lui est conforme, pas incomplet.

# yaml-language-server: $schema=https://raw.githubusercontent.com/stephrobert/dsoxlab/main/schemas/meta.schema.json
schema_version: 1
repo:
id: ma-formation
category: mon-domaine
title: "Ma formation"
blog_url: "https://blog.stephane-robert.info/docs/"
issues_url: "https://github.com/mon-compte/ma-formation/issues"
sections:
- id: getting-started
title: "Discover the tool"
labs:
- mon-domaine/l1/premier-lab

Deux règles du contrat surprennent tout auteur la première fois. La découverte se fait par chemin, jamais par id : un lab existe si et seulement si labs/**/lab.yaml existe, et sections[].labs[] ne fait qu'ordonner les labs et nommer les blocs, en comparant le chemin relatif depuis labs/. Et issues_url mérite d'être déclaré : c'est là que dsoxlab support --issue dépose le rapport d'un apprenant, au lieu de deviner le remote origin.

Les fichiers de base portent l'anglais, langue par défaut de l'outil. Un meta.fr.yml posé à côté surcharge repo.title, repo.description et les titres et descriptions de sections, appariés par id et jamais par position, et rien d'autre : l'ordre pédagogique ne se traduit pas.

Le lab.yaml : ce que le parseur exige, ce que le validateur exige

Section intitulée « Le lab.yaml : ce que le parseur exige, ce que le validateur exige »

Trois champs sont exigés par le parseur, id, title et level : sans eux, le fichier n'est pas un lab, et il disparaît du catalogue. Trois autres sont exigés par validate-structure, skills, distros et doc_url : le fichier se lit sans eux, mais le lab n'est pas publiable. doc_url est le seul lien entre le lab et le guide en ligne, celui que dsoxlab guide ouvre : il doit être en http(s) et pointer une page réelle.

# yaml-language-server: $schema=https://raw.githubusercontent.com/stephrobert/dsoxlab/main/schemas/lab.schema.json
id: premier-lab
title: "Create a user with key-only SSH access"
level: l2
skills: [users, ssh]
distros: [alma10]
doc_url: https://blog.stephane-robert.info/docs/admin-serveurs/linux/
description: "Create the account, deploy the key, forbid password login."
lab_type: lab # lab, challenge ou capstone
estimated_time: "20m"
runtime:
type: vm # shell ou vm ; jamais kvm ni incus dans un lab neuf
targets:
- name: rhel
host: alma-rhcsa-1.lab # doit figurer dans infra.hosts[].name du meta.yml
label_fr: "AlmaLinux 10"
default: rhel
snapshot_required: false
validation:
functional: true
persistence_after_reboot: true

Un lab shell remplace le bloc targets par un workdir, challenge/work par défaut, et peut déclarer des fixtures, copiées dans ce répertoire au run, et des services, des conteneurs dont le lab a besoin le temps de l'exercice. Le bloc validation est purement déclaratif : dsoxlab ne le lit jamais pour décider, ce sont les tests qui prouvent. Un lab_type: capstone avec un exam_passing_score entre 1 et 100 devient un examen blanc, et submit rend un verdict en pourcentage du barème.

Trois fichiers Markdown, et le moteur lit chacun à un moment différent. README.md est le cours : ce que le lab enseigne, et ce qu'il suppose connu ; dsoxlab course l'affiche d'un bloc avec scenario.md, la situation, c'est-à-dire où se trouve l'apprenant et ce qui ne va pas, sans les gestes qui le corrigent. challenge/README.md est la mission, affichée par dsoxlab challenge, et par run avant l'ouverture de la session SSH d'un lab vm, l'hôte n'ayant pas dsoxlab.

La mission porte le barème, et le validateur le tient en accord avec les tests. dsoxlab note par test : cinq fonctions test_ dans test_functional.py, c'est vingt points chacune, les indices déduits ensuite. Un titre ### portant (N pts) ou (N points) est une tâche notée, une ligne annonçant N tâches et M points est l'annonce, et les titres ## ne sont pas comptés, ce qui permet à un examen blanc de grouper ses tâches sans doubler son total.

5 tâches, 100 points, 20 minutes
### Tâche 1 : créer le groupe (20 pts)
### Tâche 2 : créer l'utilisateur dans ce groupe (20 pts)

validate-structure échoue quand les tâches ne totalisent pas le barème annoncé, quand leur nombre diffère de celui annoncé, ou du nombre de tests. Ajouter un test à un lab décale silencieusement tout son barème, et ce contrôle est la seule chose qui s'en aperçoit. Une mission qui n'annonce aucun point par tâche n'est pas contrôlée du tout : un examen blanc qui vérifie plusieurs choses par tâche a fait un autre choix, tout aussi valable. challenge/README.fr.md est lu en premier lorsqu'il existe.

challenge/hints.yaml porte le barème du lab et les indices, dans l'ordre où ils seront donnés. Un bon indice oriente le regard, « que dit journalctl -u <service> ? », et ne donne jamais la commande finale. Le coût de chacun est déduit de la note, et il vaut 10 points quand il n'est pas écrit.

points: 100 # le barème du lab ; 100 quand le fichier est absent
hints:
- text_en: "The group must exist before the user does."
text_fr: "Le groupe doit exister avant l'utilisateur."
cost: 10 # points retirés quand l'apprenant le prend (défaut 10)

Une clé unique text, héritée, est encore lue, et une valeur en base64 est décodée pour qu'un indice ne ressorte pas dans un git grep ; ni l'une ni l'autre n'est nécessaire dans un lab neuf. Les catalogues du site encodent leurs indices, et ce n'est pas du chiffrement : seulement de quoi éviter qu'un indice se lise par accident en ouvrant le fichier.

Un lab est noté par pytest avec pytest-testinfra, tous deux embarqués dans dsoxlab : un catalogue n'installe aucun outillage de test. pytest s'exécute depuis la racine du catalogue, donc un conftest.py posé là est collecté pour tous les labs. C'est là que va l'import qui construit les hôtes testinfra depuis l'inventaire que dsoxlab génère, et là qu'un catalogue de labs shell résout <lab>/challenge/work depuis l'emplacement du fichier de test.

# conftest.py, à la racine du catalogue : le seul import qu'un catalogue fait de dsoxlab
from dsoxlab.infra.inventory import build_inventory, read_terraform_outputs, write_ssh_config

Cet import existe pour qu'aucun lab ne code une adresse IP en dur. L'hôte à inspecter est nommé par DSOXLAB_TARGET_HOST, que dsoxlab check --target pose : sans le lire, un lab multi-distributions ne teste jamais que son hôte par défaut. Trois groupes Ansible sont injectés à l'exécution : labenv, tous les hôtes du meta.yml, lab_target, la cible résolue que les playbooks d'un lab doivent viser, et un lab_<role> par entrée de roles.

Les assertions portent sur l'état : le service tourne, le port écoute, le montage est présent, le fichier a les bonnes permissions. Avec testinfra, une telle assertion tient en une ligne, et le corrigé n'y apparaît jamais. dsoxlab new catalog n'écrit aucun conftest.py ; les catalogues Linux et Terraform du site en portent chacun un à leur racine, et l'un ou l'autre est un bon point de départ.

# challenge/tests/test_functional.py : l'état du système, jamais l'historique
def test_service_actif_et_persistant(host):
service = host.service("chronyd")
assert service.is_running
assert service.is_enabled

dsoxlab validate-structure vérifie tous les labs du catalogue, hors ligne par défaut, et sort en 1 dès qu'un lab échoue. Il joue trois familles de contrôles, et nomme chaque anomalie par une clé stable : c'est là-dessus qu'un script filtre, compte et compare. Il rapporte aussi les labs que le moteur ne voit pas, ce qui est le cas le plus déroutant : un lab.yaml présent sur le disque mais illisible, YAML cassé ou champ requis manquant, avec la ligne où regarder, et un lab déclaré dans sections[].labs[] sans lab.yaml à cet emplacement.

FamilleCe qui est vérifié
Structurelab.yaml, README.md, scenario.md, challenge/tests/test_functional.py ; pour vm, setup.yaml, cleanup.yaml, des targets non vides et un default cohérent ; pour shell, un workdir non vide ; aucun fichier interdit
Métadonnéesid, title, level et doc_url non vides, skills et distros non vides, doc_url en http(s), lab_type dans l'énuméré, exam_passing_score entre 1 et 100
ContenuTout lien relatif d'un Markdown pointe sur un fichier existant ; le barème annoncé correspond aux tests ; un document traduit d'un seul côté est signalé ; les hôtes des targets et des roles existent dans infra.hosts ; aucun fichier de solution/ n'est lisible en clair ; fixtures/ et runtime.fixtures disent la même chose

--check-urls ajoute le seul contrôle réseau, chaque doc_url doit répondre, et --json rend chaque anomalie avec sa clé, son lab et son chemin. Le contrôle des fixtures vaut d'être compris : le runtime shell itère sur runtime.fixtures, pas sur le répertoire fixtures/. Une fixture déclarée mais absente fait échouer run en code 2 en nommant toutes les fautives d'un coup ; une fixture présente mais non déclarée est signalée ici. Ce défaut avait rendu sept labs injouables le 2026-07-28, tous marqués faits, parce que les outils de vérification des corrigés copiaient, eux, le répertoire entier.

Sans course.yaml, dsoxlab course affiche scenario.md et README.md d'un bloc, ce qui explique la longueur des cours longs. Un course.yaml à côté du lab.yaml permet d'afficher une section à la fois, avec --next, --prev et --section, et de retenir où l'apprenant s'est arrêté :

sections:
- id: navigation
title: Se déplacer dans l'arborescence
file: course/01-navigation.md

Les traductions suivent une seule convention, par fichier : lab.fr.yaml surcharge title et description du lab, meta.fr.yml les titres du catalogue et de ses sections, course.fr.yaml les titres de sections du cours. Un suffixe de langue par champ, comme title_en:, ne fait pas partie du contrat, n'est lu par personne, et validate-structure le signale.

Deux schémas JSON décrivent le même contrat, schemas/meta.schema.json et schemas/lab.schema.json, et un test du projet les confronte au parseur dans les deux sens pour qu'ils ne dérivent pas. La ligne de commentaire yaml-language-server posée en tête des exemples ci-dessus suffit à tout éditeur qui fait tourner ce serveur pour compléter les champs et souligner skils: pendant qu'on l'écrit.

En CI, la validation se fait sans installer dsoxlab, parce que les URL des schémas sont de simples fichiers que n'importe quel validateur JSON Schema sait récupérer :

Fenêtre de terminal
uvx check-jsonschema \
--schemafile https://raw.githubusercontent.com/stephrobert/dsoxlab/main/schemas/lab.schema.json \
$(find labs -name lab.yaml)

Remplacez main par un tag de version dans l'URL pour figer le schéma : main suit le contrat au fil de ses évolutions, un tag ne bouge jamais sous vos pieds. Les clés inconnues sont ignorées par le parseur, refusées par les schémas et signalées par validate-structure, et cette combinaison est voulue : le moteur reste tolérant pour qu'un outil ancien survive à un catalogue plus récent, l'éditeur et le validateur, eux, ne laissent rien passer.

Un lab jamais joué n'est pas un lab. Avant toute publication, la séquence est la même que celle de l'apprenant, plus un retour à zéro pour prouver la reproductibilité : validate-structure, puis run, résoudre, check, puis reset et rejouer. Le corrigé, s'il en existe un dans solution/, doit rester illisible en clair, et le validateur le vérifie.

Fenêtre de terminal
dsoxlab validate-structure
dsoxlab run premier-lab # résoudre la mission dans la session
dsoxlab check premier-lab # le barème annoncé doit être atteint
dsoxlab reset premier-lab # puis rejouer depuis zéro

Reste à brancher le lab à son guide, dans les deux sens : le doc_url du lab pointe la page qui enseigne la capacité, et cette page annonce le lab. Sur ce site, chaque guide jumelé porte un encadré « Lab pratique » avec l'identifiant exact du lab.yaml, et la leçon passe en type lab dans le parcours de sa formation. Un lab que personne ne voit n'est joué par personne.

Ces quatre cas sont ceux que tout auteur rencontre au premier catalogue. Aucun n'est une panne du moteur : chacun vient d'un écart entre ce qu'on croyait déclarer et ce que le contrat lit.

SymptômeCauseSolution
Le lab n'apparaît pas dans list-labsSon lab.yaml ne se charge pas : YAML cassé ou id, title, level manquantdsoxlab validate-structure le nomme avec la ligne ; le détail complet est dans ~/.local/state/dsoxlab/dsoxlab.log
content_scoring_tasks_vs_testsLe nombre de tâches notées de la mission diffère du nombre de fonctions test_Aligner la mission sur les tests, ou l'inverse, puis relancer le validateur
run sort en 2 et nomme une fixtureUne entrée de runtime.fixtures n'a pas de fichier sous fixtures/Poser le fichier, ou retirer la déclaration ; c'est tout ou rien avant la moindre copie
dsoxlab challenge affiche « Aucun fichier challenge/README.md »new lab ne crée pas la missionL'écrire, avec son en-tête de barème et ses titres ### notés
  • Un catalogue est un dépôt avec meta.yml et un lab.yaml par lab ; retirer dsoxlab doit laisser les labs jouables à la main.
  • On part d'une capacité observable, le runtime est imposé par le sujet, et les tests prouvent l'état du système.
  • La découverte se fait par chemin, sections[].labs[] ne fait qu'ordonner ; runtime.type vaut vm, et runtime.host n'existe pas.
  • La mission annonce un barème par tâche, un point par test, et validate-structure refuse tout écart entre les deux.
  • Un indice coûte 10 points par défaut, et oriente le regard sans donner la commande.
  • conftest.py vit à la racine du catalogue, et DSOXLAB_TARGET_HOST dit aux tests quel hôte inspecter.
  • Un lab jamais joué n'est pas un lab : validate-structure, run, check, reset, puis rejouer.
  • Valider ses compétences Linux : Comment le catalogue Linux, 86 labs, est rattaché aux leçons du site, avec ses drills et ses examens blancs.
  • Formation Kubernetes : Un catalogue de 64 labs vm sur cluster kubeadm, jumelé leçon par leçon aux blueprints CKA, CKAD et CKS.
  • Formation Terraform : Le catalogue de 88 labs shell, sans bloc infra:, qui sert de contre-exemple au moteur.

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens ce site gratuitement, sans publicité, sans profilage et sans compte à créer. Un soutien, même symbolique, m'aide à couvrir l'hébergement et à garder ces ressources gratuites. Merci pour votre appui.

Le formulaire ne s'affiche pas ? Ouvrir Ko-fi dans un onglet.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn