From b62bf0c7cb71e26f1552d7cf5a9e3253fbd408ed Mon Sep 17 00:00:00 2001 From: Mattia Tadini Date: Mon, 17 Nov 2025 10:39:20 +0000 Subject: [PATCH] Add expand-disk.sh --- expand-disk.sh | 178 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 178 insertions(+) create mode 100644 expand-disk.sh diff --git a/expand-disk.sh b/expand-disk.sh new file mode 100644 index 0000000..a5e9f34 --- /dev/null +++ b/expand-disk.sh @@ -0,0 +1,178 @@ +#!/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 / \ No newline at end of file