#!/bin/bash # ========================================================== # ProxMenux - iSCSI Host Manager for Proxmox Host # ========================================================== # Author : MacRimi # Copyright : (c) 2024 MacRimi # License : MIT # ========================================================== # Description: # Adds iSCSI targets as Proxmox storage (pvesm add iscsi). # Proxmox manages the connection natively via open-iscsi. # iSCSI storage provides block devices for VM disk images. # ========================================================== LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts" BASE_DIR="/usr/local/share/proxmenux" UTILS_FILE="$BASE_DIR/utils.sh" if [[ -f "$UTILS_FILE" ]]; then source "$UTILS_FILE" fi load_language initialize_cache if ! command -v pveversion >/dev/null 2>&1; then dialog --backtitle "ProxMenux" --title "$(translate "Error")" \ --msgbox "$(translate "This script must be run on a Proxmox host.")" 8 60 exit 1 fi # ========================================================== # STORAGE CONFIG READER # ========================================================== get_storage_config() { local storage_id="$1" awk -v id="$storage_id" ' /^[a-z]+: / { found = ($0 ~ ": "id"$"); next } found && /^[^ \t]/ { exit } found { print } ' /etc/pve/storage.cfg } # ========================================================== # TOOLS # ========================================================== ensure_iscsi_tools() { if ! command -v iscsiadm >/dev/null 2>&1; then msg_info "$(translate "Installing iSCSI initiator tools...")" apt-get update &>/dev/null apt-get install -y open-iscsi &>/dev/null systemctl enable --now iscsid 2>/dev/null || true msg_ok "$(translate "iSCSI tools installed")" fi if ! systemctl is-active --quiet iscsid 2>/dev/null; then msg_info "$(translate "Starting iSCSI daemon...")" systemctl start iscsid 2>/dev/null || true fi } # ========================================================== # TARGET DISCOVERY # ========================================================== select_iscsi_portal() { ISCSI_PORTAL=$(whiptail --inputbox \ "$(translate "Enter iSCSI target portal IP or hostname:")\n\n$(translate "Examples:")\n 192.168.1.100\n 192.168.1.100:3260\n nas.local" \ 14 65 \ --title "$(translate "iSCSI Portal")" 3>&1 1>&2 2>&3) [[ $? -ne 0 || -z "$ISCSI_PORTAL" ]] && return 1 # Normalise: if no port specified, add default 3260 if [[ ! "$ISCSI_PORTAL" =~ :[0-9]+$ ]]; then ISCSI_PORTAL_DISPLAY="$ISCSI_PORTAL" ISCSI_PORTAL_FULL="${ISCSI_PORTAL}:3260" else ISCSI_PORTAL_DISPLAY="$ISCSI_PORTAL" ISCSI_PORTAL_FULL="$ISCSI_PORTAL" fi # Extract host for ping ISCSI_HOST=$(echo "$ISCSI_PORTAL" | cut -d: -f1) return 0 } discover_iscsi_targets() { show_proxmenux_logo msg_title "$(translate "Add iSCSI Target as Proxmox Storage")" msg_ok "$(translate "Portal:") $ISCSI_PORTAL_DISPLAY" msg_info "$(translate "Testing connectivity to portal...")" if ! ping -c 1 -W 3 "$ISCSI_HOST" >/dev/null 2>&1; then msg_error "$(translate "Cannot reach portal:") $ISCSI_HOST" echo "" msg_success "$(translate "Press Enter to continue...")" read -r return 1 fi msg_ok "$(translate "Portal is reachable")" if ! nc -z -w 3 "$ISCSI_HOST" "${ISCSI_PORTAL_FULL##*:}" 2>/dev/null; then msg_warn "$(translate "iSCSI port") ${ISCSI_PORTAL_FULL##*:} $(translate "may be closed — trying discovery anyway...")" fi msg_info "$(translate "Discovering iSCSI targets...")" DISCOVERY_OUTPUT=$(iscsiadm --mode discovery --type sendtargets \ --portal "$ISCSI_PORTAL_FULL" 2>&1) DISCOVERY_RESULT=$? if [[ $DISCOVERY_RESULT -ne 0 ]]; then msg_error "$(translate "iSCSI discovery failed")" echo -e "${TAB}$(translate "Error:"): $DISCOVERY_OUTPUT" echo "" msg_info2 "$(translate "Please check:")" echo -e "${TAB}• $(translate "Portal IP and port are correct")" echo -e "${TAB}• $(translate "iSCSI service is running on the target")" echo -e "${TAB}• $(translate "Firewall allows port") ${ISCSI_PORTAL_FULL##*:}" echo -e "${TAB}• $(translate "Initiator IQN is authorised on the target")" echo "" msg_success "$(translate "Press Enter to continue...")" read -r return 1 fi # Parse discovered targets: format is TARGETS=$(echo "$DISCOVERY_OUTPUT" | awk '{print $2}' | grep "^iqn\." | sort -u) if [[ -z "$TARGETS" ]]; then msg_warn "$(translate "No iSCSI targets found on portal") $ISCSI_PORTAL_DISPLAY" echo "" msg_success "$(translate "Press Enter to continue...")" read -r return 1 fi msg_ok "$(translate "Discovery successful")" return 0 } select_iscsi_target() { local target_count target_count=$(echo "$TARGETS" | wc -l) if [[ "$target_count" -eq 1 ]]; then ISCSI_TARGET=$(echo "$TARGETS" | head -1) msg_ok "$(translate "Single target found — selected automatically:") $ISCSI_TARGET" return 0 fi local options=() local i=1 while IFS= read -r iqn; do [[ -z "$iqn" ]] && continue # Try to get LUN info for display local lun_info lun_info=$(iscsiadm --mode node --targetname "$iqn" --portal "$ISCSI_PORTAL_FULL" \ --op show 2>/dev/null | grep "node.conn\[0\].address" | awk -F= '{print $2}' | tr -d ' ' || true) options+=("$i" "$iqn") i=$((i + 1)) done <<< "$TARGETS" local choice choice=$(dialog --backtitle "ProxMenux" --title "$(translate "Select iSCSI Target")" \ --menu "\n$(translate "Select target IQN:")" 20 90 10 \ "${options[@]}" 3>&1 1>&2 2>&3) [[ -z "$choice" ]] && return 1 ISCSI_TARGET=$(echo "$TARGETS" | sed -n "${choice}p") [[ -z "$ISCSI_TARGET" ]] && return 1 return 0 } # ========================================================== # STORAGE CONFIGURATION # ========================================================== configure_iscsi_storage() { # Suggest a storage ID derived from target IQN local iqn_suffix iqn_suffix=$(echo "$ISCSI_TARGET" | awk -F: '{print $NF}' | tr '.' '-' | cut -c1-20) local default_id="iscsi-${iqn_suffix}" STORAGE_ID=$(whiptail --inputbox "$(translate "Enter storage ID for Proxmox:")" \ 10 65 "$default_id" \ --title "$(translate "Storage ID")" 3>&1 1>&2 2>&3) [[ $? -ne 0 ]] && return 1 [[ -z "$STORAGE_ID" ]] && STORAGE_ID="$default_id" if [[ ! "$STORAGE_ID" =~ ^[a-zA-Z0-9][a-zA-Z0-9_-]*$ ]]; then whiptail --msgbox "$(translate "Invalid storage ID. Use only letters, numbers, hyphens and underscores.")" 8 70 return 1 fi # iSCSI in Proxmox exposes block devices — content is always 'images' # (no file-level access like NFS/CIFS) MOUNT_CONTENT="images" whiptail --title "$(translate "iSCSI Content Type")" \ --msgbox "$(translate "iSCSI storage provides raw block devices for VM disk images.")\n\n$(translate "Content type is fixed to:")\n\n images\n\n$(translate "Each LUN will appear as a block device assignable to VMs.")" \ 12 70 return 0 } # ========================================================== # PROXMOX INTEGRATION # ========================================================== add_proxmox_iscsi_storage() { local storage_id="$1" local portal="$2" local target="$3" local content="${4:-images}" if ! command -v pvesm >/dev/null 2>&1; then msg_error "$(translate "pvesm command not found. This should not happen on Proxmox.")" return 1 fi if pvesm status "$storage_id" >/dev/null 2>&1; then msg_warn "$(translate "Storage ID already exists:") $storage_id" if ! whiptail --yesno "$(translate "Storage ID already exists. Do you want to remove and recreate it?")" \ 8 60 --title "$(translate "Storage Exists")"; then return 0 fi pvesm remove "$storage_id" 2>/dev/null || true fi msg_ok "$(translate "Storage ID is available")" msg_info "$(translate "Adding iSCSI storage to Proxmox...")" local pvesm_output pvesm_result pvesm_output=$(pvesm add iscsi "$storage_id" \ --portal "$portal" \ --target "$target" \ --content "$content" 2>&1) pvesm_result=$? if [[ $pvesm_result -eq 0 ]]; then msg_ok "$(translate "iSCSI storage added successfully to Proxmox!")" echo -e "" echo -e "${TAB}${BOLD}$(translate "Storage Added:")${CL}" echo -e "${TAB}${BGN}$(translate "Storage ID:")${CL} ${BL}$storage_id${CL}" echo -e "${TAB}${BGN}$(translate "Portal:")${CL} ${BL}$portal${CL}" echo -e "${TAB}${BGN}$(translate "Target IQN:")${CL} ${BL}$target${CL}" echo -e "${TAB}${BGN}$(translate "Content Types:")${CL} ${BL}$content${CL}" echo -e "" msg_ok "$(translate "Storage is now available in Proxmox web interface under Datacenter > Storage")" msg_info2 "$(translate "LUNs appear as block devices assignable to VMs")" return 0 else msg_error "$(translate "Failed to add iSCSI storage to Proxmox.")" echo -e "${TAB}$(translate "Error details:"): $pvesm_output" echo -e "" msg_info2 "$(translate "You can add it manually through:")" echo -e "${TAB}• $(translate "Proxmox web interface: Datacenter > Storage > Add > iSCSI")" echo -e "${TAB}• pvesm add iscsi $storage_id --portal $portal --target $target --content $content" return 1 fi } # ========================================================== # MAIN OPERATIONS # ========================================================== add_iscsi_to_proxmox() { ensure_iscsi_tools # Step 1: Enter portal select_iscsi_portal || return # Step 2: Discover targets discover_iscsi_targets || return # Step 3: Select target select_iscsi_target || return show_proxmenux_logo msg_title "$(translate "Add iSCSI Target as Proxmox Storage")" msg_ok "$(translate "Portal:") $ISCSI_PORTAL_DISPLAY" msg_ok "$(translate "Target:") $ISCSI_TARGET" # Step 4: Configure storage configure_iscsi_storage || return # Step 5: Add to Proxmox show_proxmenux_logo msg_title "$(translate "Add iSCSI Target as Proxmox Storage")" msg_ok "$(translate "Portal:") $ISCSI_PORTAL_DISPLAY" msg_ok "$(translate "Target:") $ISCSI_TARGET" msg_ok "$(translate "Storage ID:") $STORAGE_ID" msg_ok "$(translate "Content:") $MOUNT_CONTENT" echo -e "" add_proxmox_iscsi_storage "$STORAGE_ID" "$ISCSI_PORTAL_FULL" "$ISCSI_TARGET" "$MOUNT_CONTENT" echo -e "" msg_success "$(translate "Press Enter to continue...")" read -r } view_iscsi_storages() { show_proxmenux_logo msg_title "$(translate "iSCSI Storages in Proxmox")" echo "==================================================" echo "" if ! command -v pvesm >/dev/null 2>&1; then msg_error "$(translate "pvesm not found.")" echo "" msg_success "$(translate "Press Enter to continue...")" read -r return fi ISCSI_STORAGES=$(pvesm status 2>/dev/null | awk '$2 == "iscsi" {print $1, $3}') if [[ -z "$ISCSI_STORAGES" ]]; then msg_warn "$(translate "No iSCSI storage configured in Proxmox.")" echo "" msg_info2 "$(translate "Use option 1 to add an iSCSI target as Proxmox storage.")" else echo -e "${BOLD}$(translate "iSCSI Storages:")${CL}" echo "" while IFS=" " read -r storage_id storage_status; do [[ -z "$storage_id" ]] && continue local storage_info portal target content storage_info=$(get_storage_config "$storage_id") portal=$(echo "$storage_info" | awk '$1 == "portal" {print $2}') target=$(echo "$storage_info" | awk '$1 == "target" {print $2}') content=$(echo "$storage_info" | awk '$1 == "content" {print $2}') echo -e "${TAB}${BOLD}$storage_id${CL}" echo -e "${TAB} ${BGN}$(translate "Portal:")${CL} ${BL}$portal${CL}" echo -e "${TAB} ${BGN}$(translate "Target IQN:")${CL} ${BL}$target${CL}" echo -e "${TAB} ${BGN}$(translate "Content:")${CL} ${BL}$content${CL}" if [[ "$storage_status" == "active" ]]; then echo -e "${TAB} ${BGN}$(translate "Status:")${CL} ${GN}$(translate "Active")${CL}" else echo -e "${TAB} ${BGN}$(translate "Status:")${CL} ${RD}$storage_status${CL}" fi echo "" done <<< "$ISCSI_STORAGES" fi echo "" msg_success "$(translate "Press Enter to continue...")" read -r } remove_iscsi_storage() { if ! command -v pvesm >/dev/null 2>&1; then dialog --backtitle "ProxMenux" --title "$(translate "Error")" \ --msgbox "\n$(translate "pvesm not found.")" 8 60 return fi ISCSI_STORAGES=$(pvesm status 2>/dev/null | awk '$2 == "iscsi" {print $1}') if [[ -z "$ISCSI_STORAGES" ]]; then dialog --backtitle "ProxMenux" --title "$(translate "No iSCSI Storage")" \ --msgbox "\n$(translate "No iSCSI storage found in Proxmox.")" 8 60 return fi local options=() while IFS= read -r storage_id; do [[ -z "$storage_id" ]] && continue local storage_info portal target storage_info=$(get_storage_config "$storage_id") portal=$(echo "$storage_info" | awk '$1 == "portal" {print $2}') target=$(echo "$storage_info" | awk '$1 == "target" {print $2}') options+=("$storage_id" "$portal — ${target:0:40}") done <<< "$ISCSI_STORAGES" local SELECTED SELECTED=$(dialog --backtitle "ProxMenux" --title "$(translate "Remove iSCSI Storage")" \ --menu "$(translate "Select storage to remove:")" 20 90 10 \ "${options[@]}" 3>&1 1>&2 2>&3) [[ -z "$SELECTED" ]] && return local storage_info portal target content storage_info=$(get_storage_config "$SELECTED") portal=$(echo "$storage_info" | awk '$1 == "portal" {print $2}') target=$(echo "$storage_info" | awk '$1 == "target" {print $2}') content=$(echo "$storage_info" | awk '$1 == "content" {print $2}') if whiptail --yesno "$(translate "Remove Proxmox iSCSI storage:")\n\n$SELECTED\n\n$(translate "Portal:"): $portal\n$(translate "Target:"): $target\n$(translate "Content:"): $content\n\n$(translate "This removes the storage from Proxmox. The iSCSI target is not affected.")" \ 16 80 --title "$(translate "Confirm Remove")"; then show_proxmenux_logo msg_title "$(translate "Remove iSCSI Storage")" if pvesm remove "$SELECTED" 2>/dev/null; then msg_ok "$(translate "Storage") $SELECTED $(translate "removed successfully from Proxmox.")" else msg_error "$(translate "Failed to remove storage.")" fi echo -e "" msg_success "$(translate "Press Enter to continue...")" read -r fi } test_iscsi_connectivity() { show_proxmenux_logo msg_title "$(translate "Test iSCSI Connectivity")" echo "==================================================" echo "" if command -v iscsiadm >/dev/null 2>&1; then msg_ok "$(translate "iSCSI Initiator: AVAILABLE")" local initiator_iqn initiator_iqn=$(cat /etc/iscsi/initiatorname.iscsi 2>/dev/null | grep "^InitiatorName=" | cut -d= -f2) [[ -n "$initiator_iqn" ]] && echo -e " ${BGN}$(translate "Initiator IQN:")${CL} ${BL}$initiator_iqn${CL}" if systemctl is-active --quiet iscsid 2>/dev/null; then msg_ok "$(translate "iSCSI Daemon (iscsid): RUNNING")" else msg_warn "$(translate "iSCSI Daemon (iscsid): STOPPED")" fi else msg_warn "$(translate "iSCSI Initiator: NOT INSTALLED")" echo -e " $(translate "Install with: apt-get install open-iscsi")" fi echo "" if command -v pvesm >/dev/null 2>&1; then echo -e "${BOLD}$(translate "Proxmox iSCSI Storage Status:")${CL}" ISCSI_STORAGES=$(pvesm status 2>/dev/null | awk '$2 == "iscsi" {print $1, $3}') if [[ -n "$ISCSI_STORAGES" ]]; then while IFS=" " read -r storage_id storage_status; do [[ -z "$storage_id" ]] && continue local portal portal=$(get_storage_config "$storage_id" | awk '$1 == "portal" {print $2}') local portal_host="${portal%%:*}" echo -n " $storage_id ($portal): " if ping -c 1 -W 2 "$portal_host" >/dev/null 2>&1; then echo -ne "${GN}$(translate "Reachable")${CL}" local portal_port="${portal##*:}" [[ "$portal_port" == "$portal" ]] && portal_port="3260" if nc -z -w 2 "$portal_host" "$portal_port" 2>/dev/null; then echo -e " | iSCSI port $portal_port: ${GN}$(translate "Open")${CL}" else echo -e " | iSCSI port $portal_port: ${RD}$(translate "Closed")${CL}" fi else echo -e "${RD}$(translate "Unreachable")${CL}" fi if [[ "$storage_status" == "active" ]]; then echo -e " $(translate "Proxmox status:") ${GN}$storage_status${CL}" else echo -e " $(translate "Proxmox status:") ${RD}$storage_status${CL}" fi # Show active iSCSI sessions for this target local target target=$(get_storage_config "$storage_id" | awk '$1 == "target" {print $2}') if command -v iscsiadm >/dev/null 2>&1; then local session session=$(iscsiadm --mode session 2>/dev/null | grep "$target" || true) if [[ -n "$session" ]]; then echo -e " $(translate "Active session:") ${GN}$(translate "Connected")${CL}" else echo -e " $(translate "Active session:") ${YW}$(translate "No active session")${CL}" fi fi echo "" done <<< "$ISCSI_STORAGES" else echo " $(translate "No iSCSI storage configured.")" fi else msg_warn "$(translate "pvesm not available.")" fi echo "" msg_success "$(translate "Press Enter to continue...")" read -r } # ========================================================== # MAIN MENU # ========================================================== while true; do CHOICE=$(dialog --backtitle "ProxMenux" \ --title "$(translate "iSCSI Host Manager - Proxmox Host")" \ --menu "$(translate "Choose an option:")" 18 70 6 \ "1" "$(translate "Add iSCSI Target as Proxmox Storage")" \ "2" "$(translate "View iSCSI Storages")" \ "3" "$(translate "Remove iSCSI Storage")" \ "4" "$(translate "Test iSCSI Connectivity")" \ "5" "$(translate "Exit")" \ 3>&1 1>&2 2>&3) RETVAL=$? if [[ $RETVAL -ne 0 ]]; then exit 0 fi case $CHOICE in 1) add_iscsi_to_proxmox ;; 2) view_iscsi_storages ;; 3) remove_iscsi_storage ;; 4) test_iscsi_connectivity ;; 5) exit 0 ;; *) exit 0 ;; esac done