#!/bin/bash # Script per espansione disco VM (auto-detect) # Autore: Mattia Tadini # Nome File: expand_disk.sh # Revisione: 3.3.2 # expand-disk.sh — ONLY growpart: hot-extend LVM (EXT4/XFS) su MBR/GPT # Flow: rescan → (MBR+logica) growpart → growpart → partprobe/reread → pvresize → lvextend → resize FS set -euo pipefail log(){ echo "[$(date +'%F %T')] $*"; } warn(){ echo "[$(date +'%F %T')] [WARN] $*" >&2; } die(){ echo "ERROR: $*" >&2; exit 1; } # ----- CLI ----- DRY_RUN=false; AUTO_YES=false; FORCE_DISK=""; FORCE_PART=""; LV_TARGET=""; VG_TARGET="" while (( "$#" )); do case "$1" in -n|--dry-run) DRY_RUN=true; shift;; -y|--yes) AUTO_YES=true; shift;; --disk) FORCE_DISK="${2:-}"; shift 2;; --part) FORCE_PART="${2:-}"; shift 2;; --lv) LV_TARGET="${2:-}"; shift 2;; --vg) VG_TARGET="${2:-}"; shift 2;; -h|--help) cat <<'EOF' Uso: expand_disk.sh [opzioni] -n, --dry-run Simula (growpart --dry-run), nessuna modifica. -y, --yes Con --dry-run applica subito dopo senza prompt. --disk /dev/sdX Forza disco del PV (es. /dev/sda, /dev/nvme0n1). --part N Forza numero partizione del PV (es. 5). --lv /dev/VG/LV Forza LV (default: LV montato su /). --vg NOME Forza VG (default: VG dell'LV di /). Note: usa SOLO growpart per le partizioni. EOF exit 0;; *) die "Opzione sconosciuta: $1";; esac done # ----- prerequisiti ----- need(){ command -v "$1" >/dev/null 2>&1 || die "Comando non trovato: $1"; } [ "$(id -u)" -eq 0 ] || die "Esegui come root." for c in findmnt lsblk sfdisk partprobe blockdev pvs vgs lvs pvresize lvextend growpart; do need "$c"; done FSTYPE="$(findmnt -no FSTYPE /)" case "$FSTYPE" in ext4) need resize2fs ;; xfs) need xfs_growfs ;; *) warn "Filesystem '$FSTYPE' non gestito automaticamente: dopo lvextend ridimensiona manualmente." ;; esac # ----- util ----- pretty(){ command -v numfmt >/dev/null 2>&1 && numfmt --to=iec --suffix=B "$1" || echo "${1}B"; } parse_disk_part(){ local pv="$1" if [[ "$pv" =~ ^/dev/(sd[a-z]+)([0-9]+)$ ]]; then echo "${BASH_REMATCH[1]} ${BASH_REMATCH[2]}"; return; fi if [[ "$pv" =~ ^/dev/(vd[a-z]+)([0-9]+)$ ]]; then echo "${BASH_REMATCH[1]} ${BASH_REMATCH[2]}"; return; fi if [[ "$pv" =~ ^/dev/(xvd[a-z]+)([0-9]+)$ ]]; then echo "${BASH_REMATCH[1]} ${BASH_REMATCH[2]}"; return; fi if [[ "$pv" =~ ^/dev/(nvme[0-9]+n[0-9]+)p([0-9]+)$ ]]; then echo "${BASH_REMATCH[1]} ${BASH_REMATCH[2]}"; return; fi echo " " } pttype(){ lsblk -no PTTYPE "/dev/$1" 2>/dev/null | head -n1; } # detection estesa MBR: solo LETTURA, niente awk complicato find_extended_part(){ local disk="$1" # Prendi solo righe con /dev/[1-4] # Poi matcha (case-insensitive) type= 0x05 / 0x0f / 0x85 / 5 / f / extended local line while IFS= read -r line; do case "$line" in /dev/"$disk"[1-4]*) if echo "$line" | grep -Eiq 'type *= *(0x0?f|0x05|0x85|f|5|extended)'; then # Estrai numero finale della partizione echo "${line##*/$disk}" | sed -E 's/[^0-9]*([0-9]+).*/\1/'; return 0 fi ;; esac done < <(sfdisk -d "/dev/$disk" 2>/dev/null | sed -n 's/^[[:space:]]*//;p') return 1 } sync_pt(){ partprobe "/dev/$1" || true; blockdev --rereadpt "/dev/$1" || true; udevadm settle || true; } # ----- discovery ----- ROOT_DEV="$(findmnt -no SOURCE /)" LV_PATH="${LV_TARGET:-$(lvs --noheadings -o lv_path "$ROOT_DEV" | xargs)}" VG_NAME="${VG_TARGET:-$(lvs --noheadings -o vg_name "$ROOT_DEV" | xargs)}" [ -n "$LV_PATH" ] && [ -n "$VG_NAME" ] || die "Impossibile determinare LV/VG (usa --lv/--vg)." mapfile -t PVS < <(pvs --noheadings -o pv_name,vg_name | awk -v vg="$VG_NAME" '$2==vg {print $1}') [ "${#PVS[@]}" -ge 1 ] || die "Nessun PV nel VG '$VG_NAME'." PV_DEV="${PVS[0]}" if [[ -n "$FORCE_DISK" && -n "$FORCE_PART" ]]; then DISK="$(basename "$FORCE_DISK")"; PART="$FORCE_PART" else read -r DISK PART < <(parse_disk_part "$PV_DEV") fi [ -n "$DISK" ] && [ -n "$PART" ] || die "PV $PV_DEV non è una partizione; specifica --disk /dev/XXX e --part N." PTTYPE="$(pttype "$DISK")" LV_SIZE_B="$(lvs --noheadings --units b --nosuffix -o lv_size "$LV_PATH" | xargs)" VG_FREE_B="$(vgs --noheadings --units b --nosuffix -o vg_free "$VG_NAME" | xargs)" log "Filesystem di root: $ROOT_DEV (tipo: $FSTYPE)" log "Target LV: $LV_PATH — VG: $VG_NAME" log "PV: $PV_DEV → /dev/$DISK part $PART (tabella: ${PTTYPE:-unknown})" log "LV size attuale: $(pretty "$LV_SIZE_B")" log "VG free attuale: $(pretty "$VG_FREE_B")" # ----- piano ----- echo log "=== PIANO (ONLY growpart) ===" echo "1) Rescan /dev/$DISK" EXT_PART="" if [[ "$PTTYPE" == "dos" && "$PART" -ge 5 ]]; then if EXT_PART="$(find_extended_part "$DISK")"; then echo "2) growpart /dev/$DISK $EXT_PART (estesa)" else warn "MBR rilevato ma estesa non trovata: tenterò /dev/${DISK}2 (default comune)." EXT_PART="2" echo "2) growpart /dev/$DISK 2 (estesa, default)" fi fi echo "3) growpart /dev/$DISK $PART (PV)" echo "4) partprobe + rereadpt + udevadm settle" echo "5) pvresize $PV_DEV" echo "6) lvextend -l +100%FREE $LV_PATH" echo "7) resize FS ($FSTYPE)" echo # ----- dry-run ----- if "$DRY_RUN"; then echo "[SIM] echo 1 > /sys/class/block/$DISK/device/rescan" if [[ "$PTTYPE" == "dos" && "$PART" -ge 5 ]]; then growpart --dry-run "/dev/$DISK" "$EXT_PART" || warn "[SIM] growpart estesa ha segnalato problemi." fi growpart --dry-run "/dev/$DISK" "$PART" || warn "[SIM] growpart PV ha segnalato problemi." echo "[SIM] partprobe /dev/$DISK && blockdev --rereadpt /dev/$DISK && udevadm settle" echo "[SIM] pvresize $PV_DEV" echo "[SIM] lvextend -l +100%FREE $LV_PATH" [[ "$FSTYPE" == "ext4" ]] && echo "[SIM] resize2fs $LV_PATH" || { [[ "$FSTYPE" == "xfs" ]] && echo "[SIM] xfs_growfs /"; } if ! "$AUTO_YES"; then read -r -p "Procedere davvero con l'ESPANSIONE? [y/N] " ans [[ "${ans,,}" =~ ^(y|yes)$ ]] || { log "Dry-run completato. Nessuna modifica."; exit 0; } fi fi # ----- esecuzione reale ----- log "Rescan /dev/$DISK"; echo 1 > "/sys/class/block/$DISK/device/rescan" || true if [[ "$PTTYPE" == "dos" && "$PART" -ge 5 ]]; then log "growpart /dev/$DISK $EXT_PART # estesa" growpart "/dev/$DISK" "$EXT_PART" fi log "growpart /dev/$DISK $PART # PV" growpart "/dev/$DISK" "$PART" log "Sync tabella partizioni" sync_pt "$DISK" log "pvresize $PV_DEV"; pvresize "$PV_DEV" log "lvextend -l +100%FREE $LV_PATH"; lvextend -l +100%FREE "$LV_PATH" case "$FSTYPE" in ext4) log "resize2fs $LV_PATH"; resize2fs "$LV_PATH" ;; xfs) log "xfs_growfs /"; xfs_growfs / ;; esac NEW_VG_FREE="$(vgs --noheadings --units b --nosuffix -o vg_free "$VG_NAME" | xargs)" NEW_LV_SIZE="$(lvs --noheadings --units b --nosuffix -o lv_size "$LV_PATH" | xargs)" log "=== COMPLETATO ===" log "LV: $(pretty "$LV_SIZE_B") → $(pretty "$NEW_LV_SIZE")" log "VG free: $(pretty "$VG_FREE_B") → $(pretty "$NEW_VG_FREE")" df -h /