From bbc4e4da65243b84573d0a08232b835217eca132 Mon Sep 17 00:00:00 2001 From: julien Date: Sun, 15 Mar 2026 19:58:46 +0100 Subject: [PATCH] first commit --- ARCHITECTURE.md | 277 +++++++++++ README.md | 221 +++++++++ config/codium/settings.json | 10 + config/firefox/policies.json | 86 ++++ lib.sh | 739 ++++++++++++++++++++++++++++++ profiles/cli.sh | 2 + profiles/desktop.sh | 2 + profiles/devel.sh | 2 + profiles/server.sh | 2 + roles.sh | 38 ++ roles/base/l10n.packages | 4 + roles/base/packages.list | 11 + roles/base/repo.sh | 47 ++ roles/codium/config.sh | 22 + roles/codium/packages.list | 3 + roles/codium/repo.sh | 28 ++ roles/desktop/config.sh | 14 + roles/desktop/packages.list | 14 + roles/devel/config.sh | 13 + roles/devel/packages.list | 9 + roles/docker/packages.list | 7 + roles/docker/repo.sh | 29 ++ roles/firewall/config.sh | 21 + roles/firewall/rules.common.list | 1 + roles/firewall/rules.desktop.list | 1 + roles/firewall/rules.devel.list | 2 + roles/firewall/rules.server.list | 8 + roles/server/config.sh | 28 ++ roles/server/packages.list | 3 + roles/zram/config.sh | 30 ++ roles/zram/packages.list | 3 + run.sh | 177 +++++++ 32 files changed, 1854 insertions(+) create mode 100644 ARCHITECTURE.md create mode 100644 README.md create mode 100644 config/codium/settings.json create mode 100644 config/firefox/policies.json create mode 100755 lib.sh create mode 100755 profiles/cli.sh create mode 100755 profiles/desktop.sh create mode 100755 profiles/devel.sh create mode 100755 profiles/server.sh create mode 100755 roles.sh create mode 100644 roles/base/l10n.packages create mode 100644 roles/base/packages.list create mode 100755 roles/base/repo.sh create mode 100755 roles/codium/config.sh create mode 100644 roles/codium/packages.list create mode 100755 roles/codium/repo.sh create mode 100755 roles/desktop/config.sh create mode 100644 roles/desktop/packages.list create mode 100755 roles/devel/config.sh create mode 100644 roles/devel/packages.list create mode 100644 roles/docker/packages.list create mode 100755 roles/docker/repo.sh create mode 100755 roles/firewall/config.sh create mode 100644 roles/firewall/rules.common.list create mode 100644 roles/firewall/rules.desktop.list create mode 100644 roles/firewall/rules.devel.list create mode 100644 roles/firewall/rules.server.list create mode 100755 roles/server/config.sh create mode 100644 roles/server/packages.list create mode 100755 roles/zram/config.sh create mode 100644 roles/zram/packages.list create mode 100755 run.sh diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..75c3fe9 --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,277 @@ +# Architecture NETbian + +Ce document decrit l'architecture actuelle de NETbian et le pipeline d'execution du provisioning. + +## Vue d'ensemble + +NETbian repose sur une architecture **profiles + roles**. + +```text +profile -> liste ordonnee de roles +role -> implementation technique autonome +``` + +Un profil ne contient que de la composition. Toute la logique technique est portee par les roles et par les fonctions partagees de `lib.sh`. + +## Arborescence + +```text +netbian/ +├── run.sh +├── roles.sh +├── lib.sh +├── profiles/ +│ ├── cli.sh +│ ├── desktop.sh +│ ├── devel.sh +│ └── server.sh +├── roles/ +│ ├── base/ +│ ├── codium/ +│ ├── desktop/ +│ ├── devel/ +│ ├── docker/ +│ ├── firewall/ +│ ├── server/ +│ └── zram/ +└── config/ +``` + +## Profils + +Chaque profil est un script shell qui declare `ROLE_ORDER`. + +Exemple avec `profiles/devel.sh` : + +```bash +ROLE_ORDER=(base desktop firewall zram docker codium devel) +``` + +Profils fournis : + +- `cli` -> `base zram` +- `server` -> `base firewall server zram docker` +- `desktop` -> `base desktop firewall zram` +- `devel` -> `base desktop firewall zram docker codium devel` + +## Contrat d'un role + +Un role est un repertoire sous `roles//`. + +Fichiers reconnus par le moteur : + +- `repo.sh` +- `packages.list` +- `install.sh` +- `config.sh` +- `run.sh` +- `l10n.packages` +- `rules..list` + +Tout autre fichier est rejete par `validate_role()` pour eviter les derives silencieuses. + +## Pipeline global + +### 1. `run.sh` + +`run.sh` est le point d'entree. Il : + +1. initialise l'environnement (`PROJECT_DIR`, `ROLE_DIR`, `PROFILE_DIR`) +2. charge `lib.sh` et active `set -Eeuo pipefail` +3. parse les options CLI +4. peut lister les profils et roles disponibles +5. verifie les prerequis (`root`, Debian 13, reseau HTTP) +6. valide puis lit / met a jour le fichier de configuration +7. exporte `CONFIG_FILE` +8. execute `roles.sh` + +### 2. `roles.sh` + +`roles.sh` : + +1. charge `lib.sh` +2. valide `CONFIG_FILE` +3. lit `CONFIG_FILE` +4. recupere `profile` +5. source `profiles/.sh` +6. valide la definition du profil et les roles references +7. execute `run_role` pour chaque role dans l'ordre +8. imprime un resume d'execution en sortie + +## Resolution de configuration + +Ordre de priorite : + +1. options CLI +2. variables d'environnement `NETBIAN_*` +3. fichier de configuration cible + +Variables gerees : + +- `profile` +- `lang` +- `CONFIG_FILE` (transportee par l'environnement entre `run.sh` et `roles.sh`) + +`validate_config_file()` refuse les lignes invalides et controle les formats de `profile` et `lang` avant sourcing. + +## Pipeline d'un role + +Le moteur execute les etapes suivantes dans cet ordre : + +1. `repo.sh` +2. `packages.list` +3. `install.sh` +4. `config.sh` +5. `run.sh` + +Toutes les etapes sont optionnelles. + +Implementation logique de `run_role()` : + +```text +validate_role +run_role_script repo.sh +load_role_packages +append_localized_packages +run_role_packages +run_role_script install.sh +run_role_script config.sh +run_role_script run.sh +``` + +Les erreurs sont propagees explicitement avec `|| return 1` pour eviter de dependre uniquement du comportement implicite de `set -e`. + +## Gestion des paquets + +- `load_role_packages()` charge `roles//packages.list` +- `append_localized_packages()` enrichit la liste avec les paquets localises definis dans `roles/base/l10n.packages` +- la langue utilisee provient de la configuration active (`lang`) +- `run_role_packages()` appelle `ensure_packages_installed()` + +Si un role n'a aucun paquet a installer, le moteur enregistre un `[SKIP]` sur `role/packages.list`. + +## Configuration et copie de fichiers + +`copy_config()` copie les fichiers depuis `config/` vers leur destination finale : + +- propriete `root:root` pour les fichiers systeme +- propriete utilisateur si la destination est sous `/home//` +- ecriture idempotente si le contenu est identique + +`write_if_changed()` et `write_text_file_if_changed()` centralisent les ecritures atomiques simples. + +## Firewall declaratif + +Le role `firewall` utilise un systeme declaratif base sur des listes de regles : + +```text +roles/firewall/ +├── rules.common.list +├── rules.desktop.list +├── rules.devel.list +└── rules.server.list +``` + +Ordre d'application : + +1. initialisation UFW +2. regles communes +3. regles specifiques au profil + +Chaque ligne correspond a une regle passee a `ufw allow`. + +Exemple : + +```text +# rules.common.list +ssh + +# rules.server.list +http +https +imap +imaps +smtp +submissions +``` + +Le modele est volontairement unique : les ouvertures UFW sont centralisees dans les fichiers `rules.*.list`, pas dans les autres roles. + +## Services systeme + +`restart_service_if_present()` permet de redemarrer un service uniquement s'il existe. Cela evite de rendre certains roles fragiles sur des machines ou le service n'est pas installe. + +Le role `server` s'appuie sur ce mecanisme apres ecriture de la configuration SSH. + +## Logs et resume d'execution + +Le moteur expose les helpers suivants : + +- `log_run` +- `log_ok` +- `log_skip` +- `log_info` +- `log_warn` + +Tags utilises par l'orchestrateur : + +- `[RUN]` +- `[OK]` +- `[SKIP]` +- `[INFO]` +- `[WARN]` + +Des tableaux runtime suivent les etapes et roles executes, ignores ou en echec : + +- `EXECUTED_STEPS` +- `SKIPPED_STEPS` +- `FAILED_STEPS` +- `EXECUTED_ROLES` + +Le resume est imprime par `print_execution_summary()` via un `trap EXIT` dans `roles.sh`. + +## Decouverte et validation + +Lister les profils : + +```bash +./run.sh --list-profiles +``` + +Lister les roles : + +```bash +./run.sh --list-roles +``` + +Validation minimale recommandee : + +```bash +bash -n run.sh roles.sh lib.sh roles/*/*.sh profiles/*.sh +./run.sh --list-profiles +./run.sh --list-roles +``` + +Validation recommandee avant production : + +1. machine Debian 13 fraiche +2. test de chaque profil +3. verification des services systemd critiques (`ssh`, `docker`, `zramswap`) +4. verification UFW apres run +5. rerun complet pour confirmer l'idempotence + +## Principes de conception + +- **Idempotence** : rejouer un role ne doit pas deteriorer l'etat. +- **Composition simple** : les profils ne font qu'ordonner des roles. +- **Validation stricte** : les roles, manifests et fichiers de configuration sont verifies avant execution. +- **Ecritures limitees** : seules les differences utiles sont ecrites. +- **Previsibilite** : les comportements critiques sont centralises dans `lib.sh`. +- **Source de verite unique** : un seul mecanisme par sujet critique (ex. firewall). + + +## Resume d execution + +- `Steps executed` compte les etapes ayant effectue une action. +- `Steps skipped` compte les etapes entierement conformes ou sautees par l'orchestrateur. +- `Skip events` compte tous les messages `[SKIP]` emis pendant l'execution. diff --git a/README.md b/README.md new file mode 100644 index 0000000..d338b7b --- /dev/null +++ b/README.md @@ -0,0 +1,221 @@ +# NETbian + +NETbian est un outil de provisioning pour **Debian 13** base sur une architecture **profiles + roles**. + +Chaque **profil** compose une suite de **roles fonctionnels**. Un role regroupe au meme endroit : + +- ses paquets +- sa configuration APT eventuelle +- ses actions d'installation +- sa configuration systeme +- ses actions finales eventuelles + +Le projet est pense pour rester **idempotent** : un second passage doit converger vers l'etat attendu au lieu de casser la machine. + +## Profils disponibles + +- `cli` +- `server` +- `desktop` +- `devel` + +Les profils sont definis dans `profiles/` et peuvent etre listes avec : + +```bash +./run.sh --list-profiles +``` + +## Roles disponibles + +Le depot contient actuellement les roles suivants : + +- `base` +- `codium` +- `desktop` +- `devel` +- `docker` +- `firewall` +- `server` +- `zram` + +Ils peuvent etre listes avec : + +```bash +./run.sh --list-roles +``` + +## Structure du depot + +```text +netbian/ +├── run.sh +├── roles.sh +├── lib.sh +├── profiles/ +│ ├── cli.sh +│ ├── desktop.sh +│ ├── devel.sh +│ └── server.sh +├── roles/ +│ ├── base/ +│ ├── codium/ +│ ├── desktop/ +│ ├── devel/ +│ ├── docker/ +│ ├── firewall/ +│ ├── server/ +│ └── zram/ +└── config/ +``` + +## Convention d'un role + +Un role peut contenir les fichiers suivants : + +- `packages.list` : paquets du role +- `repo.sh` : ajout de depots / cles APT +- `install.sh` : installation specifique +- `config.sh` : configuration systeme +- `run.sh` : actions finales du role +- `l10n.packages` : mappings de paquets localises +- `rules..list` : regles declaratives UFW pour le role firewall + +Tous ces fichiers sont **optionnels**. Le moteur execute uniquement ceux qui existent. + +## Profils fournis + +### `cli` + +Provisioning minimal en ligne de commande : + +```bash +ROLE_ORDER=(base zram) +``` + +### `server` + +Machine orientee services : + +```bash +ROLE_ORDER=(base firewall server zram docker) +``` + +### `desktop` + +Poste utilisateur graphique : + +```bash +ROLE_ORDER=(base desktop firewall zram) +``` + +### `devel` + +Poste de developpement : + +```bash +ROLE_ORDER=(base desktop firewall zram docker codium devel) +``` + +## Installation + +```bash +git clone https://git.netig.net/netig/netbian.git +cd netbian +sudo ./run.sh --profile devel +``` + +## Utilisation + +```text +Usage: run.sh [options] +Options: + -p, --profile Profile to install (required) + -l, --lang Language code if translations are needed (e.g. fr, es, de) + --config Config file path (default: /etc/netbian.conf) + --list-profiles List available profiles and exit + --list-roles List available roles and exit + -h, --help Show this help +``` + +Exemples : + +```bash +sudo ./run.sh --profile server +sudo ./run.sh --profile devel --lang fr +sudo ./run.sh --config /root/netbian.conf --profile desktop +NETBIAN_PROFILE=server sudo -E ./run.sh +./run.sh --list-profiles +./run.sh --list-roles +``` + +## Configuration + +`run.sh` lit et met a jour un fichier de configuration shell simple, par defaut : + +```text +/etc/netbian.conf +``` + +Variables gerees actuellement : + +```bash +profile=devel +lang=fr +``` + +Priorite de resolution : + +1. options CLI (`--profile`, `--lang`, `--config`) +2. variables d'environnement (`NETBIAN_PROFILE`, `NETBIAN_LANG`, `NETBIAN_CONFIG_FILE`) +3. fichier de configuration cible +4. absence de valeur -> erreur si `profile` reste non defini + +Le meme chemin de configuration est ensuite reutilise par `roles.sh`. + +## Logs d'execution + +NETbian utilise des logs simples pour le moteur d'orchestration : + +- `[RUN]` : etape lancee +- `[OK]` : etape executee avec succes +- `[SKIP]` : etat deja conforme ou etape vide +- `[INFO]` : information de progression +- `[WARN]` : situation non bloquante + +Les scripts de roles peuvent aussi ecrire leurs propres messages metier, mais la recommandation est d'utiliser les helpers centralises pour garder des traces homogenes. + +## Choix d'architecture pour un niveau infra mature + +- **Firewall centralise** : seules les listes `roles/firewall/rules.*.list` definissent les ouvertures UFW. +- **Validation stricte** : les roles, manifests et le fichier de configuration sont verifies avant execution. +- **Resume de run** : `roles.sh` imprime un recapitulatif des roles, etapes executees, sautees et en echec. +- **Precedence explicite** : la resolution de la configuration est documentee et stable. + +## Particularites importantes + +- `run.sh` exige les droits `root`. +- Le projet verifie qu'il tourne sur **Debian 13**. +- Un test reseau HTTP est effectue avant provisioning en utilisant `curl` puis `wget`. +- Le role `firewall` applique des regles `ufw` a partir de fichiers declaratifs. +- Le role `server` ecrit une configuration SSH dediee puis tente de recharger `ssh` / `sshd` si le service existe. +- Le role `codium` installe VSCodium, ses extensions et la configuration utilisateur si un utilisateur cible est detecte. +- Les paquets localises eventuels sont ajoutes a partir de la variable `lang` de la configuration active. + +## Validation rapide + +Avant livraison, au minimum : + +```bash +bash -n run.sh roles.sh lib.sh roles/*/*.sh profiles/*.sh +./run.sh --list-profiles +./run.sh --list-roles +``` + +Pour une validation production complete, il reste recommande de tester le provisioning sur une **Debian 13 fraiche** pour verifier le comportement reel des roles et des services. + + +## Resume d execution + +- `Steps executed` compte les etapes ayant effectue une action. +- `Steps skipped` compte les etapes entierement conformes ou sautees par l'orchestrateur. +- `Skip events` compte tous les messages `[SKIP]` emis pendant l'execution. diff --git a/config/codium/settings.json b/config/codium/settings.json new file mode 100644 index 0000000..f2c0026 --- /dev/null +++ b/config/codium/settings.json @@ -0,0 +1,10 @@ +{ + // Largeur maximale recommandée (règle d'édition) + "editor.rulers": [ + 100 + ], + // Chemin vers l'exécutable PHP système (utilisé pour la validation) + "php.validate.executablePath": "/usr/bin/php", + // Chemin relatif vers le binaire php-cs-fixer du projet + "php-cs-fixer.executablePath": "${workspaceFolder}/vendor/bin/php-cs-fixer", +} \ No newline at end of file diff --git a/config/firefox/policies.json b/config/firefox/policies.json new file mode 100644 index 0000000..96ffcdc --- /dev/null +++ b/config/firefox/policies.json @@ -0,0 +1,86 @@ +{ + "policies": { + "OverrideFirstRunPage": "", + "NoDefaultBookmarks": true, + "DisableTelemetry": true, + "DisableFirefoxAccounts": true, + "DisablePocket": true, + "FirefoxHome": { + "Search": false, + "TopSites": false, + "SponsoredTopSites": false, + "Highlights": false, + "Pocket": false, + "Stories": false, + "SponsoredPocket": false, + "SponsoredStories": false, + "Snippets": false + }, + "Homepage": { + "StartPage": "none" + }, + "NewTabPage": false, + "SearchEngines": { + "Remove": [ + "Bing", + "eBay", + "Google", + "Perplexity", + "Qwant" + ], + "Default": "DuckDuckGo" + }, + "UserMessaging": { + "ExtensionRecommendations": false, + "FeatureRecommendations": false, + "UrlbarInterventions": false, + "SkipOnboarding": false, + "MoreFromMozilla": false, + "FirefoxLabs": false + }, + "GenerativeAI": { + "Enabled": false, + "Chatbot": false, + "LinkPreviews": false, + "TabGroups": false + }, + "SearchSuggestEnabled": false, + "FirefoxSuggest": { + "WebSuggestions": false, + "SponsoredSuggestions": false, + "ImproveSuggest": false + }, + "EnableTrackingProtection": { + "Value": true, + "Cryptomining": true, + "Fingerprinting": true, + "EmailTracking": true, + "SuspectedFingerprinting": true, + "Category": "strict", + "BaselineExceptions": false, + "ConvenienceExceptions": true + }, + "AutofillAddressEnabled": false, + "AutofillCreditCardEnabled": false, + "Preferences": { + "privacy.globalprivacycontrol.enabled": true, + "network.cookie.cookieBehavior": 1, + "signon.rememberSignons": false, + "browser.formfill.enable": false, + "browser.search.suggest.enabled": false, + "browser.urlbar.suggest.searches": false, + "browser.urlbar.suggest.history": false, + "browser.urlbar.suggest.bookmark": false, + "browser.urlbar.suggest.openpage": false, + "browser.urlbar.suggest.topsites": false, + "browser.urlbar.suggest.quicksuggest": false, + "browser.urlbar.suggest.pocket": false, + "browser.urlbar.suggest.engines": false + }, + "Extensions": { + "Install": [ + "https://addons.mozilla.org/firefox/downloads/latest/ublock-origin/latest.xpi" + ] + } + } +} \ No newline at end of file diff --git a/lib.sh b/lib.sh new file mode 100755 index 0000000..1e7c724 --- /dev/null +++ b/lib.sh @@ -0,0 +1,739 @@ +#!/usr/bin/env bash +# Bibliotheque centrale + +if [[ "${NETBIAN_LIB_SOURCED:-}" == "1" ]]; then + return 0 2>/dev/null || exit 0 +fi +NETBIAN_LIB_SOURCED=1 +readonly NETBIAN_LIB_SOURCED + +############################################################################### +### Helpers generaux +# + +enable_strict_mode() { + set -Eeuo pipefail +} + +err() { echo "$@" >&2; } +errln() { printf '%s\n' "$*" >&2; } +fatal() { + err "$@" + exit 2 +} + +exists_cmd() { + command -v "$1" >/dev/null 2>&1 +} + +safe_tempfile() { + mktemp --tmpdir netbian.tmp.XXXXXX 2>/dev/null || mktemp /tmp/netbian.tmp.XXXXXX +} + +require_debian_version() { + local requested="${1:-}" + [[ -n "$requested" ]] || fatal "Missing required Debian version argument." + [[ -r /etc/os-release ]] || fatal "/etc/os-release not found or not readable." + + . /etc/os-release + [[ "${ID:-}" == "debian" ]] || fatal "Not Debian (ID=${ID:-})." + + case "${VERSION_ID:-}" in + "$requested" | "$requested".*) return 0 ;; + *) fatal "You are using Debian ${VERSION_ID:-}, Debian ${requested} is required." ;; + esac +} + +require_valid_name() { + local kind="$1" value="$2" + [[ -n "$value" ]] || fatal "Missing ${kind}." + [[ "$value" =~ ^[a-z0-9][a-z0-9_-]*$ ]] || fatal "Invalid ${kind}: ${value}" +} + +validate_config_file() { + local cfg="$1" line lineno=0 key value + [[ -f "$cfg" ]] || return 0 + + while IFS= read -r line || [[ -n "$line" ]]; do + lineno=$((lineno + 1)) + [[ -z "$line" ]] && continue + [[ "$line" =~ ^[[:space:]]*# ]] && continue + if [[ ! "$line" =~ ^[A-Za-z_][A-Za-z0-9_]*= ]]; then + fatal "Invalid config line ${cfg}:${lineno}: ${line}" + fi + key="${line%%=*}" + value="${line#*=}" + case "$key" in + profile) require_valid_name "profile" "$value" ;; + lang) + [[ -z "$value" || "$value" =~ ^[a-z]{2}([-_][a-z]{2})?$ ]] || fatal "Invalid language code in ${cfg}:${lineno}: ${value}" + ;; + esac + done <"$cfg" +} + +############################################################################### +### Logging and execution summary +# + +log_run() { printf '[RUN] %s\n' "$*"; } +log_ok() { printf '[OK] %s\n' "$*"; } +log_skip() { + NETBIAN_SKIP_EVENTS=$(( ${NETBIAN_SKIP_EVENTS:-0} + 1 )) + printf '[SKIP] %s\n' "$*" +} +log_warn() { printf '[WARN] %s\n' "$*"; } +log_info() { printf '[INFO] %s\n' "$*"; } + +# Runtime flags +NETBIAN_DRY_RUN="${NETBIAN_DRY_RUN:-0}" +NETBIAN_DEBUG="${NETBIAN_DEBUG:-0}" + +# Execution summary +if [[ -z "${NETBIAN_SUMMARY_INITIALIZED:-}" ]]; then + declare -ag EXECUTED_STEPS=() + declare -ag SKIPPED_STEPS=() + declare -ag EXECUTED_ROLES=() + declare -ag FAILED_STEPS=() + NETBIAN_SKIP_EVENTS=0 + NETBIAN_SUMMARY_INITIALIZED=1 +fi + +debug_enabled() { + [[ "${NETBIAN_DEBUG:-0}" == "1" ]] +} + +dry_run_enabled() { + [[ "${NETBIAN_DRY_RUN:-0}" == "1" ]] +} + +record_executed_step() { + EXECUTED_STEPS+=("$1") +} + +record_skipped_step() { + SKIPPED_STEPS+=("$1") +} + +record_failed_step() { + FAILED_STEPS+=("$1") +} + +record_executed_role() { + EXECUTED_ROLES+=("$1") +} + +print_execution_summary() { + local item + echo + echo "==== Execution summary ====" + printf 'Roles executed : %d\n' "${#EXECUTED_ROLES[@]}" + printf 'Steps executed : %d\n' "${#EXECUTED_STEPS[@]}" + printf 'Steps skipped : %d\n' "${#SKIPPED_STEPS[@]}" + printf 'Skip events : %d\n' "${NETBIAN_SKIP_EVENTS:-0}" + printf 'Steps failed : %d\n' "${#FAILED_STEPS[@]}" + + if ((${#EXECUTED_ROLES[@]} > 0)); then + echo "Executed roles:" + for item in "${EXECUTED_ROLES[@]}"; do + printf ' - %s\n' "$item" + done + fi + + if ((${#SKIPPED_STEPS[@]} > 0)); then + echo "Skipped steps:" + for item in "${SKIPPED_STEPS[@]}"; do + printf ' - %s\n' "$item" + done + fi + + if ((${#FAILED_STEPS[@]} > 0)); then + echo "Failed steps:" + for item in "${FAILED_STEPS[@]}"; do + printf ' - %s\n' "$item" + done + fi +} + +############################################################################### +### Gestion de fichiers +# + +write_if_changed() { + local src="$1" dst="$2" + + if [[ -f "$dst" ]] && cmp -s "$src" "$dst"; then + rm -f "$src" + return 1 + fi + + mv "$src" "$dst" + chmod 0644 "$dst" + return 0 +} + +write_text_file_if_changed() { + local content="$1" dst="$2" mode="${3:-0644}" tmp + install -d -m0755 "$(dirname "$dst")" + tmp="$(safe_tempfile)" || tmp="/tmp/netbian.write.$$" + printf '%s' "$content" >"$tmp" + if write_if_changed "$tmp" "$dst"; then + chmod "$mode" "$dst" + return 0 + fi + return 1 +} + +copy_config() { + local rel="$1" dest_dir="$2" src file dest user rootname + + [[ -n "$rel" && -n "$dest_dir" ]] || { + echo "Usage: copy_config " >&2 + return 2 + } + + src="$PROJECT_DIR/config/$rel" + [[ -f "$src" ]] || { + log_warn "$src not found - skipping" + return 0 + } + + file="$(basename "$rel")" + dest="$(realpath -m "$dest_dir")/$file" + install -d "$(dirname "$dest")" + + rootname="${rel%%/*}" + rootname="$(tr '[:lower:]' '[:upper:]' <<<"${rootname:0:1}")${rootname:1}" + if [[ ! -f "$dest" ]] || ! cmp -s "$src" "$dest"; then + log_info "${rootname} configuration copied" + cp -a "$src" "$dest" + else + log_skip "${rootname} already configured" + fi + + if [[ "$dest" =~ ^/home/([^/]+)/ ]]; then + user="${BASH_REMATCH[1]}" + if id -u "$user" >/dev/null 2>&1; then + chown "$user:$user" "$dest" + chmod 0664 "$dest" + return 0 + fi + fi + + chown root:root "$dest" + chmod 0644 "$dest" + return 0 +} + +############################################################################### +### Paquets / APT +# + +ensure_packages_installed() { + local to_install=() p prev_deb + for p in "$@"; do + if ! dpkg-query -W -f='${Status}' "$p" 2>/dev/null | grep -q '^install ok installed$'; then + to_install+=("$p") + fi + done + + if ((${#to_install[@]} == 0)); then + log_skip "packages already installed" + return 3 + fi + + prev_deb="${DEBIAN_FRONTEND:-}" + export DEBIAN_FRONTEND=noninteractive + if ! apt-get install -y "${to_install[@]}"; then + if [[ -n "$prev_deb" ]]; then + export DEBIAN_FRONTEND="$prev_deb" + else + unset DEBIAN_FRONTEND + fi + echo "Failed to install packages: ${to_install[*]}" >&2 + return 1 + fi + + if [[ -n "$prev_deb" ]]; then + export DEBIAN_FRONTEND="$prev_deb" + else + unset DEBIAN_FRONTEND + fi + return 0 +} + +add_apt_key_from_url() { + local key_url="$1" keyring_path="$2" tmpkey tmpkeyring + install -d -m0755 "$(dirname "$keyring_path")" + tmpkey="$(safe_tempfile)" || tmpkey="/tmp/key.$$" + tmpkeyring="$(safe_tempfile)" || tmpkeyring="/tmp/keyring.$$" + + ( + set -e + if exists_cmd curl; then + curl -fsSL "$key_url" -o "$tmpkey" + elif exists_cmd wget; then + wget -qO "$tmpkey" "$key_url" + else + echo "curl or wget is required to download the key, but none are installed." >&2 + exit 1 + fi + + exists_cmd gpg || { + echo "gpg is required to dearmor the key, but is not installed." >&2 + exit 1 + } + + gpg --batch --yes --dearmor -o "$tmpkeyring" "$tmpkey" + mv -f "$tmpkeyring" "$keyring_path" + chown root:root "$keyring_path" || true + chmod a+r "$keyring_path" + rm -f "$tmpkey" + ) +} + +add_apt_source_file() { + local content="$1" dest="$2" tmpfile + install -d -m0755 "$(dirname "$dest")" + tmpfile="$(safe_tempfile)" || tmpfile="/tmp/src.$$" + printf '%s\n' "$content" >"$tmpfile" + + if [[ -f "$dest" ]] && cmp -s "$tmpfile" "$dest"; then + rm -f "$tmpfile" + return 0 + fi + + mv -f "$tmpfile" "$dest" + chmod 0644 "$dest" + return 0 +} + +add_apt_sources_file() { + add_apt_source_file "$1" "$2" +} + +install_apt_repo() { + local key_url="$1" keyring_path="$2" sources_content="$3" sources_path="$4" + shift 4 + local pkgs=("$@") all_installed=true p key_added=false src_changed=false + + for p in "${pkgs[@]}"; do + if ! dpkg-query -W -f='${Status}' "$p" 2>/dev/null | grep -q 'installed'; then + all_installed=false + break + fi + done + $all_installed && return 0 + + if [[ ! -f "$keyring_path" ]]; then + if add_apt_key_from_url "$key_url" "$keyring_path"; then + key_added=true + else + log_warn "failed to add key from $key_url" + fi + fi + + if [[ ! -f "$sources_path" ]] || ! printf '%s\n' "$sources_content" | cmp -s - "$sources_path"; then + src_changed=true + add_apt_sources_file "$sources_content" "$sources_path" + fi + + if $key_added || $src_changed; then + apt-get update + fi + return 0 +} + +############################################################################### +### Systeme +# + +ufw_initialize() { + ufw --force reset >/dev/null || true + ufw default deny incoming + ufw default allow outgoing + ufw --force enable +} + +apply_ufw_rules_file() { + local rules_file="$1" rule + [[ -f "$rules_file" ]] || return 0 + + while IFS= read -r rule || [[ -n "$rule" ]]; do + rule="${rule%%#*}" + rule="$(echo "$rule" | xargs)" + [[ -z "$rule" ]] && continue + log_info "Allowing firewall rule: $rule" + ufw allow "$rule" + done <"$rules_file" + return 0 +} + +remove_primary_network_section() { + local if_file="/etc/network/interfaces" header='# The primary network interface' tmp + if [[ -f "$if_file" ]] && grep -q "^${header}" "$if_file" && sed --version >/dev/null 2>&1; then + tmp="$(mktemp --tmpdir netif.XXXXXX)" || tmp="/tmp/netif.$$" + sed "/${header}/Q" "$if_file" >"$tmp" || true + mv -f "$tmp" "$if_file" + fi +} + +ensure_grub_cmdline() { + local f="${1:-/etc/default/grub}" from="${2:-quiet}" to="${3:-quiet loglevel=3 nowatchdog}" + local pat="^GRUB_CMDLINE_LINUX_DEFAULT=\"${from}\"$" + local repl="GRUB_CMDLINE_LINUX_DEFAULT=\"${to}\"" + + [[ -r "$f" ]] || { + echo "File not found or not readable: $f" >&2 + return 1 + } + + if grep -qE "$pat" "$f"; then + sed -i "s|${pat}|${repl}|" "$f" + command -v update-grub >/dev/null 2>&1 && update-grub || true + log_ok "GRUB configuration updated" + else + log_skip "GRUB command line already compliant" + fi + return 0 +} + +service_exists() { + local service="$1" + systemctl list-unit-files --type=service --no-legend 2>/dev/null | awk '{print $1}' | grep -Fxq "${service}.service" +} + +restart_service_if_present() { + local service="$1" + if service_exists "$service"; then + log_info "Restarting ${service}" + systemctl restart "$service" || true + else + log_skip "service ${service} absent" + fi +} + +############################################################################### +### Utilisateur / roles +# + +get_target_user() { + local user="" + + if [[ -n "${SUDO_USER:-}" && "${SUDO_USER}" != "root" ]]; then + printf '%s\n' "$SUDO_USER" + return 0 + fi + + if [[ -n "${PKEXEC_UID:-}" ]] && user="$(id -nu "$PKEXEC_UID" 2>/dev/null || true)" && [[ -n "$user" && "$user" != "root" ]]; then + printf '%s\n' "$user" + return 0 + fi + + user="$(logname 2>/dev/null || true)" + if [[ -n "$user" && "$user" != "root" ]]; then + printf '%s\n' "$user" + return 0 + fi + + return 1 +} + +install_code_extensions() { + local code_bin="$1" + shift + [[ -n "$code_bin" && "$#" -gt 0 ]] || return 0 + + if ! command -v "$code_bin" >/dev/null 2>&1 && [[ -x "/usr/bin/$code_bin" ]]; then + code_bin="/usr/bin/$code_bin" + elif ! command -v "$code_bin" >/dev/null 2>&1; then + echo "Code binary $code_bin not found; skipping extensions install." >&2 + return 0 + fi + + local run_as ext installed_list + run_as="$(get_target_user 2>/dev/null || true)" + + log_info "Synchronizing ${code_bin##*/} extensions" + + if [[ -n "$run_as" ]]; then + installed_list="$(sudo -u "$run_as" env PATH="${PATH:-/usr/bin:/bin}" "$code_bin" --list-extensions 2>/dev/null || true)" + else + installed_list="$("$code_bin" --list-extensions 2>/dev/null || true)" + fi + + for ext in "$@"; do + if grep -Fqx "$ext" <<<"$installed_list"; then + log_skip "extension '$ext' already installed" + continue + fi + + log_info "Installing extension '$ext'" + if [[ -n "$run_as" ]]; then + sudo -u "$run_as" env PATH="${PATH:-/usr/bin:/bin}" "$code_bin" --install-extension "$ext" --force || true + else + "$code_bin" --install-extension "$ext" --force || true + fi + done + return 0 +} + +configure_php_no_jit() { + local php_version="${1:-$(php -r 'echo PHP_MAJOR_VERSION.".".PHP_MINOR_VERSION;' 2>/dev/null || true)}" + local mod_name="no-jit" mods_dir mod_file sapi + local config_content='; Disable JIT because it conflicts with Xdebug +opcache.jit=0 +opcache.jit_buffer_size=0 +' + + [[ -n "$php_version" ]] || { + echo "Unable to detect PHP version." >&2 + return 1 + } + + log_info "Using PHP version: ${php_version}" + [[ -d "/etc/php/${php_version}" ]] || { + echo "PHP config directory not found: /etc/php/${php_version}" >&2 + return 1 + } + + mods_dir="/etc/php/${php_version}/mods-available" + mod_file="${mods_dir}/${mod_name}.ini" + install -d "$mods_dir" + write_text_file_if_changed "$config_content" "$mod_file" >/dev/null || true + + if exists_cmd phpenmod; then + phpenmod -v "$php_version" "$mod_name" + else + echo "phpenmod not found; skipping module enablement." >&2 + fi + + if exists_cmd phpquery; then + echo + echo "Module status by SAPI:" + for sapi in cli apache2 fpm; do + if phpquery -v "$php_version" -s "$sapi" -m "$mod_name" >/dev/null 2>&1; then + echo " - ${sapi}: enabled" + elif [[ -d "/etc/php/${php_version}/${sapi}" ]]; then + echo " - ${sapi}: not enabled" + fi + done + fi + + restart_service_if_present apache2 + restart_service_if_present "php${php_version}-fpm" + + echo + echo "Verification:" + php -i | grep -E '^opcache.jit =>|^opcache.jit_buffer_size =>|^Loaded Configuration File|^Scan this dir for additional .ini files' || true + return 0 +} + +load_role_packages() { + local role="$1" + local packages_file="$ROLE_DIR/$role/packages.list" + ROLE_PACKAGES=() + + if [[ -f "$packages_file" ]]; then + # shellcheck disable=SC1090 + source "$packages_file" + fi + + return 0 +} + +append_localized_packages() { + local -n target_ref="$1" + local lang_override="${lang:-}" l10n_fragment="$ROLE_DIR/base/l10n.packages" + local mapping base prefix cand + + [[ -n "$lang_override" && -f "$l10n_fragment" ]] || return 0 + + L10N_MAP_PKGS=() + source "$l10n_fragment" + apt-get update >/dev/null 2>&1 || true + + for mapping in "${L10N_MAP_PKGS[@]:-}"; do + base="${mapping%%::*}" + prefix="${mapping##*::}" + cand="${prefix}-${lang_override}" + + if apt-cache show "$cand" >/dev/null 2>&1 || { + dpkg-query -W -f='${Status}' "$base" 2>/dev/null | grep -q 'installed' && + ! dpkg-query -W -f='${Status}' "$cand" 2>/dev/null | grep -q 'installed' + }; then + case " ${target_ref[*]} " in + *" $cand "*) : ;; + *) target_ref+=("$cand") ;; + esac + fi + done + return 0 +} + +verify_role_manifest() { + local role="$1" + local manifest="$ROLE_DIR/$role/packages.list" + local line trimmed + + [[ -f "$manifest" ]] || return 0 + + while IFS= read -r line || [[ -n "$line" ]]; do + trimmed="${line#"${line%%[![:space:]]*}"}" + [[ -z "$trimmed" ]] && continue + [[ "$trimmed" =~ ^# ]] && continue + if [[ "$trimmed" =~ [[:space:]] ]]; then + fatal "Invalid package entry in $manifest: $trimmed" + fi + done <"$manifest" + return 0 +} + +validate_role() { + local role="$1" + local role_path="$ROLE_DIR/$role" + local allowed='^(repo\.sh|install\.sh|config\.sh|run\.sh|packages\.list|l10n\.packages|rules\.[A-Za-z0-9_-]+\.list)$' + local item base + + require_valid_name "role" "$role" + [[ -d "$role_path" ]] || fatal "Role not found: $role" + + shopt -s nullglob + for item in "$role_path"/*; do + base="$(basename "$item")" + [[ "$base" =~ $allowed ]] || fatal "Unexpected file in role $role: $base" + done + shopt -u nullglob + return 0 +} + +run_role_script() { + local role="$1" + local step="$2" + local script="$ROLE_DIR/$role/$step" + local step_name="$role/$step" + + if [[ ! -f "$script" ]]; then + return 0 + fi + + log_run "$step_name" + if dry_run_enabled; then + log_ok "$step_name (dry-run)" + record_skipped_step "$step_name (dry-run)" + return 0 + fi + + if bash "$script"; then + log_ok "$step_name" + record_executed_step "$step_name" + else + record_failed_step "$step_name" + fatal "Step failed: $step_name" + fi + return 0 +} + +run_role_packages() { + local role="$1" + local -n pkgs_ref="$2" + local step_name="$role/packages.list" + + if ((${#pkgs_ref[@]} == 0)); then + log_skip "$step_name empty" + record_skipped_step "$step_name" + return 0 + fi + + log_run "$step_name" + if dry_run_enabled; then + printf '[DRY] Would ensure packages: %s\n' "${pkgs_ref[*]}" + log_ok "$step_name (dry-run)" + record_skipped_step "$step_name (dry-run)" + return 0 + fi + + local rc=0 + if ensure_packages_installed "${pkgs_ref[@]}"; then + rc=0 + else + rc=$? + fi + + case "$rc" in + 0) + log_ok "$step_name" + record_executed_step "$step_name" + ;; + 3) + log_ok "$step_name (already compliant)" + record_skipped_step "$step_name" + ;; + *) + return 1 + ;; + esac + return 0 +} + +run_role() { + local role="$1" + local -a role_packages=() + + validate_role "$role" + echo + echo "==== Role: $role ====" + + run_role_script "$role" repo.sh || return 1 + load_role_packages "$role" || return 1 + role_packages=("${ROLE_PACKAGES[@]:-}") + append_localized_packages role_packages || return 1 + run_role_packages "$role" role_packages || return 1 + run_role_script "$role" install.sh || return 1 + run_role_script "$role" config.sh || return 1 + run_role_script "$role" run.sh || return 1 + + record_executed_role "$role" + return 0 +} + +list_available_profiles() { + local p + shopt -s nullglob + for p in "$PROFILE_DIR"/*.sh; do + basename "${p%.sh}" + done + shopt -u nullglob + return 0 +} + +list_available_roles() { + local r + shopt -s nullglob + for r in "$ROLE_DIR"/*; do + [[ -d "$r" ]] || continue + basename "$r" + done + shopt -u nullglob + return 0 +} + +validate_profile_definition() { + local profile_file="$1" + [[ -f "$profile_file" ]] || fatal "Profile definition not found: $profile_file" + + declare -a ROLE_ORDER=() + # shellcheck disable=SC1090 + source "$profile_file" + ((${#ROLE_ORDER[@]} > 0)) || fatal "No roles declared in $profile_file" + return 0 +} + +validate_all_roles() { + local role + for role in "$@"; do + validate_role "$role" + verify_role_manifest "$role" + done + return 0 +} diff --git a/profiles/cli.sh b/profiles/cli.sh new file mode 100755 index 0000000..01d017c --- /dev/null +++ b/profiles/cli.sh @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +ROLE_ORDER=(base zram) diff --git a/profiles/desktop.sh b/profiles/desktop.sh new file mode 100755 index 0000000..ad66d52 --- /dev/null +++ b/profiles/desktop.sh @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +ROLE_ORDER=(base desktop firewall zram) diff --git a/profiles/devel.sh b/profiles/devel.sh new file mode 100755 index 0000000..4dd1909 --- /dev/null +++ b/profiles/devel.sh @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +ROLE_ORDER=(base desktop firewall zram docker codium devel) diff --git a/profiles/server.sh b/profiles/server.sh new file mode 100755 index 0000000..4f318aa --- /dev/null +++ b/profiles/server.sh @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +ROLE_ORDER=(base firewall server zram docker) diff --git a/roles.sh b/roles.sh new file mode 100755 index 0000000..0bc4e75 --- /dev/null +++ b/roles.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# Orchestrateur de roles +source "$PROJECT_DIR/lib.sh" +enable_strict_mode + +CONFIG_FILE="${CONFIG_FILE:-/etc/netbian.conf}" +[[ -f "$CONFIG_FILE" ]] || fatal "Config file $CONFIG_FILE not found. Create it via run.sh." +validate_config_file "$CONFIG_FILE" +source "$CONFIG_FILE" +[[ -n "${profile:-}" ]] || fatal "The 'profile' variable is not set in $CONFIG_FILE." +export profile +[[ -n "${lang:-}" ]] && export lang + +PROFILE_FILE="$PROFILE_DIR/${profile}.sh" +[[ -f "$PROFILE_FILE" ]] || fatal "Profile definition not found: $PROFILE_FILE" + +trap 'status=$?; print_execution_summary; exit $status' EXIT + +declare -a ROLE_ORDER=() +source "$PROFILE_FILE" + +((${#ROLE_ORDER[@]} > 0)) || fatal "No roles declared in $PROFILE_FILE" + +validate_profile_definition "$PROFILE_FILE" +validate_all_roles "${ROLE_ORDER[@]}" + +for role in "${ROLE_ORDER[@]}"; do + run_role "$role" +done + +cat <<'EOM' + ____ _ + / ___| _ _ ___ ___ ___ ___ ___ | | + \___ \| | | |/ __/ __/ _ \/ __/ __| | | + ___) | |_| | (_| (_| __/\__ \__ \ |_| + |____/ \__,_|\___\___\___||___/___/ (_) + +EOM diff --git a/roles/base/l10n.packages b/roles/base/l10n.packages new file mode 100644 index 0000000..3508952 --- /dev/null +++ b/roles/base/l10n.packages @@ -0,0 +1,4 @@ +L10N_MAP_PKGS=( + "firefox-esr::firefox-esr-l10n" + "libreoffice::libreoffice-l10n" +) diff --git a/roles/base/packages.list b/roles/base/packages.list new file mode 100644 index 0000000..a1a199e --- /dev/null +++ b/roles/base/packages.list @@ -0,0 +1,11 @@ +ROLE_PACKAGES=( + "ca-certificates" + "curl" + "git" + "gnupg" + "htop" + "rsync" + "tree" + "ufw" + "wget" +) diff --git a/roles/base/repo.sh b/roles/base/repo.sh new file mode 100755 index 0000000..4ee3c55 --- /dev/null +++ b/roles/base/repo.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +# Configure les dépôts Debian de base +source "$PROJECT_DIR/lib.sh" +enable_strict_mode + +cat <<'EOM' + +=> Base APT configuration + +EOM + +KEY_URL="https://ftp-master.debian.org/keys/archive-key-12.asc" +KEYRING="/usr/share/keyrings/debian-archive-keyring.pgp" +SOURCES="/etc/apt/sources.list.d/debian.sources" + +read -r -d '' CONTENT <&2 + fi +fi + +if [[ ! -f "$SOURCES" ]] || ! printf '%s\n' "$CONTENT" | cmp -s - "$SOURCES"; then + add_apt_sources_file "$CONTENT" "$SOURCES" || { + echo "Failed to write $SOURCES" >&2 + exit 1 + } + apt-get update + echo "File $SOURCES written" +else + echo "File $SOURCES unchanged" +fi diff --git a/roles/codium/config.sh b/roles/codium/config.sh new file mode 100755 index 0000000..ee5266b --- /dev/null +++ b/roles/codium/config.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +source "$PROJECT_DIR/lib.sh" +enable_strict_mode + +cat <<'EOM' + +=> Codium configuration + +EOM + +install_code_extensions codium \ + junstyle.php-cs-fixer \ + mkhl.shfmt \ + sibiraj-s.vscode-scss-formatter \ + asispts.vscode-symfony-twig || true + +TARGET_USER="$(get_target_user 2>/dev/null || true)" +if [[ -n "$TARGET_USER" ]]; then + copy_config "codium/settings.json" "/home/${TARGET_USER}/.config/VSCodium/User" +else + echo 'No regular target user detected for VSCodium settings; skipping user settings copy.' >&2 +fi diff --git a/roles/codium/packages.list b/roles/codium/packages.list new file mode 100644 index 0000000..c733402 --- /dev/null +++ b/roles/codium/packages.list @@ -0,0 +1,3 @@ +ROLE_PACKAGES=( + "codium" +) diff --git a/roles/codium/repo.sh b/roles/codium/repo.sh new file mode 100755 index 0000000..28b6f45 --- /dev/null +++ b/roles/codium/repo.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +source "$PROJECT_DIR/lib.sh" +enable_strict_mode + +cat <<'EOM' + +=> VSCodium + +EOM + +KEYRING_DIR="/etc/apt/keyrings" +KEYRING="$KEYRING_DIR/vscodium-archive-keyring.gpg" +KEY_URL="https://gitlab.com/paulcarroty/vscodium-deb-rpm-repo/-/raw/master/pub.gpg" +SRC_FILE="/etc/apt/sources.list.d/vscodium.sources" +VSCODIUM_URI="https://download.vscodium.com/debs" +ARCH_CUR=$(dpkg --print-architecture 2>/dev/null || true) +ARCH_CUR=${ARCH_CUR:-amd64} + +read -r -d '' VSCODIUM_SOURCES_CONTENT < Desktop configuration + +EOM + +ensure_grub_cmdline +remove_primary_network_section +copy_config "firefox/policies.json" "/etc/firefox/policies" diff --git a/roles/desktop/packages.list b/roles/desktop/packages.list new file mode 100644 index 0000000..9e2a88d --- /dev/null +++ b/roles/desktop/packages.list @@ -0,0 +1,14 @@ +ROLE_PACKAGES=( + "gimp" + "gnome-core" + "gnome-music" + "gnome-shell-extension-caffeine" + "gnome-tweaks" + "gufw" + "libreoffice" + "keepassxc-minimal" + "papirus-icon-theme" + "qbittorrent" + "firefox-esr" + "torbrowser-launcher" +) diff --git a/roles/devel/config.sh b/roles/devel/config.sh new file mode 100755 index 0000000..4055716 --- /dev/null +++ b/roles/devel/config.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +source "$PROJECT_DIR/lib.sh" +enable_strict_mode + +cat <<'EOM' + +=> Developer environment + +EOM + +configure_php_no_jit +log_ok "PHP developer configuration applied" +echo 'Developer profile ready.' diff --git a/roles/devel/packages.list b/roles/devel/packages.list new file mode 100644 index 0000000..86213b8 --- /dev/null +++ b/roles/devel/packages.list @@ -0,0 +1,9 @@ +ROLE_PACKAGES=( + "php-cli" + "composer" + "sqlite3" + "php-sqlite3" + "npm" + "shfmt" + "php-xdebug" +) diff --git a/roles/docker/packages.list b/roles/docker/packages.list new file mode 100644 index 0000000..ad973c8 --- /dev/null +++ b/roles/docker/packages.list @@ -0,0 +1,7 @@ +ROLE_PACKAGES=( + "docker-ce" + "docker-ce-cli" + "containerd.io" + "docker-buildx-plugin" + "docker-compose-plugin" +) diff --git a/roles/docker/repo.sh b/roles/docker/repo.sh new file mode 100755 index 0000000..29bad64 --- /dev/null +++ b/roles/docker/repo.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +source "$PROJECT_DIR/lib.sh" +enable_strict_mode + +cat <<'EOM' + +=> Docker + +EOM + +KEYRING_DIR="/etc/apt/keyrings" +KEYRING="$KEYRING_DIR/docker.gpg" +KEY_URL="https://download.docker.com/linux/debian/gpg" +SRC_FILE="/etc/apt/sources.list.d/docker.sources" +CODENAME=$(source /etc/os-release && echo "$VERSION_CODENAME") +DOCKER_URI="https://download.docker.com/linux/debian" +ARCH_CUR=$(dpkg --print-architecture 2>/dev/null || true) +ARCH_CUR=${ARCH_CUR:-amd64} + +read -r -d '' DOCKER_SOURCES_CONTENT < Firewall configuration + +EOM + +ufw_initialize + +COMMON_RULES_FILE="$ROLE_DIR/firewall/rules.common.list" +PROFILE_RULES_FILE="$ROLE_DIR/firewall/rules.${profile:-}.list" + +apply_ufw_rules_file "$COMMON_RULES_FILE" +apply_ufw_rules_file "$PROFILE_RULES_FILE" + +ufw reload +log_ok "Firewall rules applied" diff --git a/roles/firewall/rules.common.list b/roles/firewall/rules.common.list new file mode 100644 index 0000000..6979494 --- /dev/null +++ b/roles/firewall/rules.common.list @@ -0,0 +1 @@ +# Common firewall rules diff --git a/roles/firewall/rules.desktop.list b/roles/firewall/rules.desktop.list new file mode 100644 index 0000000..abfef4c --- /dev/null +++ b/roles/firewall/rules.desktop.list @@ -0,0 +1 @@ +# Desktop-specific firewall rules diff --git a/roles/firewall/rules.devel.list b/roles/firewall/rules.devel.list new file mode 100644 index 0000000..21ae19c --- /dev/null +++ b/roles/firewall/rules.devel.list @@ -0,0 +1,2 @@ +# Development-specific firewall rules +# 3000/tcp diff --git a/roles/firewall/rules.server.list b/roles/firewall/rules.server.list new file mode 100644 index 0000000..e63690a --- /dev/null +++ b/roles/firewall/rules.server.list @@ -0,0 +1,8 @@ +# Server-specific firewall rules +ssh +http +https +imap +imaps +smtp +submissions diff --git a/roles/server/config.sh b/roles/server/config.sh new file mode 100755 index 0000000..7233f55 --- /dev/null +++ b/roles/server/config.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# Configuration du rôle server +source "$PROJECT_DIR/lib.sh" +enable_strict_mode + +cat <<'EOM' + +=> Server configuration + +EOM + +ensure_grub_cmdline + +SSH_DIR="/etc/ssh/sshd_config.d" +mkdir -p "$SSH_DIR" +SSH_CONF="${SSH_DIR}/custom.conf" + +read -r -d '' SSH_CONF_CONTENT <<'EOM' || true +# SSH keys only +PasswordAuthentication no +PubkeyAuthentication yes +PermitEmptyPasswords no +EOM + +if write_text_file_if_changed "$SSH_CONF_CONTENT" "$SSH_CONF" >/dev/null; then + restart_service_if_present ssh + restart_service_if_present sshd +fi diff --git a/roles/server/packages.list b/roles/server/packages.list new file mode 100644 index 0000000..baebd08 --- /dev/null +++ b/roles/server/packages.list @@ -0,0 +1,3 @@ +ROLE_PACKAGES=( + # add server-specific packages here, e.g. "nginx" "postgresql" "fail2ban" +) diff --git a/roles/zram/config.sh b/roles/zram/config.sh new file mode 100755 index 0000000..98df9b8 --- /dev/null +++ b/roles/zram/config.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +source "${PROJECT_DIR:?}/lib.sh" +enable_strict_mode + +cat <<'EOM' + +=> ZRAM + +EOM + +ZFILE="/etc/default/zramswap" + +if [[ ! -f "$ZFILE" ]]; then + write_text_file_if_changed $'# Configuration minimale pour zramswap\nALGO=zstd\nPERCENT=50\n' "$ZFILE" >/dev/null || true +fi + +if grep -q '^ALGO=lz4' "$ZFILE" 2>/dev/null; then + sed -i 's/^ALGO=lz4/ALGO=zstd/' "$ZFILE" +elif ! grep -q '^ALGO=' "$ZFILE" 2>/dev/null; then + echo 'ALGO=zstd' >>"$ZFILE" +fi + +if ! grep -q '^PERCENT=' "$ZFILE" 2>/dev/null; then + echo 'PERCENT=50' >>"$ZFILE" +fi + +restart_service_if_present zramswap +if ! systemctl is-active --quiet zramswap.service; then + echo 'Warning: zramswap.service not active' >&2 +fi diff --git a/roles/zram/packages.list b/roles/zram/packages.list new file mode 100644 index 0000000..7d39b30 --- /dev/null +++ b/roles/zram/packages.list @@ -0,0 +1,3 @@ +ROLE_PACKAGES=( + "zram-tools" +) diff --git a/run.sh b/run.sh new file mode 100755 index 0000000..caaa67b --- /dev/null +++ b/run.sh @@ -0,0 +1,177 @@ +#!/usr/bin/env bash +# Point d'entree pour le provisioning NETbian + +export PROJECT_DIR="$(cd "$(dirname "$0")" && pwd)" +export ROLE_DIR="$PROJECT_DIR/roles" +export PROFILE_DIR="$PROJECT_DIR/profiles" + +source "$PROJECT_DIR/lib.sh" +enable_strict_mode + +PROG="$(basename "$0")" +usage() { + local exit_code="${1:-1}" + cat <&2 +Usage: $PROG [options] +Options: + -p, --profile Profile to install (required) + -l, --lang Language code if translations are needed (e.g. fr, es, de) + --config Config file path (default: /etc/netbian.conf) + --list-profiles List available profiles and exit + --list-roles List available roles and exit + -h, --help Show this help +EOM + exit "$exit_code" +} + +PROFILE_OVERRIDE="${NETBIAN_PROFILE:-}" +LANG_OVERRIDE="${NETBIAN_LANG:-}" +CONFIG_FILE="${NETBIAN_CONFIG_FILE:-/etc/netbian.conf}" +ARGS_PROVIDED=false +LIST_PROFILES=false +LIST_ROLES=false + +if [[ $# -gt 0 ]]; then + ARGS_PROVIDED=true + while [[ $# -gt 0 ]]; do + case "$1" in + -p | --profile) + shift + [[ $# -gt 0 && -n "${1:-}" ]] || { + err "Option --profile requires a value." + usage + } + PROFILE_OVERRIDE="$1" + shift + ;; + -l | --lang) + shift + [[ $# -gt 0 && -n "${1:-}" ]] || { + err "Option --lang requires a value." + usage + } + LANG_OVERRIDE="$1" + shift + ;; + --config) + shift + [[ $# -gt 0 && -n "${1:-}" ]] || { + err "Option --config requires a path." + usage + } + CONFIG_FILE="$1" + shift + ;; + --list-profiles) + LIST_PROFILES=true + shift + ;; + --list-roles) + LIST_ROLES=true + shift + ;; + -h | --help) + usage 0 + ;; + -*) + err "Unknown option: $1" + usage + ;; + *) + err "Positional arguments are not supported." + usage + ;; + esac + done +fi + +if $LIST_PROFILES; then + list_available_profiles | sort + exit 0 +fi + +if $LIST_ROLES; then + list_available_roles | sort + exit 0 +fi + +[[ "$(id -u)" -eq 0 ]] || fatal "Please run this script as root." +require_debian_version "13" +if exists_cmd curl; then + curl -fsSI --connect-timeout 10 https://deb.debian.org/ >/dev/null || fatal "Network unavailable (HTTP test failed with curl)." +elif exists_cmd wget; then + wget -q --timeout=10 --spider https://deb.debian.org/ >/dev/null 2>&1 || fatal "Network unavailable (HTTP test failed with wget)." +else + fatal "curl or wget is required for the network availability check." +fi + +cat <<'EOM' + _ _ _____ _____ _ _ + | \ | | ____|_ _| |__ (_) __ _ _ __ + | \| | _| | | | '_ \| |/ _` | '_ \ + | |\ | |___ | | | |_) | | (_| | | | | + |_| \_|_____| |_| |_.__/|_|\__,_|_| |_| + +EOM + +declare -A CFG +CONFIG_EXISTED=false +if [[ -f "$CONFIG_FILE" ]]; then + CONFIG_EXISTED=true + validate_config_file "$CONFIG_FILE" + source "$CONFIG_FILE" + [[ -n "${profile:-}" ]] && CFG[profile]="$profile" + [[ -n "${lang:-}" ]] && CFG[lang]="$lang" +fi + +if [[ -n "$PROFILE_OVERRIDE" ]]; then + PROFILE_OVERRIDE="$(tr '[:upper:]' '[:lower:]' <<<"$PROFILE_OVERRIDE")" + case "$PROFILE_OVERRIDE" in + server | desktop | devel | cli) CFG[profile]="$PROFILE_OVERRIDE" ;; + *) + err "Invalid profile" + usage + ;; + esac +fi +if [[ -n "$LANG_OVERRIDE" ]]; then + CFG[lang]="$(tr '[:upper:]' '[:lower:]' <<<"$LANG_OVERRIDE" | tr '_' '-')" +fi + +[[ -n "${CFG[profile]:-}" ]] || { + err "Profile not provided. Use --profile or NETBIAN_PROFILE." + usage +} + +TMP_IN="$(mktemp --tmpdir netbian.conf.in.XXXXXX)" || TMP_IN="/tmp/netbian.conf.in.$$" +TMP_OUT="$(mktemp --tmpdir netbian.conf.out.XXXXXX)" || TMP_OUT="/tmp/netbian.conf.out.$$" +trap 'rm -f "$TMP_IN" "$TMP_OUT"' EXIT + +if [[ -f "$CONFIG_FILE" ]]; then + grep -v -E '^(profile|lang)=' "$CONFIG_FILE" >"$TMP_IN" || true +else + : >"$TMP_IN" +fi + +{ + cat "$TMP_IN" + printf 'profile=%s\n' "${CFG[profile]}" + [[ -n "${CFG[lang]:-}" ]] && printf 'lang=%s\n' "${CFG[lang]}" +} >"$TMP_OUT" + +if write_if_changed "$TMP_OUT" "$CONFIG_FILE"; then + echo "Configuration written to $CONFIG_FILE:" + grep -E '^(profile|lang)=' "$CONFIG_FILE" || true +elif $CONFIG_EXISTED; then + if $ARGS_PROVIDED; then + echo "No changes; configuration remains in $CONFIG_FILE:" + else + echo "Configuration read from $CONFIG_FILE:" + fi + grep -E '^(profile|lang)=' "$CONFIG_FILE" || true +else + echo "No configuration file present and no changes were made." +fi + +export CONFIG_FILE +exec "$PROJECT_DIR/roles.sh"