#!/usr/bin/env bash set -euo pipefail MODE="${1:-unknown}" # 📁 Logs par date LOG_ROOT="/var/log/lxc-updater-TM" DATE="$(date +%F)" TS="$(date +%H%M%S)" LOG_DIR="${LOG_ROOT}/${DATE}" LOG_FILE="${LOG_DIR}/lxc-updater-update.log" LOG_ARCHIVE="${LOG_DIR}/lxc-updater-update-${TS}.log" mkdir -p "$LOG_DIR" : > "$LOG_FILE" : > "$LOG_ARCHIVE" log() { echo "$1" echo "$1" >>"$LOG_FILE" echo "$1" >>"$LOG_ARCHIVE" } log_file_only() { echo "[$(date -Is)] $*" >>"$LOG_FILE" echo "[$(date -Is)] $*" >>"$LOG_ARCHIVE" } # ⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏ spinner spinner_start() { local msg="$1" local -a frames=("⠋" "⠙" "⠹" "⠸" "⠼" "⠴" "⠦" "⠧" "⠇" "⠏") local i=0 printf "%s %s" "${frames[0]}" "$msg" ( while true; do i=$(( (i + 1) % ${#frames[@]} )) printf "\r%s %s" "${frames[$i]}" "$msg" sleep 0.12 done ) & SPINNER_PID=$! } spinner_stop() { local rc="${1:-0}" local msg="$2" if [[ -n "${SPINNER_PID:-}" ]]; then kill "$SPINNER_PID" >/dev/null 2>&1 || true wait "$SPINNER_PID" 2>/dev/null || true unset SPINNER_PID fi if [[ "$rc" -eq 0 ]]; then printf "\r✅ %s\n" "$msg" else printf "\r❌ %s\n" "$msg" fi } run_cmd() { local title="$1"; shift spinner_start "$title" log_file_only "----- ${title} -----" log_file_only "CMD: $*" set +e "$@" >>"$LOG_FILE" 2>&1 local rc=$? set -e spinner_stop "$rc" "$title" if [[ $rc -ne 0 ]]; then log_file_only "ERREUR (code=$rc) sur: $title" return $rc fi return 0 } # ------------------------------ # 📦 Mise à jour OS # ------------------------------ apt_upgradable_count() { LC_ALL=C apt-get -s upgrade 2>/dev/null | grep -c '^Inst ' || true } update_apt() { log "📦 Mise à jour Debian / Ubuntu (APT)" export DEBIAN_FRONTEND=noninteractive run_cmd "Mise à jour de la liste des paquets" apt-get update -y local n n="$(apt_upgradable_count)" log "🔎 Paquets disponibles : ${n}" if [[ "$n" -eq 0 ]]; then log "✅ Aucun paquet à mettre à jour" return 0 fi run_cmd "Installation des mises à jour (${n} paquet(s))" apt-get upgrade -y run_cmd "Rechargement systemd (si nécessaire)" systemctl daemon-reload log "✅ Mise à jour APT terminée" } update_apk() { log "📦 Mise à jour Alpine (APK)" run_cmd "Mise à jour des dépôts" apk update run_cmd "Installation des mises à jour" apk upgrade log "✅ Mise à jour APK terminée" } # ------------------------------ # 🐳 Mise à jour Docker # ------------------------------ docker_image_id() { docker image inspect -f '{{.Id}}' "$1" 2>/dev/null || true } compose_dirs_from_labels() { docker ps -q | while read -r cid; do docker inspect -f '{{ index .Config.Labels "com.docker.compose.project.working_dir" }}' "$cid" 2>/dev/null || true done | sed '/^$/d;/^$/d' | sort -u } is_compose_dir_valid() { local d="$1" [[ -d "$d" ]] && { [[ -f "$d/docker-compose.yml" ]] || [[ -f "$d/compose.yml" ]]; } } update_compose_dir() { local dir="$1" log "📂 Projet : $dir" # 🧾 Liste des services/containeurs du projet local services="" if docker compose version >/dev/null 2>&1; then services="$(cd "$dir" && docker compose config --services 2>/dev/null | tr '\n' ' ' | sed 's/ *$//')" elif command -v docker-compose >/dev/null 2>&1; then services="$(cd "$dir" && docker-compose config --services 2>/dev/null | tr '\n' ' ' | sed 's/ *$//')" fi if [[ -n "$services" ]]; then log "📦 Conteneurs/Services du projet : $services" log "🔁 Mise à jour des conteneurs : $services" else log "📦 Conteneurs/Services du projet : (inconnus)" fi # ✅ Pull + recreate safe if docker compose version >/dev/null 2>&1; then run_cmd "Téléchargement des images (Compose)" bash -c "cd '$dir' && docker compose pull" run_cmd "Redéploiement des services (Compose)" bash -c "cd '$dir' && docker compose up -d --remove-orphans" return 0 fi if command -v docker-compose >/dev/null 2>&1; then run_cmd "Téléchargement des images (Compose v1)" bash -c "cd '$dir' && docker-compose pull" run_cmd "Redéploiement des services (Compose v1)" bash -c "cd '$dir' && docker-compose up -d --remove-orphans" return 0 fi log "❌ Docker Compose n'est pas installé" return 2 } recreate_docker_run_container() { local name="$1" local image="$2" # jq obligatoire pour recréer proprement if ! command -v jq >/dev/null 2>&1; then run_cmd "Redémarrage du conteneur : $name" docker restart "$name" log "⚠️ 'jq' absent → redémarrage uniquement (recréation automatique impossible)" return 0 fi local tmp="/tmp/lxc-updater-${name}.json" docker inspect "$name" > "$tmp" 2>>"$LOG_FILE" || true local restart_name network_mode user workdir restart_name="$(jq -r '.[0].HostConfig.RestartPolicy.Name // empty' "$tmp")" network_mode="$(jq -r '.[0].HostConfig.NetworkMode // "default"' "$tmp")" user="$(jq -r '.[0].Config.User // empty' "$tmp")" workdir="$(jq -r '.[0].Config.WorkingDir // empty' "$tmp")" mapfile -t envs < <(jq -r '.[0].Config.Env[]? // empty' "$tmp") mapfile -t binds < <(jq -r '.[0].HostConfig.Binds[]? // empty' "$tmp") # Ports: containerPort/proto = HostIp:HostPort mapfile -t port_rules < <( jq -r '.[0].HostConfig.PortBindings | to_entries[]? | "\(.key)=\(.value[0].HostIp // ""):\(.value[0].HostPort)"' "$tmp" ) # Réseaux (si default/bridge) mapfile -t nets < <(jq -r '.[0].NetworkSettings.Networks | keys[]? // empty' "$tmp") local new_name="${name}__new" docker rm -f "$new_name" >>"$LOG_FILE" 2>&1 || true args=(docker create --name "$new_name") if [[ -n "$restart_name" && "$restart_name" != "no" ]]; then args+=(--restart "$restart_name") fi [[ -n "$user" ]] && args+=(--user "$user") [[ -n "$workdir" ]] && args+=(--workdir "$workdir") for e in "${envs[@]}"; do args+=(-e "$e") done # volumes/binds -> données conservées for b in "${binds[@]}"; do args+=(-v "$b") done # ports for pr in "${port_rules[@]}"; do cport="${pr%%=*}" # ex: 8080/tcp mapping="${pr#*=}" # ex: 0.0.0.0:8080 ou :8080 cportnum="${cport%/*}" # 8080 if [[ "$mapping" == :* ]]; then hostport="${mapping#:}" args+=(-p "${hostport}:${cportnum}") else hip="${mapping%%:*}" hp="${mapping#*:}" args+=(-p "${hip}:${hp}:${cportnum}") fi done # network mode (si spécial) if [[ "$network_mode" != "default" && "$network_mode" != "bridge" ]]; then args+=(--network "$network_mode") fi # image args+=("$image") run_cmd "Création du nouveau conteneur : $name" "${args[@]}" # connecter aux réseaux additionnels si needed if [[ "$network_mode" == "default" || "$network_mode" == "bridge" ]]; then for n in "${nets[@]}"; do [[ "$n" == "bridge" || "$n" == "host" || "$n" == "none" ]] && continue run_cmd "Connexion réseau ($n) : $name" docker network connect "$n" "$new_name" done fi # switch run_cmd "Arrêt du conteneur : $name" docker stop "$name" run_cmd "Renommage de l'ancien conteneur" docker rename "$name" "${name}__old" run_cmd "Renommage du nouveau conteneur" docker rename "$new_name" "$name" run_cmd "Démarrage du conteneur : $name" docker start "$name" run_cmd "Suppression de l'ancien conteneur" docker rm "${name}__old" log "✅ Conteneur recréé et redémarré : $name (données conservées via volumes/binds)" } update_docker() { log "🐳 Mise à jour des conteneurs Docker" run_cmd "Vérification Docker" docker ps # ---- 1) Compose via labels log "🔍 Recherche de projets Docker Compose" mapfile -t dirs < <(compose_dirs_from_labels) valid_compose=() for d in "${dirs[@]}"; do if is_compose_dir_valid "$d"; then valid_compose+=("$d") else log "⚠️ Projet Compose ignoré (chemin invalide ou sans compose.yml) : $d" fi done if (( ${#valid_compose[@]} > 0 )); then log "📁 Projet(s) Docker Compose détecté(s) : ${#valid_compose[@]}" for d in "${valid_compose[@]}"; do update_compose_dir "$d" done else log "⚠️ Aucun projet Compose détecté via labels" fi # ---- 2) Conteneurs non-Compose = docker run mapfile -t run_containers < <( docker ps -q | while read -r cid; do p="$(docker inspect -f '{{ index .Config.Labels "com.docker.compose.project" }}' "$cid" 2>/dev/null || true)" if [[ -z "$p" || "$p" == "" ]]; then echo "$cid" fi done ) if (( ${#run_containers[@]} == 0 )); then log "✅ Aucun conteneur 'docker run' détecté" log "✅ Mise à jour Docker terminée" return 0 fi log "🧩 Conteneur(s) 'docker run' détecté(s) : ${#run_containers[@]}" for cid in "${run_containers[@]}"; do name="$(docker inspect -f '{{.Name}}' "$cid" | sed 's#^/##')" image="$(docker inspect -f '{{.Config.Image}}' "$cid")" log "📦 Conteneur : $name" log " 🧱 Image : $image" old_id="$(docker_image_id "$image")" run_cmd "Téléchargement des mises à jour : $name" docker pull "$image" new_id="$(docker_image_id "$image")" if [[ -n "$old_id" && -n "$new_id" && "$old_id" == "$new_id" ]]; then log "✅ Aucun changement d'image : $name" continue fi log "⬆️ Image mise à jour pour : $name" if [[ -n "$old_id" && -n "$new_id" ]]; then log " 🔁 ${old_id:0:20} ➜ ${new_id:0:20}" fi recreate_docker_run_container "$name" "$image" done log "✅ Mise à jour Docker terminée" } # ------------------------------ # 🔎 Mode : base + docker éventuel # Exemples : apt, apk, apt+docker, apk+docker, docker, unknown # ------------------------------ BASE_MODE="${MODE%%+docker}" HAS_DOCKER="0" [[ "$MODE" == *"+docker" ]] && HAS_DOCKER="1" # Si MODE == "docker" seul (au cas où), on active docker if [[ "$MODE" == "docker" ]]; then HAS_DOCKER="1" fi # ------------------------------ # 🖥️ Sortie lisible # ------------------------------ log "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" log "📦 Mise à jour du système" log "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" case "$BASE_MODE" in apt) update_apt ;; apk) update_apk ;; *) # Si apt/apk non détecté mais apt-get existe, on tente apt if command -v apt-get >/dev/null 2>&1; then update_apt elif command -v apk >/dev/null 2>&1; then update_apk else log "⚠️ Impossible de détecter le gestionnaire de paquets (apt/apk)" fi ;; esac log "" log "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" log "🐳 Mise à jour du ou des conteneurs" log "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" if command -v docker >/dev/null 2>&1; then update_docker else log "🐳 Docker non présent" fi log "" log "🧾 Log update : ${LOG_FILE}" log "🗃️ Archive update : ${LOG_ARCHIVE}" log "🎉 Mise à jour terminée"