mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-09-15 11:16:50 +00:00
The change journal exists so a sysadmin sees what ProxMenux changed on the host — its own configuration, packages and services — not how it uses the host or configures a guest. Several scripts recorded operations that are neither: disk passthrough to a VM, container conversions, VM import/export, mounting a share into an LXC. Those are restored to their original, uninstrumented form. Scripts that operate on the host while also installing a package now record only the package: format-disk keeps its exFAT-tools install, the UUP ISO builder its build dependencies, and the share/host scripts their packages, services and /etc/fstab writes, while the mount and unmount operations they used to log are dropped. The page now reads in three sections: what ProxMenux optimized after install (each function under its menu name), what its other host scripts changed (by script), and what it installed (packages and utilities, each referencing the script that installed it). A file reads as created or modified with its diff, a service shows its state transition, and the undo line appears only when a revert is possible.
723 lines
27 KiB
Bash
723 lines
27 KiB
Bash
#!/bin/bash
|
|
# ==========================================================
|
|
# ProxMenux - Import Disk to LXC (Passthrough)
|
|
# ==========================================================
|
|
# Author : MacRimi
|
|
# Copyright : (c) 2024 MacRimi
|
|
# License : GPL-3.0
|
|
# https://github.com/MacRimi/ProxMenux/blob/main/LICENSE
|
|
# Version : 1.3
|
|
# ==========================================================
|
|
# Description:
|
|
# Assigns physical disks to existing Proxmox LXC containers
|
|
# via an interactive menu, using persistent device paths to
|
|
# avoid issues with device order changes.
|
|
#
|
|
# Features:
|
|
# - Detects and excludes the system disk.
|
|
# - Lists all containers for selection.
|
|
# - Identifies unassigned physical disks.
|
|
# - Configures the selected disks for the CT and verifies
|
|
# the assignment.
|
|
# ==========================================================
|
|
|
|
# Configuration ============================================
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
LOCAL_SCRIPTS_LOCAL="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
LOCAL_SCRIPTS_DEFAULT="/usr/local/share/proxmenux/scripts"
|
|
LOCAL_SCRIPTS="$LOCAL_SCRIPTS_DEFAULT"
|
|
BASE_DIR="/usr/local/share/proxmenux"
|
|
UTILS_FILE="$LOCAL_SCRIPTS/utils.sh"
|
|
|
|
if [[ -f "$LOCAL_SCRIPTS_LOCAL/utils.sh" ]]; then
|
|
LOCAL_SCRIPTS="$LOCAL_SCRIPTS_LOCAL"
|
|
UTILS_FILE="$LOCAL_SCRIPTS/utils.sh"
|
|
elif [[ ! -f "$UTILS_FILE" ]]; then
|
|
UTILS_FILE="$BASE_DIR/utils.sh"
|
|
fi
|
|
|
|
# shellcheck source=/dev/null
|
|
if [[ -f "$UTILS_FILE" ]]; then
|
|
source "$UTILS_FILE"
|
|
fi
|
|
|
|
# shellcheck source=/dev/null
|
|
if [[ -f "$LOCAL_SCRIPTS_LOCAL/global/vm_storage_helpers.sh" ]]; then
|
|
source "$LOCAL_SCRIPTS_LOCAL/global/vm_storage_helpers.sh"
|
|
elif [[ -f "$LOCAL_SCRIPTS_DEFAULT/global/vm_storage_helpers.sh" ]]; then
|
|
source "$LOCAL_SCRIPTS_DEFAULT/global/vm_storage_helpers.sh"
|
|
fi
|
|
|
|
BACKTITLE="ProxMenux"
|
|
UI_MENU_H=20
|
|
UI_MENU_W=84
|
|
UI_MENU_LIST_H=10
|
|
UI_SHORT_MENU_H=16
|
|
UI_SHORT_MENU_W=72
|
|
UI_SHORT_MENU_LIST_H=6
|
|
UI_MSG_H=10
|
|
UI_MSG_W=72
|
|
UI_YESNO_H=18
|
|
UI_YESNO_W=86
|
|
UI_RESULT_H=18
|
|
UI_RESULT_W=86
|
|
|
|
load_language
|
|
initialize_cache
|
|
|
|
if ! command -v pveversion >/dev/null 2>&1; then
|
|
dialog --backtitle "$BACKTITLE" --title "$(translate "Error")" \
|
|
--msgbox "$(translate "This script must be run on a Proxmox host.")" 8 60
|
|
exit 1
|
|
fi
|
|
|
|
# ==========================================================
|
|
|
|
# Returns the most stable /dev/disk/by-id symlink for a device.
|
|
# Prefers ata-/scsi-/nvme- > wwn- > other by-id > by-path > raw path.
|
|
get_preferred_disk_path() {
|
|
local disk="$1"
|
|
local real_path
|
|
real_path=$(readlink -f "$disk" 2>/dev/null)
|
|
[[ -z "$real_path" ]] && { echo "$disk"; return 0; }
|
|
|
|
local best="" best_score=99999
|
|
local link name score
|
|
for link in /dev/disk/by-id/*; do
|
|
[[ -e "$link" ]] || continue
|
|
[[ "$(readlink -f "$link" 2>/dev/null)" == "$real_path" ]] || continue
|
|
name=$(basename "$link")
|
|
[[ "$name" == *-part* ]] && continue
|
|
|
|
case "$name" in
|
|
ata-*|scsi-*|nvme-*) score=100 ;;
|
|
wwn-*) score=200 ;;
|
|
*) score=300 ;;
|
|
esac
|
|
score=$((score + ${#name}))
|
|
if (( score < best_score )); then
|
|
best="$link"
|
|
best_score=$score
|
|
fi
|
|
done
|
|
|
|
if [[ -n "$best" ]]; then
|
|
echo "$best"
|
|
return 0
|
|
fi
|
|
|
|
for link in /dev/disk/by-path/*; do
|
|
[[ -e "$link" ]] || continue
|
|
[[ "$(readlink -f "$link" 2>/dev/null)" == "$real_path" ]] || continue
|
|
echo "$link"
|
|
return 0
|
|
done
|
|
|
|
msg_warn "$(translate "No persistent path found for") $disk — $(translate "using direct path (not guaranteed to survive reboots).")"
|
|
echo "$disk"
|
|
}
|
|
|
|
install_fs_tools_in_ct() {
|
|
local ctid="$1"
|
|
local pkg="$2"
|
|
|
|
if pct exec "$ctid" -- sh -c "[ -f /etc/alpine-release ]"; then
|
|
pct exec "$ctid" -- sh -c "apk update >/dev/null 2>&1 && apk add --no-progress $pkg >/dev/null 2>&1"
|
|
elif pct exec "$ctid" -- sh -c "grep -qi 'arch' /etc/os-release 2>/dev/null"; then
|
|
pct exec "$ctid" -- sh -c "pacman -Sy --noconfirm $pkg >/dev/null 2>&1"
|
|
elif pct exec "$ctid" -- sh -c "grep -qiE 'debian|ubuntu' /etc/os-release 2>/dev/null"; then
|
|
pct exec "$ctid" -- sh -c "apt-get update -qq >/dev/null 2>&1 && apt-get install -y -qq $pkg >/dev/null 2>&1"
|
|
else
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
get_disk_info() {
|
|
local disk=$1
|
|
local model size
|
|
model=$(lsblk -dn -o MODEL "$disk" | xargs)
|
|
size=$(lsblk -dn -o SIZE "$disk" | xargs)
|
|
[[ -z "$model" ]] && model="Unknown"
|
|
printf '%s\t%s\n' "$model" "$size"
|
|
}
|
|
|
|
# Suggest an unused mount point inside the CT for the given disk.
|
|
# Reads the CT config to collect already-used mp= paths, then returns
|
|
# the first free candidate: /mnt/disk_<devname>, /mnt/disk_<devname>_2, ...
|
|
_get_suggested_mount_point() {
|
|
local ctid="$1"
|
|
local disk="$2"
|
|
local devname
|
|
devname=$(basename "$disk")
|
|
local base="/mnt/disk_${devname}"
|
|
|
|
local used_mps
|
|
used_mps=$(pct config "$ctid" 2>/dev/null | grep '^mp[0-9]*:' | \
|
|
grep -oP 'mp=\K[^,]+' | sort)
|
|
|
|
if ! grep -qxF "$base" <<< "$used_mps"; then
|
|
echo "$base"; return
|
|
fi
|
|
local n=2
|
|
while grep -qxF "${base}_${n}" <<< "$used_mps"; do
|
|
((n++))
|
|
done
|
|
echo "${base}_${n}"
|
|
}
|
|
|
|
get_all_disk_paths() {
|
|
local disk="$1"
|
|
local real_path
|
|
real_path=$(readlink -f "$disk" 2>/dev/null)
|
|
|
|
[[ -n "$disk" ]] && echo "$disk"
|
|
[[ -n "$real_path" ]] && echo "$real_path"
|
|
|
|
local link
|
|
for link in /dev/disk/by-id/* /dev/disk/by-path/*; do
|
|
[[ -e "$link" ]] || continue
|
|
[[ "$(readlink -f "$link" 2>/dev/null)" == "$real_path" ]] || continue
|
|
echo "$link"
|
|
done | sort -u
|
|
}
|
|
|
|
disk_referenced_in_config() {
|
|
local config_text="$1"
|
|
local disk="$2"
|
|
local alias
|
|
while read -r alias; do
|
|
[[ -z "$alias" ]] && continue
|
|
if grep -Fq "$alias" <<< "$config_text"; then
|
|
return 0
|
|
fi
|
|
done < <(get_all_disk_paths "$disk")
|
|
return 1
|
|
}
|
|
|
|
|
|
|
|
CT_LIST=$(pct list | awk 'NR>1 {print $1, $3}')
|
|
if [ -z "$CT_LIST" ]; then
|
|
dialog --backtitle "$BACKTITLE" \
|
|
--title "$(translate "Error")" \
|
|
--msgbox "$(translate "No CTs available in the system.")" $UI_MSG_H $UI_MSG_W
|
|
exit 1
|
|
fi
|
|
|
|
# shellcheck disable=SC2086 # CT_LIST is intentionally word-split into dialog menu pairs
|
|
CTID=$(dialog --backtitle "$BACKTITLE" \
|
|
--title "$(translate "Select CT for destination disk")" \
|
|
--menu "$(translate "Select the CT to which you want to add disks:")" $UI_MENU_H $UI_MENU_W $UI_MENU_LIST_H \
|
|
$CT_LIST \
|
|
2>&1 >/dev/tty)
|
|
|
|
if [ -z "$CTID" ]; then
|
|
dialog --backtitle "$BACKTITLE" \
|
|
--title "$(translate "Error")" \
|
|
--msgbox "$(translate "No CT was selected.")" $UI_MSG_H $UI_MSG_W
|
|
exit 1
|
|
fi
|
|
|
|
CTID=$(echo "$CTID" | tr -d '"')
|
|
|
|
CT_STATUS=$(pct status "$CTID" | awk '{print $2}')
|
|
CT_RUNNING=false
|
|
[[ "$CT_STATUS" == "running" ]] && CT_RUNNING=true
|
|
|
|
# ── Check for unprivileged container — also a dialog, stays before show_proxmenux_logo ──
|
|
CONF_FILE="/etc/pve/lxc/$CTID.conf"
|
|
CONVERT_PRIVILEGED=false
|
|
if grep -q '^unprivileged: 1' "$CONF_FILE"; then
|
|
if dialog --backtitle "$BACKTITLE" \
|
|
--title "$(translate "Privileged Container")" \
|
|
--yesno "\n\n$(translate "The selected container is unprivileged. A privileged container is required for direct device passthrough.")\\n\\n$(translate "Do you want to convert it to a privileged container now?")" $UI_YESNO_H $UI_YESNO_W; then
|
|
CONVERT_PRIVILEGED=true
|
|
else
|
|
dialog --backtitle "$BACKTITLE" \
|
|
--title "$(translate "Aborted")" \
|
|
--msgbox "$(translate "Operation cancelled. Cannot continue with an unprivileged container.")" $UI_MSG_H $UI_MSG_W
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
# ── TERMINAL PHASE 1 ──────────────────────────────────────────────────────────
|
|
show_proxmenux_logo
|
|
msg_title "$(translate "Import Disk to LXC")"
|
|
msg_ok "$(translate "CT $CTID selected successfully.")"
|
|
|
|
if [ "$CONVERT_PRIVILEGED" = true ]; then
|
|
|
|
show_proxmenux_logo
|
|
msg_title "$(translate "Import Disk to LXC")"
|
|
|
|
CURRENT_CT_STATUS=$(pct status "$CTID" | awk '{print $2}')
|
|
if [ "$CURRENT_CT_STATUS" == "running" ]; then
|
|
msg_info "$(translate "Stopping container") $CTID..."
|
|
pct shutdown "$CTID" &>/dev/null
|
|
for i in {1..10}; do
|
|
sleep 1
|
|
[ "$(pct status "$CTID" | awk '{print $2}')" != "running" ] && break
|
|
done
|
|
if [ "$(pct status "$CTID" | awk '{print $2}')" == "running" ]; then
|
|
msg_error "$(translate "Failed to stop the container.")"
|
|
exit 1
|
|
fi
|
|
msg_ok "$(translate "Container stopped.")"
|
|
fi
|
|
|
|
cp "$CONF_FILE" "$CONF_FILE.bak"
|
|
sed -i '/^unprivileged: 1/d' "$CONF_FILE"
|
|
echo "unprivileged: 0" >> "$CONF_FILE"
|
|
msg_ok "$(translate "Container successfully converted to privileged.")"
|
|
|
|
if [ "$CT_RUNNING" = true ]; then
|
|
msg_info "$(translate "Starting container") $CTID..."
|
|
pct start "$CTID" &>/dev/null
|
|
sleep 2
|
|
if [ "$(pct status "$CTID" | awk '{print $2}')" != "running" ]; then
|
|
msg_error "$(translate "Failed to start the container.")"
|
|
CT_RUNNING=false
|
|
else
|
|
msg_ok "$(translate "Container started successfully.")"
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
##########################################
|
|
msg_info "$(translate "Detecting available disks...")"
|
|
|
|
_refresh_host_storage_cache
|
|
# Read this CT's current config for the "already assigned to this CT" check
|
|
CT_CONFIG=$(pct config "$CTID" 2>/dev/null | grep -vE '^\s*#|^description:')
|
|
|
|
FREE_DISKS=()
|
|
|
|
while read -r DISK; do
|
|
[[ "$DISK" =~ /dev/zd ]] && continue
|
|
|
|
IFS=$'\t' read -r MODEL SIZE < <(get_disk_info "$DISK")
|
|
LABEL=""
|
|
SHOW_DISK=true
|
|
IS_MOUNTED=false
|
|
IS_RAID=false
|
|
IS_ZFS=false
|
|
IS_LVM=false
|
|
|
|
while read -r part fstype; do
|
|
[[ "$fstype" == "zfs_member" ]] && IS_ZFS=true
|
|
[[ "$fstype" == "linux_raid_member" ]] && IS_RAID=true
|
|
[[ "$fstype" == "LVM2_member" ]] && IS_LVM=true
|
|
if grep -q "/dev/$part" <<< "$MOUNTED_DISKS"; then
|
|
IS_MOUNTED=true
|
|
fi
|
|
done < <(lsblk -ln -o NAME,FSTYPE "$DISK" | tail -n +2)
|
|
|
|
REAL_PATH=$(readlink -f "$DISK")
|
|
if echo "$LVM_DEVICES" | grep -qFx "$REAL_PATH"; then
|
|
IS_MOUNTED=true
|
|
fi
|
|
|
|
USED_BY=""
|
|
if _disk_used_in_guest_configs "$DISK"; then
|
|
USED_BY="⚠ $(translate "In use")"
|
|
fi
|
|
|
|
if $IS_RAID && grep -q "$DISK" <<< "$(cat /proc/mdstat)"; then
|
|
if grep -q "active raid" /proc/mdstat; then
|
|
SHOW_DISK=false
|
|
fi
|
|
fi
|
|
|
|
if $IS_ZFS; then
|
|
SHOW_DISK=false
|
|
fi
|
|
|
|
# Catch whole-disk ZFS vdevs with no partitions (e.g. bare NVMe ZFS)
|
|
# The tail -n +2 trick misses them; ZFS_DISKS from _refresh_host_storage_cache covers them.
|
|
if [[ -n "$ZFS_DISKS" ]] && \
|
|
{ grep -qFx "$DISK" <<< "$ZFS_DISKS" || \
|
|
{ [[ -n "$REAL_PATH" ]] && grep -qFx "$REAL_PATH" <<< "$ZFS_DISKS"; }; }; then
|
|
SHOW_DISK=false
|
|
fi
|
|
|
|
if $IS_MOUNTED; then
|
|
SHOW_DISK=false
|
|
fi
|
|
|
|
if disk_referenced_in_config "$CT_CONFIG" "$DISK"; then
|
|
SHOW_DISK=false
|
|
fi
|
|
|
|
if $SHOW_DISK; then
|
|
[[ -n "$USED_BY" ]] && LABEL+=" [$USED_BY]"
|
|
[[ "$IS_RAID" == true ]] && LABEL+=" ⚠ RAID"
|
|
[[ "$IS_LVM" == true ]] && LABEL+=" ⚠ LVM"
|
|
[[ "$IS_ZFS" == true ]] && LABEL+=" ⚠ ZFS"
|
|
|
|
DESCRIPTION=$(printf "%-30s %10s%s" "$MODEL" "$SIZE" "$LABEL")
|
|
FREE_DISKS+=("$DISK" "$DESCRIPTION" "OFF")
|
|
fi
|
|
done < <(lsblk -dn -e 7,11 -o PATH)
|
|
|
|
if [ "${#FREE_DISKS[@]}" -eq 0 ]; then
|
|
stop_spinner
|
|
dialog --backtitle "$BACKTITLE" \
|
|
--title "$(translate "Error")" \
|
|
--msgbox "$(translate "No disks available for this CT.")" $UI_MSG_H $UI_MSG_W
|
|
exit 1
|
|
fi
|
|
|
|
stop_spinner
|
|
|
|
######################################################
|
|
MAX_WIDTH=$(printf "%s\n" "${FREE_DISKS[@]}" | awk '{print length}' | sort -nr | head -n1)
|
|
TOTAL_WIDTH=$((MAX_WIDTH + 20))
|
|
if [ $TOTAL_WIDTH -lt $UI_MENU_W ]; then
|
|
TOTAL_WIDTH=$UI_MENU_W
|
|
fi
|
|
if [ $TOTAL_WIDTH -gt 116 ]; then
|
|
TOTAL_WIDTH=116
|
|
fi
|
|
|
|
SELECTED=$(dialog --backtitle "$BACKTITLE" \
|
|
--title "$(translate "Select Disks")" \
|
|
--checklist "$(translate "Select the disks you want to add:")" $UI_MENU_H $TOTAL_WIDTH $UI_MENU_LIST_H \
|
|
"${FREE_DISKS[@]}" \
|
|
2>&1 >/dev/tty)
|
|
|
|
if [ -z "$SELECTED" ]; then
|
|
dialog --backtitle "$BACKTITLE" \
|
|
--title "$(translate "Error")" \
|
|
--msgbox "$(translate "No disks were selected.")" $UI_MSG_H $UI_MSG_W
|
|
exit 1
|
|
fi
|
|
|
|
show_proxmenux_logo
|
|
msg_title "$(translate "Import Disk to LXC")"
|
|
|
|
msg_ok "$(translate "CT $CTID selected successfully.")"
|
|
msg_info "$(translate "Analyzing selected disks...")"
|
|
|
|
|
|
|
|
# ── DIALOG PHASE: collect config for each disk ────────────────────────────────
|
|
declare -a DISK_LIST=()
|
|
declare -a DISK_DESCRIPTIONS=()
|
|
declare -a DISK_MOUNT_POINTS=()
|
|
declare -a DISK_SKIP_FORMATS=()
|
|
declare -a DISK_FORMAT_TYPES=()
|
|
declare -a DISK_NEEDS_PARTITION=()
|
|
declare -a DISK_PARTITIONS=()
|
|
declare -a DISK_ASSIGNED_TOS=()
|
|
declare -a DISK_CURRENT_FSes=()
|
|
|
|
for DISK in $SELECTED; do
|
|
DISK="${DISK//\"/}"
|
|
DISK_INFO=$(get_disk_info "$DISK")
|
|
|
|
ASSIGNED_TO=""
|
|
RUNNING_CTS=""
|
|
RUNNING_VMS=""
|
|
|
|
while read -r CT_ID CT_NAME; do
|
|
CT_CONFIG_RAW=$(pct config "$CT_ID" 2>/dev/null)
|
|
if [[ "$CT_ID" =~ ^[0-9]+$ ]] && disk_referenced_in_config "$CT_CONFIG_RAW" "$DISK"; then
|
|
ASSIGNED_TO+="CT $CT_ID $CT_NAME\n"
|
|
CT_STATUS=$(pct status "$CT_ID" | awk '{print $2}')
|
|
[[ "$CT_STATUS" == "running" ]] && RUNNING_CTS+="CT $CT_ID $CT_NAME\n"
|
|
fi
|
|
done < <(pct list | awk 'NR>1 {print $1, $3}')
|
|
|
|
while read -r VM_ID VM_NAME; do
|
|
VM_CONFIG_RAW=$(qm config "$VM_ID" 2>/dev/null)
|
|
if [[ "$VM_ID" =~ ^[0-9]+$ ]] && disk_referenced_in_config "$VM_CONFIG_RAW" "$DISK"; then
|
|
ASSIGNED_TO+="VM $VM_ID $VM_NAME\n"
|
|
VM_STATUS=$(qm status "$VM_ID" | awk '{print $2}')
|
|
[[ "$VM_STATUS" == "running" ]] && RUNNING_VMS+="VM $VM_ID $VM_NAME\n"
|
|
fi
|
|
done < <(qm list | awk 'NR>1 {print $1, $2}')
|
|
|
|
|
|
stop_spinner
|
|
if [ -n "$RUNNING_CTS" ] || [ -n "$RUNNING_VMS" ]; then
|
|
dialog --backtitle "$BACKTITLE" \
|
|
--title "$(translate "Disk In Use")" \
|
|
--msgbox "$(translate "The disk") $DISK_INFO $(translate "is in use by the following running VM(s) or CT(s):")\\n$RUNNING_CTS$RUNNING_VMS\\n\\n$(translate "Stop them first and run this script again.")" $UI_RESULT_H $UI_RESULT_W
|
|
continue
|
|
fi
|
|
|
|
if [ -n "$ASSIGNED_TO" ]; then
|
|
if ! dialog --backtitle "$BACKTITLE" \
|
|
--title "$(translate "Disk Already Assigned")" \
|
|
--yesno "\n\n$(translate "The disk") $DISK_INFO $(translate "is already assigned to the following VM(s) or CT(s):")\\n$ASSIGNED_TO\\n\\n$(translate "Do you want to continue anyway?")" $UI_YESNO_H $UI_YESNO_W; then
|
|
continue
|
|
fi
|
|
fi
|
|
|
|
if lsblk "$DISK" | grep -q "raid" || grep -q "${DISK##*/}" /proc/mdstat; then
|
|
dialog --backtitle "$BACKTITLE" \
|
|
--title "$(translate "RAID Detected")" \
|
|
--msgbox "\n$(translate "The disk") $DISK_INFO $(translate "appears to be part of a") RAID. $(translate "For security reasons, the system cannot format it.")\\n\\n$(translate "If you are sure you want to use it, please remove the") RAID metadata $(translate "or format it manually using external tools.")\\n\\n$(translate "After that, run this script again to add it.")" $UI_RESULT_H $UI_RESULT_W
|
|
continue
|
|
fi
|
|
|
|
# 1. Detect current partition/FS state
|
|
PARTITION=$(lsblk -rno NAME "$DISK" | awk -v disk="$(basename "$DISK")" '$1 != disk {print $1; exit}')
|
|
SKIP_FORMAT=false
|
|
NEEDS_PARTITION=false
|
|
|
|
if [ -n "$PARTITION" ]; then
|
|
PARTITION="/dev/$PARTITION"
|
|
CURRENT_FS=$(lsblk -no FSTYPE "$PARTITION" | xargs)
|
|
else
|
|
CURRENT_FS=$(lsblk -no FSTYPE "$DISK" | xargs)
|
|
PARTITION="$DISK"
|
|
fi
|
|
|
|
# 2. Ask what to do with this disk
|
|
if [ -n "$CURRENT_FS" ]; then
|
|
# Disk already has a filesystem — offer use-as-is or reformat
|
|
DISK_ACTION=$(dialog --backtitle "$BACKTITLE" \
|
|
--title "$(translate "Disk Setup")" \
|
|
--menu "$(translate "Disk") $DISK_INFO\n$(translate "Detected filesystem:") $CURRENT_FS\n\n$(translate "What do you want to do?")" \
|
|
$UI_SHORT_MENU_H $UI_SHORT_MENU_W $UI_SHORT_MENU_LIST_H \
|
|
"use" "$(translate "Use as-is — keep data and filesystem")" \
|
|
"format" "$(translate "Format — erase and create new filesystem")" \
|
|
2>&1 >/dev/tty)
|
|
[ -z "$DISK_ACTION" ] && continue
|
|
else
|
|
DISK_ACTION="format"
|
|
fi
|
|
|
|
FORMAT_TYPE=""
|
|
if [ "$DISK_ACTION" = "use" ]; then
|
|
SKIP_FORMAT=true
|
|
FORMAT_TYPE="$CURRENT_FS"
|
|
# PARTITION already set correctly by the detection block above — do not modify
|
|
else
|
|
# 3. Ask desired filesystem for format
|
|
FORMAT_TYPE=$(dialog --backtitle "$BACKTITLE" \
|
|
--title "$(translate "Select Filesystem")" \
|
|
--menu "$(translate "Select the filesystem for") $DISK_INFO:" \
|
|
$UI_SHORT_MENU_H $UI_SHORT_MENU_W $UI_SHORT_MENU_LIST_H \
|
|
"ext4" "$(translate "ext4 — recommended, most compatible")" \
|
|
"xfs" "$(translate "xfs — better for large files")" \
|
|
"btrfs" "$(translate "btrfs — snapshots and compression")" \
|
|
2>&1 >/dev/tty)
|
|
[ -z "$FORMAT_TYPE" ] && continue
|
|
|
|
# Check if already the right FS — otherwise need partition + format
|
|
if [ "$CURRENT_FS" = "$FORMAT_TYPE" ]; then
|
|
SKIP_FORMAT=true
|
|
elif [ -z "$CURRENT_FS" ] && [ "$PARTITION" = "$DISK" ]; then
|
|
NEEDS_PARTITION=true
|
|
PARTITION=""
|
|
fi
|
|
|
|
# 4. Warn if data will be erased
|
|
if [ "$SKIP_FORMAT" != true ]; then
|
|
if ! dialog --backtitle "$BACKTITLE" \
|
|
--title "$(translate "WARNING")" \
|
|
--yesno "\n$(translate "WARNING: This will FORMAT the disk") $DISK_INFO $(translate "as") $FORMAT_TYPE.\\n\\n$(translate "ALL DATA ON THIS DISK WILL BE PERMANENTLY LOST!")\\n\\n$(translate "Are you sure?")" \
|
|
$UI_YESNO_H $UI_YESNO_W; then
|
|
continue
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# 5. Ask mount point — suggest unique path based on device name
|
|
SUGGESTED_MP=$(_get_suggested_mount_point "$CTID" "$DISK")
|
|
MOUNT_POINT=$(dialog --backtitle "$BACKTITLE" \
|
|
--title "$(translate "Mount Point")" \
|
|
--inputbox "$(translate "Enter the mount point inside the CT for") $DISK_INFO:" \
|
|
$UI_MSG_H $UI_MSG_W "$SUGGESTED_MP" \
|
|
2>&1 >/dev/tty)
|
|
|
|
if [ -z "$MOUNT_POINT" ]; then
|
|
dialog --backtitle "$BACKTITLE" \
|
|
--title "$(translate "Error")" \
|
|
--msgbox "$(translate "No mount point was specified.")" $UI_MSG_H $UI_MSG_W
|
|
continue
|
|
fi
|
|
|
|
DISK_LIST+=("$DISK")
|
|
DISK_DESCRIPTIONS+=("$DISK_INFO")
|
|
DISK_MOUNT_POINTS+=("$MOUNT_POINT")
|
|
DISK_SKIP_FORMATS+=("$SKIP_FORMAT")
|
|
DISK_FORMAT_TYPES+=("$FORMAT_TYPE")
|
|
DISK_NEEDS_PARTITION+=("$NEEDS_PARTITION")
|
|
DISK_PARTITIONS+=("$PARTITION")
|
|
DISK_ASSIGNED_TOS+=("$ASSIGNED_TO")
|
|
DISK_CURRENT_FSes+=("$CURRENT_FS")
|
|
done
|
|
|
|
if [ "${#DISK_LIST[@]}" -eq 0 ]; then
|
|
show_proxmenux_logo
|
|
msg_title "$(translate "Import Disk to LXC")"
|
|
msg_warn "$(translate "No disks were configured for processing.")"
|
|
echo ""
|
|
msg_success "$(translate "Press Enter to return to menu...")"
|
|
read -r
|
|
exit 0
|
|
fi
|
|
|
|
# ── TERMINAL PHASE: execute all disk operations ───────────────────────────────
|
|
show_proxmenux_logo
|
|
msg_title "$(translate "Import Disk to LXC")"
|
|
msg_ok "$(translate "CT $CTID selected successfully.")"
|
|
msg_ok "$(translate "Disks to process:") ${#DISK_LIST[@]}"
|
|
for i in "${!DISK_LIST[@]}"; do
|
|
IFS=$'\t' read -r _desc_model _desc_size <<< "${DISK_DESCRIPTIONS[$i]}"
|
|
echo -e "${TAB}${BL}${DISK_LIST[$i]} $_desc_model $_desc_size${CL}"
|
|
done
|
|
echo ""
|
|
|
|
DISKS_ADDED=0
|
|
|
|
for i in "${!DISK_LIST[@]}"; do
|
|
DISK="${DISK_LIST[$i]}"
|
|
MOUNT_POINT="${DISK_MOUNT_POINTS[$i]}"
|
|
SKIP_FORMAT="${DISK_SKIP_FORMATS[$i]}"
|
|
FORMAT_TYPE="${DISK_FORMAT_TYPES[$i]}"
|
|
NEEDS_PARTITION="${DISK_NEEDS_PARTITION[$i]}"
|
|
PARTITION="${DISK_PARTITIONS[$i]}"
|
|
ASSIGNED_TO="${DISK_ASSIGNED_TOS[$i]}"
|
|
CURRENT_FS="${DISK_CURRENT_FSes[$i]}"
|
|
DISK_INFO=$(get_disk_info "$DISK")
|
|
|
|
echo ""
|
|
msg_ok "$(translate "Disk:") $DISK → $MOUNT_POINT"
|
|
|
|
if [ "$NEEDS_PARTITION" = true ]; then
|
|
msg_info "$(translate "Creating partition table and partition...")"
|
|
if ! parted -s "$DISK" mklabel gpt mkpart primary 0% 100% >/dev/null 2>&1; then
|
|
msg_error "$(translate "Failed to create partition table on disk") $DISK_INFO."
|
|
continue
|
|
fi
|
|
sleep 2
|
|
partprobe "$DISK" 2>/dev/null || true
|
|
udevadm settle 2>/dev/null || true
|
|
# Wait up to 5 s for by-id symlinks to be created by udev
|
|
for _i in {1..5}; do
|
|
for _p in /dev/disk/by-id/*; do
|
|
[[ "$(readlink -f "$_p" 2>/dev/null)" == "$DISK"* ]] && break 2
|
|
done
|
|
sleep 1
|
|
done
|
|
PARTITION=$(lsblk -rno NAME "$DISK" | awk -v disk="$(basename "$DISK")" '$1 != disk {print $1; exit}')
|
|
if [ -n "$PARTITION" ]; then
|
|
PARTITION="/dev/$PARTITION"
|
|
msg_ok "$(translate "Partition created:") $PARTITION"
|
|
else
|
|
msg_error "$(translate "Failed to detect partition on disk") $DISK_INFO."
|
|
continue
|
|
fi
|
|
fi
|
|
|
|
if [ "$SKIP_FORMAT" != true ]; then
|
|
msg_info "$(translate "Formatting partition") $PARTITION $(translate "with") $FORMAT_TYPE..."
|
|
if ! case "$FORMAT_TYPE" in
|
|
"ext4") mkfs.ext4 -F "$PARTITION" >/dev/null 2>&1 ;;
|
|
"xfs") mkfs.xfs -f "$PARTITION" >/dev/null 2>&1 ;;
|
|
"btrfs") mkfs.btrfs -f "$PARTITION" >/dev/null 2>&1 ;;
|
|
esac; then
|
|
msg_error "$(translate "Failed to format partition") $PARTITION $(translate "with") $FORMAT_TYPE."
|
|
continue
|
|
fi
|
|
msg_ok "$(translate "Partition") $PARTITION $(translate "successfully formatted with") $FORMAT_TYPE."
|
|
partprobe "$DISK" >/dev/null 2>&1 || true
|
|
sleep 2
|
|
else
|
|
msg_ok "$(translate "Disk already has") $FORMAT_TYPE $(translate "filesystem. Skipping format.")"
|
|
fi
|
|
|
|
INDEX=0
|
|
while pct config "$CTID" | grep -q "mp${INDEX}:"; do
|
|
((INDEX++))
|
|
done
|
|
|
|
FS_PKG=""
|
|
FS_BIN=""
|
|
[[ "$FORMAT_TYPE" == "xfs" ]] && FS_PKG="xfsprogs" && FS_BIN="xfs_repair"
|
|
[[ "$FORMAT_TYPE" == "btrfs" ]] && FS_PKG="btrfs-progs" && FS_BIN="btrfsck"
|
|
|
|
if [[ -n "$FS_PKG" && -n "$FS_BIN" ]]; then
|
|
if [ "$CT_RUNNING" = true ]; then
|
|
if ! pct exec "$CTID" -- sh -c "command -v $FS_BIN >/dev/null 2>&1"; then
|
|
msg_info "$(translate "Installing required tools for $FORMAT_TYPE in CT $CTID...")"
|
|
if install_fs_tools_in_ct "$CTID" "$FS_PKG"; then
|
|
msg_ok "$(translate "Required tools for $FORMAT_TYPE installed in CT $CTID.")"
|
|
else
|
|
msg_warn "$(translate "Could not install") $FS_PKG $(translate "automatically. Install it manually inside the container.")"
|
|
fi
|
|
fi
|
|
else
|
|
# CT is stopped — ask via whiptail (terminal-safe, no dialog on top of output)
|
|
if whiptail --backtitle "$BACKTITLE" \
|
|
--title "$(translate "Filesystem Tools Required")" \
|
|
--yesno "$(translate "The filesystem") $FORMAT_TYPE $(translate "requires the package") $FS_PKG $(translate "installed inside CT") $CTID.\n\n$(translate "The container is currently stopped. Do you want to start it now to install the package?")\n\n$(translate "If you choose No, install") $FS_PKG $(translate "manually inside the container before starting it.")" \
|
|
$UI_YESNO_H $UI_YESNO_W; then
|
|
msg_info "$(translate "Starting CT") $CTID..."
|
|
pct start "$CTID" &>/dev/null
|
|
sleep 2
|
|
if [ "$(pct status "$CTID" | awk '{print $2}')" != "running" ]; then
|
|
msg_error "$(translate "Failed to start CT") $CTID. $(translate "Install") $FS_PKG $(translate "manually inside the container.")"
|
|
else
|
|
msg_ok "$(translate "CT") $CTID $(translate "started.")"
|
|
CT_RUNNING=true
|
|
msg_info "$(translate "Installing") $FS_PKG $(translate "in CT") $CTID..."
|
|
if install_fs_tools_in_ct "$CTID" "$FS_PKG"; then
|
|
msg_ok "$FS_PKG $(translate "installed in CT") $CTID."
|
|
else
|
|
msg_warn "$(translate "Could not install") $FS_PKG $(translate "automatically. Install it manually inside the container.")"
|
|
fi
|
|
fi
|
|
else
|
|
msg_warn "$(translate "Manual install required inside CT") $CTID:"
|
|
echo -e "${DGN}${TAB} Debian/Ubuntu:${CL} ${BL}apt-get install -y $FS_PKG${CL}"
|
|
echo -e "${DGN}${TAB} Arch:${CL} ${BL}pacman -S --noconfirm $FS_PKG${CL}"
|
|
echo -e "${DGN}${TAB} Alpine:${CL} ${BL}apk add $FS_PKG${CL}"
|
|
echo
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
PERSISTENT_PARTITION=$(get_preferred_disk_path "$PARTITION")
|
|
|
|
msg_info "$(translate "Applying passthrough to CT") $CTID..."
|
|
if [ "$FORMAT_TYPE" == "xfs" ]; then
|
|
RESULT=$(pct set "$CTID" -mp${INDEX} "$PERSISTENT_PARTITION,mp=$MOUNT_POINT,backup=0,ro=0" 2>&1)
|
|
else
|
|
RESULT=$(pct set "$CTID" -mp${INDEX} "$PERSISTENT_PARTITION,mp=$MOUNT_POINT,backup=0,ro=0,acl=1" 2>&1)
|
|
fi
|
|
SET_STATUS=$?
|
|
|
|
if [ $SET_STATUS -eq 0 ]; then
|
|
msg_ok "$(translate "Disk assigned at") $MOUNT_POINT $(translate "using") $PERSISTENT_PARTITION"
|
|
[[ -n "$ASSIGNED_TO" ]] && msg_warn "$(translate "WARNING: This disk is also assigned to:") $(echo -e "$ASSIGNED_TO" | tr '\n' ' ')"
|
|
|
|
# Verify disk is accessible inside the CT
|
|
if [ "$CT_RUNNING" = true ]; then
|
|
msg_info "$(translate "Verifying disk accessibility in CT") $CTID..."
|
|
sleep 1
|
|
if pct exec "$CTID" -- sh -c "mountpoint -q '$MOUNT_POINT' || [ -d '$MOUNT_POINT' ]" 2>/dev/null; then
|
|
msg_ok "$(translate "Disk verified and accessible inside CT at") $MOUNT_POINT"
|
|
fi
|
|
fi
|
|
|
|
((DISKS_ADDED++))
|
|
else
|
|
msg_error "$(translate "Could not add disk") $DISK_INFO $(translate "to CT") $CTID. $(translate "Error:") $RESULT"
|
|
fi
|
|
done
|
|
|
|
echo ""
|
|
if [ "$DISKS_ADDED" -gt 0 ]; then
|
|
msg_ok "$(translate "Completed.") $DISKS_ADDED $(translate "disk(s) added to CT") $CTID."
|
|
else
|
|
msg_warn "$(translate "No disks were added.")"
|
|
fi
|
|
msg_success "$(translate "Press Enter to return to menu...")"
|
|
read -r
|
|
exit 0
|