#!/usr/bin/env bash set -euo pipefail # ========================================= # RAID- und FS-Erweiterung (mdadm/Ext4/XFS) # Interaktiv, Dry-Run-, Debug- und Yes-Option # Mit Zeitstempeln, Logfile & farbigen Headern # ========================================= # ---- Globale Defaults ---- DRYRUN=0 DEBUG=0 ASSUME_YES=0 FS_TYPE="ext4" # wird in prompt_inputs gesetzt; hier nur Default LOG_FILE="./raid6-expand-$(date +%F-%H%M%S).log" # ---- Zeit & Logging ---- ts() { date '+%F %T'; } strip_ansi() { sed -r 's/\x1B\[[0-9;]*[A-Za-z]//g'; } log_plain() { # Schreibt *ohne* ANSI-Farben ins Log mit Zeitstempel { printf "[%s] %s\n" "$(ts)" "$(printf "%s" "$*" | strip_ansi)"; } >> "$LOG_FILE" } # ---- Farben/Logging-Ausgaben ---- cecho() { # $1=colorcode, $2=prefix, $3=msg... local c="$1"; shift local pfx="$1"; shift printf "\e[%sm[%s] %s\e[0m\n" "$c" "$(ts)" "$pfx $*" log_plain "$pfx $*" } info() { cecho "36" "INFO " "$@"; } # cyan warn() { cecho "33" "WARN " "$@"; } # gelb ok() { cecho "32" "OK " "$@"; } # grün err() { cecho "31" "ERROR" "$@"; } # rot note() { cecho "35" "NOTE " "$@"; } # magenta header() { # Abschnittsüberschrift local line="============================================================" printf "\e[36m[%s] %s\e[0m\n" "$(ts)" "$line" printf "\e[36m[%s] == %s ==\e[0m\n" "$(ts)" "$*" printf "\e[36m[%s] %s\e[0m\n" "$(ts)" "$line" log_plain "$line" log_plain "== $* ==" log_plain "$line" } # ---- Usage ---- usage() { cat <<'USAGE' raid6-expand-interactive.sh — Vergrößert RAID-Mitglieds-Partitionen und das Array/FS Nutzung: ./raid6-expand-interactive.sh [Optionen] Optionen: -n, --dry-run Keine Änderungen; loggt nur, was passieren würde. (Unterdrückt Rückfragen pro Partition; aktiviert --debug) -d, --debug Zusätzliche Diagnosen (lsblk/blkid/sfdisk-Auszüge). -y, --yes Bestätigt alle Rückfragen automatisch (produktiv praktisch). -L, --log Schreibe Log nach (Default: ./raid6-expand-YYYY-MM-DD-HHMMSS.log) -h, --help Diese Hilfe anzeigen. Ablauf (produktiv): 1) Interaktive Eingaben (RAID-Device, Disks, Partitionsnummer, FS, ggf. Mountpoint) 2) Status-Checks & Partitionstabellen-Backups 3) Pro Platte: Partition bis maximal vergrößern (Start & Typ bleiben gleich) 4) Nach jeder Platte: mdadm --grow --size=max + auf Resync warten 5) Am Ende: Dateisystem vergrößern (resize2fs oder xfs_growfs) Voraussetzungen: mdadm, sfdisk, lsblk, blkid, awk, partprobe, udevadm (xfs_growfs nur, wenn FS_TYPE=xfs) USAGE } # ---- Helfer ---- need_bin() { command -v "$1" >/dev/null 2>&1 || { err "Fehlt: $1"; exit 1; }; } run() { # Zeigt und loggt den Befehl; führt ihn in produktiv aus printf "[%s] + %s\n" "$(ts)" "$*" | tee -a "$LOG_FILE" >/dev/null if [[ "$DRYRUN" -eq 0 ]]; then "$@" fi } confirm() { # Auto-yes im Dry-Run oder wenn -y/--yes gesetzt ist, sonst interaktiv fragen local prompt="$1" if [[ "$DRYRUN" -eq 1 || "$ASSUME_YES" -eq 1 ]]; then note "(auto) $prompt: yes" return 0 fi read -r -p "$prompt [yes/NO] " a if [[ "${a:-}" == "yes" ]]; then log_plain "$prompt: yes" return 0 fi log_plain "$prompt: NO" return 1 } confirm_summary() { # Zusammenfassung IMMER bestätigen (auch im Dry-Run), außer -y/--yes local prompt="$1" if [[ "$ASSUME_YES" -eq 1 ]]; then note "(auto) $prompt: yes" return 0 fi read -r -p "$prompt [yes/NO] " a if [[ "${a:-}" == "yes" ]]; then log_plain "$prompt: yes" return 0 fi log_plain "$prompt: NO" return 1 } check_clean() { local md="$1" if ! grep -q "$(basename "$md")" /proc/mdstat; then err "$md nicht in /proc/mdstat gefunden." exit 1 fi if ! mdadm --detail "$md" | grep -q "State : clean"; then warn "$md ist nicht 'clean'. Details:" mdadm --detail "$md" | sed 's/^/ /' | tee -a "$LOG_FILE" confirm "Trotzdem fortfahren?" || exit 1 fi } wait_resync() { local md="$1" info "Warte auf Abschluss von Initialisierung/Resync auf $md ..." if [[ "$DRYRUN" -eq 1 ]]; then note "(Dry-Run) Würde auf Resync/Initialisierung warten und /proc/mdstat beobachten." return 0 fi while :; do local blk blk=$(grep -E "^\s*$(basename "$md")" /proc/mdstat || true) if [[ -z "$blk" ]]; then err "$md nicht in /proc/mdstat gefunden!" exit 1 fi if ! grep -Eiq "(resync|reshape|recovery|check)" /proc/mdstat; then break fi printf "[%s] %s\n" "$(ts)" "$blk" | tee -a "$LOG_FILE" >/dev/null sleep 20 done ok "Resync/Initialisierung abgeschlossen." } backup_pt() { local disk="$1" local bkup="backup-$(basename "$disk")-$(date +%F-%H%M%S).sfdisk" if [[ "$DRYRUN" -eq 1 ]]; then note "(Dry-Run) Würde Partitionstabelle sichern: $bkup" else sfdisk -d "$disk" > "$bkup" ok "Partitionstabelle gesichert: $bkup" fi if [[ "$DEBUG" -eq 1 ]]; then header "DEBUG: sfdisk -d $disk" sfdisk -d "$disk" | tee -a "$LOG_FILE" fi } # ---- Start & Typ via lsblk/blkid (robuster als sfdisk-parsing) ---- get_part_start() { local part="$1" local start start="$(lsblk -no START "$part" 2>/dev/null | head -n1 | tr -d '[:space:]')" [[ -n "$start" ]] || { err "Konnte START für $part nicht ermitteln (lsblk)."; exit 1; } printf "%s" "$start" } get_part_type() { local part="$1" local ptype ptype="$(blkid -s PART_ENTRY_TYPE -o value "$part" 2>/dev/null | head -n1 | tr -d '[:space:]' || true)" if [[ -z "$ptype" ]]; then ptype="$(lsblk -no PARTTYPE "$part" 2>/dev/null | head -n1 | tr -d '[:space:]' || true)" fi if [[ -z "$ptype" ]]; then local line line="$(sfdisk -d "${part%[0-9]*}" | awk -v p="$part" '$1==p {print; exit}')" ptype="$(awk -F',' '{ for(i=1;i<=NF;i++){ gsub(/^ +| +$/,"",$i); if($i ~ /^type=/){sub(/^type=/,"",$i); print $i} } }' <<< "$line" | tr -d '[:space:]')" fi [[ -n "$ptype" ]] || { err "Konnte Partitions-Typ für $part nicht ermitteln."; exit 1; } printf "%s" "$ptype" } expand_partition() { local disk="$1" pn="$2" local part="${disk}${pn}" [[ -b "$part" ]] || { err "Partition $part existiert nicht."; exit 1; } if [[ "$DEBUG" -eq 1 ]]; then header "DEBUG: lsblk/blkid für $part" lsblk -no NAME,PATH,TYPE,SIZE,START,PARTTYPE "$part" | tee -a "$LOG_FILE" blkid "$part" | tee -a "$LOG_FILE" || true fi local pstart ptype pstart="$(get_part_start "$part")" ptype="$(get_part_type "$part")" warn "Geplante Änderung auf $part:" note " Startsektor bleibt: $pstart" note " Größe: bis maximal verfügbar" note " Typ bleibt: $ptype" if [[ "$DRYRUN" -eq 1 || "$ASSUME_YES" -eq 1 ]]; then note "(Dry-Run) Würde ${part} erweitern: Start=${pstart}, Typ=${ptype}, bis maximal verfügbar." else confirm "Jetzt ${part} erweitern?" || { warn "Übersprungen."; return; } fi printf "[%s] + %s\n" "$(ts)" "printf ',,type=${ptype}\n' | sfdisk --no-reread --force -N ${pn} ${disk}" | tee -a "$LOG_FILE" >/dev/null if [[ "$DRYRUN" -eq 0 ]]; then printf ",,type=%s\n" "$ptype" | sfdisk --no-reread --force -N "$pn" "$disk" run partprobe "$disk" || true run udevadm settle || true ok "Partition $part wurde bis ans Ende erweitert." else note "(Dry-Run) Würde Kernel neu einlesen: partprobe $disk ; udevadm settle" fi } grow_md_size() { local md="$1" run mdadm --grow "$md" --size=max || { :; } if [[ "$DRYRUN" -eq 1 ]]; then note "(Dry-Run) Würde mdadm-Grow ausführen." fi } final_fs_grow() { local md="$1" fs="$2" mp="${3:-}" case "$fs" in ext4) run resize2fs "$md" || { :; } if [[ "$DRYRUN" -eq 1 ]]; then note "(Dry-Run) Würde resize2fs ausführen." fi ;; xfs) [[ -n "$mp" ]] || { err "Für XFS muss ein Mountpoint angegeben werden."; exit 1; } if ! mount | grep -qE "^$md on $mp "; then warn "$md scheint nicht auf $mp gemountet zu sein (laut 'mount')." mount | sed 's/^/ /' | tee -a "$LOG_FILE" confirm "Trotzdem mit xfs_growfs $mp fortfahren?" || exit 1 fi run xfs_growfs "$mp" || { :; } if [[ "$DRYRUN" -eq 1 ]]; then note "(Dry-Run) Würde xfs_growfs ausführen." fi ;; *) err "Unbekanntes Dateisystem: $fs (erwartet: ext4 oder xfs)" exit 1 ;; esac ok "Dateisystem-Schritt abgeschlossen." } # ---- Interaktive Eingabe ---- prompt_inputs() { echo header "Interaktiver Modus: Eingaben sammeln" [[ "$DRYRUN" -eq 1 ]] && note "Dry-Run aktiviert: Es werden KEINE Änderungen durchgeführt." [[ "$DEBUG" -eq 1 ]] && note "Debug aktiviert: Zeige zusätzliche Diagnosen." [[ "$ASSUME_YES" -eq 1 ]] && note "--yes aktiviert: Bestätige alle Rückfragen automatisch." note "Logfile: $LOG_FILE" read -r -p "RAID-Device (z.B. /dev/md4): " MDDEV [[ -n "${MDDEV:-}" ]] || { err "RAID-Device darf nicht leer sein."; exit 1; } [[ -b "$MDDEV" ]] || { err "$MDDEV ist kein Block-Device."; exit 1; } echo "Physikalische Geräte (ohne Partitionsnummer), leerzeichengetrennt (z.B. /dev/sda /dev/sdb /dev/sdc /dev/sdd):" read -r -a DISKS [[ "${#DISKS[@]}" -ge 2 ]] || { err "Mindestens zwei Geräte angeben."; exit 1; } for d in "${DISKS[@]}"; do [[ -b "$d" ]] || { err "$d ist kein Block-Device."; exit 1; } done read -r -p "Partitionsnummer der RAID-Partition (Standard 1): " PARTNUM PARTNUM="${PARTNUM:-1}" [[ "$PARTNUM" =~ ^[0-9]+$ ]] || { err "Partitionsnummer muss numerisch sein."; exit 1; } read -r -p "Dateisystem auf $MDDEV [ext4|xfs] (Standard ext4): " FS_TYPE FS_TYPE="${FS_TYPE:-ext4}" FS_TYPE="$(echo "$FS_TYPE" | tr '[:upper:]' '[:lower:]')" if [[ "$FS_TYPE" != "ext4" && "$FS_TYPE" != "xfs" ]]; then err "Ungültiges Dateisystem: $FS_TYPE (erlaubt: ext4, xfs)" exit 1 fi MOUNTPOINT="" if [[ "$FS_TYPE" == "xfs" ]]; then read -r -p "Mountpoint des XFS-Dateisystems (z.B. /mnt/raid): " MOUNTPOINT [[ -n "$MOUNTPOINT" ]] || { err "Mountpoint für XFS erforderlich."; exit 1; } fi echo header "ZUSAMMENFASSUNG" note "Dry-Run : $([[ "$DRYRUN" -eq 1 ]] && echo "JA" || echo "NEIN")" note "Debug : $([[ "$DEBUG" -eq 1 ]] && echo "JA" || echo "NEIN")" note "Auto-Yes : $([[ "$ASSUME_YES" -eq 1 ]] && echo "JA" || echo "NEIN")" note "RAID-Device: $MDDEV" note "Disks : ${DISKS[*]}" note "Part.-Nr. : $PARTNUM" note "Filesystem : $FS_TYPE" [[ "$FS_TYPE" == "xfs" ]] && note "Mountpoint : $MOUNTPOINT" echo confirm_summary "Eingaben korrekt? Fortfahren?" || { warn "Abgebrochen."; exit 0; } } # ---- Argumente parsen ---- parse_args() { echo while [[ $# -gt 0 ]]; do case "$1" in -n|--dry-run) DRYRUN=1; DEBUG=1; shift ;; -d|--debug) DEBUG=1; shift ;; -y|--yes) ASSUME_YES=1; shift ;; -L|--log) shift || true [[ $# -gt 0 ]] || { err "--log erfordert einen Pfad"; exit 1; } LOG_FILE="$1"; shift ;; -h|--help) usage; echo; exit 0 ;; *) err "Unbekannte Option: $1"; usage; echo; exit 1 ;; esac done # Logfile anlegen & Kopf schreiben touch "$LOG_FILE" 2>/dev/null || { err "Kann Logfile nicht schreiben: $LOG_FILE"; exit 1; } header "Start: raid6-expand-interactive | Log: $LOG_FILE" } # ---- Main ---- main() { echo need_bin mdadm need_bin sfdisk need_bin lsblk need_bin blkid need_bin awk need_bin partprobe need_bin udevadm prompt_inputs [[ "$FS_TYPE" == "xfs" ]] && need_bin xfs_growfs || true for d in "${DISKS[@]}"; do local p="${d}${PARTNUM}" [[ -b "$p" ]] || { err "Erwartete Partition $p existiert nicht."; exit 1; } done header "RAID-Status vor Start" mdadm --detail "$MDDEV" | sed 's/^/ /' | tee -a "$LOG_FILE" echo check_clean "$MDDEV" confirm "Letzte Chance vor Änderungen. Weiter?" || exit 0 echo for d in "${DISKS[@]}"; do local p="${d}${PARTNUM}" header "Bearbeite $d / $p" if ! mdadm --detail "$MDDEV" | grep -q " active sync .*$(basename "$p")"; then warn "$p scheint nicht als active sync im $MDDEV gelistet zu sein." mdadm --detail "$MDDEV" | sed 's/^/ /' | tee -a "$LOG_FILE" confirm "Trotzdem mit $p fortfahren?" || { echo; continue; } fi backup_pt "$d" expand_partition "$d" "$PARTNUM" grow_md_size "$MDDEV" wait_resync "$MDDEV" ok "Fertig mit $p." echo done header "Final: Dateisystem vergrößern" final_fs_grow "$MDDEV" "$FS_TYPE" "${MOUNTPOINT:-}" echo header "Abschluss" lsblk -o NAME,SIZE,TYPE,MOUNTPOINT | sed 's/^/ /' | tee -a "$LOG_FILE" echo mdadm --detail "$MDDEV" | sed 's/^/ /' | tee -a "$LOG_FILE" echo df -h | sed 's/^/ /' | tee -a "$LOG_FILE" echo header "Ende" echo } # ---- Einstieg ---- parse_args "$@" main "$@" # (Ende) echo