dsoxlab 0.2.0__tar.gz → 0.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (323) hide show
  1. dsoxlab-0.2.2/.github/workflows/appliance.yml +261 -0
  2. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/CHANGELOG.fr.md +164 -0
  3. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/CHANGELOG.md +149 -0
  4. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/PKG-INFO +64 -1
  5. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/README.fr.md +65 -0
  6. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/README.md +63 -0
  7. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/RELEASING.fr.md +24 -0
  8. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/docs/README.fr.md +4 -0
  9. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/docs/README.md +4 -0
  10. dsoxlab-0.2.2/docs/appliance.fr.md +169 -0
  11. dsoxlab-0.2.2/docs/appliance.md +155 -0
  12. dsoxlab-0.2.2/docs/assets/appliance-bureau.png +0 -0
  13. dsoxlab-0.2.2/docs/assets/appliance-lab-vm.png +0 -0
  14. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/docs/machine-output.md +1 -1
  15. dsoxlab-0.2.2/packer/README.md +105 -0
  16. dsoxlab-0.2.2/packer/dsoxlab-appliance.pkr.hcl +188 -0
  17. dsoxlab-0.2.2/packer/faire-ova.sh +148 -0
  18. dsoxlab-0.2.2/packer/http/preseed.cfg +80 -0
  19. dsoxlab-0.2.2/packer/scripts/10-base.sh +87 -0
  20. dsoxlab-0.2.2/packer/scripts/20-outils.sh +33 -0
  21. dsoxlab-0.2.2/packer/scripts/30-premier-demarrage.sh +278 -0
  22. dsoxlab-0.2.2/packer/scripts/40-reseau-portable.sh +81 -0
  23. dsoxlab-0.2.2/packer/scripts/90-nettoyage.sh +81 -0
  24. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/pyproject.toml +1 -1
  25. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/cli/demarrage.py +43 -7
  26. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/i18n/strings/en.py +5 -0
  27. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/i18n/strings/fr.py +6 -0
  28. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/services/doctor.py +8 -0
  29. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/tests/test_doctor_prerequis_vm.py +19 -0
  30. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/tests/test_start_sequence.py +30 -0
  31. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/uv.lock +1 -1
  32. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/.bestpractices.json +0 -0
  33. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/.github/CODEOWNERS +0 -0
  34. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/.github/FUNDING.yml +0 -0
  35. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  36. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  37. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  38. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  39. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/.github/dependabot.yml +0 -0
  40. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/.github/workflows/ci.yml +0 -0
  41. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/.github/workflows/plumber.yml +0 -0
  42. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/.github/workflows/release.yml +0 -0
  43. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/.github/workflows/scorecard.yml +0 -0
  44. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/.gitignore +0 -0
  45. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/.plumber.yaml +0 -0
  46. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/.poutine.yml +0 -0
  47. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/.pre-commit-config.yaml +0 -0
  48. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/CODE_OF_CONDUCT.fr.md +0 -0
  49. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/CODE_OF_CONDUCT.md +0 -0
  50. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/CONTRIBUTING.fr.md +0 -0
  51. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/CONTRIBUTING.md +0 -0
  52. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/LICENSE +0 -0
  53. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/NOTICE +0 -0
  54. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/RELEASING.md +0 -0
  55. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/SECURITY.fr.md +0 -0
  56. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/SECURITY.md +0 -0
  57. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/docs/assets/brand/dsoxlab-icon-dark.svg +0 -0
  58. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/docs/assets/brand/dsoxlab-icon-mono.svg +0 -0
  59. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/docs/assets/brand/dsoxlab-icon.svg +0 -0
  60. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/docs/assets/brand/dsoxlab-lockup-dark.svg +0 -0
  61. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/docs/assets/brand/dsoxlab-lockup-light.svg +0 -0
  62. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/docs/assets/brand/dsoxlab-lockup-mono.svg +0 -0
  63. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/docs/assets/brand/png/dsoxlab-icon-1024.png +0 -0
  64. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/docs/assets/brand/png/dsoxlab-icon-256.png +0 -0
  65. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/docs/assets/brand/png/dsoxlab-icon-512.png +0 -0
  66. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/docs/assets/brand/png/dsoxlab-icon-dark-1024.png +0 -0
  67. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/docs/assets/brand/png/dsoxlab-icon-dark-512.png +0 -0
  68. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/docs/assets/brand/png/dsoxlab-lockup-dark-1024.png +0 -0
  69. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/docs/assets/brand/png/dsoxlab-lockup-dark-2048.png +0 -0
  70. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/docs/assets/brand/png/dsoxlab-lockup-light-1024.png +0 -0
  71. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/docs/assets/brand/png/dsoxlab-lockup-light-2048.png +0 -0
  72. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/docs/assets/brand/png/dsoxlab-social-preview-dark.png +0 -0
  73. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/docs/assets/brand/png/dsoxlab-social-preview.png +0 -0
  74. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/docs/brand.fr.md +0 -0
  75. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/docs/brand.md +0 -0
  76. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/docs/catalog-author.fr.md +0 -0
  77. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/docs/catalog-author.md +0 -0
  78. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/docs/commands.fr.md +0 -0
  79. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/docs/commands.md +0 -0
  80. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/docs/contract-v1.fr.md +0 -0
  81. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/docs/contract-v1.md +0 -0
  82. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/docs/demo.gif +0 -0
  83. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/docs/demo.tape +0 -0
  84. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/docs/exit-codes.fr.md +0 -0
  85. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/docs/exit-codes.md +0 -0
  86. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/docs/files.fr.md +0 -0
  87. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/docs/files.md +0 -0
  88. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/docs/infra-only.fr.md +0 -0
  89. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/docs/infra-only.md +0 -0
  90. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/docs/learner.fr.md +0 -0
  91. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/docs/learner.md +0 -0
  92. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/docs/machine-output.fr.md +0 -0
  93. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/docs/trainer.fr.md +0 -0
  94. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/docs/trainer.md +0 -0
  95. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/fuzz/corpus/context_json/champ-inconnu.json +0 -0
  96. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/fuzz/corpus/context_json/complet.json +0 -0
  97. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/fuzz/corpus/context_json/position-negative.json +0 -0
  98. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/fuzz/corpus/context_json/position-non-numerique.json +0 -0
  99. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/fuzz/corpus/context_json/racine-chaine.json +0 -0
  100. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/fuzz/corpus/context_json/racine-liste.json +0 -0
  101. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/fuzz/corpus/context_json/racine-nulle.json +0 -0
  102. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/fuzz/corpus/context_json/section-liste.json +0 -0
  103. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/fuzz/corpus/context_json/tronque.json +0 -0
  104. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/fuzz/corpus/context_json/vide.json +0 -0
  105. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/fuzz/corpus/lab_yaml/capstone-exam-score.yaml +0 -0
  106. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/fuzz/corpus/lab_yaml/schema-version-courante.yaml +0 -0
  107. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/fuzz/corpus/lab_yaml/schema-version-future.yaml +0 -0
  108. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/fuzz/corpus/lab_yaml/schema-version-mauvais-type.yaml +0 -0
  109. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/fuzz/corpus/lab_yaml/shell-fixtures-nested.yaml +0 -0
  110. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/fuzz/corpus/lab_yaml/shell-minimal.yaml +0 -0
  111. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/fuzz/corpus/lab_yaml/shell-services-init.yaml +0 -0
  112. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/fuzz/corpus/lab_yaml/shell-services.yaml +0 -0
  113. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/fuzz/corpus/lab_yaml/vm-multi-target.yaml +0 -0
  114. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/fuzz/corpus/lab_yaml/vm-snapshot-requis.yaml +0 -0
  115. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/fuzz/corpus/meta_yml/full-infra.yml +0 -0
  116. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/fuzz/corpus/meta_yml/schema-version-courante.yml +0 -0
  117. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/fuzz/corpus/meta_yml/schema-version-future.yml +0 -0
  118. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/fuzz/corpus/terraform_outputs/avec-bastion.json +0 -0
  119. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/fuzz/corpus/terraform_outputs/bastion-sans-ip.json +0 -0
  120. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/fuzz/corpus/terraform_outputs/hosts-liste.json +0 -0
  121. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/fuzz/corpus/terraform_outputs/ip-numerique.json +0 -0
  122. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/fuzz/corpus/terraform_outputs/outputs-aplatis.json +0 -0
  123. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/fuzz/corpus/terraform_outputs/outputs-encapsules.json +0 -0
  124. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/fuzz/corpus/terraform_outputs/sans-hosts.json +0 -0
  125. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/fuzz/corpus/terraform_outputs/value-non-objet.json +0 -0
  126. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/fuzz/corpus/terraform_outputs/vide.json +0 -0
  127. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/fuzz/dict/yaml_contract.dict +0 -0
  128. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/fuzz/fuzz_context_json.py +0 -0
  129. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/fuzz/fuzz_lab_yaml.py +0 -0
  130. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/fuzz/fuzz_meta_yaml.py +0 -0
  131. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/fuzz/fuzz_terraform_outputs.py +0 -0
  132. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/schemas/lab.schema.json +0 -0
  133. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/schemas/meta.schema.json +0 -0
  134. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/scripts/check-release.py +0 -0
  135. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/scripts/generer-doc.py +0 -0
  136. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/scripts/generer-png-marque.py +0 -0
  137. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/scripts/parcours-distribution.py +0 -0
  138. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/__init__.py +0 -0
  139. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/cli/__init__.py +0 -0
  140. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/cli/_amorcage.py +0 -0
  141. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/cli/_barres.py +0 -0
  142. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/cli/_commun.py +0 -0
  143. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/cli/_socle.py +0 -0
  144. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/cli/_validation.py +0 -0
  145. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/cli/auteur.py +0 -0
  146. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/cli/catalogues.py +0 -0
  147. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/cli/contexte.py +0 -0
  148. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/cli/destruction.py +0 -0
  149. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/cli/diagnostic.py +0 -0
  150. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/cli/etat.py +0 -0
  151. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/cli/indices.py +0 -0
  152. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/cli/infrastructure.py +0 -0
  153. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/cli/instructeur.py +0 -0
  154. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/cli/parcours.py +0 -0
  155. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/cli/progression.py +0 -0
  156. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/config.py +0 -0
  157. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/discovery/__init__.py +0 -0
  158. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/discovery/repo.py +0 -0
  159. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/discovery/scanner.py +0 -0
  160. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/exit_codes.py +0 -0
  161. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/i18n/__init__.py +0 -0
  162. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/i18n/strings/__init__.py +0 -0
  163. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/infra/__init__.py +0 -0
  164. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/infra/ansible.py +0 -0
  165. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/infra/credentials.py +0 -0
  166. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/infra/inventory.py +0 -0
  167. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/infra/libvirt.py +0 -0
  168. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/infra/pont.py +0 -0
  169. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/infra/snapshot/__init__.py +0 -0
  170. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/infra/snapshot/kvm.py +0 -0
  171. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/infra/terraform.py +0 -0
  172. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/interrupt.py +0 -0
  173. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/locking.py +0 -0
  174. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/logging_setup.py +0 -0
  175. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/models/__init__.py +0 -0
  176. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/models/_contract.py +0 -0
  177. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/models/course.py +0 -0
  178. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/models/hint.py +0 -0
  179. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/models/lab.py +0 -0
  180. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/models/repo.py +0 -0
  181. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/models/runtime.py +0 -0
  182. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/models/schema_version.py +0 -0
  183. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/py.typed +0 -0
  184. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/reporting/__init__.py +0 -0
  185. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/reporting/console.py +0 -0
  186. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/reporting/machine.py +0 -0
  187. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/runtimes/__init__.py +0 -0
  188. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/runtimes/base.py +0 -0
  189. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/runtimes/manager.py +0 -0
  190. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/runtimes/services.py +0 -0
  191. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/runtimes/shell.py +0 -0
  192. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/runtimes/vm.py +0 -0
  193. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/services/__init__.py +0 -0
  194. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/services/catalog.py +0 -0
  195. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/services/demo.py +0 -0
  196. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/services/guide_service.py +0 -0
  197. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/services/host_diagnosis.py +0 -0
  198. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/services/issue_service.py +0 -0
  199. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/services/lab_service.py +0 -0
  200. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/services/lab_state.py +0 -0
  201. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/services/progress_service.py +0 -0
  202. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/services/scaffold.py +0 -0
  203. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/services/support.py +0 -0
  204. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/services/update_check.py +0 -0
  205. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/sessions/__init__.py +0 -0
  206. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/sessions/store.py +0 -0
  207. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/templates/__init__.py +0 -0
  208. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/templates/catalogues.yml +0 -0
  209. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/templates/cloud-init/README.md +0 -0
  210. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/templates/cloud-init/almalinux.yaml.tmpl +0 -0
  211. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/templates/cloud-init/debian.yaml.tmpl +0 -0
  212. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/templates/cloud-init/ubuntu.yaml.tmpl +0 -0
  213. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/templates/demo/labs/demo/premiers-pas/README.fr.md +0 -0
  214. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/templates/demo/labs/demo/premiers-pas/README.md +0 -0
  215. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/templates/demo/labs/demo/premiers-pas/challenge/README.fr.md +0 -0
  216. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/templates/demo/labs/demo/premiers-pas/challenge/README.md +0 -0
  217. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/templates/demo/labs/demo/premiers-pas/challenge/hints.yaml +0 -0
  218. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/templates/demo/labs/demo/premiers-pas/challenge/tests/conftest.py +0 -0
  219. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/templates/demo/labs/demo/premiers-pas/challenge/tests/test_functional.py +0 -0
  220. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/templates/demo/labs/demo/premiers-pas/lab.fr.yaml +0 -0
  221. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/templates/demo/labs/demo/premiers-pas/lab.yaml +0 -0
  222. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/templates/demo/labs/demo/premiers-pas/scenario.fr.md +0 -0
  223. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/templates/demo/labs/demo/premiers-pas/scenario.md +0 -0
  224. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/templates/demo/meta.fr.yml +0 -0
  225. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/templates/demo/meta.yml +0 -0
  226. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/templates/terraform/README.md +0 -0
  227. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/templates/terraform/incus/main.tf +0 -0
  228. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/templates/terraform/incus/outputs.tf +0 -0
  229. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/templates/terraform/incus/variables.tf +0 -0
  230. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/templates/terraform/incus/versions.tf +0 -0
  231. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/templates/terraform/kvm/main.tf +0 -0
  232. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/templates/terraform/kvm/outputs.tf +0 -0
  233. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/templates/terraform/kvm/variables.tf +0 -0
  234. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/templates/terraform/kvm/versions.tf +0 -0
  235. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/templates/terraform/outscale/main.tf +0 -0
  236. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/templates/terraform/outscale/outputs.tf +0 -0
  237. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/templates/terraform/outscale/variables.tf +0 -0
  238. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/templates/terraform/outscale/versions.tf +0 -0
  239. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/utils/__init__.py +0 -0
  240. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/utils/fichiers.py +0 -0
  241. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/utils/shell.py +0 -0
  242. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/validators/__init__.py +0 -0
  243. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/validators/content.py +0 -0
  244. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/validators/contract.py +0 -0
  245. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/validators/metadata.py +0 -0
  246. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/src/dsoxlab/validators/structure.py +0 -0
  247. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/tests/__init__.py +0 -0
  248. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/tests/conftest.py +0 -0
  249. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/tests/test_ansible_availability.py +0 -0
  250. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/tests/test_catalog.py +0 -0
  251. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/tests/test_check_release.py +0 -0
  252. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/tests/test_ci_valide_les_templates.py +0 -0
  253. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/tests/test_cles_i18n_existantes.py +0 -0
  254. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/tests/test_cli_smoke.py +0 -0
  255. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/tests/test_cloud_init_degrade.py +0 -0
  256. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/tests/test_cloud_init_incus_agent.py +0 -0
  257. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/tests/test_cloud_init_templates.py +0 -0
  258. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/tests/test_codes_de_sortie.py +0 -0
  259. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/tests/test_commande_absente.py +0 -0
  260. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/tests/test_completion.py +0 -0
  261. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/tests/test_conteneurs_engendres.py +0 -0
  262. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/tests/test_contrat_honore.py +0 -0
  263. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/tests/test_convention_de_nommage.py +0 -0
  264. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/tests/test_demo.py +0 -0
  265. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/tests/test_description_alignee.py +0 -0
  266. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/tests/test_doc_auteur_couvre_les_controles.py +0 -0
  267. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/tests/test_doctor.py +0 -0
  268. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/tests/test_doctor_explique.py +0 -0
  269. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/tests/test_doctor_fix.py +0 -0
  270. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/tests/test_doctor_installation.py +0 -0
  271. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/tests/test_doctor_strict.py +0 -0
  272. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/tests/test_documentation_synchrone.py +0 -0
  273. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/tests/test_etat_libvirt.py +0 -0
  274. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/tests/test_executable_absent.py +0 -0
  275. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/tests/test_fixtures_declarees.py +0 -0
  276. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/tests/test_guide_service.py +0 -0
  277. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/tests/test_host_ready_timeout.py +0 -0
  278. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/tests/test_i18n_coverage.py +0 -0
  279. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/tests/test_i18n_validators.py +0 -0
  280. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/tests/test_incus_pool.py +0 -0
  281. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/tests/test_install_et_contexte.py +0 -0
  282. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/tests/test_instructor_bootstrap.py +0 -0
  283. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/tests/test_interruption.py +0 -0
  284. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/tests/test_issue_service.py +0 -0
  285. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/tests/test_journal_en_anglais.py +0 -0
  286. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/tests/test_journalisation.py +0 -0
  287. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/tests/test_json_output.py +0 -0
  288. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/tests/test_json_schemas.py +0 -0
  289. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/tests/test_kvm_disques_apparmor.py +0 -0
  290. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/tests/test_lab_state.py +0 -0
  291. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/tests/test_moteur_injoignable.py +0 -0
  292. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/tests/test_nom_de_pont.py +0 -0
  293. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/tests/test_note_sans_mesure.py +0 -0
  294. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/tests/test_outputs_terraform.py +0 -0
  295. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/tests/test_pager.py +0 -0
  296. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/tests/test_parcours_distribution.py +0 -0
  297. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/tests/test_progress_service.py +0 -0
  298. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/tests/test_provision_ciblee.py +0 -0
  299. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/tests/test_provision_et_json_services.py +0 -0
  300. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/tests/test_repertoire_de_travail_annonce.py +0 -0
  301. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/tests/test_scaffold.py +0 -0
  302. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/tests/test_schema_version.py +0 -0
  303. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/tests/test_schemas_sur_documents.py +0 -0
  304. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/tests/test_services.py +0 -0
  305. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/tests/test_session_spec.py +0 -0
  306. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/tests/test_shell_fixtures.py +0 -0
  307. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/tests/test_snapshot_kvm.py +0 -0
  308. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/tests/test_sondes_des_tests_docker.py +0 -0
  309. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/tests/test_sondes_virsh.py +0 -0
  310. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/tests/test_stack_infra.py +0 -0
  311. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/tests/test_support.py +0 -0
  312. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/tests/test_support_en_anglais.py +0 -0
  313. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/tests/test_update_check.py +0 -0
  314. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/tests/test_validator_voit_tout.py +0 -0
  315. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/tests/test_validators_content.py +0 -0
  316. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/tests/test_verrou_depot.py +0 -0
  317. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/tests/test_versions_supportees.py +0 -0
  318. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/tests/test_yaml_contract.py +0 -0
  319. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/tests_e2e/conftest.py +0 -0
  320. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/tests_e2e/pytest.ini +0 -0
  321. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/tests_e2e/test_boite_noire.py +0 -0
  322. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/tests_e2e/test_installation.py +0 -0
  323. {dsoxlab-0.2.0 → dsoxlab-0.2.2}/tests_e2e/test_parcours.py +0 -0
@@ -0,0 +1,261 @@
1
+ # Construit l'appliance dsoxlab et l'attache à une Release.
2
+ #
3
+ # Workflow SÉPARÉ de `release.yml`, pour deux raisons :
4
+ #
5
+ # 1. `workflow_dispatch` permet de l'éprouver sans poser de tag. Greffé à la
6
+ # release, ce job n'aurait tourné qu'au prochain tag mineur, c'est-à-dire
7
+ # jamais avant d'en avoir besoin : un workflow qu'on n'a jamais joué est un
8
+ # workflow qui ne marche pas ;
9
+ # 2. un build d'image de vingt minutes ne doit ni retarder une publication PyPI,
10
+ # ni pouvoir la faire échouer.
11
+ #
12
+ # Sur les tags, il ne se déclenche que sur les **mineurs** (`v0.3.0`, `v0.4.0`) :
13
+ # l'image n'épingle pas la version de dsoxlab, elle installe la dernière au
14
+ # premier démarrage, donc republier un gigaoctet à chaque correctif coûterait
15
+ # cher pour ne rien changer.
16
+
17
+ name: Appliance
18
+
19
+ on:
20
+ push:
21
+ tags: ["v[0-9]+.[0-9]+.0"]
22
+ workflow_dispatch:
23
+ inputs:
24
+ tag:
25
+ description: "Release existante à laquelle attacher les images (vide = ne rien attacher)"
26
+ required: false
27
+ type: string
28
+
29
+ permissions: {}
30
+
31
+ concurrency:
32
+ group: appliance-${{ github.ref }}
33
+ cancel-in-progress: false
34
+
35
+ jobs:
36
+ appliance:
37
+ name: Build the appliance image
38
+ # AFTER the Release exists, and never blocking it. A 20-minute image build
39
+ # must not delay a package that is already on PyPI, and its failure must not
40
+ # leave a version half-published.
41
+ runs-on: ubuntu-24.04
42
+ timeout-minutes: 120
43
+ permissions:
44
+ contents: write # attach the assets to the existing Release
45
+ id-token: write # provenance attestation
46
+ attestations: write
47
+ env:
48
+ # `/` has ~14 GB free on a runner, `/mnt` has ~70. Building anywhere else
49
+ # dies on disk space, twenty minutes in.
50
+ BUILD_DIR: /mnt/appliance
51
+
52
+ steps:
53
+ - name: Harden the runner
54
+ uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1
55
+ with:
56
+ egress-policy: audit
57
+
58
+ - name: Checkout
59
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
60
+ with:
61
+ persist-credentials: false
62
+
63
+ - name: KVM must really be available
64
+ # The one fact this whole job rests on: GitHub-hosted Linux runners expose
65
+ # /dev/kvm. Checked FIRST — without acceleration the build would exceed the
66
+ # job limit, and the failure would arrive an hour later.
67
+ run: |
68
+ set -euo pipefail
69
+ test -e /dev/kvm || { echo "::error::no /dev/kvm on this runner"; exit 1; }
70
+ sudo apt-get update -qq
71
+ sudo apt-get install -y -qq qemu-system-x86 qemu-utils cpu-checker
72
+ kvm-ok
73
+ df -h / /mnt | tail -2
74
+
75
+ - name: Install Packer
76
+ run: |
77
+ set -euo pipefail
78
+ wget -qO- https://apt.releases.hashicorp.com/gpg \
79
+ | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
80
+ echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] \
81
+ https://apt.releases.hashicorp.com $(lsb_release -cs) main" \
82
+ | sudo tee /etc/apt/sources.list.d/hashicorp.list >/dev/null
83
+ sudo apt-get update -qq && sudo apt-get install -y -qq packer
84
+ packer version
85
+
86
+ - name: Resolve the Debian netinst ISO and its checksum
87
+ id: iso
88
+ # The checksum is read from Debian's signed SHA256SUMS rather than pinned
89
+ # here: a hash copied by hand is a hash that rots.
90
+ run: |
91
+ set -euo pipefail
92
+ base="https://cdimage.debian.org/debian-cd/current/amd64/iso-cd"
93
+ name="$(curl -fsSL "$base/SHA256SUMS" | grep -oE 'debian-[0-9.]+-amd64-netinst\.iso' | head -1)"
94
+ sum="$(curl -fsSL "$base/SHA256SUMS" | grep " $name\$" | cut -d' ' -f1)"
95
+ test -n "$name" && test -n "$sum"
96
+ echo "url=$base/$name" >> "$GITHUB_OUTPUT"
97
+ echo "sum=$sum" >> "$GITHUB_OUTPUT"
98
+ echo "ISO : $name"
99
+
100
+ - name: packer init && validate
101
+ working-directory: packer
102
+ env:
103
+ ISO_URL: ${{ steps.iso.outputs.url }}
104
+ ISO_SUM: ${{ steps.iso.outputs.sum }}
105
+ run: |
106
+ set -euo pipefail
107
+ mkdir -p "${BUILD_DIR}/out" "${BUILD_DIR}/tmp"
108
+ packer init .
109
+ packer validate \
110
+ -var "image_version=${GITHUB_REF_NAME#v}" \
111
+ -var "iso_url=${ISO_URL}" \
112
+ -var "iso_checksum=sha256:${ISO_SUM}" \
113
+ -var "output_directory=${BUILD_DIR}/out" .
114
+
115
+ - name: packer build
116
+ working-directory: packer
117
+ env:
118
+ PACKER_TMP_DIR: /mnt/appliance/tmp
119
+ ISO_URL: ${{ steps.iso.outputs.url }}
120
+ ISO_SUM: ${{ steps.iso.outputs.sum }}
121
+ run: |
122
+ set -euo pipefail
123
+ packer build \
124
+ -var "image_version=${GITHUB_REF_NAME#v}" \
125
+ -var "iso_url=${ISO_URL}" \
126
+ -var "iso_checksum=sha256:${ISO_SUM}" \
127
+ -var "output_directory=${BUILD_DIR}/out" .
128
+ ls -lh "${BUILD_DIR}/out"
129
+
130
+ - name: Derive the OVA from the qcow2
131
+ # No VirtualBox, no ovftool (proprietary). An OVA is a tar of three files,
132
+ # in THIS order: the .ovf, the .mf, then the disk — VirtualBox refuses the
133
+ # archive if the .ovf is not its first member.
134
+ #
135
+ # Verified locally before this workflow existed: the resulting OVA imports
136
+ # into VirtualBox 7.0 ("Successfully imported the appliance"), and the disk
137
+ # comes back byte-identical after the conversion.
138
+ run: bash packer/faire-ova.sh "${GITHUB_REF_NAME#v}" "${BUILD_DIR}/out"
139
+
140
+ - name: The OVF must validate against the DMTF schema
141
+ # VirtualBox imports almost anything; VMware checks. Since the same OVA
142
+ # is offered for both, the only way to keep that promise without a
143
+ # Broadcom licence is to hold the file to the specification it claims to
144
+ # follow. `xmllint` resolves the imported CIM schemas itself.
145
+ #
146
+ # Blocking on purpose: an OVF that no longer validates is exactly the
147
+ # kind of drift nothing else here would catch, and it would only show up
148
+ # on a user's machine.
149
+ run: |
150
+ set -euo pipefail
151
+ sudo apt-get install -y --no-install-recommends libxml2-utils
152
+ curl -fsS -o /tmp/dsp8023.xsd \
153
+ https://schemas.dmtf.org/ovf/envelope/1/dsp8023_1.1.0.xsd
154
+ ovf=$(mktemp -d)
155
+ tar -xf "${BUILD_DIR}"/out/*.ova -C "$ovf" --wildcards '*.ovf'
156
+ xmllint --noout --schema /tmp/dsp8023.xsd "$ovf"/*.ovf
157
+
158
+ - name: Every asset must stay within its budget
159
+ env:
160
+ # A BUDGET, not the 2 GB Release limit. Measured reference: 533 MiB for
161
+ # the qcow2 and 511 for the OVA, with 20 GiB of declared disk. A check
162
+ # set at the hard limit would silently wave through a 1.1 GB image the
163
+ # day the discard setting regresses — which is exactly how this recipe
164
+ # was found to be carrying 512 MiB of rubbish.
165
+ BUDGET_MIB: 800
166
+ run: |
167
+ set -euo pipefail
168
+ # `qemu-img info` and `check` give the real shape of the image: virtual
169
+ # size, allocated size, and the share of compressed clusters. Printed
170
+ # rather than guessed, so a regression is visible in the log.
171
+ for f in "${BUILD_DIR}"/out/*.qcow2; do
172
+ [ -f "$f" ] || continue
173
+ qemu-img info "$f"
174
+ qemu-img check "$f" || true
175
+ done
176
+
177
+ depasse=0
178
+ for f in "${BUILD_DIR}"/out/*.ova "${BUILD_DIR}"/out/*.qcow2; do
179
+ [ -f "$f" ] || continue
180
+ mib=$(( $(stat -c%s "$f") / 1024 / 1024 ))
181
+ echo "$(basename "$f") : ${mib} MiB (budget ${BUDGET_MIB})"
182
+ if [ "$mib" -gt "$BUDGET_MIB" ]; then
183
+ echo "::error::$(basename "$f") is ${mib} MiB, over the ${BUDGET_MIB} MiB budget."
184
+ echo "::error::Check the trim first: the log of 90-nettoyage.sh shows what fstrim returned."
185
+ depasse=1
186
+ fi
187
+ done
188
+ test "$depasse" -eq 0
189
+
190
+ ( cd "${BUILD_DIR}/out" && sha256sum ./*.ova ./*.qcow2 > SHA256SUMS && cat SHA256SUMS )
191
+
192
+ - name: Attest build provenance
193
+ uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2
194
+ with:
195
+ subject-path: |
196
+ /mnt/appliance/out/*.ova
197
+ /mnt/appliance/out/*.qcow2
198
+
199
+ - name: Attach the images to the Release
200
+ # Sur un essai manuel sans tag, il n'y a rien à quoi les attacher : le
201
+ # build seul a prouvé ce qu'on voulait prouver.
202
+ if: github.event_name == 'push' || inputs.tag != ''
203
+ env:
204
+ GH_TOKEN: ${{ github.token }}
205
+ TAG: ${{ inputs.tag || github.ref_name }}
206
+ REPO: ${{ github.repository }}
207
+ run: |
208
+ set -euo pipefail
209
+ gh release upload "$TAG" --repo "$REPO" --clobber \
210
+ "${BUILD_DIR}"/out/*.ova \
211
+ "${BUILD_DIR}"/out/*.qcow2 \
212
+ "${BUILD_DIR}/out/SHA256SUMS"
213
+
214
+ - name: Keep only the last images, not all of them
215
+ # Roughly 900 MB per minor version, forever, for images that are worth
216
+ # nothing once the next one exists: the image pins NO dsoxlab version,
217
+ # it installs the latest at first boot. An old one therefore offers no
218
+ # reproducibility — only the weight.
219
+ #
220
+ # Two are kept, not one: if the newest turns out to be broken, the
221
+ # previous one is the fallback while it gets fixed. Below that there is
222
+ # no safety net; above, we are archiving for nobody.
223
+ #
224
+ # Only the images are removed. The Releases themselves, their changelog
225
+ # and the Python distributions stay: they are small, and pip and uv
226
+ # resolve versions through them.
227
+ if: github.event_name == 'push' || inputs.tag != ''
228
+ env:
229
+ GH_TOKEN: ${{ github.token }}
230
+ REPO: ${{ github.repository }}
231
+ GARDEES: 2
232
+ run: |
233
+ set -euo pipefail
234
+ mapfile -t tags < <(
235
+ gh release list --repo "$REPO" --limit 100 \
236
+ --json tagName,createdAt \
237
+ --jq 'sort_by(.createdAt) | reverse | .[].tagName'
238
+ )
239
+ porteuses=0
240
+ for tag in "${tags[@]}"; do
241
+ images=$(gh release view "$tag" --repo "$REPO" --json assets \
242
+ --jq '.assets[].name
243
+ | select(endswith(".ova") or endswith(".qcow2"))')
244
+ [ -n "$images" ] || continue
245
+ porteuses=$((porteuses + 1))
246
+ if [ "$porteuses" -le "$GARDEES" ]; then
247
+ echo "$tag : images conservées (${porteuses}/${GARDEES})"
248
+ continue
249
+ fi
250
+ echo "$tag : retrait des images"
251
+ while IFS= read -r nom; do
252
+ [ -n "$nom" ] || continue
253
+ gh release delete-asset "$tag" "$nom" --repo "$REPO" --yes
254
+ echo " supprimé : $nom"
255
+ done < <(
256
+ gh release view "$tag" --repo "$REPO" --json assets \
257
+ --jq '.assets[].name
258
+ | select(endswith(".ova") or endswith(".qcow2")
259
+ or . == "SHA256SUMS")'
260
+ )
261
+ done
@@ -9,6 +9,170 @@ et le projet suit le [versionnage sémantique](https://semver.org/lang/fr/).
9
9
 
10
10
  ## [Non publié]
11
11
 
12
+ ## [0.2.2] - 2026-09-26
13
+
14
+ ### Ajouté
15
+
16
+ - **L'appliance : une VM prête à jouer, construite par la CI** (issue #91,
17
+ déplacée au jalon 0.3.0 et désormais entamée). Pour Windows et macOS, où
18
+ `uv tool install dsoxlab` n'est pas une option. Sur Linux elle reste la mauvaise
19
+ réponse, et le README le dit : télécharger un demi-gigaoctet pour éviter une
20
+ commande n'a aucun sens.
21
+
22
+ `packer/` porte la recette (builder `qemu`, installation Debian 13 automatisée,
23
+ quatre scripts de provisioning) et `.github/workflows/appliance.yml` la construit
24
+ **sur un runner GitHub-hosted**, sans machine à nous : les runners Linux exposent
25
+ `/dev/kvm`, donc QEMU y est accéléré. Ce choix retire du même coup `ovftool`
26
+ (propriétaire) et VirtualBox de la chaîne : l'OVA est dérivée du qcow2 avec
27
+ `qemu-img` et `tar` seulement.
28
+
29
+ L'image **n'épingle rien**. Le premier démarrage installe le dernier dsoxlab, et
30
+ les hyperviseurs seulement si l'hôte expose la virtualisation imbriquée, vérifiée
31
+ en direct et jamais supposée. D'où sa reconstruction sur les **tags mineurs
32
+ seulement** : republier un demi-gigaoctet à chaque correctif coûterait cher pour
33
+ ne rien changer.
34
+
35
+ Mesuré, non estimé : **461 Mio** pour le qcow2 et **446 Mio** pour l'OVA,
36
+ construits en 4 min 50 s, contre un budget que le workflow impose à 800 Mio. Une
37
+ appliance comparable construite avec `virtualbox-iso` pèse 979 Mio.
38
+
39
+ - **Le README présente désormais les deux façons d'entrer**, côte à côte :
40
+ installer l'outil, ou télécharger l'appliance. Il dit aussi laquelle est la
41
+ bonne sous Linux, plutôt que de les vendre à égalité.
42
+
43
+ - **Seuls les deux derniers jeux d'images sont conservés sur les Releases.**
44
+ Environ 900 Mo par version mineure, pour toujours, pour des images qui
45
+ n'épinglent aucune version de dsoxlab et installent la dernière au premier
46
+ démarrage : une ancienne n'offre aucune reproductibilité, seulement son
47
+ poids. Deux plutôt qu'une, pour qu'une image cassée ait un recours. Les
48
+ Releases, leur changelog et les distributions Python ne sont pas touchés.
49
+
50
+ - **L'OVA est proposée pour VMware autant que pour VirtualBox**, et le workflow
51
+ tient maintenant cette promesse : l'OVF est validé contre le schéma DMTF à
52
+ chaque build. VirtualBox importe à peu près n'importe quoi, VMware contrôle —
53
+ et sans licence Broadcom, la conformité à la spécification est la seule façon
54
+ honnête de tenir l'affirmation. La page de l'appliance dit franchement ce qui
55
+ a été éprouvé et ce qui ne l'a pas été, et qu'**Apple Silicon est hors de
56
+ portée** : les deux images sont x86-64, les runners arm64 de GitHub n'exposent
57
+ aucun `/dev/kvm` pour en construire une autre, et la virtualisation imbriquée
58
+ sur ces Mac n'existe qu'à partir du M3.
59
+
60
+ ### Corrigé
61
+
62
+ - **Trois défauts de cette recette, chacun attrapé par une mesure et non par une
63
+ relecture.** Ils valent d'être consignés parce qu'ils sont du genre qui se livre
64
+ en silence :
65
+
66
+ `fstrim` **rend un succès sans rien libérer** quand le disque de build est
67
+ attaché en `discard=ignore`, qui est le défaut de Packer. L'invité annonçait
68
+ « 520.4 MiB trimmed » pendant que le qcow2 gardait tout : 1110 Mio d'artefact
69
+ contre 597 pour le témoin. Le disque est donc attaché en `unmap`, avec
70
+ `detect_zeroes=unmap` pour que la mise à zéro ne coûte rien, et le repli
71
+ `|| dd` qui ne se déclenchait jamais a disparu, avec les 17 Gio qu'il écrivait
72
+ quand il se déclenchait.
73
+
74
+ `vm_name` **n'avait pas d'extension**, et Packer n'en ajoute aucune : les globs
75
+ `*.qcow2` du contrôle de budget, des empreintes et de l'envoi n'auraient jamais
76
+ vu le fichier. Le premier build local, qui a produit `dsoxlab-appliance-dev`,
77
+ l'a prouvé.
78
+
79
+ Le contrôle de taille testait la **limite dure de 2 Go**, donc il aurait laissé
80
+ passer une image de 1,1 Gio le jour où le trim se recasse. C'est désormais un
81
+ **budget de 800 Mio**, avec `qemu-img info` et `check` affichés, et une erreur
82
+ qui dit où regarder.
83
+
84
+ - **`lsb_release` n'existe plus** dès que la tâche `standard` quitte le preseed, or
85
+ `20-outils.sh` s'en servait pour le dépôt HashiCorp. Lu dans `/etc/os-release` à
86
+ la place : sans cela le build aurait échoué net.
87
+
88
+ - **Dix défauts de plus, trouvés en jouant l'appliance plutôt qu'en relisant la
89
+ recette.** Chacun échappait à tous les contrôles précédents, et le premier à lui
90
+ seul rendait l'image inutilisable pour ceux à qui elle est destinée :
91
+
92
+ **Le réseau ne survivait pas au changement d'hyperviseur.** L'installateur
93
+ Debian fige le nom de l'interface qu'il a vue — `enp0s2` sous le QEMU qui
94
+ construit l'image — et ce nom dérive de la position PCI de la carte. Sous
95
+ VirtualBox, la carte s'appelle `enp0s17` : la configuration ne s'appliquait donc
96
+ à rien. L'appliance importée démarrait avec `enp0s17 DOWN`, un `resolv.conf`
97
+ sans le moindre `nameserver`, et un premier démarrage qui échouait sur
98
+ « Temporary failure in name resolution ». Rien n'était installé : ni dsoxlab, ni
99
+ les hyperviseurs, ni le bureau. Le réseau passe désormais par
100
+ `systemd-networkd`, dont le `[Match]` **décrit** la carte (`en*`, `eth*`) au
101
+ lieu de la nommer.
102
+
103
+ **Le premier démarrage se marquait fait même quand tout avait échoué**, et la
104
+ machine n'avait plus aucun moyen de se rattraper. Il dit maintenant ce qui a
105
+ échoué, ne pose pas de marqueur, et recommence au démarrage suivant. Il attend
106
+ aussi qu'un nom se résolve vraiment avant de commencer :
107
+ `network-online.target` était atteinte sans qu'aucun bail DHCP soit pris, d'où
108
+ les huit secondes du défaut ci-dessus.
109
+
110
+ **Root héritait du `HOME` de student** pendant la construction (`sudo -E`),
111
+ si bien que `terraform version` et `ansible-playbook --version` créaient un
112
+ `~/.ansible` et un `~/.terraform.d` appartenant à root. Ansible refusait alors
113
+ de démarrer pour l'apprenant, et dsoxlab rendait un `rc=5, Stats: {}` que rien
114
+ ne reliait à la cause. Corrigé par `sudo -H`, avec une ceinture `chown`.
115
+
116
+ **`ovmf` et `qemu-utils` manquaient** : sur Debian, tous deux ne sont que des
117
+ *recommandations* de `qemu-kvm`, donc `--no-install-recommends` les écartait.
118
+ Sans le premier, libvirt n'expose aucun firmware EFI et tout `provision`
119
+ s'arrête net ; sans le second, il ne sait pas créer de volume qcow2. Trouvés en
120
+ provisionnant pour de vrai depuis l'appliance — et le premier a été nommé en
121
+ 2,3 secondes par le garde-fou de l'issue #234.
122
+
123
+ **Aucun pool libvirt `default`, aucune appartenance de groupe** : une Debian
124
+ fraîche n'en définit ni l'un ni l'autre, donc `doctor --strict` sortait en 9 et
125
+ `student` ne pouvait pas ouvrir `/dev/kvm`. Les deux sont posés au premier
126
+ démarrage, ce qui est aussi la raison de son redémarrage final.
127
+
128
+ **Le bureau n'avait pas de serveur X.** Même motif qu'`ovmf` et `qemu-utils`,
129
+ pour la troisième fois : `xserver-xorg` est une *recommandation* de `xfce4` et
130
+ de `lightdm`, jamais une dépendance, donc `--no-install-recommends` l'écartait.
131
+ `/usr/bin/Xorg` n'existait pas, `lightdm` était « failed », et la machine
132
+ arrivait sur une console malgré `graphical.target` — sans que rien ne relie ce
133
+ symptôme à un paquet manquant. Le serveur et ses pilotes vidéo (`vmware` pour
134
+ le contrôleur VMSVGA que présentent VirtualBox et VMware, `vesa` et `fbdev` en
135
+ secours) sont désormais nommés explicitement.
136
+
137
+ **`~/.ssh/config` n'incluait pas ce que dsoxlab y écrit.** `provision` dépose un
138
+ `~/.ssh/config.d/<catalogue>.conf` et avertit, à chaque exécution, que rien ne
139
+ le lit. L'`Include` est maintenant posé en tête du fichier, là où OpenSSH
140
+ l'exige.
141
+
142
+ **`start` déclarait « déjà provisionnée » une infrastructure inutilisable.**
143
+ Il tranchait sur le seul state, donc une adresse suffisait. L'enchaînement qui
144
+ suivait était absurde : `provision` sortait en 8 sur « les hôtes n'ont pas
145
+ répondu : l'infrastructure existe, mais elle n'est pas utilisable en l'état »,
146
+ et le `start` relancé juste après — donc relancé *parce que* rien ne répondait
147
+ — annonçait « déjà provisionné, rien à reconstruire », avant d'échouer plus
148
+ loin sur un `UNREACHABLE` brut d'Ansible que rien ne reliait à la cause. Il
149
+ sonde désormais aussi le port 22 : une adresse n'est pas une machine. Rejouer
150
+ `provision` ne coûte rien, puisqu'il reprend sans rien recréer, et il attend.
151
+
152
+ **La console était noyée sous des erreurs `AF_VSOCK`**, une par rechargement de
153
+ systemd, soit une dizaine de lignes rouges en cinq secondes pendant
154
+ l'installation des paquets. Rien n'était cassé, mais rien ne le disait non
155
+ plus. `systemd-ssh-generator` est maintenant neutralisé par un lien vers
156
+ `/dev/null`, la façon documentée de le faire.
157
+
158
+ ## [0.2.1] - 2026-09-25
159
+
160
+ ### Ajouté
161
+
162
+ - **Une section `Remerciements` dans les deux README.** Trois personnes
163
+ extérieures au projet ont remonté des défauts avec le diagnostic, et souvent le
164
+ correctif. La section nomme **ce que chaque retour a changé** plutôt que de
165
+ lister des pseudonymes : quatre versions viennent de
166
+ [@cedric-ribier](https://github.com/cedric-ribier) (tailles de disque nominales,
167
+ libvirt 8, l'agent Incus sur AlmaLinux avec l'attente réglable, et la
168
+ virtualisation imbriquée nommée), le `conftest.py` qui bloquait en silence tous
169
+ les labs `shell` vient de [@Gedd18](https://github.com/Gedd18), et deux labs dont
170
+ l'état de départ était inutilisable de
171
+ [@VictorVare](https://github.com/VictorVare).
172
+
173
+ Un crédit qui dit ce qu'il a corrigé vaut mieux qu'une liste, et il apprend au
174
+ lecteur suivant quel genre de rapport est utile.
175
+
12
176
  ## [0.2.0] - 2026-09-25
13
177
 
14
178
  **Passage au produit.** Cette version n'ajoute aucun code : elle marque le
@@ -9,6 +9,155 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
9
9
 
10
10
  ## [Unreleased]
11
11
 
12
+ ## [0.2.2] - 2026-09-26
13
+
14
+ ### Added
15
+
16
+ - **The appliance: a ready-to-play VM, built by the CI** (issue #91, moved to the
17
+ 0.3.0 milestone and now started). For Windows and macOS, where `uv tool install
18
+ dsoxlab` is not an option. On Linux it stays the wrong answer, and the README
19
+ says so: downloading half a gigabyte to avoid one command makes no sense.
20
+
21
+ `packer/` holds the recipe — `qemu` builder, automated Debian 13 install, four
22
+ provisioning scripts — and `.github/workflows/appliance.yml` builds it **on a
23
+ GitHub-hosted runner**, with no self-hosted machine: Linux runners expose
24
+ `/dev/kvm`, so QEMU is accelerated. That choice also removes `ovftool`
25
+ (proprietary) and VirtualBox from the chain: the OVA is derived from the qcow2
26
+ with `qemu-img` and `tar` alone.
27
+
28
+ The image **pins nothing**. The first boot installs the latest dsoxlab, and the
29
+ hypervisors only if the host exposes nested virtualization — checked live, never
30
+ assumed. Which is why it is rebuilt on **minor tags only**: republishing half a
31
+ gigabyte for every patch would cost a lot and change nothing.
32
+
33
+ Measured, not estimated: **461 MiB** for the qcow2 and **446 MiB** for the OVA,
34
+ built in 4 min 50 s, against a budget the workflow enforces at 800 MiB. A
35
+ comparable appliance built with `virtualbox-iso` weighs 979 MiB.
36
+
37
+ - **The README now presents both ways in**, side by side: install the tool, or
38
+ download the appliance. It also says which one is right on Linux, rather than
39
+ selling them as equals.
40
+
41
+ - **Only the last two sets of images are kept on the Releases.** Roughly 900 MB
42
+ per minor version, forever, for images that pin no dsoxlab version and
43
+ install the latest at first boot: an old one offers no reproducibility, only
44
+ weight. Two rather than one, so a broken image has a fallback. The Releases,
45
+ their changelog and the Python distributions are untouched.
46
+
47
+ - **The OVA is offered for VMware as well as VirtualBox**, and the workflow now
48
+ holds it to that promise: the OVF is validated against the DMTF schema at
49
+ every build. VirtualBox imports almost anything, VMware checks — and without
50
+ a Broadcom licence, conformity to the specification is the only honest way to
51
+ keep the claim. The appliance page says plainly what was tested and what was
52
+ not, and that **Apple Silicon is out of reach**: both images are x86-64,
53
+ GitHub's arm64 runners expose no `/dev/kvm` to build another, and nested
54
+ virtualization on those Macs only exists from the M3.
55
+
56
+ ### Fixed
57
+
58
+ - **Three defects in that recipe, each caught by a measurement rather than a
59
+ reading.** They are worth recording because they are the kind that ship
60
+ silently:
61
+
62
+ `fstrim` **returns success without freeing anything** when the build disk is
63
+ attached with `discard=ignore`, which is Packer's default. The guest announced
64
+ "520.4 MiB trimmed" while the qcow2 kept every byte: 1110 MiB of artifact
65
+ against 597 for the control. So the disk is now attached `unmap`, with
66
+ `detect_zeroes=unmap` so that zeroing costs nothing — and the never-triggered
67
+ `|| dd` fallback is gone, along with the 17 GiB it wrote when it did fire.
68
+
69
+ `vm_name` **had no extension**, and Packer adds none: the `*.qcow2` globs of the
70
+ budget check, the checksums and the upload would never have seen the file. The
71
+ first local build produced `dsoxlab-appliance-dev`, which proved it.
72
+
73
+ The size check tested the **2 GB hard limit**, so it would have waved through a
74
+ 1.1 GB image the day the trim regressed. It is now a **budget of 800 MiB**, with
75
+ `qemu-img info` and `check` printed, and an error that says where to look.
76
+
77
+ - **`lsb_release` no longer exists** once the `standard` task is dropped from the
78
+ preseed, and `20-outils.sh` used it for the HashiCorp repository. Read from
79
+ `/etc/os-release` instead — without this the build would have failed outright.
80
+
81
+ - **Ten more defects, found by playing the appliance rather than reading the
82
+ recipe.** Each was invisible to every check that came before, and the first one
83
+ alone made the image useless to the very people it is built for:
84
+
85
+ **The network did not survive a change of hypervisor.** Debian's installer
86
+ freezes the interface name it saw at install time — `enp0s2` under the QEMU that
87
+ builds the image — and that name is derived from the card's PCI position. Under
88
+ VirtualBox the card is `enp0s17`, so the configuration applied to nothing: the
89
+ imported appliance came up with `enp0s17 DOWN`, a `resolv.conf` without a single
90
+ `nameserver`, and a first boot that failed on "Temporary failure in name
91
+ resolution". Nothing was installed — no dsoxlab, no hypervisors, no desktop. The
92
+ image now configures the network with `systemd-networkd`, whose `[Match]`
93
+ **describes** the card (`en*`, `eth*`) instead of naming it.
94
+
95
+ **The first boot marked itself done even when everything had failed**, so the
96
+ machine had no way left to catch up. It now reports what failed, keeps no
97
+ marker, and starts over at the next boot. It also waits for a name to actually
98
+ resolve before it begins: `network-online.target` was reached with no DHCP lease
99
+ taken, which is how the failure above got its eight-second timeout.
100
+
101
+ **Root inherited the student's `HOME`** during the build (`sudo -E`), so
102
+ `terraform version` and `ansible-playbook --version` created a root-owned
103
+ `~/.ansible` and `~/.terraform.d`. Ansible then refused to start for the learner
104
+ and dsoxlab reported `rc=5, Stats: {}` — a symptom nothing connected to the
105
+ cause. Fixed with `sudo -H`, plus a `chown` belt.
106
+
107
+ **`ovmf` and `qemu-utils` were missing**: both are mere *recommendations* of
108
+ `qemu-kvm` on Debian, so `--no-install-recommends` dropped them. Without the
109
+ first, libvirt exposes no EFI firmware and every `provision` stops dead; without
110
+ the second, it cannot create a qcow2 volume. Found by provisioning for real from
111
+ inside the appliance — and the first was named in 2.3 seconds by the guard added
112
+ for issue #234.
113
+
114
+ **No libvirt `default` pool and no group membership**: a fresh Debian defines
115
+ neither, so `doctor --strict` exited 9 and `student` could not open `/dev/kvm`.
116
+ Both are now set up at first boot, which is also why it reboots at the end.
117
+
118
+ **The desktop had no X server.** Same pattern as `ovmf` and `qemu-utils`, for
119
+ the third time: `xserver-xorg` is a *recommendation* of `xfce4` and `lightdm`,
120
+ never a dependency, so `--no-install-recommends` dropped it. `/usr/bin/Xorg`
121
+ did not exist, `lightdm` was `failed`, and the machine came up on a console
122
+ despite `graphical.target` — with nothing tying that to a missing package. The
123
+ server and its video drivers (`vmware` for the VMSVGA controller VirtualBox and
124
+ VMware present, `vesa` and `fbdev` as fallbacks) are now named explicitly.
125
+
126
+ **`~/.ssh/config` did not include what dsoxlab writes.** `provision` drops a
127
+ `~/.ssh/config.d/<catalog>.conf` and warns, at every run, that nothing reads it.
128
+ The `Include` is now placed at the top of the file, where OpenSSH requires it.
129
+
130
+ **`start` called an unusable infrastructure "already provisioned".** It
131
+ decided from the state alone, so an address was enough. The sequence that
132
+ followed was absurd: `provision` exited 8 on "some hosts did not answer: the
133
+ infrastructure exists, but it is not usable as it stands", and the `start`
134
+ relaunched right after — relaunched *because* nothing answered — announced
135
+ "already provisioned, nothing to build again", then failed further down on a
136
+ raw Ansible `UNREACHABLE` that nothing tied to the cause. It now probes port
137
+ 22 as well: an address is not a machine. Replaying `provision` costs nothing,
138
+ since it resumes without recreating anything, and it waits.
139
+
140
+ **The console was flooded with `AF_VSOCK` errors**, one per systemd reload, so
141
+ a dozen red lines in five seconds while packages installed. Nothing was broken,
142
+ but nothing said so either. `systemd-ssh-generator` is now masked, the
143
+ documented way, with a symlink to `/dev/null`.
144
+
145
+ ## [0.2.1] - 2026-09-25
146
+
147
+ ### Added
148
+
149
+ - **A `Thanks` section in both READMEs.** Three people outside the project have
150
+ reported defects with the diagnosis, and often the fix. The section names **what
151
+ each report changed** rather than listing handles: four releases come from
152
+ [@cedric-ribier](https://github.com/cedric-ribier) (nominal disk sizes, libvirt 8,
153
+ the Incus agent on AlmaLinux with a configurable host wait, and nested
154
+ virtualization named), the `conftest.py` that silently blocked every `shell` lab
155
+ from [@Gedd18](https://github.com/Gedd18), and two labs whose starting state was
156
+ unusable from [@VictorVare](https://github.com/VictorVare).
157
+
158
+ A credit that says what it fixed is worth more than a list, and it tells the next
159
+ reader what kind of report is useful.
160
+
12
161
  ## [0.2.0] - 2026-09-25
13
162
 
14
163
  **Productization.** This version adds no code: it marks a milestone, and its