From 4f8bb61458793099e2909ca7671525a7ff27cc22 Mon Sep 17 00:00:00 2001 From: bastien Date: Tue, 22 Sep 2026 17:34:18 +0200 Subject: [PATCH] feat(cloudpex): site values out of the script, prompted at install into /etc/cloudpex.conf cloudpex/cloudpex no longer carries the NAS host, share name, SMB user, mount point or SMB version. It reads /etc/cloudpex.conf (root:root 0600, KEY=value) line by line, never sources it, and dies with a hint when the file is missing, incomplete or has a relative mount point. cloudpex/install.sh prompts for the five values (regex-validated, re-asked on bad input so the main installer never aborts), shows and keeps an existing config unless answered n, and skips the config when no terminal is attached. README (FR) + root README + CLAUDE.md updated. --- .claude/tasks/TODO.md | 9 +++++ CLAUDE.md | 3 +- README.md | 4 +- cloudpex/README.md | 49 +++++++++++++++++------- cloudpex/cloudpex | 32 +++++++++++----- cloudpex/install.sh | 86 ++++++++++++++++++++++++++++++++++++------- 6 files changed, 144 insertions(+), 39 deletions(-) diff --git a/.claude/tasks/TODO.md b/.claude/tasks/TODO.md index 0425793..efadf73 100644 --- a/.claude/tasks/TODO.md +++ b/.claude/tasks/TODO.md @@ -41,3 +41,12 @@ Root cause: /tmp is tmpfs (50% RAM) → agents fill it → RAM halved + ENOSPC b - [x] README.md steps 12-14 + table rows; CLAUDE.md layout - [x] shellcheck + bash -n (install.sh, cloudpex/install.sh); stub-sudo dry run of the offers - [x] commit on feature branch (no gitea-deploy/, no .githooks changes) + +## Round 2 — cloudpex config out of script, reconcile main/develop, capitalize, merge (2026-09-22) +- [x] cloudpex/cloudpex: constants → /etc/cloudpex.conf parsed line by line (never sourced), die if missing +- [x] cloudpex/install.sh: prompt host/share/user/mnt/vers (regex-validated), keep-existing [Y/n], no-TTY skip +- [x] cloudpex/README.md + README.md + CLAUDE.md: no site values, describe prompts + conf file +- [x] registries: BDR-010/011/012, LRN-009/010/011, BLK-005/006, EVAL-002, journal +- [ ] reconcile: merge main (a210d01 dtach) into develop via lib helper +- [ ] gitflow finish feature → develop (explicit user signal: "puis merge") +- [ ] runbook for live apply on this machine diff --git a/CLAUDE.md b/CLAUDE.md index 2975c04..c2ec799 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -26,7 +26,8 @@ etc/profile.d/disk-usage-warning.sh login-time low-disk warning → /etc/pro etc/tmpfiles.d/tmp.conf disk-backed /tmp cleanup rules (offer: /tmp on disk) etc/systemd/ssh.service.d/override.conf sshd OOM-exempt drop-in (offer: SSH memory guard) etc/default/earlyoom earlyoom args, spare sshd / kill node first (same offer) -cloudpex/{cloudpex,install.sh,README.md} on-demand CloudPex SMB mount helper → /usr/local/bin (FR docs) +cloudpex/{cloudpex,install.sh,README.md} on-demand SMB mount helper → /usr/local/bin; site values + prompted at install → /etc/cloudpex.conf, never in the script (FR docs) .claude/{tasks,memory,audits}/ Claude working state ``` diff --git a/README.md b/README.md index 08f2eee..48ad7bf 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ curl -fsSL https://git.bchanot.fr/bchanot/config/raw/branch/master/remote-instal | Path | Purpose | | -------------------- | -------------------------------------------------------------- | | `install.sh` | Installs apt packages + Docker + code-server + RDP (gnome-remote-desktop), backs up old config, deploys vim + bashrc (OS-detected), installs CLI scripts, pipx tools, a low-disk login warning and the `cloudpex` NAS mount helper; ends by offering two system changes (`/tmp` on disk, SSH memory guard). | -| `cloudpex/` | On-demand SMB mount of the CloudPex NAS share (`cloudpex` command + its installer). French README inside. | +| `cloudpex/` | On-demand SMB mount of a NAS share (`cloudpex` command + its installer). Site values (host, share, SMB user, mount point, SMB version) are prompted at install and stored in `/etc/cloudpex.conf`, never in the script. French README inside. | | `etc/tmpfiles.d/tmp.conf` | Cleanup rules for a disk-backed `/tmp` (wiped at boot, 10-day purge). Deployed by the `/tmp` on disk offer. | | `etc/systemd/ssh.service.d/override.conf` | `ssh.service` drop-in: sshd exempt from the OOM killer + memory reclaim protection. Deployed by the SSH memory guard offer. | | `etc/default/earlyoom` | earlyoom arguments: spare sshd/systemd, kill node/java first. Deployed by the SSH memory guard offer. | @@ -65,7 +65,7 @@ What it does: 9. On Linux, installs `etc/profile.d/disk-usage-warning.sh` to `/etc/profile.d/` (needs `sudo`) so each login warns when `/` or `/home` cross 85% usage. 10. On Linux, installs **code-server** (VS Code in the browser) via its vendor script — skipped if already present — and enables the `code-server@$USER` systemd service. 11. On Linux, sets up **RDP remote login** via `gnome-remote-desktop` (Wayland-native): installs the daemon + `openssl`, generates a self-signed TLS cert once, and prompts interactively for shared "gate" credentials (skipped when no terminal is attached, or already set). Disables `xrdp` if present; opens UFW port `3389` only when UFW is already active. -12. On Linux, installs the **`cloudpex`** NAS mount helper to `/usr/local/bin` via `cloudpex/install.sh` (nothing is mounted, no credential stored, see [`cloudpex/README.md`](cloudpex/README.md)). +12. On Linux, installs the **`cloudpex`** NAS mount helper to `/usr/local/bin` via `cloudpex/install.sh`, which prompts for the NAS host, share name, SMB user, mount point and SMB version and writes them to `/etc/cloudpex.conf` (root, `0600`; an existing config is shown and kept unless you say `n`; skipped when no terminal is attached). Nothing is mounted, no password stored, see [`cloudpex/README.md`](cloudpex/README.md). 13. On Linux, at the very end, **offers** (`[y/N]`, skipped when no terminal is attached) to move **`/tmp` to disk**: Ubuntu mounts `/tmp` as a RAM-backed tmpfs capped at 50% of RAM, which agent runs fill, halving the RAM and breaking every shell with "No space left on device". Accepting masks `tmp.mount` and installs `etc/tmpfiles.d/tmp.conf` (wipe at boot, 10-day purge). Effective at the next reboot. 14. On Linux, at the very end, **offers** to keep **SSH reachable under memory pressure**: installs the `ssh.service` drop-in (`OOMScoreAdjust=-1000`, `MemoryMin=256M`) and `earlyoom` with `etc/default/earlyoom` (kills the largest process, `node`/`java` first and never `sshd`, once free RAM and swap both drop under 10%). Restarting `ssh` keeps open sessions. Note: `MemoryMin` protects the sshd daemon only; login sessions live in `user.slice`, so no setting can reserve RAM for a future shell. earlyoom acting in time is the real protection. diff --git a/cloudpex/README.md b/cloudpex/README.md index d0d9415..de9e7fd 100644 --- a/cloudpex/README.md +++ b/cloudpex/README.md @@ -1,9 +1,13 @@ -# cloudpex : montage à la demande du partage SMB CloudPex +# cloudpex : montage à la demande d'un partage SMB (NAS) -`cloudpex` monte et démonte le partage SMB `CloudPex` du NAS (`//192.168.1.111/CloudPex`) -sur `/mnt/cloudpex`. Le mot de passe SMB est demandé à chaque montage. Rien n'est -écrit sur disque, rien ne passe en argument (le mot de passe est transmis à -`mount.cifs` par la variable d'environnement `PASSWD`, invisible dans `ps`). +`cloudpex` monte et démonte un partage SMB du NAS sur un point de montage local. +Le mot de passe SMB est demandé à chaque montage. Rien n'est écrit sur disque, +rien ne passe en argument (le mot de passe est transmis à `mount.cifs` par la +variable d'environnement `PASSWD`, invisible dans `ps`). + +Les valeurs propres au site (hôte du NAS, nom du partage, utilisateur SMB, point +de montage, version SMB) ne sont pas dans le script. Elles sont demandées à +l'installation et écrites dans `/etc/cloudpex.conf`, lisible par root seulement. ## Pourquoi à la demande, et pas dans fstab @@ -26,7 +30,7 @@ tes propres processus. ## Usage ```sh -cloudpex # monte (demande le mot de passe de bchanot_smb) +cloudpex # monte (demande le mot de passe SMB) cloudpex -s # état cloudpex -u # démonte (alias : -d, dc, disconnect, disable) cloudpex -h # aide @@ -45,19 +49,36 @@ Ce que ça fait, à l'identique de cette machine : | Cible | Détail | | --- | --- | | `/usr/local/bin/cloudpex` | copie du script, `root:root`, `0755` | -| `/mnt/cloudpex` | point de montage, créé vide | +| `/etc/cloudpex.conf` | les cinq valeurs du site, demandées au clavier, `root:root`, `0600` | +| point de montage | créé vide (`/mnt/cloudpex` par défaut) | | `cifs-utils` | installé via `apt-get` seulement si `mount.cifs` manque | -Réexécutable. `../install.sh` l'appelle sur Linux, donc une installation complète -du dépôt suffit. Rien n'est monté à l'installation. +Questions posées (défaut entre crochets) : -## Paramètres +``` +Hôte du NAS (IP ou nom) : +Nom du partage SMB : +Utilisateur SMB : +Point de montage [/mnt/cloudpex] : +Version SMB [3.0] : +``` -En tête de `cloudpex` : `SHARE`, `MNT`, `SMB_USER`, `SMB_VERS`. Pour changer de NAS -ou de compte, édite ces valeurs puis relance `./cloudpex/install.sh`. +Réexécutable : si `/etc/cloudpex.conf` existe, il est affiché et gardé sauf +réponse `n`. Sans terminal (`curl | bash`), le script est réinstallé mais la +config n'est ni créée ni modifiée. `../install.sh` appelle cet installeur sur +Linux. Rien n'est monté à l'installation. + +## Changer de NAS, de partage ou de compte + +Relance `./cloudpex/install.sh` et réponds `n` à « La garder ? », ou édite +`/etc/cloudpex.conf` en root (format `CLÉ=valeur`, une par ligne : `HOST`, +`SHARE`, `SMB_USER`, `MNT`, `SMB_VERS`). Le script lit ce fichier ligne à ligne, +il ne l'exécute jamais. ## Dépannage +- `config absente` : lance `./cloudpex/install.sh` depuis un terminal. - Échec du montage : `dmesg | tail` (mot de passe, réseau, ou version SMB - refusée par le NAS : essayer `SMB_VERS="3.1.1"`). -- Démontage refusé (fichiers ouverts) : `lsof +D /mnt/cloudpex`, fermer, réessayer. + refusée par le NAS : essayer `SMB_VERS=3.1.1` dans la config). +- Démontage refusé (fichiers ouverts) : `lsof +D `, fermer, + réessayer. diff --git a/cloudpex/cloudpex b/cloudpex/cloudpex index 7daaa0f..26ddb6f 100644 --- a/cloudpex/cloudpex +++ b/cloudpex/cloudpex @@ -1,18 +1,16 @@ #!/usr/bin/env bash -# cloudpex-mount — monte / démonte le partage SMB CloudPex du NAS -# Usage : cloudpex-mount -> monte (demande le mot de passe) -# cloudpex-mount -u -> démonte -# cloudpex-mount -s -> état +# cloudpex — monte / démonte le partage SMB du NAS déclaré dans /etc/cloudpex.conf +# Usage : cloudpex -> monte (demande le mot de passe) +# cloudpex -u -> démonte +# cloudpex -s -> état # Aucun credential n'est écrit sur disque ni passé en argument (invisible dans ps). set -euo pipefail -SHARE="//192.168.1.111/CloudPex" -MNT="/mnt/cloudpex" -SMB_USER="bchanot_smb" -SMB_VERS="3.0" +CONF="/etc/cloudpex.conf" -# Re-lance le script en root si nécessaire (avant toute saisie du mot de passe) +# Re-lance le script en root si nécessaire (avant toute saisie du mot de passe, +# et avant de lire la config, lisible par root seulement) if [[ $EUID -ne 0 ]]; then exec sudo -- "$0" "$@" fi @@ -23,6 +21,22 @@ OWNER_GID="${SUDO_GID:-1000}" die() { echo "Erreur : $*" >&2; exit 1; } +# Valeurs propres au site (hôte, partage, utilisateur, point de montage, version), +# écrites par cloudpex/install.sh au format CLÉ=valeur. Lues ligne à ligne, +# jamais sourcées : le fichier de config n'exécute rien. +conf_get() { sed -n "s/^$1=//p" "$CONF" | head -n 1; } + +[[ -r $CONF ]] || die "config absente : $CONF (lance cloudpex/install.sh)" +HOST="$(conf_get HOST)" +SHARE_NAME="$(conf_get SHARE)" +SMB_USER="$(conf_get SMB_USER)" +MNT="$(conf_get MNT)" +SMB_VERS="$(conf_get SMB_VERS)" +[[ -n $HOST && -n $SHARE_NAME && -n $SMB_USER && -n $MNT && -n $SMB_VERS ]] \ + || die "config incomplète : $CONF (relance cloudpex/install.sh)" +[[ $MNT == /* ]] || die "MNT doit être un chemin absolu ($CONF)" +SHARE="//${HOST}/${SHARE_NAME}" + is_mounted() { mountpoint -q "$MNT"; } do_status() { diff --git a/cloudpex/install.sh b/cloudpex/install.sh index 9e8f9f4..e668d27 100755 --- a/cloudpex/install.sh +++ b/cloudpex/install.sh @@ -1,26 +1,86 @@ #!/usr/bin/env bash -# cloudpex/install.sh — installe la commande `cloudpex` (montage à la demande du -# partage SMB CloudPex du NAS, voir README.md) telle qu'elle est déployée ici : -# /usr/local/bin/cloudpex root:root 0755 -# /mnt/cloudpex point de montage (vide tant que rien n'est monté) +# cloudpex/install.sh — installe la commande `cloudpex` (montage à la demande d'un +# partage SMB, voir README.md) telle qu'elle est déployée ici : +# /usr/local/bin/cloudpex le script, root:root 0755 +# /etc/cloudpex.conf hôte, partage, utilisateur SMB, point de montage, +# version SMB : demandés ici, root:root 0600 # cifs-utils installé si mount.cifs manque (apt-get) -# Réexécutable : réinstalle en place. Ne monte rien, ne stocke aucun identifiant. +# Réexécutable : réinstalle le script en place et propose de garder la config +# existante. Sans terminal (curl | bash), la config n'est ni créée ni modifiée. +# Ne monte rien, ne stocke aucun mot de passe. # Usage : ./cloudpex/install.sh (appelé aussi par ../install.sh sur Linux) set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" TARGET=/usr/local/bin/cloudpex -MNT=/mnt/cloudpex +CONF=/etc/cloudpex.conf + +die() { echo "Erreur : $*" >&2; exit 1; } + +# Saisie validée : ask VAR "libellé" "défaut" "regex autorisée". Redemande tant +# que la valeur ne correspond pas ; la valeur vide prend le défaut. +ask() { + local value + while :; do + read -rp "$2${3:+ [$3]} : " value || die "saisie interrompue" + value="${value:-$3}" + [[ $value =~ ^$4$ ]] && break + echo " valeur invalide, format attendu : $4" >&2 + done + printf -v "$1" '%s' "$value" +} + +# Demande les cinq valeurs propres au site et les écrit dans $CONF (root, 0600). +# Les formats refusent ce qui casserait la ligne d'options de mount.cifs +# (virgule, espace, guillemet) ; seul le nom de partage admet des espaces. +write_conf() { + local host share user mnt vers tmp + ask host "Hôte du NAS (IP ou nom)" "" '[A-Za-z0-9.-]+' + ask share "Nom du partage SMB" "" '[A-Za-z0-9._ -]+' + ask user "Utilisateur SMB" "" '[A-Za-z0-9._-]+' + ask mnt "Point de montage" "/mnt/cloudpex" '/[A-Za-z0-9._/-]+' + ask vers "Version SMB" "3.0" '[0-9]+(\.[0-9]+)*' + tmp="$(mktemp)" + printf 'HOST=%s\nSHARE=%s\nSMB_USER=%s\nMNT=%s\nSMB_VERS=%s\n' \ + "$host" "$share" "$user" "$mnt" "$vers" > "$tmp" + sudo install -m 0600 -o root -g root "$tmp" "$CONF" + rm -f "$tmp" +} + +# Config : créée au clavier, ou gardée si elle existe déjà (répondre n pour la +# refaire). Sans terminal, rien n'est demandé. +configure() { + local keep="" + if [ ! -t 0 ]; then + [ -f "$CONF" ] || echo "Pas de terminal : $CONF non créé, relance ./cloudpex/install.sh depuis un terminal" >&2 + return 0 + fi + if [ -f "$CONF" ]; then + echo "Configuration existante ($CONF) :" + sudo sed 's/^/ /' "$CONF" + read -rp "La garder ? [Y/n] " keep || true + case "$keep" in + [nN]*) write_conf ;; + esac + else + write_conf + fi +} + +# Le point de montage déclaré dans la config, créé vide s'il manque. +ensure_mountpoint() { + local mnt + [ -f "$CONF" ] || return 0 + mnt="$(sudo sed -n 's/^MNT=//p' "$CONF" | head -n 1)" + [ -n "$mnt" ] && sudo install -d -m 0755 "$mnt" +} if ! command -v mount.cifs >/dev/null 2>&1; then - if command -v apt-get >/dev/null 2>&1; then - sudo apt-get install -y cifs-utils - else - echo "mount.cifs absent et apt-get introuvable : installe cifs-utils à la main" >&2 - exit 1 - fi + command -v apt-get >/dev/null 2>&1 || die "mount.cifs absent et apt-get introuvable : installe cifs-utils à la main" + sudo apt-get install -y cifs-utils fi sudo install -m 0755 -o root -g root "$SCRIPT_DIR/cloudpex" "$TARGET" -sudo install -d -m 0755 "$MNT" +configure +ensure_mountpoint echo "cloudpex installé : $TARGET (monter : cloudpex · état : cloudpex -s · démonter : cloudpex -u)"