426 lines
13 KiB
Bash
Executable File
426 lines
13 KiB
Bash
Executable File
#!/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 <pfad> Schreibe Log nach <pfad> (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
|
|
|