From 7ac487f640b8d2951dd2d22c4ec8e5530ef996d7 Mon Sep 17 00:00:00 2001 From: Gilles Soulier Date: Sun, 15 Mar 2026 04:54:51 +0100 Subject: [PATCH] first --- AGENTS.md | 147 +++++ CHANGELOG.md | 8 + README.md | 113 ++++ config/default.conf | 3 + config/mdns-avahi.yaml | 3 + config/nfs-client.shares.yaml | 19 + config/nfs-server.exports.yaml | 6 + config/samba-shares.yaml | 10 + config/ssh-server.yaml | 4 + consigne.md | 524 ++++++++++++++++++ core/bootstrap.sh | 98 ++++ core/dispatcher.sh | 190 +++++++ core/registry.sh | 42 ++ core/runtime.sh | 12 + docs/architecture.md | 26 + docs/lan-validation.md | 117 ++++ hardware/README.md | 3 + install.sh | 14 + lib/log.sh | 19 + lib/network.sh | 9 + lib/package.sh | 17 + lib/prompts.sh | 125 +++++ lib/system.sh | 59 ++ lib/ui.sh | 61 ++ lib/validation.sh | 10 + menus/main.sh | 101 ++++ modules/boot/grub-theme/config.sh | 4 + modules/boot/grub-theme/metadata.conf | 4 + modules/boot/grub-theme/module.sh | 85 +++ modules/boot/grub-theme/tests.sh | 28 + modules/containers/docker-engine/config.sh | 5 + .../containers/docker-engine/metadata.conf | 4 + modules/containers/docker-engine/module.sh | 74 +++ modules/containers/docker-engine/tests.sh | 28 + modules/hardware/detect/config.sh | 1 + modules/hardware/detect/metadata.conf | 4 + modules/hardware/detect/module.sh | 43 ++ modules/hardware/detect/tests.sh | 35 ++ modules/network/identity/config.sh | 3 + modules/network/identity/metadata.conf | 4 + modules/network/identity/module.sh | 68 +++ modules/network/identity/tests.sh | 28 + modules/network/ip-config/config.sh | 7 + modules/network/ip-config/metadata.conf | 4 + modules/network/ip-config/module.sh | 157 ++++++ modules/network/ip-config/tests.sh | 28 + modules/network/mdns-avahi/config.sh | 4 + modules/network/mdns-avahi/metadata.conf | 4 + modules/network/mdns-avahi/module.sh | 104 ++++ modules/network/mdns-avahi/tests.sh | 40 ++ modules/network/nfs-client/config.sh | 2 + modules/network/nfs-client/metadata.conf | 4 + modules/network/nfs-client/module.sh | 219 ++++++++ modules/network/nfs-client/tests.sh | 35 ++ modules/network/nfs-server/config.sh | 5 + modules/network/nfs-server/metadata.conf | 4 + modules/network/nfs-server/module.sh | 144 +++++ modules/network/nfs-server/tests.sh | 40 ++ modules/network/samba-share/config.sh | 8 + modules/network/samba-share/metadata.conf | 4 + modules/network/samba-share/module.sh | 186 +++++++ modules/network/samba-share/tests.sh | 40 ++ modules/network/ssh-server/config.sh | 6 + modules/network/ssh-server/metadata.conf | 4 + modules/network/ssh-server/module.sh | 132 +++++ modules/network/ssh-server/tests.sh | 45 ++ modules/system/user-groups/config.sh | 2 + modules/system/user-groups/metadata.conf | 4 + modules/system/user-groups/module.sh | 93 ++++ modules/system/user-groups/tests.sh | 25 + modules/system/user-sudo/config.sh | 2 + modules/system/user-sudo/metadata.conf | 4 + modules/system/user-sudo/module.sh | 103 ++++ modules/system/user-sudo/tests.sh | 32 ++ profiles/README.md | 3 + tests/smoke.sh | 12 + themes/grub/debian-1080p.zip | Bin 0 -> 2067048 bytes themes/grub/debian-2K.zip | Bin 0 -> 3229186 bytes themes/grub/linux-1080p.zip | Bin 0 -> 2040261 bytes themes/grub/linux-2K.zip | Bin 0 -> 3182025 bytes tools.md | 203 +++++++ 81 files changed, 3867 insertions(+) create mode 100644 AGENTS.md create mode 100644 CHANGELOG.md create mode 100644 config/default.conf create mode 100644 config/mdns-avahi.yaml create mode 100644 config/nfs-client.shares.yaml create mode 100644 config/nfs-server.exports.yaml create mode 100644 config/samba-shares.yaml create mode 100644 config/ssh-server.yaml create mode 100644 consigne.md create mode 100644 core/bootstrap.sh create mode 100644 core/dispatcher.sh create mode 100644 core/registry.sh create mode 100644 core/runtime.sh create mode 100644 docs/architecture.md create mode 100644 docs/lan-validation.md create mode 100644 hardware/README.md create mode 100755 install.sh create mode 100644 lib/log.sh create mode 100644 lib/network.sh create mode 100644 lib/package.sh create mode 100644 lib/prompts.sh create mode 100644 lib/system.sh create mode 100644 lib/ui.sh create mode 100644 lib/validation.sh create mode 100644 menus/main.sh create mode 100644 modules/boot/grub-theme/config.sh create mode 100644 modules/boot/grub-theme/metadata.conf create mode 100644 modules/boot/grub-theme/module.sh create mode 100644 modules/boot/grub-theme/tests.sh create mode 100644 modules/containers/docker-engine/config.sh create mode 100644 modules/containers/docker-engine/metadata.conf create mode 100644 modules/containers/docker-engine/module.sh create mode 100644 modules/containers/docker-engine/tests.sh create mode 100644 modules/hardware/detect/config.sh create mode 100644 modules/hardware/detect/metadata.conf create mode 100644 modules/hardware/detect/module.sh create mode 100644 modules/hardware/detect/tests.sh create mode 100644 modules/network/identity/config.sh create mode 100644 modules/network/identity/metadata.conf create mode 100644 modules/network/identity/module.sh create mode 100644 modules/network/identity/tests.sh create mode 100644 modules/network/ip-config/config.sh create mode 100644 modules/network/ip-config/metadata.conf create mode 100644 modules/network/ip-config/module.sh create mode 100644 modules/network/ip-config/tests.sh create mode 100644 modules/network/mdns-avahi/config.sh create mode 100644 modules/network/mdns-avahi/metadata.conf create mode 100644 modules/network/mdns-avahi/module.sh create mode 100755 modules/network/mdns-avahi/tests.sh create mode 100644 modules/network/nfs-client/config.sh create mode 100644 modules/network/nfs-client/metadata.conf create mode 100644 modules/network/nfs-client/module.sh create mode 100644 modules/network/nfs-client/tests.sh create mode 100644 modules/network/nfs-server/config.sh create mode 100644 modules/network/nfs-server/metadata.conf create mode 100644 modules/network/nfs-server/module.sh create mode 100644 modules/network/nfs-server/tests.sh create mode 100644 modules/network/samba-share/config.sh create mode 100644 modules/network/samba-share/metadata.conf create mode 100644 modules/network/samba-share/module.sh create mode 100644 modules/network/samba-share/tests.sh create mode 100644 modules/network/ssh-server/config.sh create mode 100644 modules/network/ssh-server/metadata.conf create mode 100644 modules/network/ssh-server/module.sh create mode 100755 modules/network/ssh-server/tests.sh create mode 100644 modules/system/user-groups/config.sh create mode 100644 modules/system/user-groups/metadata.conf create mode 100644 modules/system/user-groups/module.sh create mode 100755 modules/system/user-groups/tests.sh create mode 100644 modules/system/user-sudo/config.sh create mode 100644 modules/system/user-sudo/metadata.conf create mode 100644 modules/system/user-sudo/module.sh create mode 100755 modules/system/user-sudo/tests.sh create mode 100644 profiles/README.md create mode 100755 tests/smoke.sh create mode 100644 themes/grub/debian-1080p.zip create mode 100644 themes/grub/debian-2K.zip create mode 100644 themes/grub/linux-1080p.zip create mode 100644 themes/grub/linux-2K.zip create mode 100644 tools.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..f6cce94 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,147 @@ +# AGENTS.md + +## But du depot + +Ce projet construit un framework de post-installation pour Debian 13, lance via : + +```bash +curl -fsSL https://gitea.maison43.duckdns.org/gilles/postinstall-debian/raw/branch/main/install.sh | bash +``` + +L'objectif est de proposer une base modulaire, testable et evolutive pour installer et configurer un systeme Debian apres installation. + +## Regles de travail +- ecrire les commentaires et les explication en francais +- Toujours privilegier une architecture modulaire. +- Chaque outil doit vivre dans son propre module. +- Ne pas melanger logique framework, logique UI et logique d'installation d'un outil. +- Eviter les actions destructives ou silencieuses sur le systeme. +- Garder le code compatible avec Debian 13 et un shell Bash standard. +- Favoriser du code lisible, structure et compose de fonctions courtes. + +## Structure cible + +Le depot doit converger vers cette structure : + +```text +install.sh +README.md +consigne.md +tools.md +CHANGELOG.md +AGENTS.md + +assets/ +config/ +core/ +docs/ +hardware/ +lib/ +menus/ +modules/ +profiles/ +tests/ +``` + +## Responsabilites par zone + +- `install.sh` : point d'entree compatible `curl | bash`. +- `core/` : bootstrap, runtime, registre des modules, dispatch. +- `lib/` : fonctions reutilisables UI, logs, validations, paquets, systeme, reseau. +- `menus/` : menus interactifs. +- `modules/` : outils installables, un dossier par outil. +- `profiles/` : groupes de modules predefinis. +- `hardware/` : selections liees au materiel. +- `assets/` : themes, polices, wallpapers, icones et ressources visuelles. +- `tests/` : tests framework et tests de modules. +- `docs/` : documentation de conception et d'usage. + +## Contrat d'un module + +Chaque module doit idealement contenir : + +```text +modules/// +module.sh +config.sh +metadata.conf +tests.sh +``` + +Fonctions attendues quand elles sont pertinentes : + +- `module__metadata` +- `module__check` +- `module__install` +- `module__configure` +- `module__test` + +## Standards Bash + +- Utiliser `set -u` et une gestion d'erreurs explicite lorsque l'architecture sera en place. +- Citer correctement les variables. +- Preferer `command -v` pour verifier les dependances. +- Centraliser les logs, messages UI et validations dans `lib/`. +- Garder les effets de bord dans des fonctions nommees clairement. +- Prevoir ShellCheck des que les scripts existent. + +## Experience utilisateur + +Le framework doit fournir : + +- une interface console claire +- des messages colores et coherents +- des etapes visibles +- des confirmations quand une action est sensible +- un resume final des operations + +Fonctions UI a prevoir : + +- `ui_header` +- `ui_section` +- `ui_info` +- `ui_success` +- `ui_warn` +- `ui_error` +- `ui_menu` +- `ui_confirm` +- `ui_pause` + +## Securite + +- Verifier la distribution et les privileges avant toute installation. +- Verifier le reseau si une action distante est necessaire. +- Ne pas modifier silencieusement des fichiers critiques. +- Preferer des changements explicites, journalises et reversibles quand possible. + +## Methode pour ajouter un outil + +Avant implementation, passer par : + +1. Brainstorming de l'outil. +2. Decision d'architecture. +3. Implementation du module. +4. Integration menu, profil ou materiel si necessaire. +5. Tests. +6. Documentation. + +Les informations minimales d'un outil dans `tools.md` sont : + +- nom +- description +- categorie +- questions +- methode d'installation +- tests +- statut + +## Priorite immediate + +Avant d'ajouter des outils, construire l'ossature du framework : + +- point d'entree `install.sh` +- dossiers standards +- bibliotheques de base +- mecanisme de chargement +- premiers menus +- documentation de demarrage diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..31d37e2 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,8 @@ +# CHANGELOG + +## Unreleased + +- Creation de l'architecture initiale du depot +- Ajout du bootstrap Bash minimal +- Ajout des bibliotheques de base et du menu principal +- Ajout du premier module `system/user-sudo` diff --git a/README.md b/README.md index e69de29..f88202f 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,113 @@ +# postinstall-debian + +Framework Bash modulaire de post-installation pour Debian 13, pense pour etre lance via `curl | bash`. + +## Objectif + +Fournir une base propre pour : + +- installer des outils +- appliquer des configurations +- regrouper des modules par profil ou materiel +- proposer une interface console simple +- tester les modules isolemment + +## Etat actuel + +Le depot contient maintenant une architecture initiale : + +- un point d'entree `install.sh` +- un noyau `core/` +- des bibliotheques reutilisables `lib/` +- des menus interactifs par categorie +- des modules systeme, reseau, hardware, boot et conteneurs +- l'arborescence cible du framework + +## Demarrage local + +Execution recommandee pour tester le bootstrap : + +```bash +sudo bash install.sh +``` + +Le script ouvre ensuite un menu interactif. Pour le premier module disponible : + +```bash +sudo bash install.sh +``` + +Puis : + +```text +4. Configuration systeme +1. system/user-groups +2. system/user-sudo +``` + +Exemple pour `system/user-groups` : + +```text +4 +1 +gilles +audio,video,plugdev,dialout,netdev,lpadmin,scanner +``` + +Exemple pour `network/ssh-server` : + +```text +1 +1 +7 +22 +y +n +``` + +Exemple pour `network/mdns-avahi` : + +```text +1 +1 +3 +y +y +``` + +Exemple pour `network/identity` : + +```text +1 +1 +1 +monpc +local +``` + +Exemple pour `network/ip-config` : + +```text +1 +1 +2 +enp0s31f6 +static +10.0.0.25 +22 +10.0.0.1 +10.0.0.1 +``` + +## Prochaines etapes + +- remplir `tools.md` +- ajouter les modules suivants +- implementer les menus interactifs +- ajouter des tests shell et ShellCheck + +## Validation reseau + +Une checklist de validation LAN est disponible ici : + +- [docs/lan-validation.md](/home/gilles/Documents/vscode/postinstall-debian/docs/lan-validation.md) diff --git a/config/default.conf b/config/default.conf new file mode 100644 index 0000000..938fb4c --- /dev/null +++ b/config/default.conf @@ -0,0 +1,3 @@ +# Configuration par defaut du framework +FRAMEWORK_NAME="postinstall-debian" +FRAMEWORK_TARGET="debian-13" diff --git a/config/mdns-avahi.yaml b/config/mdns-avahi.yaml new file mode 100644 index 0000000..1f24473 --- /dev/null +++ b/config/mdns-avahi.yaml @@ -0,0 +1,3 @@ +mdns: + enable: yes + publish_workstation: yes diff --git a/config/nfs-client.shares.yaml b/config/nfs-client.shares.yaml new file mode 100644 index 0000000..b06017c --- /dev/null +++ b/config/nfs-client.shares.yaml @@ -0,0 +1,19 @@ +shares: + - id: media + name: Media + description: Partage multimedia principal + server: 10.0.0.20 + remote_path: /srv/nfs/media + mount_path: /mnt/nfs/media + access: rw + mount_options: defaults,_netdev,x-systemd.automount,noatime + enabled_by_default: false + - id: backup + name: Backup + description: Partage de sauvegarde + server: 10.0.0.30 + remote_path: /srv/nfs/backup + mount_path: /mnt/nfs/backup + access: ro + mount_options: defaults,_netdev,x-systemd.automount,noatime + enabled_by_default: false diff --git a/config/nfs-server.exports.yaml b/config/nfs-server.exports.yaml new file mode 100644 index 0000000..2b0d68d --- /dev/null +++ b/config/nfs-server.exports.yaml @@ -0,0 +1,6 @@ +exports: + - id: public_data + path: /srv/nfs/public + clients: 10.0.0.0/22 + options: rw,sync,no_subtree_check + description: Export principal reseau local diff --git a/config/samba-shares.yaml b/config/samba-shares.yaml new file mode 100644 index 0000000..d3b1a47 --- /dev/null +++ b/config/samba-shares.yaml @@ -0,0 +1,10 @@ +workgroup: home +wsdd2: yes +shares: + - id: public_home + name: public + path: /home/gilles + user: gilles + read_only: yes + public: yes + description: Partage public du dossier personnel diff --git a/config/ssh-server.yaml b/config/ssh-server.yaml new file mode 100644 index 0000000..f2dac43 --- /dev/null +++ b/config/ssh-server.yaml @@ -0,0 +1,4 @@ +ssh: + port: 22 + password_authentication: yes + permit_root_login: no diff --git a/consigne.md b/consigne.md new file mode 100644 index 0000000..af277d8 --- /dev/null +++ b/consigne.md @@ -0,0 +1,524 @@ +# CONSIGNE.md + +## Objectif du projet + +Créer un dépôt Git hébergé sur Gitea : + +https://gitea.maison43.duckdns.org/gilles/postinstall-debian + +Ce dépôt doit permettre l'exécution d'un script **post‑installation Debian 13** via une commande simple : + +```bash +curl -fsSL https://gitea.maison43.duckdns.org/gilles/postinstall-debian/raw/branch/main/install.sh | bash +``` + +Le script doit permettre : + +- d'installer automatiquement des outils +- de configurer le système +- de proposer des menus interactifs +- d'être évolutif et modulaire +- d'intégrer facilement de nouveaux outils +- d'offrir une interface console claire et agréable + +Le projet doit être pensé comme un **framework de post‑installation Debian 13**. + +--- + +# Principes généraux + +## Modularité + +Chaque outil doit être installé via un **module indépendant**. + +Un module correspond à : + +- un script d'installation +- un script de configuration +- des métadonnées +- des tests éventuels + +Chaque module doit pouvoir être : + +- installé indépendamment +- intégré dans un menu +- utilisé dans un profil +- testé isolément + +--- + +# Structure du dépôt + +Structure recommandée : + +```text +postinstall-debian/ + +install.sh +README.md +CONSIGNE.md +tools.md +CHANGELOG.md + +assets/ +config/ +core/ +lib/ +menus/ +modules/ +profiles/ +hardware/ +tests/ +docs/ +``` + +--- + +# Organisation détaillée + +## install.sh + +Script principal. + +Fonctions : + +- vérification environnement +- vérification Debian +- gestion sudo/root +- chargement librairies +- chargement thème console +- affichage menu principal + +Doit fonctionner via **curl | bash**. + +--- + +# Dossier core/ + +Scripts internes du framework. + +Exemples : + +- bootstrap.sh +- runtime.sh +- registry.sh +- dispatcher.sh + +Rôle : + +- gestion des modules +- gestion de l'environnement +- découverte des outils + +--- + +# Dossier lib/ + +Fonctions réutilisables. + +Exemples : + +ui.sh +log.sh +package.sh +system.sh +network.sh +prompts.sh +validation.sh + +--- + +# Dossier menus/ + +Gestion des menus interactifs. + +Menus possibles : + +Menu principal +Installation par catégorie +Installation par profil +Installation par matériel +Configuration système +Tests + +--- + +# Dossier modules/ + +Chaque outil possède un dossier dédié. + +Exemple : + +``` +modules/dev/git/ + +module.sh +config.sh +metadata.conf +tests.sh +``` + +Fonctions attendues : + +module_git_metadata +module_git_check +module_git_install +module_git_configure +module_git_test + +--- + +# Catégories d'outils + +Les outils doivent être classés. + +Exemples : + +system +network +dev +gnome +desktop +multimedia +containers +virtualization +security +monitoring +backup +shell +productivity +communication +selfhosted +hardware + +--- + +# Profils + +Les profils installent plusieurs outils. + +Exemples : + +Desktop minimal +Développeur +Homelab +Laptop +VM de test + +Un profil contient une **liste de modules**. + +--- + +# Installation par matériel + +Possibilité d'installer un ensemble d'outils selon le matériel. + +Exemples : + +Laptop ASUS +HP EliteDesk +VM Debian +Poste GNOME + +--- + +# Interface utilisateur + +Le script doit être user friendly. + +Prévoir : + +couleurs +messages clairs +étapes visibles +résumé final + +Fonctions UI : + +ui_header +ui_section +ui_info +ui_success +ui_warn +ui_error +ui_menu +ui_confirm +ui_pause + +--- + +# Journalisation + +Créer un système de logs. + +Objectifs : + +- log installation +- log erreurs +- log tests + +--- + +# Sécurité curl | bash + +Le script doit : + +- vérifier Debian +- vérifier réseau +- vérifier droits + +Éviter : + +- suppression brutale +- modification silencieuse fichiers critiques + +--- + +# Gestion des erreurs + +Prévoir : + +- arrêt propre +- messages clairs +- gestion codes retour + +--- + +# Qualité Bash + +Le code doit : + +- être lisible +- être structuré +- utiliser des fonctions + +Utiliser si possible : + +shellcheck + +--- + +# tools.md + +Ce fichier liste les outils demandés. + +Pour chaque outil : + +nom +description +catégorie +questions +méthode installation +tests +statut + +--- + +# Brainstorming avant ajout d'un outil + +Questions obligatoires : + +Quel est l'outil + +Quel usage + +Installation via apt ? + +Dépendances + +Configuration + +Service systemd + +Interface graphique + +Compatibilité Debian 13 + +Tests possibles + +Menu cible + +Profil cible + +--- + +# Tests + +Types de tests possibles : + +- présence binaire +- version +- service actif +- port ouvert + +Tests VM Debian 13 recommandés. + +--- + +# Intégration d'un outil + +Cycle : + +1 Brainstorming + +2 Décision architecture + +3 Implémentation module + +4 Intégration menu + +5 Tests + +6 Documentation + +--- + +# Gestion des thèmes et ressources visuelles + +Le projet doit permettre d'intégrer des **ressources graphiques et thèmes**. + +Exemples : + +- thèmes GRUB +- thèmes GNOME +- thèmes GTK +- packs d'icônes +- curseurs +- wallpapers +- polices +- thèmes terminal + +Ces ressources doivent être stockées dans **assets/**. + +Structure possible : + +```text +assets/ + +assets/grub/ +assets/gnome/ +assets/icons/ +assets/cursors/ +assets/fonts/ +assets/wallpapers/ +assets/terminal/ +``` + +Chaque thème doit idéalement posséder : + +metadata.conf + +Exemple : + +THEME_ID +THEME_NAME +THEME_TYPE +THEME_TARGET +THEME_DESCRIPTION +THEME_DEPENDENCIES + +--- + +# Menus de personnalisation + +Ajouter un menu : + +Personnalisation + +Sous menus : + +Thèmes GRUB +Thèmes GNOME +Icônes +Curseurs +Wallpapers +Fonts +Terminal + +--- + +# Bonnes pratiques pour GRUB + +Toujours : + +sauvegarder config +régénérer grub +avertir utilisateur + +--- + +# GNOME + +Distinguer : + +GTK +Shell +Icônes +Curseurs + +--- + +# Métadonnées modules + +Chaque module peut définir : + +TOOL_NAME +TOOL_ID +TOOL_CATEGORY +TOOL_DESCRIPTION +TOOL_DEPENDENCIES +TOOL_SUPPORTED_ON +TOOL_TEST_AVAILABLE + +--- + +# Documentation + +Créer documentation : + +architecture.md +add_tool_workflow.md +menu_strategy.md +testing_strategy.md +security_notes.md + +--- + +# README + +Le README doit contenir : + +objectif +installation +structure dépôt +ajout outils + +--- + +# Priorités du projet + +1 Clarté + +2 Stabilité + +3 Modularité + +4 Testabilité + +5 Évolutivité + +6 UX terminal + +--- + +# Conclusion + +Ce dépôt doit devenir une **plateforme de post‑installation Debian 13 modulaire** permettant : + +- installation d'outils +- configuration système +- personnalisation +- évolution continue + +Le projet doit rester lisible, modulaire et maintenable sur le long terme. + diff --git a/core/bootstrap.sh b/core/bootstrap.sh new file mode 100644 index 0000000..bbc2566 --- /dev/null +++ b/core/bootstrap.sh @@ -0,0 +1,98 @@ +#!/usr/bin/env bash + +BOOTSTRAP_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +# shellcheck source=lib/log.sh +source "$BOOTSTRAP_ROOT/lib/log.sh" +# shellcheck source=lib/ui.sh +source "$BOOTSTRAP_ROOT/lib/ui.sh" +# shellcheck source=lib/system.sh +source "$BOOTSTRAP_ROOT/lib/system.sh" +# shellcheck source=lib/network.sh +source "$BOOTSTRAP_ROOT/lib/network.sh" +# shellcheck source=lib/prompts.sh +source "$BOOTSTRAP_ROOT/lib/prompts.sh" +# shellcheck source=lib/validation.sh +source "$BOOTSTRAP_ROOT/lib/validation.sh" +# shellcheck source=core/runtime.sh +source "$BOOTSTRAP_ROOT/core/runtime.sh" +# shellcheck source=core/registry.sh +source "$BOOTSTRAP_ROOT/core/registry.sh" +# shellcheck source=core/dispatcher.sh +source "$BOOTSTRAP_ROOT/core/dispatcher.sh" +# shellcheck source=menus/main.sh +source "$BOOTSTRAP_ROOT/menus/main.sh" + +bootstrap_parse_args() { + BOOTSTRAP_MODE="menu" + BOOTSTRAP_MODULE_ID="" + BOOTSTRAP_TARGET_USER="" + + while [[ $# -gt 0 ]]; do + case "$1" in + --module) + BOOTSTRAP_MODE="module" + BOOTSTRAP_MODULE_ID="${2:-}" + shift 2 + ;; + --user) + BOOTSTRAP_TARGET_USER="${2:-}" + shift 2 + ;; + --help|-h) + BOOTSTRAP_MODE="help" + shift + ;; + *) + ui_error "Argument non reconnu : $1" + return 1 + ;; + esac + done + + if [[ "$BOOTSTRAP_MODE" == "module" && -z "$BOOTSTRAP_MODULE_ID" ]]; then + ui_error "Option --module incomplete" + return 1 + fi +} + +bootstrap_print_help() { + cat <<'EOF' +Usage: + bash install.sh + bash install.sh --module [--user ] + +Exemples: + bash install.sh --module system/user-sudo + bash install.sh --module system/user-sudo --user gilles +EOF +} + +bootstrap_run() { + bootstrap_parse_args "$@" || exit 1 + + runtime_init "$BOOTSTRAP_ROOT" + log_init + + ui_header "Postinstall Debian" + ui_info "Initialisation du framework" + + system_require_bash + system_require_debian + system_require_root + network_warn_if_offline + + registry_init + + case "$BOOTSTRAP_MODE" in + help) + bootstrap_print_help + ;; + module) + dispatcher_run_module "$BOOTSTRAP_MODULE_ID" "$BOOTSTRAP_TARGET_USER" + ;; + *) + menu_main + ;; + esac +} diff --git a/core/dispatcher.sh b/core/dispatcher.sh new file mode 100644 index 0000000..39dfccf --- /dev/null +++ b/core/dispatcher.sh @@ -0,0 +1,190 @@ +#!/usr/bin/env bash + +dispatcher_prompt_and_run_module() { + local module_id="$1" + local target_user="" + local selected_groups="" + local ssh_port="" + local ssh_password_auth="" + local ssh_root_login="" + local avahi_enable="" + local avahi_publish_workstation="" + local host_name="" + local domain_name="" + local iface="" + local ip_mode="" + local ip_address="" + local ip_prefix="" + local ip_gateway="" + local ip_dns="" + local selected_nfs_shares="" + local share_name="" + local share_path="" + local share_user="" + local share_read_only="" + local share_public="" + local archive_name="" + local docker_data_dir="" + local default_nfs_ids="" + local nfs_entry="" + local nfs_item_id="" + local nfs_item_name="" + local nfs_mount_path="" + local nfs_access="" + local nfs_enabled="" + local nfs_item_labels=() + local nfs_item_ids=() + local nfs_labels_blob="" + local default_nfs_indices="" + local nfs_action="" + local nfs_mount_now="" + local nfs_server_mode="" + local samba_mode="" + + case "$module_id" in + system/user-sudo) + target_user="$(prompt_read_default "Nom de l'utilisateur a configurer" "${POSTINSTALL_DEFAULT_USER:-gilles}")" + dispatcher_run_module "$module_id" "$target_user" + ;; + system/user-groups) + target_user="$(prompt_read_default "Nom de l'utilisateur a configurer" "${POSTINSTALL_DEFAULT_USER:-gilles}")" + selected_groups="$(prompt_read_csv_default "Groupes a ajouter, separes par des virgules" "${POSTINSTALL_USER_GROUPS_DEFAULT_GROUPS:-audio,video,plugdev,dialout,netdev,lpadmin,scanner}")" + dispatcher_run_module "$module_id" "$target_user" "$selected_groups" + ;; + network/ssh-server) + ssh_port="$(prompt_read_default "Port SSH" "${POSTINSTALL_SSH_PORT:-22}")" + ssh_password_auth="$(prompt_confirm_default "Autoriser l'authentification par mot de passe" "${POSTINSTALL_SSH_PASSWORD_AUTH:-yes}")" + ssh_root_login="$(prompt_confirm_default "Autoriser la connexion root SSH" "${POSTINSTALL_SSH_ROOT_LOGIN:-no}")" + dispatcher_run_module "$module_id" "$ssh_port" "$ssh_password_auth" "$ssh_root_login" + ;; + network/mdns-avahi) + avahi_enable="$(prompt_confirm_default "Activer la publication mDNS de la machine" "${POSTINSTALL_MDNS_AVAHI_ENABLE:-yes}")" + avahi_publish_workstation="$(prompt_confirm_default "Publier la machine comme poste de travail" "${POSTINSTALL_MDNS_AVAHI_PUBLISH_WORKSTATION:-yes}")" + dispatcher_run_module "$module_id" "$avahi_enable" "$avahi_publish_workstation" + ;; + network/identity) + host_name="$(prompt_read_default "Hostname" "${POSTINSTALL_NETWORK_IDENTITY_DEFAULT_HOSTNAME:-debian}")" + domain_name="$(prompt_read_default "Domaine local" "${POSTINSTALL_NETWORK_IDENTITY_DEFAULT_DOMAIN:-local}")" + dispatcher_run_module "$module_id" "$host_name" "$domain_name" + ;; + network/ip-config) + iface="$(prompt_read_default "Interface reseau" "${POSTINSTALL_NETWORK_IP_DEFAULT_INTERFACE:-$(system_primary_interface)}")" + ip_mode="$(prompt_read_default "Mode reseau (dhcp ou static)" "${POSTINSTALL_NETWORK_IP_DEFAULT_MODE:-dhcp}")" + if [[ "$ip_mode" == "static" ]]; then + ip_address="$(prompt_read_default "Adresse IP" "${POSTINSTALL_NETWORK_IP_DEFAULT_ADDRESS:-10.0.0.10}")" + ip_prefix="$(prompt_read_default "Prefixe CIDR" "${POSTINSTALL_NETWORK_IP_DEFAULT_PREFIX:-22}")" + ip_gateway="$(prompt_read_default "Passerelle" "${POSTINSTALL_NETWORK_IP_DEFAULT_GATEWAY:-10.0.0.1}")" + ip_dns="$(prompt_read_default "DNS" "${POSTINSTALL_NETWORK_IP_DEFAULT_DNS:-10.0.0.1}")" + else + ip_address="" + ip_prefix="" + ip_gateway="" + ip_dns="" + fi + dispatcher_run_module "$module_id" "$iface" "$ip_mode" "$ip_address" "$ip_prefix" "$ip_gateway" "$ip_dns" + ;; + network/nfs-client) + # shellcheck source=/dev/null + source "$RUNTIME_PROJECT_ROOT/modules/network/nfs-client/module.sh" + nfs_action="$(prompt_select_from_list "Action NFS client" "activer des partages" "desactiver des partages")" + nfs_item_labels=() + nfs_item_ids=() + if [[ "$nfs_action" == "desactiver des partages" ]]; then + ui_section "Partages NFS actifs dans fstab" + while IFS= read -r nfs_entry; do + [[ -n "$nfs_entry" ]] || continue + IFS='|' read -r nfs_item_id nfs_item_name nfs_mount_path nfs_remote nfs_access <<< "$nfs_entry" + nfs_item_ids+=("$nfs_item_id") + nfs_item_labels+=("$nfs_item_id : $nfs_item_name -> $nfs_mount_path ($nfs_access)") + done < <(module_nfs_client_active_entries) + if [[ "${#nfs_item_ids[@]}" -eq 0 ]]; then + ui_info "Aucun partage NFS actif a desactiver" + return 0 + fi + nfs_labels_blob="$(printf '%s\n' "${nfs_item_labels[@]}")" + selected_nfs_shares="$(prompt_select_multiple_from_list "Selectionner les partages a retirer de fstab, indexes separes par des virgules" "" "$nfs_labels_blob" "${nfs_item_ids[@]}")" + dispatcher_run_module "$module_id" "disable" "$selected_nfs_shares" + else + ui_section "Partages NFS client disponibles" + while IFS= read -r nfs_entry; do + [[ -n "$nfs_entry" ]] || continue + IFS='|' read -r nfs_item_id nfs_item_name _ _ _ nfs_mount_path nfs_access _ nfs_enabled <<< "$nfs_entry" + nfs_item_ids+=("$nfs_item_id") + nfs_item_labels+=("$nfs_item_id : $nfs_item_name -> $nfs_mount_path ($nfs_access)") + done < <(module_nfs_client_entries) + default_nfs_indices="$(module_nfs_client_default_indices)" + nfs_labels_blob="$(printf '%s\n' "${nfs_item_labels[@]}")" + selected_nfs_shares="$(prompt_select_multiple_from_list "Selectionner les partages a activer dans fstab, indexes separes par des virgules" "$default_nfs_indices" "$nfs_labels_blob" "${nfs_item_ids[@]}")" + nfs_mount_now="$(prompt_confirm_default "Monter immediatement les partages selectionnes" "yes")" + dispatcher_run_module "$module_id" "enable" "$selected_nfs_shares" "$nfs_mount_now" + fi + ;; + network/nfs-server) + nfs_server_mode="$(prompt_select_from_list "Mode de synchronisation NFS serveur" "add-only" "strict")" + ui_info "Synchronisation des exports NFS depuis le fichier YAML du repo" + dispatcher_run_module "$module_id" "$nfs_server_mode" + ;; + network/samba-share) + samba_mode="$(prompt_select_from_list "Mode de synchronisation Samba" "add-only" "strict")" + ui_info "Synchronisation des partages Samba depuis le fichier YAML du repo" + dispatcher_run_module "$module_id" "$samba_mode" + ;; + hardware/detect) + dispatcher_run_module "$module_id" + ;; + boot/grub-theme) + # shellcheck source=/dev/null + source "$RUNTIME_PROJECT_ROOT/modules/boot/grub-theme/module.sh" + mapfile -t grub_archives < <(module_grub_theme_list_archives) + if [[ "${#grub_archives[@]}" -eq 0 ]]; then + ui_warn "Aucune archive de theme GRUB disponible" + return 1 + fi + archive_name="$(prompt_select_from_list "Selectionner une archive de theme" "${grub_archives[@]}")" + dispatcher_run_module "$module_id" "$archive_name" + ;; + containers/docker-engine) + target_user="$(prompt_read_default "Utilisateur a ajouter au groupe docker" "${POSTINSTALL_DOCKER_TARGET_USER:-gilles}")" + docker_data_dir="$(prompt_read_default "Dossier Docker" "/home/${target_user}/docker")" + dispatcher_run_module "$module_id" "$target_user" "$docker_data_dir" + ;; + *) + dispatcher_run_module "$module_id" + ;; + esac +} + +dispatcher_run_module() { + local module_id="$1" + local module_path="" + local module_slug="" + local install_function="" + + shift + + if ! registry_has_module "$module_id"; then + ui_error "Module introuvable : $module_id" + return 1 + fi + + module_path="$(registry_module_path "$module_id")" + # shellcheck source=/dev/null + source "$module_path" + + module_slug="${module_id##*/}" + module_slug="${module_slug//-/_}" + install_function="module_${module_slug}_install" + + if ! declare -F "$install_function" >/dev/null 2>&1; then + ui_error "Fonction d'installation absente pour le module : $module_id" + return 1 + fi + + ui_section "Execution du module $module_id" + "$install_function" "$@" +} + +dispatcher_not_implemented() { + local feature="$1" + ui_warn "Fonction non implementee : $feature" +} diff --git a/core/registry.sh b/core/registry.sh new file mode 100644 index 0000000..033e7ce --- /dev/null +++ b/core/registry.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash + +REGISTRY_MODULE_COUNT=0 +declare -a REGISTRY_MODULES=() +declare -A REGISTRY_MODULE_PATHS=() + +registry_init() { + local module_file="" + local module_id="" + REGISTRY_MODULE_COUNT=0 + REGISTRY_MODULES=() + REGISTRY_MODULE_PATHS=() + + while IFS= read -r module_file; do + # shellcheck source=/dev/null + source "$module_file" + module_id="${module_file#"$RUNTIME_PROJECT_ROOT/modules/"}" + module_id="${module_id%/module.sh}" + REGISTRY_MODULES+=("$module_id") + REGISTRY_MODULE_PATHS["$module_id"]="$module_file" + done < <(find "$RUNTIME_PROJECT_ROOT/modules" -mindepth 3 -maxdepth 3 -type f -name 'module.sh' | sort) + + REGISTRY_MODULE_COUNT="${#REGISTRY_MODULES[@]}" +} + +registry_summary() { + printf '%s' "$REGISTRY_MODULE_COUNT" +} + +registry_list() { + printf '%s\n' "${REGISTRY_MODULES[@]}" +} + +registry_has_module() { + local module_id="$1" + [[ -n "${REGISTRY_MODULE_PATHS[$module_id]:-}" ]] +} + +registry_module_path() { + local module_id="$1" + printf '%s\n' "${REGISTRY_MODULE_PATHS[$module_id]:-}" +} diff --git a/core/runtime.sh b/core/runtime.sh new file mode 100644 index 0000000..132474e --- /dev/null +++ b/core/runtime.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +RUNTIME_PROJECT_ROOT="" +RUNTIME_LOG_DIR="" +RUNTIME_LOG_FILE="" + +runtime_init() { + RUNTIME_PROJECT_ROOT="$1" + RUNTIME_LOG_DIR="${TMPDIR:-/tmp}/postinstall-debian" + RUNTIME_LOG_FILE="$RUNTIME_LOG_DIR/install.log" + mkdir -p "$RUNTIME_LOG_DIR" +} diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..9610a69 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,26 @@ +# Architecture + +## Vue d'ensemble + +Le framework est organise en couches : + +- `install.sh` demarre le bootstrap. +- `core/` orchestre l'execution. +- `lib/` expose les fonctions reutilisables. +- `menus/` porte l'interface interactive. +- `modules/` contient les outils installables. + +## Flux de demarrage + +1. `install.sh` charge `core/bootstrap.sh`. +2. `bootstrap_run` initialise le runtime et le log. +3. Les verifications systeme sont executees. +4. Le registre scanne les modules disponibles. +5. Le menu principal est affiche. + +## Decisions initiales + +- Bash est conserve comme socle unique. +- Les effets de bord systeme sont isoles dans `lib/`. +- Le registre decouvre les modules par arborescence. +- Les modules metier restent independants du framework. diff --git a/docs/lan-validation.md b/docs/lan-validation.md new file mode 100644 index 0000000..eebf193 --- /dev/null +++ b/docs/lan-validation.md @@ -0,0 +1,117 @@ +# Validation LAN + +Ce document sert a verifier le comportement reel du poste Debian sur le reseau local apres application des modules reseau. + +## Preparation + +- appliquer les modules reseau utiles depuis `bash install.sh` +- verifier l'adresse IP finale de la machine +- verifier que le poste Windows 11 est sur le meme reseau local + +## Verification Debian + +### Identite reseau + +```bash +hostnamectl +cat /etc/hostname +getent hosts "$(hostname)" +``` + +### Connectivite IP + +```bash +ip addr +ip route +ping -c 3 10.0.0.1 +``` + +### mDNS et Avahi + +```bash +systemctl status avahi-daemon +grep -E 'disable-publishing|publish-workstation' /etc/avahi/avahi-daemon.conf +avahi-browse -a +ping -c 3 "$(hostname)".local +``` + +### SSH + +```bash +systemctl status ssh +ss -ltnp | grep ':22' +cat /etc/ssh/sshd_config.d/postinstall-debian.conf +``` + +### Samba + +```bash +systemctl status smbd +testparm -s +cat /etc/samba/smb.conf.d/postinstall-home.conf +systemctl status wsdd2 +``` + +### NFS client + +```bash +grep nfs /etc/fstab +mount | grep nfs +``` + +### NFS serveur + +```bash +cat /etc/exports.d/postinstall.exports +exportfs -v +systemctl status nfs-kernel-server +``` + +## Verification Windows 11 + +### Ping et resolution + +Depuis PowerShell : + +```powershell +ping +ping .local +``` + +### SSH + +Si OpenSSH Client est present sur Windows : + +```powershell +ssh gilles@ +``` + +### Samba + +Dans l'explorateur Windows : + +- tester `\\\public` +- tester `\\\public` + +Dans les parametres reseau Windows : + +- verifier que la decouverte reseau est activee +- verifier que le partage de fichiers SMB est autorise + +### Visibilite reseau + +Verifier si la machine Debian apparait dans : + +- `Reseau` +- le workgroup `home` + +Si le poste n'apparait pas mais que `\\ip\public` fonctionne, verifier `wsdd2` sur Debian. + +## Resultat attendu + +- le poste Debian repond au ping sur son IP +- le nom `.local` fonctionne +- SSH accepte les connexions selon la politique definie dans `config/ssh-server.yaml` +- le partage Samba `public` est accessible depuis Linux et Windows 11 +- le poste apparait idealement dans l'explorateur reseau Windows via Samba et `wsdd2` +- les exports NFS et montages NFS sont conformes aux fichiers YAML du repo diff --git a/hardware/README.md b/hardware/README.md new file mode 100644 index 0000000..e609d4e --- /dev/null +++ b/hardware/README.md @@ -0,0 +1,3 @@ +# Hardware + +Ce dossier contiendra des selections de modules associees a un materiel ou a un type de machine. diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..43939c1 --- /dev/null +++ b/install.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash + +set -u + +PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# shellcheck source=core/bootstrap.sh +source "$PROJECT_ROOT/core/bootstrap.sh" + +main() { + bootstrap_run "$@" +} + +main "$@" diff --git a/lib/log.sh b/lib/log.sh new file mode 100644 index 0000000..24820cf --- /dev/null +++ b/lib/log.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +log_init() { + : > "$RUNTIME_LOG_FILE" +} + +log_write() { + local level="$1" + local message="$2" + printf '%s [%s] %s\n' "$(date '+%F %T')" "$level" "$message" >> "$RUNTIME_LOG_FILE" +} + +log_info() { + log_write "INFO" "$1" +} + +log_error() { + log_write "ERROR" "$1" +} diff --git a/lib/network.sh b/lib/network.sh new file mode 100644 index 0000000..7d66160 --- /dev/null +++ b/lib/network.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +network_warn_if_offline() { + if command -v ping >/dev/null 2>&1 && ping -c 1 -W 1 1.1.1.1 >/dev/null 2>&1; then + ui_success "Connectivite reseau disponible" + else + ui_warn "Reseau non verifie ou indisponible" + fi +} diff --git a/lib/package.sh b/lib/package.sh new file mode 100644 index 0000000..acc1819 --- /dev/null +++ b/lib/package.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash + +package_is_installed() { + dpkg -s "$1" >/dev/null 2>&1 +} + +package_refresh_indexes() { + apt-get update +} + +package_install() { + apt-get install -y "$@" +} + +package_remove() { + apt-get remove -y "$@" +} diff --git a/lib/prompts.sh b/lib/prompts.sh new file mode 100644 index 0000000..ec8af97 --- /dev/null +++ b/lib/prompts.sh @@ -0,0 +1,125 @@ +#!/usr/bin/env bash + +prompt_read_default() { + local label="$1" + local default_value="${2:-}" + local answer="" + + if [[ -n "$default_value" ]]; then + read -r -p "$label [$default_value] : " answer + printf '%s\n' "${answer:-$default_value}" + else + read -r -p "$label : " answer + printf '%s\n' "$answer" + fi +} + +prompt_select_number() { + local label="$1" + local min_value="$2" + local max_value="$3" + local answer="" + + while true; do + read -r -p "$label [$min_value-$max_value] : " answer + if [[ "$answer" =~ ^[0-9]+$ ]] && (( answer >= min_value && answer <= max_value )); then + printf '%s\n' "$answer" + return 0 + fi + ui_warn "Choix invalide" + done +} + +prompt_read_csv_default() { + local label="$1" + local default_value="$2" + local answer="" + + read -r -p "$label [$default_value] : " answer + printf '%s\n' "${answer:-$default_value}" +} + +prompt_confirm_default() { + local label="$1" + local default_answer="${2:-n}" + local answer="" + local prompt_suffix="[y/N]" + + if [[ "$default_answer" =~ ^[Yy]$ ]]; then + prompt_suffix="[Y/n]" + fi + + while true; do + read -r -p "$label $prompt_suffix : " answer + answer="${answer:-$default_answer}" + + case "$answer" in + y|Y|yes|YES) + printf 'yes\n' + return 0 + ;; + n|N|no|NO) + printf 'no\n' + return 0 + ;; + esac + + ui_warn "Repondre par y ou n" + done +} + +prompt_select_from_list() { + local label="$1" + shift + local options=("$@") + local index=1 + local selection="" + + for selection in "${options[@]}"; do + printf ' %d. %s\n' "$index" "$selection" + index=$((index + 1)) + done + + selection="$(prompt_select_number "$label" 1 "${#options[@]}")" + printf '%s\n' "${options[$((selection - 1))]}" +} + +prompt_select_multiple_from_list() { + local label="$1" + local default_indices="${2:-}" + local labels_csv="$3" + shift 3 + local values=("$@") + local labels=() + local answer="" + local raw_index="" + local selected_values="" + local index=1 + + IFS=$'\n' read -r -d '' -a labels < <(printf '%s\0' "$labels_csv") + + for raw_index in "${labels[@]}"; do + printf ' %d. %s\n' "$index" "$raw_index" + index=$((index + 1)) + done + + read -r -p "$label [${default_indices:-aucun}] : " answer + answer="${answer:-$default_indices}" + + if [[ -z "$answer" ]]; then + printf '\n' + return 0 + fi + + while IFS= read -r raw_index; do + [[ -n "$raw_index" ]] || continue + raw_index="$(printf '%s' "$raw_index" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')" + if [[ ! "$raw_index" =~ ^[0-9]+$ ]] || (( raw_index < 1 || raw_index > ${#values[@]} )); then + ui_warn "Index ignore : $raw_index" + continue + fi + selected_values="${selected_values:+$selected_values,}${values[$((raw_index - 1))]}" + done < <(printf '%s\n' "$answer" | tr ',' '\n') + + printf '%s\n' "$selected_values" +} diff --git a/lib/system.sh b/lib/system.sh new file mode 100644 index 0000000..156b3fb --- /dev/null +++ b/lib/system.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash + +system_require_bash() { + if [[ -z "${BASH_VERSION:-}" ]]; then + printf 'Bash est requis.\n' >&2 + exit 1 + fi +} + +system_require_debian() { + if [[ ! -r /etc/os-release ]]; then + ui_error "Impossible de detecter le systeme" + exit 1 + fi + + if ! grep -Eq '^ID=debian$|^ID_LIKE=.*debian' /etc/os-release; then + ui_error "Ce script cible Debian" + exit 1 + fi + + ui_success "Systeme Debian detecte" +} + +system_require_root() { + if [[ "${EUID:-$(id -u)}" -ne 0 ]]; then + ui_error "Relancer ce script avec sudo ou en root" + exit 1 + fi + + ui_success "Privileges root valides" +} + +system_user_exists() { + local user_name="$1" + id "$user_name" >/dev/null 2>&1 +} + +system_group_exists() { + local group_name="$1" + getent group "$group_name" >/dev/null 2>&1 +} + +system_user_in_group() { + local user_name="$1" + local group_name="$2" + id -nG "$user_name" 2>/dev/null | tr ' ' '\n' | grep -Fx "$group_name" >/dev/null 2>&1 +} + +system_primary_interface() { + ip route get 1.1.1.1 2>/dev/null | awk ' + /dev/ { + for (i = 1; i <= NF; i++) { + if ($i == "dev") { + print $(i + 1) + exit + } + } + }' +} diff --git a/lib/ui.sh b/lib/ui.sh new file mode 100644 index 0000000..1ebd689 --- /dev/null +++ b/lib/ui.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash + +UI_RESET=$'\033[0m' +UI_BOLD=$'\033[1m' +UI_BLUE=$'\033[34m' +UI_GREEN=$'\033[32m' +UI_YELLOW=$'\033[33m' +UI_RED=$'\033[31m' + +ui_header() { + local message="$1" + printf '\n%s%s== %s ==%s\n\n' "$UI_BOLD" "$UI_BLUE" "$message" "$UI_RESET" +} + +ui_section() { + local message="$1" + printf '%s[%s]%s\n' "$UI_BLUE" "$message" "$UI_RESET" +} + +ui_info() { + local message="$1" + printf '%s[INFO]%s %s\n' "$UI_BLUE" "$UI_RESET" "$message" +} + +ui_success() { + local message="$1" + printf '%s[OK]%s %s\n' "$UI_GREEN" "$UI_RESET" "$message" +} + +ui_warn() { + local message="$1" + printf '%s[WARN]%s %s\n' "$UI_YELLOW" "$UI_RESET" "$message" +} + +ui_error() { + local message="$1" + printf '%s[ERR]%s %s\n' "$UI_RED" "$UI_RESET" "$message" >&2 +} + +ui_menu() { + local title="$1" + shift + local option + + ui_section "$title" + for option in "$@"; do + printf ' - %s\n' "$option" + done +} + +ui_confirm() { + local prompt="$1" + local answer="" + + read -r -p "$prompt [y/N] " answer + [[ "$answer" =~ ^[Yy]$ ]] +} + +ui_pause() { + read -r -p "Appuyer sur Entree pour continuer... " _ +} diff --git a/lib/validation.sh b/lib/validation.sh new file mode 100644 index 0000000..bdbb1c1 --- /dev/null +++ b/lib/validation.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +validation_require_file() { + local file_path="$1" + + if [[ ! -f "$file_path" ]]; then + ui_error "Fichier requis manquant : $file_path" + exit 1 + fi +} diff --git a/menus/main.sh b/menus/main.sh new file mode 100644 index 0000000..93fd526 --- /dev/null +++ b/menus/main.sh @@ -0,0 +1,101 @@ +#!/usr/bin/env bash + +menu_modules_by_prefix() { + local prefix="$1" + local title="$2" + local matching_modules=() + local module_id="" + local selection="" + local index=1 + + while IFS= read -r module_id; do + [[ "$module_id" == "$prefix"/* ]] || continue + matching_modules+=("$module_id") + done < <(registry_list) + + if [[ "${#matching_modules[@]}" -eq 0 ]]; then + ui_warn "Aucun module disponible pour $title" + ui_pause + return 0 + fi + + ui_section "$title" + for module_id in "${matching_modules[@]}"; do + printf ' %d. %s\n' "$index" "$module_id" + index=$((index + 1)) + done + printf ' 0. Retour\n' + + selection="$(prompt_select_number "Selectionner un module" 0 "${#matching_modules[@]}")" + if [[ "$selection" == "0" ]]; then + return 0 + fi + + dispatcher_prompt_and_run_module "${matching_modules[$((selection - 1))]}" + ui_pause +} + +menu_category_selection() { + local categories=("network" "containers" "boot" "hardware") + local selection="" + + ui_section "Installation par categorie" + printf ' 1. network\n' + printf ' 2. containers\n' + printf ' 3. boot\n' + printf ' 4. hardware\n' + printf ' 0. Retour\n' + + selection="$(prompt_select_number "Selectionner une categorie" 0 4)" + case "$selection" in + 0) return 0 ;; + 1) menu_modules_by_prefix "network" "Configuration reseau" ;; + 2) menu_modules_by_prefix "containers" "Conteneurs" ;; + 3) menu_modules_by_prefix "boot" "Configuration du boot" ;; + 4) menu_modules_by_prefix "hardware" "Materiel" ;; + esac +} + +menu_system_configuration() { + menu_modules_by_prefix "system" "Configuration systeme" +} + +menu_main() { + local selection="" + + while true; do + ui_menu \ + "Menu principal" \ + "1. Installation par categorie" \ + "2. Installation par profil" \ + "3. Installation par materiel" \ + "4. Configuration systeme" \ + "5. Tests" \ + "0. Quitter" + + ui_info "Modules detectes : $(registry_summary)" + ui_info "Log : $RUNTIME_LOG_FILE" + + selection="$(prompt_select_number "Choisir une action" 0 5)" + + case "$selection" in + 0) + ui_info "Sortie du programme" + return 0 + ;; + 1) + menu_category_selection + ;; + 3) + menu_modules_by_prefix "hardware" "Materiel" + ;; + 4) + menu_system_configuration + ;; + *) + dispatcher_not_implemented "menu $selection" + ui_pause + ;; + esac + done +} diff --git a/modules/boot/grub-theme/config.sh b/modules/boot/grub-theme/config.sh new file mode 100644 index 0000000..3851299 --- /dev/null +++ b/modules/boot/grub-theme/config.sh @@ -0,0 +1,4 @@ +POSTINSTALL_GRUB_THEME_ARCHIVE_DIR_PRIMARY="assets/grub" +POSTINSTALL_GRUB_THEME_ARCHIVE_DIR_FALLBACK="themes/grub" +POSTINSTALL_GRUB_THEME_INSTALL_DIR="/boot/grub/themes/postinstall-debian" +POSTINSTALL_GRUB_THEME_CONFIG_FILE="/etc/default/grub.d/postinstall-debian.cfg" diff --git a/modules/boot/grub-theme/metadata.conf b/modules/boot/grub-theme/metadata.conf new file mode 100644 index 0000000..e7d2878 --- /dev/null +++ b/modules/boot/grub-theme/metadata.conf @@ -0,0 +1,4 @@ +MODULE_ID="boot/grub-theme" +MODULE_NAME="Theme GRUB" +MODULE_CATEGORY="boot" +MODULE_DESCRIPTION="Installe un theme GRUB depuis les archives locales du depot" diff --git a/modules/boot/grub-theme/module.sh b/modules/boot/grub-theme/module.sh new file mode 100644 index 0000000..03c3c57 --- /dev/null +++ b/modules/boot/grub-theme/module.sh @@ -0,0 +1,85 @@ +#!/usr/bin/env bash + +MODULE_GRUB_THEME_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +MODULE_GRUB_THEME_PROJECT_ROOT="$(cd "$MODULE_GRUB_THEME_DIR/../../.." && pwd)" + +# shellcheck source=lib/package.sh +source "$MODULE_GRUB_THEME_PROJECT_ROOT/lib/package.sh" +# shellcheck source=modules/boot/grub-theme/config.sh +source "$MODULE_GRUB_THEME_DIR/config.sh" +# shellcheck source=modules/boot/grub-theme/metadata.conf +source "$MODULE_GRUB_THEME_DIR/metadata.conf" + +module_grub_theme_metadata() { + printf '%s|%s|%s\n' "$MODULE_ID" "$MODULE_NAME" "$MODULE_DESCRIPTION" +} + +module_grub_theme_archive_dir() { + if [[ -d "$MODULE_GRUB_THEME_PROJECT_ROOT/$POSTINSTALL_GRUB_THEME_ARCHIVE_DIR_PRIMARY" ]]; then + printf '%s\n' "$MODULE_GRUB_THEME_PROJECT_ROOT/$POSTINSTALL_GRUB_THEME_ARCHIVE_DIR_PRIMARY" + else + printf '%s\n' "$MODULE_GRUB_THEME_PROJECT_ROOT/$POSTINSTALL_GRUB_THEME_ARCHIVE_DIR_FALLBACK" + fi +} + +module_grub_theme_list_archives() { + local archive_dir="" + archive_dir="$(module_grub_theme_archive_dir)" + find "$archive_dir" -maxdepth 1 -type f \( -name '*.zip' -o -name '*.tar.gz' -o -name '*.tgz' \) -printf '%f\n' | sort +} + +module_grub_theme_extract() { + local archive_name="$1" + local archive_dir="" + local archive_path="" + local target_dir="" + + archive_dir="$(module_grub_theme_archive_dir)" + archive_path="$archive_dir/$archive_name" + target_dir="$POSTINSTALL_GRUB_THEME_INSTALL_DIR/${archive_name%.*}" + + mkdir -p "$target_dir" + + case "$archive_name" in + *.zip) + if ! package_is_installed unzip; then + package_refresh_indexes + package_install unzip + fi + unzip -o "$archive_path" -d "$target_dir" >/dev/null + ;; + *.tar.gz|*.tgz) + tar -xzf "$archive_path" -C "$target_dir" + ;; + *) + ui_error "Archive de theme non supportee : $archive_name" + return 1 + ;; + esac + + find "$target_dir" -type f -name 'theme.txt' | head -n 1 +} + +module_grub_theme_install() { + local archive_name="$1" + local theme_path="" + + if [[ -z "$archive_name" ]]; then + ui_error "Aucune archive de theme specifiee" + return 1 + fi + + mkdir -p /etc/default/grub.d + theme_path="$(module_grub_theme_extract "$archive_name")" || return 1 + + printf 'GRUB_THEME="%s"\n' "$theme_path" > "$POSTINSTALL_GRUB_THEME_CONFIG_FILE" + update-grub + + log_info "Theme GRUB configure : $archive_name" + ui_success "Theme GRUB configure : $archive_name" +} + +module_grub_theme_test() { + test -f "$POSTINSTALL_GRUB_THEME_CONFIG_FILE" || return 1 + grep -q '^GRUB_THEME=' "$POSTINSTALL_GRUB_THEME_CONFIG_FILE" || return 1 +} diff --git a/modules/boot/grub-theme/tests.sh b/modules/boot/grub-theme/tests.sh new file mode 100644 index 0000000..dd75063 --- /dev/null +++ b/modules/boot/grub-theme/tests.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash + +MODULE_GRUB_THEME_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$MODULE_GRUB_THEME_DIR/../../.." && pwd)" + +# shellcheck source=lib/ui.sh +source "$PROJECT_ROOT/lib/ui.sh" +# shellcheck source=lib/log.sh +source "$PROJECT_ROOT/lib/log.sh" +# shellcheck source=core/runtime.sh +source "$PROJECT_ROOT/core/runtime.sh" +# shellcheck source=modules/boot/grub-theme/module.sh +source "$MODULE_GRUB_THEME_DIR/module.sh" + +runtime_init "$PROJECT_ROOT" +log_init + +if ! test -f /etc/default/grub.d/postinstall-debian.cfg; then + printf 'grub-theme test SKIPPED: module configuration not applied\n' + exit 0 +fi + +if module_grub_theme_test; then + printf 'grub-theme test OK\n' +else + printf 'grub-theme test FAILED\n' >&2 + exit 1 +fi diff --git a/modules/containers/docker-engine/config.sh b/modules/containers/docker-engine/config.sh new file mode 100644 index 0000000..92f8176 --- /dev/null +++ b/modules/containers/docker-engine/config.sh @@ -0,0 +1,5 @@ +POSTINSTALL_DOCKER_TARGET_USER="${POSTINSTALL_DEFAULT_USER:-gilles}" +POSTINSTALL_DOCKER_DATA_DIR="/home/${POSTINSTALL_DEFAULT_USER:-gilles}/docker" +POSTINSTALL_DOCKER_KEYRING_DIR="/etc/apt/keyrings" +POSTINSTALL_DOCKER_KEYRING_FILE="/etc/apt/keyrings/docker.asc" +POSTINSTALL_DOCKER_SOURCES_FILE="/etc/apt/sources.list.d/docker.list" diff --git a/modules/containers/docker-engine/metadata.conf b/modules/containers/docker-engine/metadata.conf new file mode 100644 index 0000000..354f361 --- /dev/null +++ b/modules/containers/docker-engine/metadata.conf @@ -0,0 +1,4 @@ +MODULE_ID="containers/docker-engine" +MODULE_NAME="Installation de Docker Engine" +MODULE_CATEGORY="containers" +MODULE_DESCRIPTION="Installe Docker Engine via le depot officiel Docker pour Debian" diff --git a/modules/containers/docker-engine/module.sh b/modules/containers/docker-engine/module.sh new file mode 100644 index 0000000..65ce1d7 --- /dev/null +++ b/modules/containers/docker-engine/module.sh @@ -0,0 +1,74 @@ +#!/usr/bin/env bash + +MODULE_DOCKER_ENGINE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +MODULE_DOCKER_ENGINE_PROJECT_ROOT="$(cd "$MODULE_DOCKER_ENGINE_DIR/../../.." && pwd)" + +# shellcheck source=lib/package.sh +source "$MODULE_DOCKER_ENGINE_PROJECT_ROOT/lib/package.sh" +# shellcheck source=lib/system.sh +source "$MODULE_DOCKER_ENGINE_PROJECT_ROOT/lib/system.sh" +# shellcheck source=modules/containers/docker-engine/config.sh +source "$MODULE_DOCKER_ENGINE_DIR/config.sh" +# shellcheck source=modules/containers/docker-engine/metadata.conf +source "$MODULE_DOCKER_ENGINE_DIR/metadata.conf" + +module_docker_engine_metadata() { + printf '%s|%s|%s\n' "$MODULE_ID" "$MODULE_NAME" "$MODULE_DESCRIPTION" +} + +module_docker_engine_remove_conflicts() { + local packages=(docker.io docker-doc docker-compose podman-docker containerd runc) + local pkg="" + + for pkg in "${packages[@]}"; do + if package_is_installed "$pkg"; then + package_remove "$pkg" + fi + done +} + +module_docker_engine_install() { + local target_user="${1:-$POSTINSTALL_DOCKER_TARGET_USER}" + local data_dir="${2:-/home/$target_user/docker}" + local version_codename="" + local arch="" + + package_refresh_indexes + package_install ca-certificates curl + install -m 0755 -d "$POSTINSTALL_DOCKER_KEYRING_DIR" + curl -fsSL https://download.docker.com/linux/debian/gpg -o "$POSTINSTALL_DOCKER_KEYRING_FILE" + chmod a+r "$POSTINSTALL_DOCKER_KEYRING_FILE" + + version_codename="$(. /etc/os-release && printf '%s' "$VERSION_CODENAME")" + arch="$(dpkg --print-architecture)" + printf 'deb [arch=%s signed-by=%s] https://download.docker.com/linux/debian %s stable\n' "$arch" "$POSTINSTALL_DOCKER_KEYRING_FILE" "$version_codename" > "$POSTINSTALL_DOCKER_SOURCES_FILE" + + module_docker_engine_remove_conflicts + package_refresh_indexes + package_install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin + + if ! system_group_exists docker; then + groupadd docker + fi + + if system_user_exists "$target_user" && ! system_user_in_group "$target_user" docker; then + usermod -aG docker "$target_user" + fi + + mkdir -p "$data_dir" + if system_user_exists "$target_user"; then + chown "$target_user:$target_user" "$data_dir" + fi + + systemctl enable --now docker + systemctl restart docker + + log_info "Docker Engine installe pour $target_user" + ui_success "Docker Engine installe" +} + +module_docker_engine_test() { + command -v docker >/dev/null 2>&1 || return 1 + docker --version >/dev/null 2>&1 || return 1 + docker compose version >/dev/null 2>&1 || return 1 +} diff --git a/modules/containers/docker-engine/tests.sh b/modules/containers/docker-engine/tests.sh new file mode 100644 index 0000000..4f8bf39 --- /dev/null +++ b/modules/containers/docker-engine/tests.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash + +MODULE_DOCKER_ENGINE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$MODULE_DOCKER_ENGINE_DIR/../../.." && pwd)" + +# shellcheck source=lib/ui.sh +source "$PROJECT_ROOT/lib/ui.sh" +# shellcheck source=lib/log.sh +source "$PROJECT_ROOT/lib/log.sh" +# shellcheck source=core/runtime.sh +source "$PROJECT_ROOT/core/runtime.sh" +# shellcheck source=modules/containers/docker-engine/module.sh +source "$MODULE_DOCKER_ENGINE_DIR/module.sh" + +runtime_init "$PROJECT_ROOT" +log_init + +if ! command -v docker >/dev/null 2>&1; then + printf 'docker-engine test SKIPPED: docker not installed\n' + exit 0 +fi + +if module_docker_engine_test; then + printf 'docker-engine test OK\n' +else + printf 'docker-engine test FAILED\n' >&2 + exit 1 +fi diff --git a/modules/hardware/detect/config.sh b/modules/hardware/detect/config.sh new file mode 100644 index 0000000..64ad33f --- /dev/null +++ b/modules/hardware/detect/config.sh @@ -0,0 +1 @@ +POSTINSTALL_HARDWARE_DETECT_REPORT_FILE="/var/log/postinstall-debian/hardware-report.txt" diff --git a/modules/hardware/detect/metadata.conf b/modules/hardware/detect/metadata.conf new file mode 100644 index 0000000..4d39879 --- /dev/null +++ b/modules/hardware/detect/metadata.conf @@ -0,0 +1,4 @@ +MODULE_ID="hardware/detect" +MODULE_NAME="Detection hardware" +MODULE_CATEGORY="hardware" +MODULE_DESCRIPTION="Installe les outils de detection et genere un rapport materiel" diff --git a/modules/hardware/detect/module.sh b/modules/hardware/detect/module.sh new file mode 100644 index 0000000..c278ded --- /dev/null +++ b/modules/hardware/detect/module.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash + +MODULE_HARDWARE_DETECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +MODULE_HARDWARE_DETECT_PROJECT_ROOT="$(cd "$MODULE_HARDWARE_DETECT_DIR/../../.." && pwd)" + +# shellcheck source=lib/package.sh +source "$MODULE_HARDWARE_DETECT_PROJECT_ROOT/lib/package.sh" +# shellcheck source=modules/hardware/detect/config.sh +source "$MODULE_HARDWARE_DETECT_DIR/config.sh" +# shellcheck source=modules/hardware/detect/metadata.conf +source "$MODULE_HARDWARE_DETECT_DIR/metadata.conf" + +module_detect_metadata() { + printf '%s|%s|%s\n' "$MODULE_ID" "$MODULE_NAME" "$MODULE_DESCRIPTION" +} + +module_detect_install() { + package_refresh_indexes + package_install lshw pciutils usbutils dmidecode + + mkdir -p "$(dirname "$POSTINSTALL_HARDWARE_DETECT_REPORT_FILE")" + { + printf '== lshw -short ==\n' + lshw -short 2>/dev/null || true + printf '\n== lspci ==\n' + lspci 2>/dev/null || true + printf '\n== lsusb ==\n' + lsusb 2>/dev/null || true + printf '\n== dmidecode -t system ==\n' + dmidecode -t system 2>/dev/null || true + } > "$POSTINSTALL_HARDWARE_DETECT_REPORT_FILE" + + log_info "Rapport hardware genere : $POSTINSTALL_HARDWARE_DETECT_REPORT_FILE" + ui_success "Rapport hardware genere" +} + +module_detect_test() { + package_is_installed lshw || return 1 + package_is_installed pciutils || return 1 + package_is_installed usbutils || return 1 + package_is_installed dmidecode || return 1 + test -s "$POSTINSTALL_HARDWARE_DETECT_REPORT_FILE" +} diff --git a/modules/hardware/detect/tests.sh b/modules/hardware/detect/tests.sh new file mode 100644 index 0000000..62039bf --- /dev/null +++ b/modules/hardware/detect/tests.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash + +MODULE_HARDWARE_DETECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$MODULE_HARDWARE_DETECT_DIR/../../.." && pwd)" + +# shellcheck source=lib/ui.sh +source "$PROJECT_ROOT/lib/ui.sh" +# shellcheck source=lib/log.sh +source "$PROJECT_ROOT/lib/log.sh" +# shellcheck source=lib/package.sh +source "$PROJECT_ROOT/lib/package.sh" +# shellcheck source=core/runtime.sh +source "$PROJECT_ROOT/core/runtime.sh" +# shellcheck source=modules/hardware/detect/module.sh +source "$MODULE_HARDWARE_DETECT_DIR/module.sh" + +runtime_init "$PROJECT_ROOT" +log_init + +if ! package_is_installed lshw; then + printf 'hardware-detect test SKIPPED: lshw not installed\n' + exit 0 +fi + +if ! test -f /var/log/postinstall-debian/hardware-report.txt; then + printf 'hardware-detect test SKIPPED: report not generated\n' + exit 0 +fi + +if module_detect_test; then + printf 'hardware-detect test OK\n' +else + printf 'hardware-detect test FAILED\n' >&2 + exit 1 +fi diff --git a/modules/network/identity/config.sh b/modules/network/identity/config.sh new file mode 100644 index 0000000..ce3c946 --- /dev/null +++ b/modules/network/identity/config.sh @@ -0,0 +1,3 @@ +POSTINSTALL_NETWORK_IDENTITY_DEFAULT_HOSTNAME="${HOSTNAME:-debian}" +POSTINSTALL_NETWORK_IDENTITY_DEFAULT_DOMAIN="local" +POSTINSTALL_NETWORK_IDENTITY_STATE_FILE="/etc/postinstall-debian/network-identity.conf" diff --git a/modules/network/identity/metadata.conf b/modules/network/identity/metadata.conf new file mode 100644 index 0000000..98011c1 --- /dev/null +++ b/modules/network/identity/metadata.conf @@ -0,0 +1,4 @@ +MODULE_ID="network/identity" +MODULE_NAME="Identite reseau" +MODULE_CATEGORY="network" +MODULE_DESCRIPTION="Configure le hostname et l'identite locale de la machine" diff --git a/modules/network/identity/module.sh b/modules/network/identity/module.sh new file mode 100644 index 0000000..8633949 --- /dev/null +++ b/modules/network/identity/module.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash + +MODULE_NETWORK_IDENTITY_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +MODULE_NETWORK_IDENTITY_PROJECT_ROOT="$(cd "$MODULE_NETWORK_IDENTITY_DIR/../../.." && pwd)" + +# shellcheck source=modules/network/identity/config.sh +source "$MODULE_NETWORK_IDENTITY_DIR/config.sh" +# shellcheck source=modules/network/identity/metadata.conf +source "$MODULE_NETWORK_IDENTITY_DIR/metadata.conf" + +module_identity_metadata() { + printf '%s|%s|%s\n' "$MODULE_ID" "$MODULE_NAME" "$MODULE_DESCRIPTION" +} + +module_identity_validate_hostname() { + local host_name="$1" + [[ "$host_name" =~ ^[a-zA-Z0-9][a-zA-Z0-9-]{0,62}$ ]] +} + +module_identity_update_hosts() { + local host_name="$1" + local domain_name="${2:-}" + local fqdn="$host_name" + local temp_file="" + + if [[ -n "$domain_name" ]]; then + fqdn="$host_name.$domain_name" + fi + + temp_file="$(mktemp)" + awk '!/^127\.0\.1\.1[[:space:]]/' /etc/hosts > "$temp_file" + printf '127.0.1.1 %s %s\n' "$fqdn" "$host_name" >> "$temp_file" + cat "$temp_file" > /etc/hosts + rm -f "$temp_file" +} + +module_identity_install() { + local host_name="${1:-$POSTINSTALL_NETWORK_IDENTITY_DEFAULT_HOSTNAME}" + local domain_name="${2:-$POSTINSTALL_NETWORK_IDENTITY_DEFAULT_DOMAIN}" + + if ! module_identity_validate_hostname "$host_name"; then + ui_error "Hostname invalide : $host_name" + return 1 + fi + + if command -v hostnamectl >/dev/null 2>&1; then + hostnamectl set-hostname "$host_name" + else + hostname "$host_name" + fi + printf '%s\n' "$host_name" > /etc/hostname + module_identity_update_hosts "$host_name" "$domain_name" + + mkdir -p "$(dirname "$POSTINSTALL_NETWORK_IDENTITY_STATE_FILE")" + { + printf 'HOSTNAME=%s\n' "$host_name" + printf 'DOMAIN=%s\n' "$domain_name" + } > "$POSTINSTALL_NETWORK_IDENTITY_STATE_FILE" + + log_info "Hostname configure : $host_name" + ui_success "Hostname configure : $host_name" +} + +module_identity_test() { + test -f "$POSTINSTALL_NETWORK_IDENTITY_STATE_FILE" || return 1 + test -s /etc/hostname || return 1 + hostnamectl >/dev/null 2>&1 || return 1 +} diff --git a/modules/network/identity/tests.sh b/modules/network/identity/tests.sh new file mode 100644 index 0000000..8caca47 --- /dev/null +++ b/modules/network/identity/tests.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash + +MODULE_NETWORK_IDENTITY_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$MODULE_NETWORK_IDENTITY_DIR/../../.." && pwd)" + +# shellcheck source=lib/ui.sh +source "$PROJECT_ROOT/lib/ui.sh" +# shellcheck source=lib/log.sh +source "$PROJECT_ROOT/lib/log.sh" +# shellcheck source=core/runtime.sh +source "$PROJECT_ROOT/core/runtime.sh" +# shellcheck source=modules/network/identity/module.sh +source "$MODULE_NETWORK_IDENTITY_DIR/module.sh" + +runtime_init "$PROJECT_ROOT" +log_init + +if ! test -f /etc/postinstall-debian/network-identity.conf; then + printf 'network-identity test SKIPPED: module configuration not applied\n' + exit 0 +fi + +if module_identity_test; then + printf 'network-identity test OK\n' +else + printf 'network-identity test FAILED\n' >&2 + exit 1 +fi diff --git a/modules/network/ip-config/config.sh b/modules/network/ip-config/config.sh new file mode 100644 index 0000000..de4684a --- /dev/null +++ b/modules/network/ip-config/config.sh @@ -0,0 +1,7 @@ +POSTINSTALL_NETWORK_IP_DEFAULT_INTERFACE="" +POSTINSTALL_NETWORK_IP_DEFAULT_MODE="dhcp" +POSTINSTALL_NETWORK_IP_DEFAULT_ADDRESS="10.0.0.10" +POSTINSTALL_NETWORK_IP_DEFAULT_PREFIX="22" +POSTINSTALL_NETWORK_IP_DEFAULT_GATEWAY="10.0.0.1" +POSTINSTALL_NETWORK_IP_DEFAULT_DNS="10.0.0.1" +POSTINSTALL_NETWORK_IP_STATE_FILE="/etc/postinstall-debian/network-ip-config.conf" diff --git a/modules/network/ip-config/metadata.conf b/modules/network/ip-config/metadata.conf new file mode 100644 index 0000000..151fa95 --- /dev/null +++ b/modules/network/ip-config/metadata.conf @@ -0,0 +1,4 @@ +MODULE_ID="network/ip-config" +MODULE_NAME="Configuration IP initiale" +MODULE_CATEGORY="network" +MODULE_DESCRIPTION="Configure une interface reseau en DHCP ou en IP statique" diff --git a/modules/network/ip-config/module.sh b/modules/network/ip-config/module.sh new file mode 100644 index 0000000..1046c1a --- /dev/null +++ b/modules/network/ip-config/module.sh @@ -0,0 +1,157 @@ +#!/usr/bin/env bash + +MODULE_NETWORK_IP_CONFIG_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +MODULE_NETWORK_IP_CONFIG_PROJECT_ROOT="$(cd "$MODULE_NETWORK_IP_CONFIG_DIR/../../.." && pwd)" + +# shellcheck source=lib/system.sh +source "$MODULE_NETWORK_IP_CONFIG_PROJECT_ROOT/lib/system.sh" +# shellcheck source=modules/network/ip-config/config.sh +source "$MODULE_NETWORK_IP_CONFIG_DIR/config.sh" +# shellcheck source=modules/network/ip-config/metadata.conf +source "$MODULE_NETWORK_IP_CONFIG_DIR/metadata.conf" + +module_ip_config_metadata() { + printf '%s|%s|%s\n' "$MODULE_ID" "$MODULE_NAME" "$MODULE_DESCRIPTION" +} + +module_ip_config_detect_backend() { + if command -v nmcli >/dev/null 2>&1 && systemctl is-active --quiet NetworkManager; then + printf 'networkmanager\n' + elif [[ -d /etc/systemd/network ]]; then + printf 'networkd\n' + else + printf 'ifupdown\n' + fi +} + +module_ip_config_validate_mode() { + [[ "$1" == "dhcp" || "$1" == "static" ]] +} + +module_ip_config_nmcli_connection_for_device() { + local iface="$1" + nmcli -t -f NAME,DEVICE connection show | awk -F: -v iface="$iface" '$2 == iface { print $1; exit }' +} + +module_ip_config_write_networkd() { + local iface="$1" + local mode="$2" + local address="$3" + local prefix="$4" + local gateway="$5" + local dns="$6" + local file_path="/etc/systemd/network/10-postinstall-${iface}.network" + + mkdir -p /etc/systemd/network + { + printf '[Match]\nName=%s\n\n' "$iface" + printf '[Network]\n' + if [[ "$mode" == "dhcp" ]]; then + printf 'DHCP=yes\n' + else + printf 'Address=%s/%s\n' "$address" "$prefix" + printf 'Gateway=%s\n' "$gateway" + printf 'DNS=%s\n' "$dns" + fi + } > "$file_path" + + if systemctl is-active --quiet systemd-networkd; then + systemctl restart systemd-networkd + fi +} + +module_ip_config_write_ifupdown() { + local iface="$1" + local mode="$2" + local address="$3" + local prefix="$4" + local gateway="$5" + local dns="$6" + local netmask="255.255.252.0" + local file_path="/etc/network/interfaces.d/postinstall-${iface}" + + mkdir -p /etc/network/interfaces.d + { + printf 'auto %s\n' "$iface" + if [[ "$mode" == "dhcp" ]]; then + printf 'iface %s inet dhcp\n' "$iface" + else + printf 'iface %s inet static\n' "$iface" + printf ' address %s/%s\n' "$address" "$prefix" + printf ' gateway %s\n' "$gateway" + printf ' dns-nameservers %s\n' "$dns" + printf ' netmask %s\n' "$netmask" + fi + } > "$file_path" +} + +module_ip_config_write_networkmanager() { + local iface="$1" + local mode="$2" + local address="$3" + local prefix="$4" + local gateway="$5" + local dns="$6" + local connection_name="" + + connection_name="$(module_ip_config_nmcli_connection_for_device "$iface")" + if [[ -z "$connection_name" ]]; then + connection_name="postinstall-${iface}" + nmcli connection add type ethernet ifname "$iface" con-name "$connection_name" >/dev/null + fi + + if [[ "$mode" == "dhcp" ]]; then + nmcli connection modify "$connection_name" ipv4.method auto ipv4.addresses "" ipv4.gateway "" ipv4.dns "" + else + nmcli connection modify "$connection_name" ipv4.method manual ipv4.addresses "$address/$prefix" ipv4.gateway "$gateway" ipv4.dns "$dns" + fi + nmcli connection up "$connection_name" >/dev/null +} + +module_ip_config_install() { + local iface="${1:-$POSTINSTALL_NETWORK_IP_DEFAULT_INTERFACE}" + local mode="${2:-$POSTINSTALL_NETWORK_IP_DEFAULT_MODE}" + local address="${3:-$POSTINSTALL_NETWORK_IP_DEFAULT_ADDRESS}" + local prefix="${4:-$POSTINSTALL_NETWORK_IP_DEFAULT_PREFIX}" + local gateway="${5:-$POSTINSTALL_NETWORK_IP_DEFAULT_GATEWAY}" + local dns="${6:-$POSTINSTALL_NETWORK_IP_DEFAULT_DNS}" + local backend="" + + iface="${iface:-$(system_primary_interface)}" + + if [[ -z "$iface" ]]; then + ui_error "Impossible de detecter l'interface reseau" + return 1 + fi + + if ! module_ip_config_validate_mode "$mode"; then + ui_error "Mode reseau invalide : $mode" + return 1 + fi + + backend="$(module_ip_config_detect_backend)" + case "$backend" in + networkmanager) module_ip_config_write_networkmanager "$iface" "$mode" "$address" "$prefix" "$gateway" "$dns" ;; + networkd) module_ip_config_write_networkd "$iface" "$mode" "$address" "$prefix" "$gateway" "$dns" ;; + *) module_ip_config_write_ifupdown "$iface" "$mode" "$address" "$prefix" "$gateway" "$dns" ;; + esac + + mkdir -p "$(dirname "$POSTINSTALL_NETWORK_IP_STATE_FILE")" + { + printf 'INTERFACE=%s\n' "$iface" + printf 'MODE=%s\n' "$mode" + printf 'BACKEND=%s\n' "$backend" + printf 'ADDRESS=%s\n' "$address" + printf 'PREFIX=%s\n' "$prefix" + printf 'GATEWAY=%s\n' "$gateway" + printf 'DNS=%s\n' "$dns" + } > "$POSTINSTALL_NETWORK_IP_STATE_FILE" + + log_info "Configuration IP appliquee sur $iface via $backend" + ui_success "Configuration IP appliquee sur $iface via $backend" +} + +module_ip_config_test() { + test -f "$POSTINSTALL_NETWORK_IP_STATE_FILE" || return 1 + ip addr >/dev/null 2>&1 || return 1 +} diff --git a/modules/network/ip-config/tests.sh b/modules/network/ip-config/tests.sh new file mode 100644 index 0000000..d45c5c7 --- /dev/null +++ b/modules/network/ip-config/tests.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash + +MODULE_NETWORK_IP_CONFIG_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$MODULE_NETWORK_IP_CONFIG_DIR/../../.." && pwd)" + +# shellcheck source=lib/ui.sh +source "$PROJECT_ROOT/lib/ui.sh" +# shellcheck source=lib/log.sh +source "$PROJECT_ROOT/lib/log.sh" +# shellcheck source=core/runtime.sh +source "$PROJECT_ROOT/core/runtime.sh" +# shellcheck source=modules/network/ip-config/module.sh +source "$MODULE_NETWORK_IP_CONFIG_DIR/module.sh" + +runtime_init "$PROJECT_ROOT" +log_init + +if ! test -f /etc/postinstall-debian/network-ip-config.conf; then + printf 'network-ip-config test SKIPPED: module configuration not applied\n' + exit 0 +fi + +if module_ip_config_test; then + printf 'network-ip-config test OK\n' +else + printf 'network-ip-config test FAILED\n' >&2 + exit 1 +fi diff --git a/modules/network/mdns-avahi/config.sh b/modules/network/mdns-avahi/config.sh new file mode 100644 index 0000000..a2ebf86 --- /dev/null +++ b/modules/network/mdns-avahi/config.sh @@ -0,0 +1,4 @@ +POSTINSTALL_MDNS_AVAHI_ENABLE="yes" +POSTINSTALL_MDNS_AVAHI_PUBLISH_WORKSTATION="yes" +POSTINSTALL_MDNS_AVAHI_CONFIG_FILE="/etc/avahi/avahi-daemon.conf" +POSTINSTALL_MDNS_AVAHI_SETTINGS_FILE="config/mdns-avahi.yaml" diff --git a/modules/network/mdns-avahi/metadata.conf b/modules/network/mdns-avahi/metadata.conf new file mode 100644 index 0000000..518fec0 --- /dev/null +++ b/modules/network/mdns-avahi/metadata.conf @@ -0,0 +1,4 @@ +MODULE_ID="network/mdns-avahi" +MODULE_NAME="Publication mDNS Avahi" +MODULE_CATEGORY="network" +MODULE_DESCRIPTION="Installe et configure Avahi pour publier la machine sur le reseau local" diff --git a/modules/network/mdns-avahi/module.sh b/modules/network/mdns-avahi/module.sh new file mode 100644 index 0000000..22722d8 --- /dev/null +++ b/modules/network/mdns-avahi/module.sh @@ -0,0 +1,104 @@ +#!/usr/bin/env bash + +MODULE_MDNS_AVAHI_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +MODULE_MDNS_AVAHI_PROJECT_ROOT="$(cd "$MODULE_MDNS_AVAHI_DIR/../../.." && pwd)" + +# shellcheck source=lib/package.sh +source "$MODULE_MDNS_AVAHI_PROJECT_ROOT/lib/package.sh" +# shellcheck source=modules/network/mdns-avahi/config.sh +source "$MODULE_MDNS_AVAHI_DIR/config.sh" +# shellcheck source=modules/network/mdns-avahi/metadata.conf +source "$MODULE_MDNS_AVAHI_DIR/metadata.conf" + +module_mdns_avahi_metadata() { + printf '%s|%s|%s\n' "$MODULE_ID" "$MODULE_NAME" "$MODULE_DESCRIPTION" +} + +module_mdns_avahi_config_path() { + printf '%s/%s\n' "$MODULE_MDNS_AVAHI_PROJECT_ROOT" "$POSTINSTALL_MDNS_AVAHI_SETTINGS_FILE" +} + +module_mdns_avahi_settings() { + local config_path="" + local enable_value="$POSTINSTALL_MDNS_AVAHI_ENABLE" + local publish_workstation="$POSTINSTALL_MDNS_AVAHI_PUBLISH_WORKSTATION" + + config_path="$(module_mdns_avahi_config_path)" + if [[ -f "$config_path" ]]; then + while IFS='=' read -r key value; do + case "$key" in + enable) enable_value="$value" ;; + publish_workstation) publish_workstation="$value" ;; + esac + done < <( + awk ' + /^[[:space:]]*enable:/ { print "enable=" $2 } + /^[[:space:]]*publish_workstation:/ { print "publish_workstation=" $2 } + ' "$config_path" + ) + fi + + printf '%s|%s\n' "$enable_value" "$publish_workstation" +} + +module_mdns_avahi_require_package() { + if package_is_installed "avahi-daemon"; then + ui_info "Paquet avahi-daemon deja installe" + return 0 + fi + + ui_warn "Paquet avahi-daemon absent, installation en cours" + package_refresh_indexes + package_install avahi-daemon avahi-utils libnss-mdns + log_info "Paquets Avahi installes" + ui_success "Paquets Avahi installes" +} + +module_mdns_avahi_write_config() { + local enable_value="${1:-$POSTINSTALL_MDNS_AVAHI_ENABLE}" + local publish_workstation="${2:-$POSTINSTALL_MDNS_AVAHI_PUBLISH_WORKSTATION}" + local disable_value="yes" + + if [[ "$enable_value" == "yes" ]]; then + disable_value="no" + fi + + cp "$POSTINSTALL_MDNS_AVAHI_CONFIG_FILE" "${POSTINSTALL_MDNS_AVAHI_CONFIG_FILE}.bak.postinstall" 2>/dev/null || true + + sed -i \ + -e "s/^#*disable-publishing=.*/disable-publishing=$disable_value/" \ + -e "s/^#*publish-workstation=.*/publish-workstation=$publish_workstation/" \ + "$POSTINSTALL_MDNS_AVAHI_CONFIG_FILE" +} + +module_mdns_avahi_check() { + package_is_installed "avahi-daemon" || return 1 + systemctl is-active --quiet avahi-daemon || return 1 + grep -Eq '^disable-publishing=no$' "$POSTINSTALL_MDNS_AVAHI_CONFIG_FILE" || return 1 +} + +module_mdns_avahi_install() { + local settings="" + local enable_value="" + local publish_workstation="" + + settings="$(module_mdns_avahi_settings)" + IFS='|' read -r enable_value publish_workstation <<< "$settings" + + module_mdns_avahi_require_package || return 1 + module_mdns_avahi_write_config "$enable_value" "$publish_workstation" + + systemctl enable --now avahi-daemon + systemctl restart avahi-daemon + + log_info "Avahi configure, publication=$enable_value workstation=$publish_workstation" + ui_success "Avahi configure" +} + +module_mdns_avahi_test() { + package_is_installed "avahi-daemon" || return 1 + test -f "$POSTINSTALL_MDNS_AVAHI_CONFIG_FILE" || return 1 + test -f "$(module_mdns_avahi_config_path)" || return 1 + systemctl is-active --quiet avahi-daemon || return 1 + grep -Eq '^disable-publishing=(no|yes)$' "$POSTINSTALL_MDNS_AVAHI_CONFIG_FILE" || return 1 +} diff --git a/modules/network/mdns-avahi/tests.sh b/modules/network/mdns-avahi/tests.sh new file mode 100755 index 0000000..13144b2 --- /dev/null +++ b/modules/network/mdns-avahi/tests.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash + +MODULE_MDNS_AVAHI_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$MODULE_MDNS_AVAHI_DIR/../../.." && pwd)" + +# shellcheck source=lib/ui.sh +source "$PROJECT_ROOT/lib/ui.sh" +# shellcheck source=lib/log.sh +source "$PROJECT_ROOT/lib/log.sh" +# shellcheck source=lib/package.sh +source "$PROJECT_ROOT/lib/package.sh" +# shellcheck source=core/runtime.sh +source "$PROJECT_ROOT/core/runtime.sh" +# shellcheck source=modules/network/mdns-avahi/module.sh +source "$MODULE_MDNS_AVAHI_DIR/module.sh" + +runtime_init "$PROJECT_ROOT" +log_init + +if ! package_is_installed avahi-daemon; then + printf 'mdns-avahi test SKIPPED: avahi-daemon not installed\n' + exit 0 +fi + +if ! test -f "$PROJECT_ROOT/config/mdns-avahi.yaml"; then + printf 'mdns-avahi test FAILED: missing repository config\n' >&2 + exit 1 +fi + +if ! systemctl status avahi-daemon >/dev/null 2>&1; then + printf 'mdns-avahi test SKIPPED: systemd status unavailable in this environment\n' + exit 0 +fi + +if module_mdns_avahi_test; then + printf 'mdns-avahi test OK\n' +else + printf 'mdns-avahi test FAILED\n' >&2 + exit 1 +fi diff --git a/modules/network/nfs-client/config.sh b/modules/network/nfs-client/config.sh new file mode 100644 index 0000000..d72d81a --- /dev/null +++ b/modules/network/nfs-client/config.sh @@ -0,0 +1,2 @@ +POSTINSTALL_NFS_CLIENT_STATE_FILE="/etc/postinstall-debian/nfs-client.conf" +POSTINSTALL_NFS_CLIENT_SHARES_FILE="config/nfs-client.shares.yaml" diff --git a/modules/network/nfs-client/metadata.conf b/modules/network/nfs-client/metadata.conf new file mode 100644 index 0000000..1624323 --- /dev/null +++ b/modules/network/nfs-client/metadata.conf @@ -0,0 +1,4 @@ +MODULE_ID="network/nfs-client" +MODULE_NAME="Client NFS" +MODULE_CATEGORY="network" +MODULE_DESCRIPTION="Installe les utilitaires client NFS" diff --git a/modules/network/nfs-client/module.sh b/modules/network/nfs-client/module.sh new file mode 100644 index 0000000..a07c400 --- /dev/null +++ b/modules/network/nfs-client/module.sh @@ -0,0 +1,219 @@ +#!/usr/bin/env bash + +MODULE_NFS_CLIENT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +MODULE_NFS_CLIENT_PROJECT_ROOT="$(cd "$MODULE_NFS_CLIENT_DIR/../../.." && pwd)" + +# shellcheck source=lib/package.sh +source "$MODULE_NFS_CLIENT_PROJECT_ROOT/lib/package.sh" +# shellcheck source=modules/network/nfs-client/config.sh +source "$MODULE_NFS_CLIENT_DIR/config.sh" +# shellcheck source=modules/network/nfs-client/metadata.conf +source "$MODULE_NFS_CLIENT_DIR/metadata.conf" + +module_nfs_client_metadata() { + printf '%s|%s|%s\n' "$MODULE_ID" "$MODULE_NAME" "$MODULE_DESCRIPTION" +} + +module_nfs_client_config_path() { + printf '%s/%s\n' "$MODULE_NFS_CLIENT_PROJECT_ROOT" "$POSTINSTALL_NFS_CLIENT_SHARES_FILE" +} + +module_nfs_client_entries() { + local config_path="" + config_path="$(module_nfs_client_config_path)" + + awk ' + function flush() { + if (id != "") { + print id "|" name "|" description "|" server "|" remote_path "|" mount_path "|" access "|" mount_options "|" enabled + } + } + /^[[:space:]]*-[[:space:]]id:/ { + flush() + id=$0; sub(/.*id:[[:space:]]*/, "", id) + name=description=server=remote_path=mount_path=access=mount_options=enabled="" + next + } + /^[[:space:]]*name:/ { name=$0; sub(/.*name:[[:space:]]*/, "", name); next } + /^[[:space:]]*description:/ { description=$0; sub(/.*description:[[:space:]]*/, "", description); next } + /^[[:space:]]*server:/ { server=$0; sub(/.*server:[[:space:]]*/, "", server); next } + /^[[:space:]]*remote_path:/ { remote_path=$0; sub(/.*remote_path:[[:space:]]*/, "", remote_path); next } + /^[[:space:]]*mount_path:/ { mount_path=$0; sub(/.*mount_path:[[:space:]]*/, "", mount_path); next } + /^[[:space:]]*access:/ { access=$0; sub(/.*access:[[:space:]]*/, "", access); next } + /^[[:space:]]*mount_options:/ { mount_options=$0; sub(/.*mount_options:[[:space:]]*/, "", mount_options); next } + /^[[:space:]]*enabled_by_default:/ { enabled=$0; sub(/.*enabled_by_default:[[:space:]]*/, "", enabled); next } + END { flush() } + ' "$config_path" +} + +module_nfs_client_default_ids() { + local entry="" + local ids="" + + while IFS= read -r entry; do + [[ -n "$entry" ]] || continue + IFS='|' read -r share_id _ _ _ _ _ _ _ enabled <<< "$entry" + if [[ "$enabled" == "true" ]]; then + ids="${ids:+$ids,}$share_id" + fi + done < <(module_nfs_client_entries) + + printf '%s\n' "$ids" +} + +module_nfs_client_default_indices() { + local entry="" + local indices="" + local index=1 + + while IFS= read -r entry; do + [[ -n "$entry" ]] || continue + IFS='|' read -r share_id _ _ _ _ _ _ _ enabled <<< "$entry" + if [[ "$enabled" == "true" ]]; then + indices="${indices:+$indices,}$index" + fi + index=$((index + 1)) + done < <(module_nfs_client_entries) + + printf '%s\n' "$indices" +} + +module_nfs_client_fstab_line() { + local server="$1" + local remote_path="$2" + local mount_path="$3" + local access="$4" + local mount_options="$5" + local options="$mount_options" + + if [[ "$access" == "ro" && "$options" != *ro* ]]; then + options="${options},ro" + elif [[ "$access" == "rw" && "$options" != *rw* ]]; then + options="${options},rw" + fi + + printf '%s:%s %s nfs %s 0 0' "$server" "$remote_path" "$mount_path" "$options" +} + +module_nfs_client_enable_share() { + local share_id="$1" + local mount_now="${2:-no}" + local entry="" + local line="" + + while IFS= read -r entry; do + [[ -n "$entry" ]] || continue + IFS='|' read -r current_id name description server remote_path mount_path access mount_options enabled <<< "$entry" + [[ "$current_id" == "$share_id" ]] || continue + + mkdir -p "$mount_path" + line="$(module_nfs_client_fstab_line "$server" "$remote_path" "$mount_path" "$access" "$mount_options")" + if ! grep -Fq "$server:$remote_path $mount_path nfs" /etc/fstab; then + printf '%s\n' "$line" >> /etc/fstab + log_info "Partage NFS client ajoute a fstab : $share_id" + ui_success "Partage NFS active : $share_id" + else + ui_info "Partage NFS deja present dans fstab : $share_id" + fi + + if [[ "$mount_now" == "yes" ]]; then + if command -v mountpoint >/dev/null 2>&1 && mountpoint -q "$mount_path"; then + ui_info "Partage deja monte : $mount_path" + elif mount "$mount_path"; then + log_info "Partage NFS monte : $share_id" + ui_success "Partage NFS monte : $share_id" + else + log_info "Echec du montage NFS : $share_id" + ui_warn "Impossible de monter immediatement $mount_path" + fi + fi + + return 0 + done < <(module_nfs_client_entries) + + ui_warn "Partage NFS introuvable dans la configuration : $share_id" + return 1 +} + +module_nfs_client_active_entries() { + local entry="" + + while IFS= read -r entry; do + [[ -n "$entry" ]] || continue + IFS='|' read -r current_id name description server remote_path mount_path access mount_options enabled <<< "$entry" + if grep -Fq "$server:$remote_path $mount_path nfs" /etc/fstab; then + printf '%s|%s|%s|%s|%s\n' "$current_id" "$name" "$mount_path" "$server:$remote_path" "$access" + fi + done < <(module_nfs_client_entries) +} + +module_nfs_client_disable_share() { + local share_id="$1" + local entry="" + local temp_file="" + + while IFS= read -r entry; do + [[ -n "$entry" ]] || continue + IFS='|' read -r current_id name description server remote_path mount_path access mount_options enabled <<< "$entry" + [[ "$current_id" == "$share_id" ]] || continue + + if command -v mountpoint >/dev/null 2>&1 && mountpoint -q "$mount_path"; then + if umount "$mount_path"; then + log_info "Partage NFS demonte : $share_id" + ui_success "Partage NFS demonte : $share_id" + else + log_info "Echec du demontage NFS : $share_id" + ui_warn "Impossible de demonter $mount_path, suppression de l'entree fstab quand meme" + fi + fi + + temp_file="$(mktemp)" + grep -Fv "$server:$remote_path $mount_path nfs" /etc/fstab > "$temp_file" + cat "$temp_file" > /etc/fstab + rm -f "$temp_file" + + log_info "Partage NFS retire de fstab : $share_id" + ui_success "Partage NFS desactive : $share_id" + return 0 + done < <(module_nfs_client_entries) + + ui_warn "Partage NFS introuvable dans la configuration : $share_id" + return 1 +} + +module_nfs_client_install() { + local action="${1:-enable}" + local selected_ids="${2:-}" + local mount_now="${3:-no}" + local share_id="" + + package_refresh_indexes + package_install nfs-common + + if [[ -n "$selected_ids" ]]; then + while IFS= read -r share_id; do + [[ -n "$share_id" ]] || continue + if [[ "$action" == "disable" ]]; then + module_nfs_client_disable_share "$share_id" + else + module_nfs_client_enable_share "$share_id" "$mount_now" + fi + done < <(printf '%s\n' "$selected_ids" | tr ',' '\n' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | sed '/^$/d') + fi + + mkdir -p "$(dirname "$POSTINSTALL_NFS_CLIENT_STATE_FILE")" + { + printf 'ENABLED=yes\n' + printf 'ACTION=%s\n' "$action" + printf 'SHARES=%s\n' "$selected_ids" + printf 'MOUNT_NOW=%s\n' "$mount_now" + } > "$POSTINSTALL_NFS_CLIENT_STATE_FILE" + log_info "Client NFS installe" + ui_success "Client NFS installe" +} + +module_nfs_client_test() { + package_is_installed nfs-common || return 1 + command -v mount.nfs >/dev/null 2>&1 || return 1 + test -f "$(module_nfs_client_config_path)" || return 1 +} diff --git a/modules/network/nfs-client/tests.sh b/modules/network/nfs-client/tests.sh new file mode 100644 index 0000000..89fbd79 --- /dev/null +++ b/modules/network/nfs-client/tests.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash + +MODULE_NFS_CLIENT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$MODULE_NFS_CLIENT_DIR/../../.." && pwd)" + +# shellcheck source=lib/ui.sh +source "$PROJECT_ROOT/lib/ui.sh" +# shellcheck source=lib/log.sh +source "$PROJECT_ROOT/lib/log.sh" +# shellcheck source=lib/package.sh +source "$PROJECT_ROOT/lib/package.sh" +# shellcheck source=core/runtime.sh +source "$PROJECT_ROOT/core/runtime.sh" +# shellcheck source=modules/network/nfs-client/module.sh +source "$MODULE_NFS_CLIENT_DIR/module.sh" + +runtime_init "$PROJECT_ROOT" +log_init + +if ! package_is_installed nfs-common; then + printf 'nfs-client test SKIPPED: nfs-common not installed\n' + exit 0 +fi + +if ! test -f "$PROJECT_ROOT/config/nfs-client.shares.yaml"; then + printf 'nfs-client test FAILED: missing repository config\n' >&2 + exit 1 +fi + +if module_nfs_client_test; then + printf 'nfs-client test OK\n' +else + printf 'nfs-client test FAILED\n' >&2 + exit 1 +fi diff --git a/modules/network/nfs-server/config.sh b/modules/network/nfs-server/config.sh new file mode 100644 index 0000000..d11a0b7 --- /dev/null +++ b/modules/network/nfs-server/config.sh @@ -0,0 +1,5 @@ +POSTINSTALL_NFS_SERVER_EXPORT_PATH="/srv/nfs/share" +POSTINSTALL_NFS_SERVER_CLIENTS="10.0.0.0/22" +POSTINSTALL_NFS_SERVER_EXPORT_MODE="rw" +POSTINSTALL_NFS_SERVER_EXPORT_FILE="/etc/exports.d/postinstall.exports" +POSTINSTALL_NFS_SERVER_EXPORTS_FILE="config/nfs-server.exports.yaml" diff --git a/modules/network/nfs-server/metadata.conf b/modules/network/nfs-server/metadata.conf new file mode 100644 index 0000000..fade4de --- /dev/null +++ b/modules/network/nfs-server/metadata.conf @@ -0,0 +1,4 @@ +MODULE_ID="network/nfs-server" +MODULE_NAME="Serveur NFS" +MODULE_CATEGORY="network" +MODULE_DESCRIPTION="Installe et configure un export NFS" diff --git a/modules/network/nfs-server/module.sh b/modules/network/nfs-server/module.sh new file mode 100644 index 0000000..7b7d463 --- /dev/null +++ b/modules/network/nfs-server/module.sh @@ -0,0 +1,144 @@ +#!/usr/bin/env bash + +MODULE_NFS_SERVER_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +MODULE_NFS_SERVER_PROJECT_ROOT="$(cd "$MODULE_NFS_SERVER_DIR/../../.." && pwd)" + +# shellcheck source=lib/package.sh +source "$MODULE_NFS_SERVER_PROJECT_ROOT/lib/package.sh" +# shellcheck source=modules/network/nfs-server/config.sh +source "$MODULE_NFS_SERVER_DIR/config.sh" +# shellcheck source=modules/network/nfs-server/metadata.conf +source "$MODULE_NFS_SERVER_DIR/metadata.conf" + +module_nfs_server_metadata() { + printf '%s|%s|%s\n' "$MODULE_ID" "$MODULE_NAME" "$MODULE_DESCRIPTION" +} + +module_nfs_server_config_path() { + printf '%s/%s\n' "$MODULE_NFS_SERVER_PROJECT_ROOT" "$POSTINSTALL_NFS_SERVER_EXPORTS_FILE" +} + +module_nfs_server_entries() { + local config_path="" + config_path="$(module_nfs_server_config_path)" + + awk ' + function flush() { + if (id != "") { + print id "|" path "|" clients "|" options "|" description + } + } + /^[[:space:]]*-[[:space:]]id:/ { + flush() + id=$0; sub(/.*id:[[:space:]]*/, "", id) + path=clients=options=description="" + next + } + /^[[:space:]]*path:/ { path=$0; sub(/.*path:[[:space:]]*/, "", path); next } + /^[[:space:]]*clients:/ { clients=$0; sub(/.*clients:[[:space:]]*/, "", clients); next } + /^[[:space:]]*options:/ { options=$0; sub(/.*options:[[:space:]]*/, "", options); next } + /^[[:space:]]*description:/ { description=$0; sub(/.*description:[[:space:]]*/, "", description); next } + END { flush() } + ' "$config_path" +} + +module_nfs_server_repo_lines() { + local entry="" + + while IFS= read -r entry; do + [[ -n "$entry" ]] || continue + IFS='|' read -r export_id export_path clients options description <<< "$entry" + printf '%s %s(%s)\n' "$export_path" "$clients" "$options" + done < <(module_nfs_server_entries) +} + +module_nfs_server_write_managed_file() { + local temp_file="$1" + + { + printf '# BEGIN postinstall-debian managed exports\n' + cat "$temp_file" + printf '# END postinstall-debian managed exports\n' + } > "$POSTINSTALL_NFS_SERVER_EXPORT_FILE" +} + +module_nfs_server_sync_add_only() { + local line="" + local current_content="" + local temp_file="" + + mkdir -p /etc/exports.d + temp_file="$(mktemp)" + + if [[ -f "$POSTINSTALL_NFS_SERVER_EXPORT_FILE" ]]; then + awk ' + /^# BEGIN postinstall-debian managed exports$/ { skip=1; next } + /^# END postinstall-debian managed exports$/ { skip=0; next } + !skip { print } + ' "$POSTINSTALL_NFS_SERVER_EXPORT_FILE" > "$temp_file" + fi + + while IFS= read -r line; do + [[ -n "$line" ]] || continue + mkdir -p "${line%% *}" + if ! grep -Fq "$line" "$temp_file"; then + printf '%s\n' "$line" >> "$temp_file" + log_info "Export NFS ajoute depuis le repo : $line" + ui_success "Export NFS ajoute" + else + ui_info "Export NFS deja present : $line" + fi + done < <(module_nfs_server_repo_lines) + + module_nfs_server_write_managed_file "$temp_file" + rm -f "$temp_file" +} + +module_nfs_server_sync_strict() { + local temp_file="" + local line="" + + mkdir -p /etc/exports.d + temp_file="$(mktemp)" + + while IFS= read -r line; do + [[ -n "$line" ]] || continue + mkdir -p "${line%% *}" + printf '%s\n' "$line" >> "$temp_file" + done < <(module_nfs_server_repo_lines) + + module_nfs_server_write_managed_file "$temp_file" + rm -f "$temp_file" + + log_info "Exports NFS synchronises en mode strict" + ui_success "Exports NFS synchronises en mode strict" +} + +module_nfs_server_install() { + local sync_mode="${1:-add-only}" + + package_refresh_indexes + package_install nfs-kernel-server + + if [[ "$sync_mode" == "strict" ]]; then + module_nfs_server_sync_strict + elif [[ "$sync_mode" == "add-only" || "$sync_mode" == "repo" ]]; then + module_nfs_server_sync_add_only + else + mkdir -p "$POSTINSTALL_NFS_SERVER_EXPORT_PATH" + printf '%s %s(%s,sync,no_subtree_check)\n' "$POSTINSTALL_NFS_SERVER_EXPORT_PATH" "$POSTINSTALL_NFS_SERVER_CLIENTS" "$POSTINSTALL_NFS_SERVER_EXPORT_MODE" > "$POSTINSTALL_NFS_SERVER_EXPORT_FILE" + fi + + exportfs -ra + systemctl enable --now nfs-kernel-server + systemctl restart nfs-kernel-server + + log_info "Serveur NFS synchronise depuis le repo" + ui_success "Serveur NFS configure" +} + +module_nfs_server_test() { + package_is_installed nfs-kernel-server || return 1 + test -f "$POSTINSTALL_NFS_SERVER_EXPORT_FILE" || return 1 + test -f "$(module_nfs_server_config_path)" || return 1 +} diff --git a/modules/network/nfs-server/tests.sh b/modules/network/nfs-server/tests.sh new file mode 100644 index 0000000..8c75cbd --- /dev/null +++ b/modules/network/nfs-server/tests.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash + +MODULE_NFS_SERVER_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$MODULE_NFS_SERVER_DIR/../../.." && pwd)" + +# shellcheck source=lib/ui.sh +source "$PROJECT_ROOT/lib/ui.sh" +# shellcheck source=lib/log.sh +source "$PROJECT_ROOT/lib/log.sh" +# shellcheck source=lib/package.sh +source "$PROJECT_ROOT/lib/package.sh" +# shellcheck source=core/runtime.sh +source "$PROJECT_ROOT/core/runtime.sh" +# shellcheck source=modules/network/nfs-server/module.sh +source "$MODULE_NFS_SERVER_DIR/module.sh" + +runtime_init "$PROJECT_ROOT" +log_init + +if ! package_is_installed nfs-kernel-server; then + printf 'nfs-server test SKIPPED: nfs-kernel-server not installed\n' + exit 0 +fi + +if ! test -f "$PROJECT_ROOT/config/nfs-server.exports.yaml"; then + printf 'nfs-server test FAILED: missing repository config\n' >&2 + exit 1 +fi + +if ! test -f /etc/exports.d/postinstall.exports; then + printf 'nfs-server test SKIPPED: module configuration not applied\n' + exit 0 +fi + +if module_nfs_server_test; then + printf 'nfs-server test OK\n' +else + printf 'nfs-server test FAILED\n' >&2 + exit 1 +fi diff --git a/modules/network/samba-share/config.sh b/modules/network/samba-share/config.sh new file mode 100644 index 0000000..665cbd5 --- /dev/null +++ b/modules/network/samba-share/config.sh @@ -0,0 +1,8 @@ +POSTINSTALL_SAMBA_SHARE_NAME="public" +POSTINSTALL_SAMBA_SHARE_PATH="/home/gilles" +POSTINSTALL_SAMBA_SHARE_USER="gilles" +POSTINSTALL_SAMBA_SHARE_READ_ONLY="yes" +POSTINSTALL_SAMBA_SHARE_PUBLIC="yes" +POSTINSTALL_SAMBA_CONFIG_DIR="/etc/samba" +POSTINSTALL_SAMBA_INCLUDE_FILE="/etc/samba/smb.conf.d/postinstall-home.conf" +POSTINSTALL_SAMBA_SHARES_FILE="config/samba-shares.yaml" diff --git a/modules/network/samba-share/metadata.conf b/modules/network/samba-share/metadata.conf new file mode 100644 index 0000000..b791c53 --- /dev/null +++ b/modules/network/samba-share/metadata.conf @@ -0,0 +1,4 @@ +MODULE_ID="network/samba-share" +MODULE_NAME="Partage Samba public" +MODULE_CATEGORY="network" +MODULE_DESCRIPTION="Installe Samba et partage un dossier sur le reseau local" diff --git a/modules/network/samba-share/module.sh b/modules/network/samba-share/module.sh new file mode 100644 index 0000000..71de25d --- /dev/null +++ b/modules/network/samba-share/module.sh @@ -0,0 +1,186 @@ +#!/usr/bin/env bash + +MODULE_SAMBA_SHARE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +MODULE_SAMBA_SHARE_PROJECT_ROOT="$(cd "$MODULE_SAMBA_SHARE_DIR/../../.." && pwd)" + +# shellcheck source=lib/package.sh +source "$MODULE_SAMBA_SHARE_PROJECT_ROOT/lib/package.sh" +# shellcheck source=lib/system.sh +source "$MODULE_SAMBA_SHARE_PROJECT_ROOT/lib/system.sh" +# shellcheck source=modules/network/samba-share/config.sh +source "$MODULE_SAMBA_SHARE_DIR/config.sh" +# shellcheck source=modules/network/samba-share/metadata.conf +source "$MODULE_SAMBA_SHARE_DIR/metadata.conf" + +module_samba_share_metadata() { + printf '%s|%s|%s\n' "$MODULE_ID" "$MODULE_NAME" "$MODULE_DESCRIPTION" +} + +module_samba_share_config_path() { + printf '%s/%s\n' "$MODULE_SAMBA_SHARE_PROJECT_ROOT" "$POSTINSTALL_SAMBA_SHARES_FILE" +} + +module_samba_share_global_settings() { + local config_path="" + local workgroup="WORKGROUP" + local wsdd2_enabled="yes" + + config_path="$(module_samba_share_config_path)" + if [[ -f "$config_path" ]]; then + while IFS='=' read -r key value; do + case "$key" in + workgroup) workgroup="$value" ;; + wsdd2) wsdd2_enabled="$value" ;; + esac + done < <( + awk ' + /^[[:space:]]*workgroup:/ { print "workgroup=" $2 } + /^[[:space:]]*wsdd2:/ { print "wsdd2=" $2 } + ' "$config_path" + ) + fi + + printf '%s|%s\n' "$workgroup" "$wsdd2_enabled" +} + +module_samba_share_entries() { + local config_path="" + config_path="$(module_samba_share_config_path)" + + awk ' + function flush() { + if (id != "") { + print id "|" name "|" path "|" user "|" read_only "|" public "|" description + } + } + /^[[:space:]]*-[[:space:]]id:/ { + flush() + id=$0; sub(/.*id:[[:space:]]*/, "", id) + name=path=user=read_only=public=description="" + next + } + /^[[:space:]]*name:/ { name=$0; sub(/.*name:[[:space:]]*/, "", name); next } + /^[[:space:]]*path:/ { path=$0; sub(/.*path:[[:space:]]*/, "", path); next } + /^[[:space:]]*user:/ { user=$0; sub(/.*user:[[:space:]]*/, "", user); next } + /^[[:space:]]*read_only:/ { read_only=$0; sub(/.*read_only:[[:space:]]*/, "", read_only); next } + /^[[:space:]]*public:/ { public=$0; sub(/.*public:[[:space:]]*/, "", public); next } + /^[[:space:]]*description:/ { description=$0; sub(/.*description:[[:space:]]*/, "", description); next } + END { flush() } + ' "$config_path" +} + +module_samba_share_ensure_include() { + mkdir -p /etc/samba/smb.conf.d + if ! grep -Fq 'include = /etc/samba/smb.conf.d/postinstall-home.conf' /etc/samba/smb.conf; then + printf '\ninclude = /etc/samba/smb.conf.d/postinstall-home.conf\n' >> /etc/samba/smb.conf + fi +} + +module_samba_share_manage_wsdd2() { + local wsdd2_enabled="$1" + + if [[ "$wsdd2_enabled" == "yes" ]]; then + if package_install wsdd2; then + systemctl enable --now wsdd2 || ui_warn "Impossible d'activer wsdd2" + else + ui_warn "Paquet wsdd2 indisponible ou installation echouee" + fi + fi +} + +module_samba_share_render_block() { + local share_name="$1" + local share_path="$2" + local share_user="$3" + local read_only="$4" + local is_public="$5" + + cat < "$temp_file" + fi + + while IFS= read -r entry; do + [[ -n "$entry" ]] || continue + IFS='|' read -r share_id share_name share_path share_user read_only is_public description <<< "$entry" + + if ! system_user_exists "$share_user"; then + ui_warn "Utilisateur Samba introuvable, partage ignore : $share_user" + continue + fi + + mkdir -p "$share_path" + chown "$share_user:$share_user" "$share_path" + module_samba_share_render_block "$share_name" "$share_path" "$share_user" "$read_only" "$is_public" >> "$rendered_file" + printf '\n' >> "$rendered_file" + done < <(module_samba_share_entries) + + { + cat "$temp_file" + printf '# BEGIN postinstall-debian managed samba\n' + printf '[global]\n' + printf ' workgroup = %s\n' "$workgroup" + printf ' map to guest = Bad User\n' + printf ' server min protocol = SMB2\n\n' + cat "$rendered_file" + printf '# END postinstall-debian managed samba\n' + } > "$POSTINSTALL_SAMBA_INCLUDE_FILE" + + module_samba_share_manage_wsdd2 "$wsdd2_enabled" + rm -f "$temp_file" "$rendered_file" +} + +module_samba_share_install() { + local sync_mode="${1:-add-only}" + + package_refresh_indexes + package_install samba + module_samba_share_ensure_include + module_samba_share_sync_file "$sync_mode" + + testparm -s >/dev/null || return 1 + systemctl enable --now smbd + systemctl restart smbd + + log_info "Partages Samba synchronises depuis le repo" + ui_success "Partages Samba configures" +} + +module_samba_share_test() { + package_is_installed samba || return 1 + test -f "$POSTINSTALL_SAMBA_INCLUDE_FILE" || return 1 + test -f "$(module_samba_share_config_path)" || return 1 + testparm -s >/dev/null 2>&1 || return 1 +} diff --git a/modules/network/samba-share/tests.sh b/modules/network/samba-share/tests.sh new file mode 100644 index 0000000..d82646a --- /dev/null +++ b/modules/network/samba-share/tests.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash + +MODULE_SAMBA_SHARE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$MODULE_SAMBA_SHARE_DIR/../../.." && pwd)" + +# shellcheck source=lib/ui.sh +source "$PROJECT_ROOT/lib/ui.sh" +# shellcheck source=lib/log.sh +source "$PROJECT_ROOT/lib/log.sh" +# shellcheck source=lib/package.sh +source "$PROJECT_ROOT/lib/package.sh" +# shellcheck source=core/runtime.sh +source "$PROJECT_ROOT/core/runtime.sh" +# shellcheck source=modules/network/samba-share/module.sh +source "$MODULE_SAMBA_SHARE_DIR/module.sh" + +runtime_init "$PROJECT_ROOT" +log_init + +if ! package_is_installed samba; then + printf 'samba-share test SKIPPED: samba not installed\n' + exit 0 +fi + +if ! test -f "$PROJECT_ROOT/config/samba-shares.yaml"; then + printf 'samba-share test FAILED: missing repository config\n' >&2 + exit 1 +fi + +if ! test -f /etc/samba/smb.conf.d/postinstall-home.conf; then + printf 'samba-share test SKIPPED: module configuration not applied\n' + exit 0 +fi + +if module_samba_share_test; then + printf 'samba-share test OK\n' +else + printf 'samba-share test FAILED\n' >&2 + exit 1 +fi diff --git a/modules/network/ssh-server/config.sh b/modules/network/ssh-server/config.sh new file mode 100644 index 0000000..764c199 --- /dev/null +++ b/modules/network/ssh-server/config.sh @@ -0,0 +1,6 @@ +POSTINSTALL_SSH_PORT="22" +POSTINSTALL_SSH_PASSWORD_AUTH="yes" +POSTINSTALL_SSH_ROOT_LOGIN="no" +POSTINSTALL_SSH_CONFIG_DIR="/etc/ssh/sshd_config.d" +POSTINSTALL_SSH_CONFIG_FILE="/etc/ssh/sshd_config.d/postinstall-debian.conf" +POSTINSTALL_SSH_SETTINGS_FILE="config/ssh-server.yaml" diff --git a/modules/network/ssh-server/metadata.conf b/modules/network/ssh-server/metadata.conf new file mode 100644 index 0000000..4097114 --- /dev/null +++ b/modules/network/ssh-server/metadata.conf @@ -0,0 +1,4 @@ +MODULE_ID="network/ssh-server" +MODULE_NAME="Serveur SSH" +MODULE_CATEGORY="network" +MODULE_DESCRIPTION="Installe et configure openssh-server pour l'administration distante" diff --git a/modules/network/ssh-server/module.sh b/modules/network/ssh-server/module.sh new file mode 100644 index 0000000..5a9dd37 --- /dev/null +++ b/modules/network/ssh-server/module.sh @@ -0,0 +1,132 @@ +#!/usr/bin/env bash + +MODULE_SSH_SERVER_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +MODULE_SSH_SERVER_PROJECT_ROOT="$(cd "$MODULE_SSH_SERVER_DIR/../../.." && pwd)" + +# shellcheck source=lib/package.sh +source "$MODULE_SSH_SERVER_PROJECT_ROOT/lib/package.sh" +# shellcheck source=modules/network/ssh-server/config.sh +source "$MODULE_SSH_SERVER_DIR/config.sh" +# shellcheck source=modules/network/ssh-server/metadata.conf +source "$MODULE_SSH_SERVER_DIR/metadata.conf" + +module_ssh_server_metadata() { + printf '%s|%s|%s\n' "$MODULE_ID" "$MODULE_NAME" "$MODULE_DESCRIPTION" +} + +module_ssh_server_config_path() { + printf '%s/%s\n' "$MODULE_SSH_SERVER_PROJECT_ROOT" "$POSTINSTALL_SSH_SETTINGS_FILE" +} + +module_ssh_server_settings() { + local config_path="" + local port="$POSTINSTALL_SSH_PORT" + local password_auth="$POSTINSTALL_SSH_PASSWORD_AUTH" + local root_login="$POSTINSTALL_SSH_ROOT_LOGIN" + + config_path="$(module_ssh_server_config_path)" + if [[ -f "$config_path" ]]; then + while IFS='=' read -r key value; do + case "$key" in + port) port="$value" ;; + password_authentication) password_auth="$value" ;; + permit_root_login) root_login="$value" ;; + esac + done < <( + awk ' + /^[[:space:]]*port:/ { print "port=" $2 } + /^[[:space:]]*password_authentication:/ { print "password_authentication=" $2 } + /^[[:space:]]*permit_root_login:/ { print "permit_root_login=" $2 } + ' "$config_path" + ) + fi + + printf '%s|%s|%s\n' "$port" "$password_auth" "$root_login" +} + +module_ssh_server_validate_port() { + local port="$1" + + [[ "$port" =~ ^[0-9]+$ ]] || return 1 + (( port >= 1 && port <= 65535 )) +} + +module_ssh_server_require_package() { + if package_is_installed "openssh-server"; then + ui_info "Paquet openssh-server deja installe" + return 0 + fi + + ui_warn "Paquet openssh-server absent, installation en cours" + package_refresh_indexes + package_install openssh-server + log_info "Paquet openssh-server installe" + ui_success "Paquet openssh-server installe" +} + +module_ssh_server_write_config() { + local ssh_port="${1:-$POSTINSTALL_SSH_PORT}" + local password_auth="${2:-$POSTINSTALL_SSH_PASSWORD_AUTH}" + local root_login="${3:-$POSTINSTALL_SSH_ROOT_LOGIN}" + + mkdir -p "$POSTINSTALL_SSH_CONFIG_DIR" + cat > "$POSTINSTALL_SSH_CONFIG_FILE" </dev/null | awk '{print $4}' | grep -Eq "(^|:)$ssh_port$" +} + +module_ssh_server_install() { + local settings="" + local ssh_port="" + local password_auth="" + local root_login="" + + settings="$(module_ssh_server_settings)" + IFS='|' read -r ssh_port password_auth root_login <<< "$settings" + + if ! module_ssh_server_validate_port "$ssh_port"; then + ui_error "Port SSH invalide : $ssh_port" + return 1 + fi + + module_ssh_server_require_package || return 1 + module_ssh_server_write_config "$ssh_port" "$password_auth" "$root_login" + + if command -v sshd >/dev/null 2>&1; then + sshd -t || return 1 + fi + + systemctl enable --now ssh + systemctl restart ssh + + log_info "Serveur SSH configure sur le port $ssh_port" + ui_success "Serveur SSH configure sur le port $ssh_port" +} + +module_ssh_server_test() { + local settings="" + local ssh_port="" + + settings="$(module_ssh_server_settings)" + IFS='|' read -r ssh_port _ _ <<< "$settings" + + package_is_installed "openssh-server" || return 1 + command -v ssh >/dev/null 2>&1 || return 1 + test -f "$POSTINSTALL_SSH_CONFIG_FILE" || return 1 + test -f "$(module_ssh_server_config_path)" || return 1 + systemctl is-active --quiet ssh || return 1 + ss -ltn 2>/dev/null | awk '{print $4}' | grep -Eq "(^|:)$ssh_port$" +} diff --git a/modules/network/ssh-server/tests.sh b/modules/network/ssh-server/tests.sh new file mode 100755 index 0000000..080fb56 --- /dev/null +++ b/modules/network/ssh-server/tests.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash + +MODULE_SSH_SERVER_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$MODULE_SSH_SERVER_DIR/../../.." && pwd)" + +# shellcheck source=lib/ui.sh +source "$PROJECT_ROOT/lib/ui.sh" +# shellcheck source=lib/log.sh +source "$PROJECT_ROOT/lib/log.sh" +# shellcheck source=lib/package.sh +source "$PROJECT_ROOT/lib/package.sh" +# shellcheck source=core/runtime.sh +source "$PROJECT_ROOT/core/runtime.sh" +# shellcheck source=modules/network/ssh-server/module.sh +source "$MODULE_SSH_SERVER_DIR/module.sh" + +runtime_init "$PROJECT_ROOT" +log_init + +if ! package_is_installed openssh-server; then + printf 'ssh-server test SKIPPED: openssh-server not installed\n' + exit 0 +fi + +if ! test -f "$PROJECT_ROOT/config/ssh-server.yaml"; then + printf 'ssh-server test FAILED: missing repository config\n' >&2 + exit 1 +fi + +if ! systemctl status ssh >/dev/null 2>&1; then + printf 'ssh-server test SKIPPED: systemd status unavailable in this environment\n' + exit 0 +fi + +if ! test -f /etc/ssh/sshd_config.d/postinstall-debian.conf; then + printf 'ssh-server test SKIPPED: module configuration not applied\n' + exit 0 +fi + +if module_ssh_server_test "${1:-22}"; then + printf 'ssh-server test OK\n' +else + printf 'ssh-server test FAILED\n' >&2 + exit 1 +fi diff --git a/modules/system/user-groups/config.sh b/modules/system/user-groups/config.sh new file mode 100644 index 0000000..29a03c8 --- /dev/null +++ b/modules/system/user-groups/config.sh @@ -0,0 +1,2 @@ +POSTINSTALL_USER_GROUPS_TARGET_USER="${POSTINSTALL_DEFAULT_USER:-gilles}" +POSTINSTALL_USER_GROUPS_DEFAULT_GROUPS="audio,video,plugdev,dialout,netdev,lpadmin,scanner" diff --git a/modules/system/user-groups/metadata.conf b/modules/system/user-groups/metadata.conf new file mode 100644 index 0000000..99b1f6f --- /dev/null +++ b/modules/system/user-groups/metadata.conf @@ -0,0 +1,4 @@ +MODULE_ID="system/user-groups" +MODULE_NAME="Configuration groupes utilisateur" +MODULE_CATEGORY="system" +MODULE_DESCRIPTION="Ajoute un utilisateur cible a une liste de groupes systeme utiles" diff --git a/modules/system/user-groups/module.sh b/modules/system/user-groups/module.sh new file mode 100644 index 0000000..cd491e6 --- /dev/null +++ b/modules/system/user-groups/module.sh @@ -0,0 +1,93 @@ +#!/usr/bin/env bash + +MODULE_USER_GROUPS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +MODULE_USER_GROUPS_PROJECT_ROOT="$(cd "$MODULE_USER_GROUPS_DIR/../../.." && pwd)" + +# shellcheck source=modules/system/user-groups/config.sh +source "$MODULE_USER_GROUPS_DIR/config.sh" +# shellcheck source=modules/system/user-groups/metadata.conf +source "$MODULE_USER_GROUPS_DIR/metadata.conf" + +module_user_groups_metadata() { + printf '%s|%s|%s\n' "$MODULE_ID" "$MODULE_NAME" "$MODULE_DESCRIPTION" +} + +module_user_groups_normalize_csv() { + local raw_groups="$1" + + printf '%s\n' "$raw_groups" \ + | tr ',' '\n' \ + | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' \ + | sed '/^$/d' +} + +module_user_groups_check() { + local target_user="${1:-$POSTINSTALL_USER_GROUPS_TARGET_USER}" + local requested_groups="${2:-$POSTINSTALL_USER_GROUPS_DEFAULT_GROUPS}" + local group_name="" + + if ! system_user_exists "$target_user"; then + ui_error "Utilisateur introuvable : $target_user" + return 1 + fi + + while IFS= read -r group_name; do + if ! system_group_exists "$group_name"; then + ui_warn "Groupe introuvable : $group_name" + return 1 + fi + + if ! system_user_in_group "$target_user" "$group_name"; then + ui_warn "Utilisateur $target_user non membre du groupe $group_name" + return 1 + fi + done < <(module_user_groups_normalize_csv "$requested_groups") + + ui_success "Tous les groupes demandes sont deja appliques a $target_user" +} + +module_user_groups_install() { + local target_user="${1:-$POSTINSTALL_USER_GROUPS_TARGET_USER}" + local requested_groups="${2:-$POSTINSTALL_USER_GROUPS_DEFAULT_GROUPS}" + local group_name="" + local changed=0 + + if ! system_user_exists "$target_user"; then + ui_error "Impossible de configurer les groupes : utilisateur absent ($target_user)" + return 1 + fi + + while IFS= read -r group_name; do + if ! system_group_exists "$group_name"; then + ui_warn "Groupe ignore car absent : $group_name" + continue + fi + + if system_user_in_group "$target_user" "$group_name"; then + ui_info "Aucun changement : $target_user est deja dans $group_name" + continue + fi + + usermod -aG "$group_name" "$target_user" + log_info "Utilisateur $target_user ajoute au groupe $group_name" + ui_success "Utilisateur $target_user ajoute au groupe $group_name" + changed=1 + done < <(module_user_groups_normalize_csv "$requested_groups") + + if [[ "$changed" -eq 0 ]]; then + ui_info "Aucun changement de groupes necessaire" + fi +} + +module_user_groups_test() { + local target_user="${1:-$POSTINSTALL_USER_GROUPS_TARGET_USER}" + local requested_groups="${2:-$POSTINSTALL_USER_GROUPS_DEFAULT_GROUPS}" + local group_name="" + + system_user_exists "$target_user" || return 1 + + while IFS= read -r group_name; do + system_group_exists "$group_name" || return 1 + system_user_in_group "$target_user" "$group_name" || return 1 + done < <(module_user_groups_normalize_csv "$requested_groups") +} diff --git a/modules/system/user-groups/tests.sh b/modules/system/user-groups/tests.sh new file mode 100755 index 0000000..8b7be51 --- /dev/null +++ b/modules/system/user-groups/tests.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash + +MODULE_USER_GROUPS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$MODULE_USER_GROUPS_DIR/../../.." && pwd)" + +# shellcheck source=lib/ui.sh +source "$PROJECT_ROOT/lib/ui.sh" +# shellcheck source=lib/log.sh +source "$PROJECT_ROOT/lib/log.sh" +# shellcheck source=lib/system.sh +source "$PROJECT_ROOT/lib/system.sh" +# shellcheck source=core/runtime.sh +source "$PROJECT_ROOT/core/runtime.sh" +# shellcheck source=modules/system/user-groups/module.sh +source "$MODULE_USER_GROUPS_DIR/module.sh" + +runtime_init "$PROJECT_ROOT" +log_init + +if module_user_groups_test "${1:-gilles}" "${2:-audio,video}"; then + printf 'user-groups test OK\n' +else + printf 'user-groups test FAILED\n' >&2 + exit 1 +fi diff --git a/modules/system/user-sudo/config.sh b/modules/system/user-sudo/config.sh new file mode 100644 index 0000000..a1007da --- /dev/null +++ b/modules/system/user-sudo/config.sh @@ -0,0 +1,2 @@ +POSTINSTALL_USER_SUDO_TARGET_USER="${POSTINSTALL_DEFAULT_USER:-gilles}" +POSTINSTALL_USER_SUDO_TARGET_GROUP="sudo" diff --git a/modules/system/user-sudo/metadata.conf b/modules/system/user-sudo/metadata.conf new file mode 100644 index 0000000..7d3fa4c --- /dev/null +++ b/modules/system/user-sudo/metadata.conf @@ -0,0 +1,4 @@ +MODULE_ID="system/user-sudo" +MODULE_NAME="Configuration sudo utilisateur" +MODULE_CATEGORY="system" +MODULE_DESCRIPTION="Ajoute un utilisateur cible au groupe sudo et valide la configuration sudo" diff --git a/modules/system/user-sudo/module.sh b/modules/system/user-sudo/module.sh new file mode 100644 index 0000000..37b4ec6 --- /dev/null +++ b/modules/system/user-sudo/module.sh @@ -0,0 +1,103 @@ +#!/usr/bin/env bash + +MODULE_USER_SUDO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +MODULE_USER_SUDO_PROJECT_ROOT="$(cd "$MODULE_USER_SUDO_DIR/../../.." && pwd)" + +# shellcheck source=lib/package.sh +source "$MODULE_USER_SUDO_PROJECT_ROOT/lib/package.sh" +# shellcheck source=modules/system/user-sudo/config.sh +source "$MODULE_USER_SUDO_DIR/config.sh" +# shellcheck source=modules/system/user-sudo/metadata.conf +source "$MODULE_USER_SUDO_DIR/metadata.conf" + +module_user_sudo_require_package() { + if package_is_installed "sudo"; then + ui_info "Paquet sudo deja installe" + return 0 + fi + + ui_warn "Paquet sudo absent, installation en cours" + package_refresh_indexes + package_install sudo + log_info "Paquet sudo installe" + ui_success "Paquet sudo installe" +} + +module_user_sudo_metadata() { + printf '%s|%s|%s\n' "$MODULE_ID" "$MODULE_NAME" "$MODULE_DESCRIPTION" +} + +module_user_sudo_check() { + local target_user="${1:-$POSTINSTALL_USER_SUDO_TARGET_USER}" + + if ! package_is_installed "sudo"; then + ui_warn "Paquet sudo non installe" + return 1 + fi + + if ! system_user_exists "$target_user"; then + ui_error "Utilisateur introuvable : $target_user" + return 1 + fi + + if ! system_group_exists "$POSTINSTALL_USER_SUDO_TARGET_GROUP"; then + ui_error "Groupe requis introuvable : $POSTINSTALL_USER_SUDO_TARGET_GROUP" + return 1 + fi + + if system_user_in_group "$target_user" "$POSTINSTALL_USER_SUDO_TARGET_GROUP"; then + ui_success "Utilisateur $target_user deja membre du groupe sudo" + return 0 + fi + + ui_warn "Utilisateur $target_user non membre du groupe sudo" + return 1 +} + +module_user_sudo_install() { + local target_user="${1:-$POSTINSTALL_USER_SUDO_TARGET_USER}" + + module_user_sudo_require_package || return 1 + + if ! system_user_exists "$target_user"; then + ui_error "Impossible de configurer sudo : utilisateur absent ($target_user)" + return 1 + fi + + if ! system_group_exists "$POSTINSTALL_USER_SUDO_TARGET_GROUP"; then + ui_error "Impossible de configurer sudo : groupe absent ($POSTINSTALL_USER_SUDO_TARGET_GROUP)" + return 1 + fi + + if system_user_in_group "$target_user" "$POSTINSTALL_USER_SUDO_TARGET_GROUP"; then + log_info "Aucun changement sudo requis pour $target_user" + ui_info "Aucun changement : $target_user est deja dans sudo" + else + usermod -aG "$POSTINSTALL_USER_SUDO_TARGET_GROUP" "$target_user" + log_info "Utilisateur $target_user ajoute au groupe sudo" + ui_success "Utilisateur $target_user ajoute au groupe sudo" + fi + + module_user_sudo_configure "$target_user" +} + +module_user_sudo_configure() { + local target_user="${1:-$POSTINSTALL_USER_SUDO_TARGET_USER}" + + if command -v visudo >/dev/null 2>&1; then + visudo -cf /etc/sudoers >/dev/null + log_info "Validation visudo reussie pour $target_user" + ui_success "Configuration sudo validee avec visudo" + else + ui_warn "visudo indisponible, validation sudo non effectuee" + fi +} + +module_user_sudo_test() { + local target_user="${1:-$POSTINSTALL_USER_SUDO_TARGET_USER}" + + package_is_installed "sudo" || return 1 + system_user_exists "$target_user" || return 1 + system_group_exists "$POSTINSTALL_USER_SUDO_TARGET_GROUP" || return 1 + system_user_in_group "$target_user" "$POSTINSTALL_USER_SUDO_TARGET_GROUP" +} diff --git a/modules/system/user-sudo/tests.sh b/modules/system/user-sudo/tests.sh new file mode 100755 index 0000000..3772f59 --- /dev/null +++ b/modules/system/user-sudo/tests.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash + +MODULE_USER_SUDO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$MODULE_USER_SUDO_DIR/../../.." && pwd)" + +# shellcheck source=lib/ui.sh +source "$PROJECT_ROOT/lib/ui.sh" +# shellcheck source=lib/log.sh +source "$PROJECT_ROOT/lib/log.sh" +# shellcheck source=lib/package.sh +source "$PROJECT_ROOT/lib/package.sh" +# shellcheck source=lib/system.sh +source "$PROJECT_ROOT/lib/system.sh" +# shellcheck source=core/runtime.sh +source "$PROJECT_ROOT/core/runtime.sh" +# shellcheck source=modules/system/user-sudo/module.sh +source "$MODULE_USER_SUDO_DIR/module.sh" + +runtime_init "$PROJECT_ROOT" +log_init + +if ! package_is_installed sudo; then + printf 'user-sudo test SKIPPED: sudo package not installed\n' + exit 0 +fi + +if module_user_sudo_test "${1:-gilles}"; then + printf 'user-sudo test OK\n' +else + printf 'user-sudo test FAILED\n' >&2 + exit 1 +fi diff --git a/profiles/README.md b/profiles/README.md new file mode 100644 index 0000000..041e72c --- /dev/null +++ b/profiles/README.md @@ -0,0 +1,3 @@ +# Profiles + +Ce dossier contiendra des profils de modules predefinis comme `desktop-minimal`, `developer` ou `homelab`. diff --git a/tests/smoke.sh b/tests/smoke.sh new file mode 100755 index 0000000..505e121 --- /dev/null +++ b/tests/smoke.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +set -u + +PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +test -f "$PROJECT_ROOT/install.sh" +test -f "$PROJECT_ROOT/core/bootstrap.sh" +test -f "$PROJECT_ROOT/lib/ui.sh" +test -f "$PROJECT_ROOT/menus/main.sh" + +printf 'Smoke test OK\n' diff --git a/themes/grub/debian-1080p.zip b/themes/grub/debian-1080p.zip new file mode 100644 index 0000000000000000000000000000000000000000..37cd075859f7b32a07c7b56792a742789b46ae2e GIT binary patch literal 2067048 zcmY(KLy#^E%%$75ZQHhO+qP}nyltDeZQizRyWh4k-%QO^&7Vb*#i`2nNu5MV78DE( z=zoV#^pD#AY5s45{jZuCyV%+rySbUWFe?3DArz4Le?l}xD|T)uAfN*rARyHLA*6S+ zGPgHpaPpqf1}Knnqx>d}@M5{DbdjgzgI~Crgli0#X}b+1h^((zVLiKd?L>=@C)jZ! z3GeQge!36>c8~JU42%qoYp#DX6+otLwztWVcsx4w)a$Da(>HdYUFt%RF@aHE%{Vww zsS(=R+uA*#p@)-}8DHZ>Pn@*kyD%AiBKq5;-P1^@uxnp3Zv639lNyGKQ@Itnjwqfzdx9(OS+7h{73f^D^GeVMD24|qL zf9CtIhkeZMQB`wfouyuU0snH==3;P%!LVm06qBWjDhAHQp<2DeNYt2CY*-E2uSPXaciQr+bF1 z`%S{9-`ptD7q)b7gU|igR9PvXuVq#e980`qK-L;Os#rNQ?UCXkET0SE{Jy}2s3 z_*qk0rBUNzAY)j|V0#39p={k^On#KTXmu`%2-SWJM+7J{L!SUg-01hoNx%mn233LS0>zZ=#&yJlMLX#4}UOtmIE< zp2IG2u>M9MY;#9DBqm@^!!j*e9$=a}WSWE&hbmlJJVG#vhmPaHPdNBgY!|d*%E&YU);k$%e3Lysx(F z$~+cV2QNT0++cR5Oa?}=Z;~Bag4+KsuWWPwb+ba?_XT;9um61NlbFelQej>%w)5!7mI6{U}Au5Es?;{sN=1?=w%3&rjw(BDNs zV5s*5dA#@g_R0ABp-UZ+4&&U&aA$SF*iB|kN&wxcTzV+2?jyA`OmwSFxwJ1wT8USS z2`;Ok^Lgw3_Orce2d{z~%Tq}Ik&6gOjQqZPdf;E&s&@CmludqMzK~fo3L@;~!e3(F zs3JF!P4OTDetu#&unDhk6PrY-B54Fm%TBFiYLws3t!w&)aJ~NZ@V$0q`uDTe3I$5$ zTlT`2?wt{ZAOwyXT_<9X2c(ICTXo7`IoTnv5)bot?W(SUkJggj4l;Qq#U7b&gE3a- zdu6GL>y373$=pZ-IdZ7A8ke7^6<(2?KbmFw-8IwZnP5U3IXtXnQYJS=e@Y|W-qu(q z_<;M%iSKLh0y50>-x6Z_3Y39KZTl9f?@|F%-=8Rw8B>qkRh>1Et*G;C zN!5`N8)1V6|4_HTcATt1OlY$(5G*SU^H#cQ3_TiD4D7VFt-l?29ybU8;+hoMT#X%y zdi#8!>4Zky74JCTQtynJ8oGQn_$1n#bSqb+MEQ6 zzzAxWLGhmsoVGNP^?(kDXv7I`nNt?Z9A=~pTR8$dM1P-25?}%FjRx#SKI5Ugw9w^7 zx%45i8Ti5aQEgce0{cu}(V$2HDJJA)38_BI_WaY$4wVRSxa~-*=;X6%^eaIm2Sii? zMLKUdsB|eI#db-WCVobJ1p8{2wH(wWJfN48SOibShk;^V_|ro4{C3}cnr&Prz4%_B z5l&5bs-H7M+eF>Sk$=nBGGeMPC(fOdWi=h!|2P}=7aYgV7~_g<$7aln#v<{UP`;ILaL1N zK^T+t%7L*7HIhSn&Ye-^$QKv>|WS~iN5JtDvm%U{YNCn2X+5ZnrPTG%bQxfrj1Tye|u>Ysr#Mso<(#6r;!HmJl!ScH6L=}IIkj=*l3Sn>u zb$jQ(VDI1^+}Y7bxV^296mmM-58HR(oQpkM`S!25C3LE>u&p_5X<1z2-_h+>o{UlX zXL#5DzeiwOJF5c$F+c$Yokm1{uemmJQ9%IhUw&5Fc30(irSNoe>1=*A61hKmG!y;3 zYiRGjIaZiEf)h83c?Pn4K%ih7mX<;mGeLF#>%NG1_NH3Y3S5>n#IS__h1_;S< z`RBu8^A`nt37AUhXLjjRAjXXWmnBK{?S$+O?i?w4*83(f?yq)h?yn_^{`mpY?tc}1 zo%@W(?qZ%>JDZw1qJw-KP)J|%41OJbtWJ!7j9k02B$a-7md!c(5fFXN`a$d&K>VFr zzu!*c8MAmfiSf*!A^qDP^j)-2v@+htTxU8ye1sTCKpNADz`S{dOoB%$0H~Vq=dR{^ z5$_yd>l2@ns?M7^``YS^G?>MqotI!n-!XFA%!AeJ-f&t9->bzOr)=7m{dq@N2QB!G zZg!xciM09L6r7eipQ-#k(s@|S7d9V!0xz9X$*e&(s2=x*0DXUdc%XV> z=RdRE0inG#6u6T;qq5w*OO)2#0w8wwHM8%F z45pFMiqTMPh;3~^rIRk5IJ%+={K>4S*!e^2*j@{FXeN~KxXkRkVxzuQj84dBYCCh; zeB!?Kfl|<;t)IKPdV)u2nfTb8ZeK8kvKL}3&G(vNc+?S+p&3Yah56C`&1>NLzN42w z*`3Jhi{p^P#s+^4;9k7q4Gh8R{%z|;apWUeNV>!SF;GJT5j1zkM(dJaYjXsT+j~r= z`V^2;UwaLR+i?N6(O26NFgNRe?egnlpii<&!vd&ZU8Cb#2O+kDw*M7qPnzYZDV-tLP zYP?G%pAqnw;60Vy^qPdOmki%nzO1(kHkfSy$mwGjUI0?v6Dsl2q8NXSHf;PFPnl%l;p82)6-~L_b{TTHvZ12WCJ0_+!Yx0f2C@n3zqjXCg zTWMc&ONDnBZf*?KJts_&P!O3Y;&!Xf&@HjXdG}g1ydP}Uf4nz(53A3&P2YSjQ+fVM z9w#U|6R4Vk%^^5gkeZ1qzD8R)7+LtqU!JA8ij_>Yyb>8?Ysv}O0QpE7C_V2 z@hjk1g4-uuoeT*ki0A&XR=PbYRO;OFrMM^9wts;>BEW%wp@Q@+_l&|Y3BIQ}NnLso zw>L88t}H4lRX$eXci}_tT4UA}Z4S?DjMwx#{G1%53c)ujJNUuX`?(cX^TXb+zjeU^ z7}HEf8cVW3JN^t= zR-tR)vKb|irZ~{_UKNwLygMSz7is92^7@d}k$dUI^LGWIb*2+2AK+nALXoq0sMf+vFqe>3D{ra7biu z0x59c(9d2Y`dJ$GUGF~m(G4AyP<)e5yY@j65a-jBbd#tKXCf{{)ts-@8HJtef87-| z+7-}j6}vusRM(Oh%}nl+4-7Y6SU;s5W=GFoZcPp`XXvwwFB;$lJFn-7(N}4mKo1hJ z<5;TP*?g!TH03RsHrgXqH7j8F)+jSO(`!Gi??c7r$>}_#i_?sIj}cZkCV58o*36G{ zjM!F_(i`j^I4D~#wrGW5y_bh#pm9KlXE&o(nhh_zCQCW)uWKKDkNVsQXt?vo;mms3 zZ+HydCFJUbp>e0j#-g>?(%2KAyguiAaNC zJT4dbGm={T1*;QX^*yhoFMeI=-g7AAbumZ|7;|f@=d0wL%iq0{gyg)5o};+W`)