mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-09-14 18:56:52 +00:00
ProxMenux modifies the host: it rewrites configuration files, installs packages, enables services. Until now nobody could say afterwards what had changed, and showing the script does not answer that question — a four-hundred-line function may alter two values, and the reader has no way to know which two. This adds the two halves of an answer. The change journal records what ProxMenux does as it does it. Eleven bash primitives capture the previous state, apply the change and record it in the same step, writing to a spool that the Monitor reads back. One hundred and thirteen functions across twenty-five scripts are instrumented, covering post-install, shared storage, security tooling, container conversions, disk operations and the PVE 8 to 9 upgrade path. The page shows the difference — rotate 7 becoming rotate 14 — and never the script. Restore and backup scripts are deliberately left out: a restore puts the host back to a state some other script already recorded. The Audit and reports page answers the other half: what state is this host in, regardless of who put it there. Forty-three checks across seven areas read the host and classify each result as critical, warning, observation, conformant, unverified or not applicable, with the evidence they read attached to each one. A declared policy lets the reader say what this particular host is expected to do — which guests must have a backup, which storages are essential — so the report judges the host against its own intent rather than a generic template. An inventory records the hardware, network and guest topology behind those readings, a comparison shows what moved between two runs, and six report profiles produce a printable document scoped to what the reader needs. Everything is available in the eight supported languages. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
753 lines
29 KiB
Bash
753 lines
29 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
|
|
|
|
if [[ -f "$LOCAL_SCRIPTS/global/pmx_journal.sh" ]]; then
|
|
source "$LOCAL_SCRIPTS/global/pmx_journal.sh"
|
|
fi
|
|
|
|
FUNC_VERSION="1.3"
|
|
|
|
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"
|
|
local FUNC_VERSION="1.3"
|
|
pmx_journal_context "install_fs_tools_in_ct" "$FUNC_VERSION"
|
|
|
|
if pct exec "$ctid" -- sh -c "[ -f /etc/alpine-release ]"; then
|
|
pmx_record_execution "install ${pkg} in CT ${ctid}" \
|
|
"pct exec ${ctid} -- apk update and apk add ${pkg}"
|
|
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
|
|
pmx_record_execution "install ${pkg} in CT ${ctid}" \
|
|
"pct exec ${ctid} -- pacman -Sy --noconfirm ${pkg}"
|
|
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
|
|
pmx_record_execution "install ${pkg} in CT ${ctid}" \
|
|
"pct exec ${ctid} -- apt-get update and apt-get install ${pkg}"
|
|
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
|
|
|
|
pmx_journal_context "disk_passthrough_ct" "$FUNC_VERSION"
|
|
|
|
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..."
|
|
pmx_record_execution "stop CT ${CTID} for privileged conversion" "pct shutdown ${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"
|
|
pmx_edit_file "$CONF_FILE" '/^unprivileged: 1/d'
|
|
echo "unprivileged: 0" | pmx_append_file "$CONF_FILE"
|
|
msg_ok "$(translate "Container successfully converted to privileged.")"
|
|
|
|
if [ "$CT_RUNNING" = true ]; then
|
|
msg_info "$(translate "Starting container") $CTID..."
|
|
pmx_record_execution "start CT ${CTID} after privileged conversion" "pct start ${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
|
|
pmx_journal_context "disk_passthrough_ct" "$FUNC_VERSION"
|
|
|
|
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...")"
|
|
pmx_record_execution "create GPT partition on ${DISK} for CT ${CTID}" \
|
|
"parted -s ${DISK} mklabel gpt mkpart primary 0% 100%"
|
|
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..."
|
|
pmx_record_execution "format ${PARTITION} as ${FORMAT_TYPE} for CT ${CTID}" \
|
|
"mkfs ${FORMAT_TYPE} ${PARTITION}"
|
|
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..."
|
|
pmx_record_execution "start CT ${CTID} to install filesystem tools" "pct start ${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..."
|
|
pmx_journal_context "disk_passthrough_ct" "$FUNC_VERSION"
|
|
if [ "$FORMAT_TYPE" == "xfs" ]; then
|
|
pmx_record_execution "assign ${PERSISTENT_PARTITION} to CT ${CTID} at ${MOUNT_POINT}" \
|
|
"pct set ${CTID} -mp${INDEX} ${PERSISTENT_PARTITION},mp=${MOUNT_POINT},backup=0,ro=0"
|
|
RESULT=$(pct set "$CTID" -mp${INDEX} "$PERSISTENT_PARTITION,mp=$MOUNT_POINT,backup=0,ro=0" 2>&1)
|
|
else
|
|
pmx_record_execution "assign ${PERSISTENT_PARTITION} to CT ${CTID} at ${MOUNT_POINT}" \
|
|
"pct set ${CTID} -mp${INDEX} ${PERSISTENT_PARTITION},mp=${MOUNT_POINT},backup=0,ro=0,acl=1"
|
|
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
|