mirror of
https://github.com/community-scripts/ProxmoxVE.git
synced 2026-06-29 18:55:01 +02:00
Improve storage prep & mounting in host-migrate
Add interactive storage preparation and safer mount handling for host-migrate. - Track and clean up on-demand mounts via TEMP_MOUNTS and extend cleanup handler. - Add helpers: _new_mountpoint, _offer_fstab, mount_existing_fs, format_and_mount, create_lv_and_mount to mount, format, or create LVs and optionally persist to /etc/fstab. - Enhance browse_mounts to list unmounted block devices and LVM VGs, offer mount/format/LV creation, and return prepared mount via BROWSE_RESULT. - Integrate prepared target into choose_location and do_export; show free-space warning before export. - Improve vzdump output detection to pick the newest non-log file. - Minor UX/message tweaks and quoting fixes for backup filenames when restoring storage.cfg and /etc/hosts. These changes let users pick or prepare target storage (mount existing FS, format disks, create LVs) interactively and ensures temporary mounts are cleaned up.
This commit is contained in:
+211
-11
@@ -14,6 +14,12 @@ BACKTITLE="Proxmox VE Helper Scripts - Host Migrate"
|
||||
BUNDLE_PREFIX="pve-migrate"
|
||||
NFS_MOUNTPOINT=""
|
||||
NFS_MOUNTED=0
|
||||
# Mountpoints this script created on demand and should clean up on exit
|
||||
# (only those the user did NOT choose to make persistent via fstab).
|
||||
TEMP_MOUNTS=()
|
||||
# Result holders for the storage picker / disk preparation helpers.
|
||||
BROWSE_RESULT=""
|
||||
PREPARED_MP=""
|
||||
|
||||
function header_info {
|
||||
clear
|
||||
@@ -46,14 +52,127 @@ fi
|
||||
# Helpers
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
# Cleanup handler: unmount NFS if we mounted it ourselves.
|
||||
# Cleanup handler: unmount things we mounted on demand (NFS + non-persistent
|
||||
# disk mounts). Disk DATA is never touched here, we only unmount.
|
||||
function cleanup {
|
||||
if [ "$NFS_MOUNTED" -eq 1 ] && mountpoint -q "$NFS_MOUNTPOINT"; then
|
||||
umount "$NFS_MOUNTPOINT" 2>/dev/null && rmdir "$NFS_MOUNTPOINT" 2>/dev/null
|
||||
fi
|
||||
local mp
|
||||
for mp in "${TEMP_MOUNTS[@]}"; do
|
||||
if mountpoint -q "$mp"; then
|
||||
umount "$mp" 2>/dev/null && rmdir "$mp" 2>/dev/null
|
||||
fi
|
||||
done
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
# Convert a size string like "37.9G" / "931.51g" to a human label as-is.
|
||||
# Echo a fresh, unique mountpoint path under /mnt for a given label.
|
||||
function _new_mountpoint {
|
||||
local base="/mnt/${1}"
|
||||
local mp="$base"
|
||||
local n=1
|
||||
while [ -e "$mp" ] && ! { [ -d "$mp" ] && [ -z "$(ls -A "$mp" 2>/dev/null)" ]; }; do
|
||||
mp="${base}-${n}"
|
||||
n=$((n + 1))
|
||||
done
|
||||
echo "$mp"
|
||||
}
|
||||
|
||||
# Offer to persist a mount in /etc/fstab. $1=device(or UUID source) $2=mountpoint $3=fstype
|
||||
function _offer_fstab {
|
||||
local dev="$1" mp="$2" fstype="$3"
|
||||
local uuid
|
||||
uuid=$(blkid -s UUID -o value "$dev" 2>/dev/null)
|
||||
if whiptail --backtitle "$BACKTITLE" --yesno \
|
||||
"Make this mount permanent (survives reboot) by adding it to /etc/fstab?\n\n${dev} -> ${mp}" 11 72; then
|
||||
if [ -n "$uuid" ]; then
|
||||
echo "UUID=${uuid} ${mp} ${fstype} defaults 0 2" >>/etc/fstab
|
||||
else
|
||||
echo "${dev} ${mp} ${fstype} defaults 0 2" >>/etc/fstab
|
||||
fi
|
||||
msg_ok "Added to /etc/fstab"
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
# Mount an existing filesystem on a device. Sets PREPARED_MP on success.
|
||||
function mount_existing_fs {
|
||||
local dev="$1" fstype="$2"
|
||||
local mp
|
||||
PREPARED_MP=""
|
||||
mp=$(_new_mountpoint "$(basename "$dev")")
|
||||
mkdir -p "$mp"
|
||||
msg_info "Mounting ${dev}"
|
||||
if mount "$dev" "$mp" 2>/tmp/host-migrate-mount.log; then
|
||||
msg_ok "Mounted ${dev} at ${mp}"
|
||||
if _offer_fstab "$dev" "$mp" "${fstype:-auto}"; then :; else TEMP_MOUNTS+=("$mp"); fi
|
||||
PREPARED_MP="$mp"
|
||||
return 0
|
||||
fi
|
||||
msg_error "Could not mount ${dev} (see /tmp/host-migrate-mount.log)"
|
||||
rmdir "$mp" 2>/dev/null
|
||||
return 1
|
||||
}
|
||||
|
||||
# Format a raw/empty device with ext4 and mount it. DESTRUCTIVE.
|
||||
function format_and_mount {
|
||||
local dev="$1"
|
||||
local confirm
|
||||
confirm=$(whiptail --backtitle "$BACKTITLE" --title "!! DESTRUCTIVE - FORMAT !!" --inputbox \
|
||||
"\nThis will ERASE ALL DATA on:\n ${dev}\n\nand create a fresh ext4 filesystem.\n\nType exactly FORMAT to proceed:" 14 72 \
|
||||
3>&1 1>&2 2>&3) || return 1
|
||||
[ "$confirm" != "FORMAT" ] && {
|
||||
msg_warn "Confirmation mismatch - nothing changed"
|
||||
sleep 2
|
||||
return 1
|
||||
}
|
||||
msg_info "Creating ext4 on ${dev}"
|
||||
if ! mkfs.ext4 -F "$dev" &>/tmp/host-migrate-mkfs.log; then
|
||||
msg_error "mkfs.ext4 failed (see /tmp/host-migrate-mkfs.log)"
|
||||
return 1
|
||||
fi
|
||||
msg_ok "Formatted ${dev}"
|
||||
mount_existing_fs "$dev" "ext4"
|
||||
}
|
||||
|
||||
# Create a logical volume in a VG with free space, format ext4 and mount it.
|
||||
function create_lv_and_mount {
|
||||
local vg="$1" vgfree="$2"
|
||||
local lvname size
|
||||
lvname=$(whiptail --backtitle "$BACKTITLE" --inputbox \
|
||||
"\nName for the new logical volume in VG '${vg}':" 10 64 \
|
||||
"backup" --title "New LV name" 3>&1 1>&2 2>&3) || return 1
|
||||
lvname="${lvname:-backup}"
|
||||
size=$(whiptail --backtitle "$BACKTITLE" --inputbox \
|
||||
"\nSize for /dev/${vg}/${lvname}\n(${vgfree} free).\nUse e.g. 500G, or leave empty for ALL free space:" 12 68 \
|
||||
--title "LV size" 3>&1 1>&2 2>&3) || return 1
|
||||
|
||||
msg_info "Creating LV ${lvname} in ${vg}"
|
||||
if [ -z "$size" ]; then
|
||||
lvcreate -l 100%FREE -n "$lvname" "$vg" &>/tmp/host-migrate-lv.log || {
|
||||
msg_error "lvcreate failed (see /tmp/host-migrate-lv.log)"
|
||||
return 1
|
||||
}
|
||||
else
|
||||
lvcreate -L "$size" -n "$lvname" "$vg" &>/tmp/host-migrate-lv.log || {
|
||||
msg_error "lvcreate failed (see /tmp/host-migrate-lv.log)"
|
||||
return 1
|
||||
}
|
||||
fi
|
||||
msg_ok "Created /dev/${vg}/${lvname}"
|
||||
local dev="/dev/${vg}/${lvname}"
|
||||
msg_info "Creating ext4 on ${dev}"
|
||||
mkfs.ext4 -F "$dev" &>/tmp/host-migrate-mkfs.log || {
|
||||
msg_error "mkfs.ext4 failed (see /tmp/host-migrate-mkfs.log)"
|
||||
return 1
|
||||
}
|
||||
msg_ok "Formatted ${dev}"
|
||||
mount_existing_fs "$dev" "ext4"
|
||||
}
|
||||
|
||||
# Show currently mounted filesystems (real storage only) and let the user pick
|
||||
# one. Echoes the chosen mountpoint on stdout. Returns non-zero on cancel.
|
||||
function browse_mounts {
|
||||
@@ -98,14 +217,74 @@ function browse_mounts {
|
||||
done < <(pvesm status 2>/dev/null | awk 'NR>1 && $3=="active" && $2=="dir" {printf "%s\t%.1fG\n", $1, $6/1048576}')
|
||||
fi
|
||||
|
||||
# --- Unmounted block devices: offer mount / format -----------------------
|
||||
# Columns: NAME TYPE FSTYPE MOUNTPOINT SIZE TRAN
|
||||
local name dtype dfs dmnt dsize dtran dev
|
||||
while read -r name dtype dfs dmnt dsize dtran; do
|
||||
[ -z "$name" ] && continue
|
||||
[[ "$name" =~ ^(loop|zram|sr|fd) ]] && continue
|
||||
[ -n "$dmnt" ] && continue # already mounted
|
||||
[ "$dtype" = "disk" ] || [ "$dtype" = "part" ] || continue
|
||||
dev="/dev/${name}"
|
||||
# skip if device or any child is mounted (system disks)
|
||||
if lsblk -rno MOUNTPOINT "$dev" 2>/dev/null | grep -q .; then continue; fi
|
||||
case "$dfs" in
|
||||
LVM2_member | swap | crypto_LUKS) continue ;; # handled via VG / not a target
|
||||
"")
|
||||
# Only offer to format reasonably sized devices (GiB/TiB), skip tiny ones.
|
||||
[[ "$dsize" =~ [GT]$ ]] || continue
|
||||
menu+=("FORMAT:${dev}" "[format] empty ${dtype} ${name} (${dsize}, ${dtran:-?}) - ERASES DATA")
|
||||
;;
|
||||
ext2 | ext3 | ext4 | xfs | btrfs | vfat | exfat | ntfs)
|
||||
menu+=("MOUNT:${dev}" "[mount] ${dfs} on ${name} (${dsize}, ${dtran:-?})")
|
||||
;;
|
||||
esac
|
||||
done < <(lsblk -rno NAME,TYPE,FSTYPE,MOUNTPOINT,SIZE,TRAN 2>/dev/null)
|
||||
|
||||
# --- Volume groups with free space: offer to create an LV ----------------
|
||||
if command -v vgs >/dev/null 2>&1; then
|
||||
local vg vgfree
|
||||
while read -r vg vgfree; do
|
||||
[ -z "$vg" ] && continue
|
||||
# only offer if there is meaningful free space (> 1 GiB)
|
||||
awk -v f="$vgfree" 'BEGIN{exit !(f+0 > 1)}' || continue
|
||||
menu+=("LV:${vg}" "[lvm] create volume in VG '${vg}' (${vgfree}G free)")
|
||||
done < <(vgs --noheadings --nosuffix --units g -o vg_name,vg_free 2>/dev/null | awk '{print $1, $2}')
|
||||
fi
|
||||
|
||||
if [ "${#menu[@]}" -eq 0 ]; then
|
||||
msg_error "No usable mounts found. Mount an SSD/USB/NFS first or use the NFS option."
|
||||
msg_error "No usable target found. Attach an SSD/USB/NFS first or use the NFS option."
|
||||
sleep 3
|
||||
return 1
|
||||
fi
|
||||
|
||||
whiptail --backtitle "$BACKTITLE" --title "Mounted Filesystems / Storages" --menu \
|
||||
"\nSelect a target location (free space shown):" 22 110 12 "${menu[@]}" 3>&1 1>&2 2>&3
|
||||
local picked
|
||||
picked=$(whiptail --backtitle "$BACKTITLE" --title "Select / Prepare Target Storage" --menu \
|
||||
"\nPick a ready location, or prepare a disk/LVM ([mount]/[format]/[lvm]):" 24 112 14 "${menu[@]}" 3>&1 1>&2 2>&3) || return 1
|
||||
|
||||
BROWSE_RESULT=""
|
||||
case "$picked" in
|
||||
MOUNT:*)
|
||||
dev="${picked#MOUNT:}"
|
||||
mount_existing_fs "$dev" "$(lsblk -rno FSTYPE "$dev" 2>/dev/null | head -n1)" || return 1
|
||||
BROWSE_RESULT="$PREPARED_MP"
|
||||
;;
|
||||
FORMAT:*)
|
||||
format_and_mount "${picked#FORMAT:}" || return 1
|
||||
BROWSE_RESULT="$PREPARED_MP"
|
||||
;;
|
||||
LV:*)
|
||||
vg="${picked#LV:}"
|
||||
vgfree=$(vgs --noheadings --nosuffix --units g -o vg_free "$vg" 2>/dev/null | awk '{print $1}')
|
||||
create_lv_and_mount "$vg" "$vgfree" || return 1
|
||||
BROWSE_RESULT="$PREPARED_MP"
|
||||
;;
|
||||
*)
|
||||
BROWSE_RESULT="$picked"
|
||||
;;
|
||||
esac
|
||||
[ -n "$BROWSE_RESULT" ] || return 1
|
||||
return 0
|
||||
}
|
||||
|
||||
# Ask the user whether the destination/source is a local path or an NFS share.
|
||||
@@ -117,14 +296,15 @@ function choose_location {
|
||||
|
||||
choice=$(whiptail --backtitle "$BACKTITLE" --title "$prompt_title" --menu \
|
||||
"\nWhere is the migration bundle located?" 16 74 4 \
|
||||
"browse" "Pick from currently mounted filesystems" \
|
||||
"browse" "Pick / prepare storage (mounts, disks, LVM)" \
|
||||
"local" "Type a local path manually (SSD, USB, mount)" \
|
||||
"nfs" "NFS share (mount on demand)" \
|
||||
3>&1 1>&2 2>&3) || return 1
|
||||
|
||||
if [ "$choice" = "browse" ]; then
|
||||
local picked sub
|
||||
picked=$(browse_mounts) || return 1
|
||||
browse_mounts || return 1
|
||||
picked="$BROWSE_RESULT"
|
||||
[ -z "$picked" ] && return 1
|
||||
sub=$(whiptail --backtitle "$BACKTITLE" --inputbox \
|
||||
"\nOptional subfolder under:\n${picked}\n\nLeave empty to use it directly." 12 70 \
|
||||
@@ -220,6 +400,22 @@ function collect_guests {
|
||||
function do_export {
|
||||
choose_location "Export Destination" "/mnt/" || return
|
||||
|
||||
# Free-space awareness: show target capacity and warn when it's tight.
|
||||
local avail_h avail_g
|
||||
avail_h=$(df -h --output=avail "$BASE_DIR" 2>/dev/null | tail -n1 | tr -d ' ')
|
||||
avail_g=$(df -BG --output=avail "$BASE_DIR" 2>/dev/null | tail -n1 | tr -dc '0-9')
|
||||
if [ -n "$avail_g" ]; then
|
||||
if [ "$avail_g" -lt 10 ]; then
|
||||
if ! whiptail --backtitle "$BACKTITLE" --title "Low Disk Space" --yesno \
|
||||
"Target '${BASE_DIR}' has only ${avail_h:-?} free.\n\nA full export with vzdump can easily exceed this.\nConsider a larger disk/LVM target.\n\nContinue anyway?" 13 74; then
|
||||
return
|
||||
fi
|
||||
else
|
||||
whiptail --backtitle "$BACKTITLE" --title "Target Space" --msgbox \
|
||||
"Target: ${BASE_DIR}\nFree space: ${avail_h:-?}" 9 70
|
||||
fi
|
||||
fi
|
||||
|
||||
local bundle="${BASE_DIR%/}/${BUNDLE_PREFIX}-$(hostname)-$(date +%Y_%m_%dT%H_%M)"
|
||||
mkdir -p "$bundle/host" "$bundle/guests" || {
|
||||
msg_error "Could not create bundle directory"
|
||||
@@ -327,9 +523,13 @@ function do_export {
|
||||
if [ "$guest_method" = "vzdump" ]; then
|
||||
msg_info "vzdump ${type} ${id} (${name})"
|
||||
if vzdump "$id" --dumpdir "$bundle/guests" --mode "$guest_mode" --compress zstd &>>"$bundle/guests/vzdump.log"; then
|
||||
local file
|
||||
file=$(ls -1t "$bundle/guests"/vzdump-*-"${id}"-*.* 2>/dev/null | grep -v '\.log$' | head -n1)
|
||||
file="$(basename "$file")"
|
||||
local file newest=""
|
||||
for file in "$bundle/guests"/vzdump-*-"${id}"-*; do
|
||||
[ -f "$file" ] || continue
|
||||
case "$file" in *.log) continue ;; esac
|
||||
[ -z "$newest" ] || [ "$file" -nt "$newest" ] && newest="$file"
|
||||
done
|
||||
file="$(basename "$newest")"
|
||||
echo -e "${type}\t${id}\t${name}\tvzdump\t${file}" >>"$bundle/guests.tsv"
|
||||
msg_ok "vzdump ${type} ${id} -> ${file}"
|
||||
else
|
||||
@@ -545,7 +745,7 @@ function import_hostcfg {
|
||||
|
||||
if [[ "$sel" == *storage* ]]; then
|
||||
if [ -f "$bundle/host/etc-pve/storage.cfg" ]; then
|
||||
cp /etc/pve/storage.cfg /etc/pve/storage.cfg.bak.$(date +%s) 2>/dev/null
|
||||
cp /etc/pve/storage.cfg "/etc/pve/storage.cfg.bak.$(date +%s)" 2>/dev/null
|
||||
cp "$bundle/host/etc-pve/storage.cfg" /etc/pve/storage.cfg
|
||||
msg_ok "Restored storage.cfg (review with: pvesm status)"
|
||||
else
|
||||
@@ -570,7 +770,7 @@ function import_hostcfg {
|
||||
fi
|
||||
|
||||
if [[ "$sel" == *hosts* ]]; then
|
||||
[ -f "$bundle/host/hosts" ] && cp /etc/hosts /etc/hosts.bak.$(date +%s) && cp "$bundle/host/hosts" /etc/hosts && msg_ok "Restored /etc/hosts"
|
||||
[ -f "$bundle/host/hosts" ] && cp /etc/hosts "/etc/hosts.bak.$(date +%s)" && cp "$bundle/host/hosts" /etc/hosts && msg_ok "Restored /etc/hosts"
|
||||
fi
|
||||
|
||||
if [[ "$sel" == *network* ]]; then
|
||||
|
||||
Reference in New Issue
Block a user