396 lines
12 KiB
Bash
396 lines
12 KiB
Bash
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
MODE="${1:-unknown}"
|
|
STAGE="${2:-all}" # system | docker | all
|
|
|
|
# ─────────────────────────────────────────────
|
|
# 📁 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"
|
|
}
|
|
|
|
# ─────────────────────────────────────────────
|
|
# 🎛️ Header (style Proxmox)
|
|
# ─────────────────────────────────────────────
|
|
header() {
|
|
local title="$1"
|
|
log "╔══════════════════════════════════════╗"
|
|
log "║ LXC Updater (TM) ║"
|
|
log "╚══════════════════════════════════════╝"
|
|
log "🖥️ Conteneur : $(hostname)"
|
|
log "📅 Date : ${DATE} 🕒 Heure : ${TS}"
|
|
log "📌 Étape : ${title}"
|
|
log ""
|
|
}
|
|
|
|
# ─────────────────────────────────────────────
|
|
# ⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏ 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 '/^<no value>$/d;/^$/d' | sort -u
|
|
}
|
|
|
|
is_compose_dir_valid() {
|
|
local d="$1"
|
|
[[ -d "$d" ]] && { [[ -f "$d/docker-compose.yml" ]] || [[ -f "$d/compose.yml" ]]; }
|
|
}
|
|
|
|
compose_services_in_dir() {
|
|
local dir="$1"
|
|
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
|
|
|
|
echo "$services"
|
|
}
|
|
|
|
update_compose_dir() {
|
|
local dir="$1"
|
|
log "📂 Projet : $dir"
|
|
|
|
local services
|
|
services="$(compose_services_in_dir "$dir")"
|
|
if [[ -n "$services" ]]; then
|
|
log "📦 Conteneurs/Services du projet : $services"
|
|
log "🔁 Mise à jour des conteneurs : $services"
|
|
else
|
|
log "📦 Conteneurs/Services du projet : (inconnus)"
|
|
fi
|
|
|
|
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"
|
|
|
|
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")
|
|
mapfile -t port_rules < <(
|
|
jq -r '.[0].HostConfig.PortBindings | to_entries[]? | "\(.key)=\(.value[0].HostIp // ""):\(.value[0].HostPort)"' "$tmp"
|
|
)
|
|
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
|
|
for b in "${binds[@]}"; do
|
|
args+=(-v "$b")
|
|
done
|
|
|
|
for pr in "${port_rules[@]}"; do
|
|
cport="${pr%%=*}" # 8080/tcp
|
|
mapping="${pr#*=}" # 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
|
|
|
|
if [[ "$network_mode" != "default" && "$network_mode" != "bridge" ]]; then
|
|
args+=(--network "$network_mode")
|
|
fi
|
|
|
|
args+=("$image")
|
|
|
|
run_cmd "Création du nouveau conteneur : $name" "${args[@]}"
|
|
|
|
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
|
|
|
|
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
|
|
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) docker run (non-Compose)
|
|
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" == "<no value>" ]]; 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"
|
|
}
|
|
|
|
# ─────────────────────────────────────────────
|
|
# 🔎 Détection base_mode depuis MODE
|
|
# MODE attendu : apt / apk / apt+docker / apk+docker / docker / unknown
|
|
# ─────────────────────────────────────────────
|
|
BASE_MODE="${MODE%%+docker}"
|
|
|
|
# ─────────────────────────────────────────────
|
|
# 🚦 Exécution selon STAGE
|
|
# ─────────────────────────────────────────────
|
|
if [[ "$STAGE" == "system" || "$STAGE" == "all" ]]; then
|
|
header "Mise à jour du système"
|
|
|
|
case "$BASE_MODE" in
|
|
apt) update_apt ;;
|
|
apk) update_apk ;;
|
|
*)
|
|
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 update : ${LOG_FILE}"
|
|
log "🗃️ Archive update : ${LOG_ARCHIVE}"
|
|
log "🎉 Étape terminée"
|
|
fi
|
|
|
|
if [[ "$STAGE" == "docker" || "$STAGE" == "all" ]]; then
|
|
header "Mise à jour des conteneurs Docker"
|
|
|
|
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 "🎉 Étape terminée"
|
|
fi |