first commit

This commit is contained in:
julien
2026-03-15 19:58:46 +01:00
commit bbc4e4da65
32 changed files with 1854 additions and 0 deletions

277
ARCHITECTURE.md Normal file
View File

@@ -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/<nom>/`.
Fichiers reconnus par le moteur :
- `repo.sh`
- `packages.list`
- `install.sh`
- `config.sh`
- `run.sh`
- `l10n.packages`
- `rules.<suffix>.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/<profile>.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/<role>/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/<user>/`
- 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.

221
README.md Normal file
View File

@@ -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.<suffix>.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 <server|desktop|devel|cli> Profile to install (required)
-l, --lang <code> Language code if translations are needed (e.g. fr, es, de)
--config <path> 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.

View File

@@ -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",
}

View File

@@ -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"
]
}
}
}

739
lib.sh Executable file
View File

@@ -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 <relpath> <dest_dir>" >&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
}

2
profiles/cli.sh Executable file
View File

@@ -0,0 +1,2 @@
#!/usr/bin/env bash
ROLE_ORDER=(base zram)

2
profiles/desktop.sh Executable file
View File

@@ -0,0 +1,2 @@
#!/usr/bin/env bash
ROLE_ORDER=(base desktop firewall zram)

2
profiles/devel.sh Executable file
View File

@@ -0,0 +1,2 @@
#!/usr/bin/env bash
ROLE_ORDER=(base desktop firewall zram docker codium devel)

2
profiles/server.sh Executable file
View File

@@ -0,0 +1,2 @@
#!/usr/bin/env bash
ROLE_ORDER=(base firewall server zram docker)

38
roles.sh Executable file
View File

@@ -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

4
roles/base/l10n.packages Normal file
View File

@@ -0,0 +1,4 @@
L10N_MAP_PKGS=(
"firefox-esr::firefox-esr-l10n"
"libreoffice::libreoffice-l10n"
)

11
roles/base/packages.list Normal file
View File

@@ -0,0 +1,11 @@
ROLE_PACKAGES=(
"ca-certificates"
"curl"
"git"
"gnupg"
"htop"
"rsync"
"tree"
"ufw"
"wget"
)

47
roles/base/repo.sh Executable file
View File

@@ -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 <<EOM || true
Types: deb
URIs: https://deb.debian.org/debian
Suites: trixie trixie-updates
Components: main non-free-firmware contrib
Signed-By: $KEYRING
Types: deb
URIs: https://security.debian.org/debian-security
Suites: trixie-security
Components: main non-free-firmware contrib
Signed-By: $KEYRING
EOM
[[ -f /etc/apt/sources.list ]] && rm -f /etc/apt/sources.list && echo "Old /etc/apt/sources.list removed."
if [[ ! -f "$KEYRING" ]]; then
if ! add_apt_key_from_url "$KEY_URL" "$KEYRING"; then
echo "Warning: failed to add key from $KEY_URL" >&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

22
roles/codium/config.sh Executable file
View File

@@ -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

View File

@@ -0,0 +1,3 @@
ROLE_PACKAGES=(
"codium"
)

28
roles/codium/repo.sh Executable file
View File

@@ -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 <<EOM || true
Types: deb
URIs: $VSCODIUM_URI
Suites: vscodium
Components: main
Architectures: $ARCH_CUR
Signed-By: $KEYRING
EOM
install_apt_repo "$KEY_URL" "$KEYRING" "$VSCODIUM_SOURCES_CONTENT" "$SRC_FILE" codium

14
roles/desktop/config.sh Executable file
View File

@@ -0,0 +1,14 @@
#!/usr/bin/env bash
# Configuration du rôle desktop
source "$PROJECT_DIR/lib.sh"
enable_strict_mode
cat <<'EOM'
=> Desktop configuration
EOM
ensure_grub_cmdline
remove_primary_network_section
copy_config "firefox/policies.json" "/etc/firefox/policies"

View File

@@ -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"
)

13
roles/devel/config.sh Executable file
View File

@@ -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.'

View File

@@ -0,0 +1,9 @@
ROLE_PACKAGES=(
"php-cli"
"composer"
"sqlite3"
"php-sqlite3"
"npm"
"shfmt"
"php-xdebug"
)

View File

@@ -0,0 +1,7 @@
ROLE_PACKAGES=(
"docker-ce"
"docker-ce-cli"
"containerd.io"
"docker-buildx-plugin"
"docker-compose-plugin"
)

29
roles/docker/repo.sh Executable file
View File

@@ -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 <<EOM || true
Types: deb
URIs: $DOCKER_URI
Suites: $CODENAME
Components: stable
Architectures: $ARCH_CUR
Signed-By: $KEYRING
EOM
install_apt_repo "$KEY_URL" "$KEYRING" "$DOCKER_SOURCES_CONTENT" "$SRC_FILE" docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

21
roles/firewall/config.sh Executable file
View File

@@ -0,0 +1,21 @@
#!/usr/bin/env bash
# Declarative firewall configuration with common + profile-specific rules
source "$PROJECT_DIR/lib.sh"
enable_strict_mode
cat <<'EOM'
=> 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"

View File

@@ -0,0 +1 @@
# Common firewall rules

View File

@@ -0,0 +1 @@
# Desktop-specific firewall rules

View File

@@ -0,0 +1,2 @@
# Development-specific firewall rules
# 3000/tcp

View File

@@ -0,0 +1,8 @@
# Server-specific firewall rules
ssh
http
https
imap
imaps
smtp
submissions

28
roles/server/config.sh Executable file
View File

@@ -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

View File

@@ -0,0 +1,3 @@
ROLE_PACKAGES=(
# add server-specific packages here, e.g. "nginx" "postgresql" "fail2ban"
)

30
roles/zram/config.sh Executable file
View File

@@ -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

3
roles/zram/packages.list Normal file
View File

@@ -0,0 +1,3 @@
ROLE_PACKAGES=(
"zram-tools"
)

177
run.sh Executable file
View File

@@ -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 <<EOM >&2
Usage: $PROG [options]
Options:
-p, --profile <server|desktop|devel|cli> Profile to install (required)
-l, --lang <code> Language code if translations are needed (e.g. fr, es, de)
--config <path> 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"