Compare commits

..

No commits in common. "main" and "v1.1.2" have entirely different histories.
main ... v1.1.2

39 changed files with 842 additions and 11504 deletions

View File

@ -1,76 +0,0 @@
import requests, json
from pathlib import Path
# GitHub API URL to fetch all .json files describing scripts
API_URL = "https://api.github.com/repos/community-scripts/ProxmoxVE/contents/frontend/public/json"
# Base path to build the full URL for the installable scripts
SCRIPT_BASE = "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main"
# Output file where the consolidated helper scripts cache will be stored
OUTPUT_FILE = Path("json/helpers_cache.json")
OUTPUT_FILE.parent.mkdir(parents=True, exist_ok=True)
res = requests.get(API_URL)
data = res.json()
cache = []
# Loop over each file in the JSON directory
for item in data:
url = item.get("download_url")
if not url or not url.endswith(".json"):
continue
try:
raw = requests.get(url).json()
if not isinstance(raw, dict):
continue
except:
continue
# Extract fields required to identify a valid helper script
name = raw.get("name", "")
slug = raw.get("slug")
type_ = raw.get("type", "")
script = raw.get("install_methods", [{}])[0].get("script", "")
if not slug or not script:
continue # Skip if it's not a valid script
desc = raw.get("description", "")
categories = raw.get("categories", [])
notes = [note.get("text", "") for note in raw.get("notes", []) if isinstance(note, dict)]
full_script_url = f"{SCRIPT_BASE}/{script}"
credentials = raw.get("default_credentials", {})
cred_username = credentials.get("username")
cred_password = credentials.get("password")
add_credentials = (
(cred_username is not None and str(cred_username).strip() != "") or
(cred_password is not None and str(cred_password).strip() != "")
)
entry = {
"name": name,
"slug": slug,
"desc": desc,
"script": script,
"script_url": full_script_url,
"categories": categories,
"notes": notes,
"type": type_
}
if add_credentials:
entry["default_credentials"] = {
"username": cred_username,
"password": cred_password
}
cache.append(entry)
# Write the JSON cache to disk
with open(OUTPUT_FILE, "w", encoding="utf-8") as f:
json.dump(cache, f, indent=2)
print(f"✅ helpers_cache.json created at {OUTPUT_FILE} with {len(cache)} valid scripts.")

View File

@ -1,38 +0,0 @@
name: Update Helper Scripts Cache
on:
# Manual trigger from GitHub Actions UI
workflow_dispatch:
# Automatic run every 6 hours
schedule:
- cron: "0 */6 * * *"
jobs:
update-cache:
runs-on: ubuntu-latest
permissions:
contents: write # Required to push changes to the repository
steps:
- name: ⬇️ Checkout the repository
uses: actions/checkout@v3
- name: 🐍 Set up Python
uses: actions/setup-python@v4
with:
python-version: 3.11
- name: 📦 Install Python dependencies
run: pip install requests
- name: ⚙️ Generate json/helpers_cache.json
run: python .github/scripts/generate_helpers_cache.py
- name: 📤 Commit and push if updated
run: |
git config user.name "ProxMenuxBot"
git config user.email "bot@proxmenux.local"
git add json/helpers_cache.json
git diff --cached --quiet || git commit -m "Update helpers_cache.json"
git push

View File

@ -1,102 +1,3 @@
## 2025-06-06
### Added
- **New Menu: Proxmox PVE Helper Scripts**
Officially introduced the new **Proxmox PVE Helper Scripts** menu, replacing the previous: Esenciales Proxmox.
This new menu includes:
- Script search by name in real time
- Category-based browsing
Its a cleaner, faster, and more functional way to access community scripts in Proxmox.
![Helper Scripts Menu](https://macrimi.github.io/ProxMenux/menu-helpers-script.png)
- **New CPU Models in VM Creation**
The CPU selection menu in VM creation has been greatly expanded to support advanced QEMU and x86-64 CPU profiles.
This allows better compatibility with modern guest systems and fine-tuning performance for specific workloads, including nested virtualization and hardware-assisted features.
![CPU Config](https://macrimi.github.io/ProxMenux/vm/config-cpu.png)
Thanks to **@Nida Légé (Nidouille)** for suggesting this enhancement.
- **Support for `.raw` Disk Images**
The disk import tool for VMs now supports `.raw` files, in addition to `.img`, `.qcow2`, and `.vmdk`.
This improves compatibility when working with disk exports from other hypervisors or backup tools.
💡 Suggested by **@guilloking** in [GitHub Issue #5](https://github.com/MacRimi/ProxMenux/issues/5)
- **Locale Detection in Language Skipping**
The function that disables extra APT languages now includes:
- Automatic locale detection (`LANG`)
- Auto-generation of `en_US.UTF-8` if none is found
- Prevents warnings during script execution due to undefined locale
### Improved
- **APT Language Skipping Logic**
Improved locale handling ensures system compatibility before disabling translations:
```bash
if ! locale -a | grep -qi "^${default_locale//-/_}$"; then
echo "$default_locale UTF-8" >> /etc/locale.gen
locale-gen "$default_locale"
fi
```
- **System Update Speed**
Post-install system upgrades are now faster:
- The upgrade process (`dist-upgrade`) is separated from container template index updates.
- Index refresh is now an optional feature selected in the script.
## 2025-05-27
### Fixed
- **Kali Linux ISO URL Updated**
Fixed the incorrect download URL for Kali Linux ISO in the Linux installer module. The new correct path is:
```
https://cdimage.kali.org/kali-2025.1c/kali-linux-2025.1c-installer-amd64.iso
```
### Improved
- **Faster Dialog Menu Transitions**
Improved UI responsiveness across all interactive menus by replacing `whiptail` with `dialog`, offering faster transitions and smoother navigation.
- **Coral USB Support in LXC**
Improved the logic for configuring Coral USB TPU passthrough into LXC containers:
- Refactored configuration into modular blocks with better structure and inline comments.
- Clear separation of Coral USB (`/dev/coral`) and Coral M.2 (`/dev/apex_0`) logic.
- Maintains backward compatibility with existing LXC configurations.
- Introduced persistent Coral USB passthrough using a udev rule:
```bash
# Create udev rule for Coral USB
SUBSYSTEM=="usb", ATTRS{idVendor}=="18d1", ATTRS{idProduct}=="9302", MODE="0666", TAG+="uaccess", SYMLINK+="coral"
# Map /dev/coral if it exists
if [ -e /dev/coral ]; then
echo "lxc.mount.entry: /dev/coral dev/coral none bind,optional,create=file" >> "$CONFIG_FILE"
fi
```
- Special thanks to **@Blaspt** for validating the persistent Coral USB passthrough and suggesting the use of `/dev/coral` symbolic link.
### Added
- **Persistent Coral USB Passthrough Support**
Added udev rule support for Coral USB devices to persistently map them as `/dev/coral`, enabling consistent passthrough across reboots. This path is automatically detected and mapped in the container configuration.
- **RSS Feed Integration**
Added support for generating an RSS feed for the changelog, allowing users to stay informed of updates through news clients.
- **Release Service Automation**
Implemented a new release management service to automate publishing and tagging of versions, starting with version **v1.1.2**.
## 2025-05-13
### Fixed

View File

@ -29,7 +29,6 @@ Instead, please report it privately via email:
📧 proxmenux@macrimi.pro
For detailed information on security vulnerabilities and how to report them, please refer to our [Security Policy](./SECURITY.md).
## 🤝 3. Community Guidelines

View File

@ -1,41 +0,0 @@
# 🔒 Security Policy
## 📅 Supported Versions
We actively maintain the latest release of ProxMenux. Only the most recent version receives security updates.
| Version | Supported |
| ------- | --------- |
| Latest | ✅ |
| Older versions | ❌ |
## 📢 Reporting a Vulnerability
If you discover a **security vulnerability**, please help us keep the community safe by reporting it **privately**.
**Do not report vulnerabilities in public GitHub Issues or Discussions.**
### 📬 Contact
To report a vulnerability, email:
**📧 proxmenux@macrimi.pro**
Please include as much detail as possible, including:
- Steps to reproduce the issue
- A description of the impact
- Any known mitigations
We aim to respond as soon as possible, typically within **48 hours**.
## ⚠️ Coordinated Disclosure
We follow responsible disclosure principles. If a vulnerability is confirmed, we will:
1. Work on a fix immediately.
2. Inform you of the resolution status.
---
🔐 Thank you for helping make ProxMenux a safer project for everyone!

View File

@ -223,12 +223,12 @@
"it": "Hardware: GPUs e Coral-TPU",
"pt": "Hardware: GPUs e Coral-TPU"
},
"Proxmox VE Helper Scripts": {
"es": "Proxmox VE Helper Scripts",
"fr": "Proxmox VE Helper Scripts",
"de": "Proxmox VE Helper Scriptse",
"it": "Proxmox VE Helper Scripts",
"pt": "Proxmox VE Helper Scripts"
"Essential Proxmox VE Helper-Scripts": {
"es": "Esenciales Proxmox VE Helper-Scripts",
"fr": "Essentiels Proxmox VE Helper-Scripts",
"de": "Essentielle Proxmox VE Helper-Scriptse",
"it": "Essenziali Proxmox VE Helper-Scripts",
"pt": "Essenciais Proxmox VE Helper-Scripts"
},
"Generating automatic translations...": {
"es": "Generando traducciones...",

File diff suppressed because it is too large Load Diff

4
menu
View File

@ -49,11 +49,9 @@ fi
# Initialize language configuration
initialize_config() {
show_proxmenux_logo
# Check if config file exists and has language field
if [ ! -f "$CONFIG_FILE" ] || [ -z "$(jq -r '.language // empty' "$CONFIG_FILE")" ]; then
show_proxmenux_logo
sleep 2
LANGUAGE=$(whiptail --title "$(translate "Select Language")" --menu "$(translate "Choose a language for the menu:")" 20 60 12 \
"en" "$(translate "English (Recommended)")" \
"es" "$(translate "Spanish")" \

View File

@ -1,917 +0,0 @@
#!/usr/bin/env bash
# Configuration ============================================
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
BASE_DIR="/usr/local/share/proxmenux"
UTILS_FILE="$BASE_DIR/utils.sh"
VENV_PATH="/opt/googletrans-env"
if [[ -f "$UTILS_FILE" ]]; then
source "$UTILS_FILE"
fi
load_language
initialize_cache
# ==========================================================
get_external_backup_mount_point() {
local BACKUP_MOUNT_FILE="/usr/local/share/proxmenux/last_backup_mount.txt"
local STORAGE_REPO="$REPO_URL/scripts/backup_restore"
local MOUNT_POINT
if [[ -f "$BACKUP_MOUNT_FILE" ]]; then
MOUNT_POINT=$(head -n1 "$BACKUP_MOUNT_FILE" | tr -d '\r\n' | xargs)
>&2 echo "DEBUG: Valor MOUNT_POINT='$MOUNT_POINT'"
if [[ ! -d "$MOUNT_POINT" ]]; then
msg_error "Mount point does not exist: $MOUNT_POINT"
rm -f "$BACKUP_MOUNT_FILE"
return 1
fi
if ! mountpoint -q "$MOUNT_POINT"; then
msg_error "Mount point is not mounted: $MOUNT_POINT"
rm -f "$BACKUP_MOUNT_FILE"
return 1
fi
echo "$MOUNT_POINT"
return 0
else
source <(curl -s "$STORAGE_REPO/mount_disk_host_bk.sh")
MOUNT_POINT=$(mount_disk_host_bk)
[[ -z "$MOUNT_POINT" ]] && msg_error "$(translate "No disk mounted.")" && return 1
echo "$MOUNT_POINT"
return 0
fi
}
# === Host Backup Main Menu ===
host_backup_menu() {
while true; do
local CHOICE
CHOICE=$(dialog --backtitle "ProxMenux" \
--title "$(translate 'Host Backup')" \
--menu "\n$(translate 'Select backup option:')" 22 70 12 \
"" "$(translate '--- FULL BACKUP ---')" \
1 "$(translate 'Full backup to Proxmox Backup Server (PBS)')" \
2 "$(translate 'Full backup with BorgBackup')" \
3 "$(translate 'Full backup to local .tar.gz')" \
"" "$(translate '--- CUSTOM BACKUP ---')" \
4 "$(translate 'Custom backup to PBS')" \
5 "$(translate 'Custom backup with BorgBackup')" \
6 "$(translate 'Custom backup to local .tar.gz')" \
0 "$(translate 'Return')" \
3>&1 1>&2 2>&3) || return 0
case "$CHOICE" in
1) backup_full_pbs_root ;;
2) backup_with_borg "/boot/efi /etc/pve /etc/network /var/lib/pve-cluster /root /etc/ssh /home /usr/local/bin /etc/cron.d /etc/systemd/system /var/lib/vz" ;;
3) backup_to_local_tar "/boot/efi /etc/pve /etc/network /var/lib/pve-cluster /root /etc/ssh /home /usr/local/bin /etc/cron.d /etc/systemd/system /var/lib/vz" ;;
4) custom_backup_menu backup_to_pbs ;;
5) custom_backup_menu backup_with_borg ;;
6) custom_backup_menu backup_to_local_tar ;;
0) break ;;
esac
done
}
# === Menu checklist for custom backup ===
custom_backup_menu() {
declare -A BACKUP_PATHS=(
[etc-pve]="/etc/pve"
[etc-network]="/etc/network"
[var-lib-pve-cluster]="/var/lib/pve-cluster"
[root-dir]="/root"
[etc-ssh]="/etc/ssh"
[home]="/home"
[local-bin]="/usr/local/bin"
[cron]="/etc/cron.d"
[custom-systemd]="/etc/systemd/system"
[var-lib-vz]="/var/lib/vz"
)
local CHECKLIST_OPTIONS=()
for KEY in "${!BACKUP_PATHS[@]}"; do
DIR="${BACKUP_PATHS[$KEY]}"
CHECKLIST_OPTIONS+=("$KEY" "$DIR" "off")
done
SELECTED_KEYS=$(dialog --separate-output --checklist \
"$(translate 'Select directories to backup:')" 22 70 12 \
"${CHECKLIST_OPTIONS[@]}" \
3>&1 1>&2 2>&3) || return 1
local BACKUP_DIRS=()
for KEY in $SELECTED_KEYS; do
BACKUP_DIRS+=("${BACKUP_PATHS[$KEY]}")
done
# "$1" "${BACKUP_DIRS[*]}"
"$1" "${BACKUP_DIRS[@]}"
}
# === Configure PBS ===
configure_pbs_repository() {
local PBS_REPO_FILE="/usr/local/share/proxmenux/pbs-repo.conf"
local PBS_PASS_FILE="/usr/local/share/proxmenux/pbs-pass.txt"
local PBS_MANUAL_CONFIGS="/usr/local/share/proxmenux/pbs-manual-configs.txt"
[[ ! -f "$PBS_MANUAL_CONFIGS" ]] && touch "$PBS_MANUAL_CONFIGS"
local PBS_CONFIGS=()
local PBS_SOURCES=()
if [[ -f "/etc/pve/storage.cfg" ]]; then
local current_pbs="" server="" datastore="" username=""
while IFS= read -r line; do
if [[ $line =~ ^pbs:\ (.+)$ ]]; then
if [[ -n "$current_pbs" && -n "$server" && -n "$datastore" && -n "$username" ]]; then
PBS_CONFIGS+=("$current_pbs|$username@$server:$datastore")
PBS_SOURCES+=("proxmox|$current_pbs")
fi
current_pbs="${BASH_REMATCH[1]}"
server="" datastore="" username=""
elif [[ -n "$current_pbs" ]]; then
if [[ $line =~ ^[[:space:]]*server[[:space:]]+(.+)$ ]]; then
server="${BASH_REMATCH[1]}"
elif [[ $line =~ ^[[:space:]]*datastore[[:space:]]+(.+)$ ]]; then
datastore="${BASH_REMATCH[1]}"
elif [[ $line =~ ^[[:space:]]*username[[:space:]]+(.+)$ ]]; then
username="${BASH_REMATCH[1]}"
elif [[ $line =~ ^[a-zA-Z]+: ]]; then
if [[ -n "$server" && -n "$datastore" && -n "$username" ]]; then
PBS_CONFIGS+=("$current_pbs|$username@$server:$datastore")
PBS_SOURCES+=("proxmox|$current_pbs")
fi
current_pbs=""
fi
fi
done < "/etc/pve/storage.cfg"
if [[ -n "$current_pbs" && -n "$server" && -n "$datastore" && -n "$username" ]]; then
PBS_CONFIGS+=("$current_pbs|$username@$server:$datastore")
PBS_SOURCES+=("proxmox|$current_pbs")
fi
fi
if [[ -f "$PBS_MANUAL_CONFIGS" ]]; then
while IFS= read -r line; do
if [[ -n "$line" ]]; then
PBS_CONFIGS+=("$line")
local name="${line%%|*}"
PBS_SOURCES+=("manual|$name")
fi
done < "$PBS_MANUAL_CONFIGS"
fi
local menu_options=()
local i=1
for j in "${!PBS_CONFIGS[@]}"; do
local config="${PBS_CONFIGS[$j]}"
local source="${PBS_SOURCES[$j]}"
local name="${config%%|*}"
local repo="${config##*|}"
local source_type="${source%%|*}"
if [[ "$source_type" == "proxmox" ]]; then
menu_options+=("$i" " $name ($repo) [Proxmox]")
else
menu_options+=("$i" " $name ($repo) [Manual]")
fi
((i++))
done
menu_options+=("" "")
menu_options+=("$i" "\Z4\Zb $(translate 'Configure new PBS')\Zn")
local choice
choice=$(dialog --colors --backtitle "ProxMenux" --title "PBS Server Selection" \
--menu "\n$(translate 'Select PBS server for this backup:')" 22 70 12 "${menu_options[@]}" 3>&1 1>&2 2>&3)
local dialog_result=$?
clear
if [[ $dialog_result -ne 0 ]]; then
return 1
fi
if [[ $choice -eq $i ]]; then
configure_pbs_manually
else
local selected_config="${PBS_CONFIGS[$((choice-1))]}"
local selected_source="${PBS_SOURCES[$((choice-1))]}"
local pbs_name="${selected_config%%|*}"
local source_type="${selected_source%%|*}"
PBS_REPO="${selected_config##*|}"
{
mkdir -p "$(dirname "$PBS_REPO_FILE")"
echo "$PBS_REPO" > "$PBS_REPO_FILE"
} >/dev/null 2>&1
local password_found=false
if [[ "$source_type" == "proxmox" ]]; then
local password_file="/etc/pve/priv/storage/${pbs_name}.pw"
if [[ -f "$password_file" ]]; then
{
cp "$password_file" "$PBS_PASS_FILE"
chmod 600 "$PBS_PASS_FILE"
} >/dev/null 2>&1
password_found=true
fi
else
local manual_pass_file="/usr/local/share/proxmenux/pbs-pass-${pbs_name}.txt"
if [[ -f "$manual_pass_file" ]]; then
{
cp "$manual_pass_file" "$PBS_PASS_FILE"
chmod 600 "$PBS_PASS_FILE"
} >/dev/null 2>&1
password_found=true
dialog --backtitle "ProxMenux" --title "PBS Selected" --msgbox "$(translate 'Using manual PBS:') $pbs_name\n\n$(translate 'Repository:') $PBS_REPO\n$(translate 'Password:') $(translate 'Previously saved')" 12 80
fi
fi
if ! $password_found; then
dialog --backtitle "ProxMenux" --title "Password Required" --msgbox "$(translate 'Password not found for:') $pbs_name\n$(translate 'Please enter the password.')" 10 60
get_pbs_password "$pbs_name"
fi
clear
fi
}
configure_pbs_manually() {
local PBS_REPO_FILE="/usr/local/share/proxmenux/pbs-repo.conf"
local PBS_MANUAL_CONFIGS="/usr/local/share/proxmenux/pbs-manual-configs.txt"
local PBS_NAME
PBS_NAME=$(dialog --backtitle "ProxMenux" --title "New PBS Configuration" --inputbox "$(translate 'Enter a name for this PBS configuration:')" 10 60 "PBS-$(date +%m%d)" 3>&1 1>&2 2>&3) || return 1
PBS_USER=$(dialog --backtitle "ProxMenux" --title "New PBS Configuration" --inputbox "$(translate 'Enter PBS username:')" 10 50 "root@pam" 3>&1 1>&2 2>&3) || return 1
PBS_HOST=$(dialog --backtitle "ProxMenux" --title "New PBS Configuration" --inputbox "$(translate 'Enter PBS host or IP:')" 10 50 "" 3>&1 1>&2 2>&3) || return 1
PBS_DATASTORE=$(dialog --backtitle "ProxMenux" --title "New PBS Configuration" --inputbox "$(translate 'Enter PBS datastore name:')" 10 50 "" 3>&1 1>&2 2>&3) || return 1
if [[ -z "$PBS_NAME" || -z "$PBS_USER" || -z "$PBS_HOST" || -z "$PBS_DATASTORE" ]]; then
dialog --backtitle "ProxMenux" --title "Error" --msgbox "$(translate 'All fields are required!')" 8 40
return 1
fi
PBS_REPO="${PBS_USER}@${PBS_HOST}:${PBS_DATASTORE}"
{
mkdir -p "$(dirname "$PBS_REPO_FILE")"
echo "$PBS_REPO" > "$PBS_REPO_FILE"
} >/dev/null 2>&1
local config_line="$PBS_NAME|$PBS_REPO"
if ! grep -Fxq "$config_line" "$PBS_MANUAL_CONFIGS" 2>/dev/null; then
echo "$config_line" >> "$PBS_MANUAL_CONFIGS"
fi
get_pbs_password "$PBS_NAME"
dialog --backtitle "ProxMenux" --title "Success" --msgbox "$(translate 'PBS configuration saved:') $PBS_NAME\n\n$(translate 'Repository:') $PBS_REPO\n\n$(translate 'This configuration will appear in future backups.')" 12 80
}
get_pbs_password() {
local PBS_NAME="$1"
local PBS_PASS_FILE="/usr/local/share/proxmenux/pbs-pass.txt"
local PBS_MANUAL_PASS_FILE="/usr/local/share/proxmenux/pbs-pass-${PBS_NAME}.txt"
while true; do
PBS_REPO_PASS=$(dialog --backtitle "ProxMenux" --title "PBS Password" --insecure --passwordbox "$(translate 'Enter PBS repository password for:') $PBS_NAME" 10 70 "" 3>&1 1>&2 2>&3) || return 1
PBS_REPO_PASS2=$(dialog --backtitle "ProxMenux" --title "PBS Password" --insecure --passwordbox "$(translate 'Confirm PBS repository password:')" 10 60 "" 3>&1 1>&2 2>&3) || return 1
if [[ "$PBS_REPO_PASS" == "$PBS_REPO_PASS2" ]]; then
break
else
dialog --backtitle "ProxMenux" --title "Error" --msgbox "$(translate 'Repository passwords do not match! Please try again.')" 8 50
fi
done
{
echo "$PBS_REPO_PASS" > "$PBS_PASS_FILE"
chmod 600 "$PBS_PASS_FILE"
} >/dev/null 2>&1
{
echo "$PBS_REPO_PASS" > "$PBS_MANUAL_PASS_FILE"
chmod 600 "$PBS_MANUAL_PASS_FILE"
} >/dev/null 2>&1
}
# ===============================
# ========== PBS BACKUP ==========
backup_full_pbs_root() {
local HOSTNAME PBS_REPO PBS_KEY_FILE PBS_PASS_FILE PBS_ENCRYPTION_PASS_FILE ENCRYPT_OPT=""
HOSTNAME=$(hostname)
local PBS_REPO_FILE="/usr/local/share/proxmenux/pbs-repo.conf"
PBS_KEY_FILE="/usr/local/share/proxmenux/pbs-key.conf"
PBS_PASS_FILE="/usr/local/share/proxmenux/pbs-pass.txt"
PBS_ENCRYPTION_PASS_FILE="/usr/local/share/proxmenux/pbs-encryption-pass.txt"
LOGFILE="/tmp/pbs-backup-${HOSTNAME}.log"
configure_pbs_repository
if [[ ! -f "$PBS_REPO_FILE" ]]; then
msg_error "$(translate "Failed to configure PBS connection")"
sleep 3
return 1
fi
PBS_REPO=$(<"$PBS_REPO_FILE")
if [[ ! -f "$PBS_PASS_FILE" ]]; then
msg_error "$(translate "PBS password not configured")"
sleep 3
return 1
fi
dialog --backtitle "ProxMenux" --title "Encryption" --yesno "$(translate 'Do you want to encrypt the backup?')" 8 60
if [[ $? -eq 0 ]]; then
if [[ ! -f "$PBS_ENCRYPTION_PASS_FILE" ]]; then
while true; do
PBS_KEY_PASS=$(dialog --backtitle "ProxMenux" --title "Encryption Password" --insecure --passwordbox "$(translate 'Enter encryption password (different from PBS login):')" 12 70 "" 3>&1 1>&2 2>&3) || return 1
PBS_KEY_PASS2=$(dialog --backtitle "ProxMenux" --title "Encryption Password" --insecure --passwordbox "$(translate 'Confirm encryption password:')" 10 60 "" 3>&1 1>&2 2>&3) || return 1
if [[ "$PBS_KEY_PASS" == "$PBS_KEY_PASS2" ]]; then
break
else
dialog --backtitle "ProxMenux" --title "Error" --msgbox "$(translate 'Passwords do not match! Please try again.')" 8 50
fi
done
{
echo "$PBS_KEY_PASS" > "$PBS_ENCRYPTION_PASS_FILE"
chmod 600 "$PBS_ENCRYPTION_PASS_FILE"
} >/dev/null 2>&1
dialog --backtitle "ProxMenux" --title "Success" --msgbox "$(translate 'Encryption password saved successfully!')" 8 50
fi
if [[ ! -f "$PBS_KEY_FILE" ]]; then
PBS_ENCRYPTION_PASS=$(<"$PBS_ENCRYPTION_PASS_FILE")
dialog --backtitle "ProxMenux" --title "Encryption" --infobox "$(translate 'Creating encryption key...')" 5 50
expect -c "
set timeout 30
spawn proxmox-backup-client key create \"$PBS_KEY_FILE\"
expect {
\"Encryption Key Password:\" {
send \"$PBS_ENCRYPTION_PASS\r\"
exp_continue
}
\"Verify Password:\" {
send \"$PBS_ENCRYPTION_PASS\r\"
exp_continue
}
eof
}
" >/dev/null 2>&1
if [[ ! -f "$PBS_KEY_FILE" ]]; then
dialog --backtitle "ProxMenux" --title "Error" --msgbox "$(translate 'Error creating encryption key.')" 8 40
return 1
fi
dialog --backtitle "ProxMenux" --title "Important" --msgbox "$(translate 'IMPORTANT: Save the key file. Without it you will not be able to restore your backups!')\n\n$(translate 'Key file location:') $PBS_KEY_FILE" 12 70
fi
ENCRYPT_OPT="--keyfile $PBS_KEY_FILE"
else
ENCRYPT_OPT=""
fi
clear
show_proxmenux_logo
echo -e
msg_info2 "$(translate "Starting backup to PBS")"
echo -e
echo -e "${BL}$(translate "PBS Repository:")${WHITE} $PBS_REPO${RESET}"
echo -e "${BL}$(translate "Backup ID:")${WHITE} $HOSTNAME${RESET}"
echo -e "${BL}$(translate "Included:")${WHITE} /boot/efi /etc/pve (all root)${RESET}"
echo -e "${BL}$(translate "Encryption:")${WHITE} $([[ -n "$ENCRYPT_OPT" ]] && echo "Enabled" || echo "Disabled")${RESET}"
echo -e "${BL}$(translate "Log file:")${WHITE} $LOGFILE${RESET}"
echo -e "${BOLD}${NEON_PURPLE_BLUE}-------------------------------${RESET}"
echo ""
PBS_REPO_PASS=$(<"$PBS_PASS_FILE")
if [[ -n "$ENCRYPT_OPT" ]]; then
PBS_ENCRYPTION_PASS=$(<"$PBS_ENCRYPTION_PASS_FILE")
echo "$(translate "Starting encrypted full backup...")"
echo ""
expect -c "
set timeout 3600
log_file $LOGFILE
spawn proxmox-backup-client backup \
--include-dev /boot/efi \
--include-dev /etc/pve \
root-${HOSTNAME}.pxar:/ \
--repository \"$PBS_REPO\" \
$ENCRYPT_OPT \
--backup-type host \
--backup-id \"$HOSTNAME\" \
--backup-time \"$(date +%s)\"
expect {
-re \"Password for .*:\" {
send \"$PBS_REPO_PASS\r\"
exp_continue
}
\"Encryption Key Password:\" {
send \"$PBS_ENCRYPTION_PASS\r\"
exp_continue
}
eof
}
" | tee -a "$LOGFILE"
else
echo "$(translate "Starting unencrypted full backup...")"
echo ""
expect -c "
set timeout 3600
log_file $LOGFILE
spawn proxmox-backup-client backup \
--include-dev /boot/efi \
--include-dev /etc/pve \
root-${HOSTNAME}.pxar:/ \
--repository \"$PBS_REPO\" \
--backup-type host \
--backup-id \"$HOSTNAME\" \
--backup-time \"$(date +%s)\"
expect {
-re \"Password for .*:\" {
send \"$PBS_REPO_PASS\r\"
exp_continue
}
eof
}
" | tee -a "$LOGFILE"
fi
local backup_result=$?
echo -e "${BOLD}${NEON_PURPLE_BLUE}===============================${RESET}\n"
if [[ $backup_result -eq 0 ]]; then
msg_ok "$(translate "Full backup process completed successfully")"
else
msg_error "$(translate "Backup process finished with errors")"
fi
echo ""
msg_success "$(translate "Press Enter to return to the main menu...")"
read -r
}
backup_to_pbs() {
local HOSTNAME TIMESTAMP SNAPSHOT
HOSTNAME=$(hostname)
TIMESTAMP=$(date +%Y-%m-%d_%H-%M)
SNAPSHOT="${HOSTNAME}-${TIMESTAMP}"
local PBS_REPO_FILE="/usr/local/share/proxmenux/pbs-repo.conf"
local PBS_KEY_FILE="/usr/local/share/proxmenux/pbs-key.conf"
local PBS_PASS_FILE="/usr/local/share/proxmenux/pbs-pass.txt"
local PBS_ENCRYPTION_PASS_FILE="/usr/local/share/proxmenux/pbs-encryption-pass.txt"
local PBS_REPO ENCRYPT_OPT USE_ENCRYPTION
local PBS_KEY_PASS PBS_REPO_PASS
configure_pbs_repository
PBS_REPO=$(<"$PBS_REPO_FILE")
USE_ENCRYPTION=false
dialog --backtitle "ProxMenux" --yesno "$(translate 'Do you want to encrypt the backup?')" 8 60
[[ $? -eq 0 ]] && USE_ENCRYPTION=true
if $USE_ENCRYPTION && ! command -v expect >/dev/null 2>&1; then
apt-get update -qq >/dev/null 2>&1
apt-get install -y expect >/dev/null 2>&1
fi
if [[ "$#" -lt 1 ]]; then
clear
show_proxmenux_logo
msg_error "$(translate "No directories specified for backup.")"
sleep 2
return 1
fi
local TOTAL="$#"
local COUNT=1
for dir in "$@"; do
local SAFE_NAME SAFE_ID PXAR_NAME
SAFE_NAME=$(basename "$dir" | tr '.-/' '_')
PXAR_NAME="root-custom-${SAFE_NAME}-${SNAPSHOT}.pxar"
SAFE_ID="custom-${HOSTNAME}-${SAFE_NAME}"
msg_info2 "$(translate "[$COUNT/$TOTAL] Backing up") $dir $(translate "as") $PXAR_NAME"
ENCRYPT_OPT=""
if $USE_ENCRYPTION; then
if [[ -f "$PBS_KEY_FILE" ]]; then
ENCRYPT_OPT="--keyfile $PBS_KEY_FILE"
else
while true; do
PBS_KEY_PASS=$(dialog --backtitle "ProxMenux" --insecure --passwordbox "$(translate 'Enter encryption password (different from PBS login):')" 10 60 "" 3>&1 1>&2 2>&3) || return 1
PBS_KEY_PASS2=$(dialog --backtitle "ProxMenux" --insecure --passwordbox "$(translate 'Confirm encryption password:')" 10 60 "" 3>&1 1>&2 2>&3) || return 1
if [[ "$PBS_KEY_PASS" == "$PBS_KEY_PASS2" ]]; then
break
else
dialog --backtitle "ProxMenux" --msgbox "$(translate 'Passwords do not match! Please try again.')" 8 50
fi
done
{
echo "$PBS_KEY_PASS" > "$PBS_ENCRYPTION_PASS_FILE"
chmod 600 "$PBS_ENCRYPTION_PASS_FILE"
} >/dev/null 2>&1
expect -c "
set timeout 30
spawn proxmox-backup-client key create \"$PBS_KEY_FILE\"
expect {
\"Encryption Key Password:\" {
send \"$PBS_KEY_PASS\r\"
exp_continue
}
\"Verify Password:\" {
send \"$PBS_KEY_PASS\r\"
exp_continue
}
eof
}
" >/dev/null 2>&1
if [[ ! -f "$PBS_KEY_FILE" ]]; then
dialog --backtitle "ProxMenux" --msgbox "$(translate 'Error creating encryption key.')" 8 40
return 1
fi
ENCRYPT_OPT="--keyfile $PBS_KEY_FILE"
dialog --backtitle "ProxMenux" --msgbox "$(translate 'Encryption key generated. Save it in a safe place!')" 10 60
fi
fi
clear
show_proxmenux_logo
echo -e
msg_info2 "$(translate "Starting backup to PBS")"
TOTAL_SIZE=$(du -cb "$@" | awk '/total$/ {print $1}')
TOTAL_SIZE_GB=$(awk "BEGIN {printf \"%.2f\", $TOTAL_SIZE/1024/1024/1024}")
echo -e
echo -e "${BL}$(translate "PBS Repository:")${WHITE} $PBS_REPO${RESET}"
echo -e "${BL}$(translate "Backup ID:")${WHITE} $HOSTNAME${RESET}"
echo -e "${BL}$(translate "Encryption:")${WHITE} $([[ -n "$ENCRYPT_OPT" ]] && echo "Enabled" || echo "Disabled")${RESET}"
echo -e "${BL}$(translate "Included directories:")${WHITE} $*${RESET}"
echo -e "${BL}$(translate "Total size:")${WHITE} ${TOTAL_SIZE_GB} GB${RESET}"
echo -e "${BOLD}${NEON_PURPLE_BLUE}-------------------------------${RESET}"
PBS_REPO_PASS=$(<"$PBS_PASS_FILE")
if $USE_ENCRYPTION && [[ -f "$PBS_ENCRYPTION_PASS_FILE" ]]; then
PBS_KEY_PASS=$(<"$PBS_ENCRYPTION_PASS_FILE")
expect -c "
set timeout 300
spawn proxmox-backup-client backup \"${PXAR_NAME}:$dir\" --repository \"$PBS_REPO\" $ENCRYPT_OPT --backup-type host --backup-id \"$SAFE_ID\" --backup-time \"$(date +%s)\"
expect {
-re \"Password for .*:\" {
send \"$PBS_REPO_PASS\r\"
exp_continue
}
\"Encryption Key Password:\" {
send \"$PBS_KEY_PASS\r\"
exp_continue
}
eof
}
"
else
expect -c "
set timeout 300
spawn proxmox-backup-client backup \"${PXAR_NAME}:$dir\" --repository \"$PBS_REPO\" $ENCRYPT_OPT --backup-type host --backup-id \"$SAFE_ID\" --backup-time \"$(date +%s)\"
expect {
-re \"Password for .*:\" {
send \"$PBS_REPO_PASS\r\"
exp_continue
}
eof
}
"
fi
COUNT=$((COUNT+1))
done
echo -e "${BOLD}${NEON_PURPLE_BLUE}===============================${RESET}\n"
msg_ok "$(translate "Backup process finished.")"
echo ""
msg_success "$(translate "Press Enter to return to the main menu...")"
read -r
}
# ===============================
# ========== BORGBACKUP ==========
backup_with_borg() {
# local SRC="$1"
local BORG_APPIMAGE="/usr/local/share/proxmenux/borg"
local LOGFILE="/tmp/borg-backup.log"
local DEST
local TYPE
local ENCRYPT_OPT=""
local BORG_KEY
if [[ ! -x "$BORG_APPIMAGE" ]]; then
clear
show_proxmenux_logo
msg_info "$(translate "BorgBackup not found. Downloading AppImage...")"
mkdir -p /usr/local/share/proxmenux
wget -qO "$BORG_APPIMAGE" "https://github.com/borgbackup/borg/releases/download/1.2.8/borg-linux64"
chmod +x "$BORG_APPIMAGE"
msg_ok "$(translate "BorgBackup downloaded and ready.")"
fi
TYPE=$(dialog --backtitle "ProxMenux" --menu "$(translate 'Select Borg backup destination:')" 15 60 3 \
"local" "$(translate 'Local directory')" \
"usb" "$(translate 'Internal/External dedicated disk')" \
"remote" "$(translate 'Remote server')" \
3>&1 1>&2 2>&3) || return 1
if [[ "$TYPE" == "local" ]]; then
DEST=$(dialog --backtitle "ProxMenux" --inputbox "$(translate 'Enter local directory for backup:')" 10 60 "/backup/borgbackup" 3>&1 1>&2 2>&3) || return 1
mkdir -p "$DEST"
elif [[ "$TYPE" == "usb" ]]; then
while true; do
BASE_DEST=$(get_external_backup_mount_point)
if [[ -z "$BASE_DEST" ]]; then
dialog --backtitle "ProxMenux" --yesno "$(translate 'No external disk detected or mounted. Would you like to retry?')" 8 60
[[ $? -eq 0 ]] && continue
return 1
fi
DEST="$BASE_DEST/borgbackup"
mkdir -p "$DEST"
DISK_DEV=$(df "$BASE_DEST" | awk 'NR==2{print $1}')
PKNAME=$(lsblk -no PKNAME "$DISK_DEV" 2>/dev/null)
[[ -z "$PKNAME" ]] && PKNAME=$(basename "$DISK_DEV" | sed 's/[0-9]*$//')
if [[ -n "$PKNAME" && -b /dev/$PKNAME ]]; then
DISK_MODEL=$(lsblk -no MODEL "/dev/$PKNAME")
else
DISK_MODEL="(unknown)"
fi
FREE_SPACE=$(df -h "$BASE_DEST" | awk 'NR==2{print $4}')
dialog --backtitle "ProxMenux" \
--title "$(translate "Dedicated Backup Disk")" \
--yesno "\n$(translate "Mount point:") $DEST\n\n\
$(translate "Disk model:") $DISK_MODEL\n\
$(translate "Available space:") $FREE_SPACE\n\n\
$(translate "Use this disk for backup?")" 12 70
if [[ $? -eq 0 ]]; then
break
else
return 1
fi
done
elif [[ "$TYPE" == "remote" ]]; then
REMOTE_USER=$(dialog --backtitle "ProxMenux" --inputbox "$(translate 'Enter SSH user for remote:')" 10 60 "root" 3>&1 1>&2 2>&3) || return 1
REMOTE_HOST=$(dialog --backtitle "ProxMenux" --inputbox "$(translate 'Enter SSH host:')" 10 60 "" 3>&1 1>&2 2>&3) || return 1
REMOTE_PATH=$(dialog --backtitle "ProxMenux" --inputbox "$(translate 'Enter remote path:')" 10 60 "/backup/borgbackup" 3>&1 1>&2 2>&3) || return 1
DEST="ssh://$REMOTE_USER@$REMOTE_HOST:$REMOTE_PATH"
fi
dialog --backtitle "ProxMenux" --yesno "$(translate 'Do you want to encrypt the backup?')" 8 60
if [[ $? -eq 0 ]]; then
BORG_KEY=$(dialog --backtitle "ProxMenux" --inputbox "$(translate 'Enter Borg encryption passphrase (will be saved):')" 10 60 "" 3>&1 1>&2 2>&3) || return 1
ENCRYPT_OPT="--encryption=repokey"
export BORG_PASSPHRASE="$BORG_KEY"
else
ENCRYPT_OPT="--encryption=none"
fi
if [[ "$TYPE" == "local" || "$TYPE" == "usb" ]]; then
if [[ ! -f "$DEST/config" ]]; then
"$BORG_APPIMAGE" init $ENCRYPT_OPT "$DEST"
if [[ $? -ne 0 ]]; then
clear
show_proxmenux_logo
msg_error "$(translate "Failed to initialize Borg repo at") $DEST"
sleep 5
return 1
fi
fi
fi
dialog --backtitle "ProxMenux" --msgbox "$(translate 'Borg backup will start now. This may take a while.')" 8 40
clear
show_proxmenux_logo
msg_info2 "$(translate "Starting backup with BorgBackup...")"
echo -e
TOTAL_SIZE=$(du -cb "$@" | awk '/total$/ {print $1}')
TOTAL_SIZE_GB=$(awk "BEGIN {printf \"%.2f\", $TOTAL_SIZE/1024/1024/1024}")
echo -e "${BL}$(translate "Included directories:")${WHITE} $*${RESET}"
echo -e "${BL}$(translate "Total size:")${WHITE} ${TOTAL_SIZE_GB} GB${RESET}"
# 6. Lanzar el backup y guardar log
# "$BORG_APPIMAGE" create --progress "$DEST"::"root-$(hostname)-$(date +%Y%m%d_%H%M)" $SRC 2>&1 | tee "$LOGFILE"
"$BORG_APPIMAGE" create --progress "$DEST"::"root-$(hostname)-$(date +%Y%m%d_%H%M)" "$@" 2>&1 | tee "$LOGFILE"
echo -e "${BOLD}${NEON_PURPLE_BLUE}===============================${RESET}\n"
msg_ok "$(translate "Backup process finished.")"
echo
msg_success "$(translate "Press Enter to return to the main menu...")"
read -r
}
# ===============================
# ========== LOCAL TAR ==========
backup_to_local_tar() {
# local SRC="$1"
local TYPE
local DEST
local LOGFILE="/tmp/tar-backup.log"
if ! command -v pv &>/dev/null; then
apt-get update -qq && apt-get install -y pv >/dev/null 2>&1
fi
TYPE=$(dialog --backtitle "ProxMenux" --menu "$(translate 'Select backup destination:')" 15 60 2 \
"local" "$(translate 'Local directory')" \
"usb" "$(translate 'Internal/External dedicated disk')" \
3>&1 1>&2 2>&3) || return 1
if [[ "$TYPE" == "local" ]]; then
DEST=$(dialog --backtitle "ProxMenux" --inputbox "$(translate 'Enter directory for backup:')" 10 60 "/backup" 3>&1 1>&2 2>&3) || return 1
mkdir -p "$DEST"
else
while true; do
DEST=$(get_external_backup_mount_point)
if [[ -z "$DEST" ]]; then
dialog --backtitle "ProxMenux" --yesno "No external disk detected or mounted. Would you like to retry?" 8 60
[[ $? -eq 0 ]] && continue
return 1
fi
DISK_DEV=$(df "$DEST" | awk 'NR==2{print $1}')
PKNAME=$(lsblk -no PKNAME "$DISK_DEV" 2>/dev/null)
[[ -z "$PKNAME" ]] && PKNAME=$(basename "$DISK_DEV" | sed 's/[0-9]*$//')
if [[ -n "$PKNAME" && -b /dev/$PKNAME ]]; then
DISK_MODEL=$(lsblk -no MODEL "/dev/$PKNAME")
else
DISK_MODEL="(unknown)"
fi
FREE_SPACE=$(df -h "$DEST" | awk 'NR==2{print $4}')
dialog --backtitle "ProxMenux" \
--title "$(translate "Dedicated Backup Disk")" \
--yesno "\n$(translate "Mount point:") $DEST\n\n\
$(translate "Disk model:") $DISK_MODEL\n\
$(translate "Available space:") $FREE_SPACE\n\n\
$(translate "Use this disk for backup?")" 12 70
if [[ $? -eq 0 ]]; then
mkdir -p "$DEST"
break
else
return 1
fi
done
fi
TAR_INPUT=""
TOTAL_SIZE=0
for src in $SRC; do
sz=$(du -sb "$src" 2>/dev/null | awk '{print $1}')
TOTAL_SIZE=$((TOTAL_SIZE + sz))
TAR_INPUT="$TAR_INPUT $src"
done
local FILENAME="root-$(hostname)-$(date +%Y%m%d_%H%M).tar.gz"
clear
show_proxmenux_logo
msg_info2 "$(translate "Starting backup with tar...")"
echo -e
TOTAL_SIZE=$(du -cb "$@" | awk '/total$/ {print $1}')
TOTAL_SIZE_GB=$(awk "BEGIN {printf \"%.2f\", $TOTAL_SIZE/1024/1024/1024}")
echo -e "${BL}$(translate "Included directories:")${WHITE} $*${RESET}"
echo -e "${BL}$(translate "Total size:")${WHITE} ${TOTAL_SIZE_GB} GB${RESET}"
tar -cf - "$@" 2> >(grep -v "Removing leading \`/'" >&2) \
| pv -s "$TOTAL_SIZE" \
| gzip > "$DEST/$FILENAME"
echo -ne "\033[1A\r\033[K"
echo -e "${BOLD}${NEON_PURPLE_BLUE}===============================${RESET}\n"
msg_ok "$(translate "Backup process finished. Review log above or in /tmp/tar-backup.log")"
echo
msg_success "$(translate "Press Enter to return to the main menu...")"
read -r
}
# ===============================
host_backup_menu

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,433 +0,0 @@
#!/bin/bash
# ==========================================================
# ProxMenu - Mount disk on Proxmox host for backups
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : MIT
# Version : 1.3-dialog
# Last Updated: 13/12/2024
# ==========================================================
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
BASE_DIR="/usr/local/share/proxmenux"
UTILS_FILE="$BASE_DIR/utils.sh"
VENV_PATH="/opt/googletrans-env"
if [[ -f "$UTILS_FILE" ]]; then
source "$UTILS_FILE"
fi
load_language
initialize_cache
mount_disk_host_bk() {
get_disk_info() {
local disk=$1
MODEL=$(lsblk -dn -o MODEL "$disk" | xargs)
SIZE=$(lsblk -dn -o SIZE "$disk" | xargs)
echo "$MODEL" "$SIZE"
}
is_usb_disk() {
local disk=$1
local disk_name=$(basename "$disk")
if readlink -f "/sys/block/$disk_name/device" 2>/dev/null | grep -q "usb"; then
return 0
fi
if udevadm info --query=property --name="$disk" 2>/dev/null | grep -q "ID_BUS=usb"; then
return 0
fi
return 1
}
is_system_disk() {
local disk=$1
local disk_name=$(basename "$disk")
local system_mounts=$(df -h | grep -E '^\S+\s+\S+\s+\S+\s+\S+\s+\S+\s+(/|/boot|/usr|/var|/home)$' | awk '{print $1}')
for mount_dev in $system_mounts; do
local mount_disk=""
if [[ "$mount_dev" =~ ^/dev/mapper/ ]]; then
local vg_name=$(lvs --noheadings -o vg_name "$mount_dev" 2>/dev/null | xargs)
if [[ -n "$vg_name" ]]; then
local pvs_list=$(pvs --noheadings -o pv_name -S vg_name="$vg_name" 2>/dev/null | xargs)
for pv in $pvs_list; do
if [[ -n "$pv" && -e "$pv" ]]; then
mount_disk=$(lsblk -no PKNAME "$pv" 2>/dev/null)
if [[ -n "$mount_disk" && "/dev/$mount_disk" == "$disk" ]]; then
return 0
fi
fi
done
fi
elif [[ "$mount_dev" =~ ^/dev/[hsv]d[a-z][0-9]* || "$mount_dev" =~ ^/dev/nvme[0-9]+n[0-9]+p[0-9]+ ]]; then
mount_disk=$(lsblk -no PKNAME "$mount_dev" 2>/dev/null)
if [[ -n "$mount_disk" && "/dev/$mount_disk" == "$disk" ]]; then
return 0
fi
fi
done
local fs_type=$(lsblk -no FSTYPE "$disk" 2>/dev/null | head -1)
if [[ "$fs_type" == "btrfs" ]]; then
local temp_mount=$(mktemp -d)
if mount -o ro "$disk" "$temp_mount" 2>/dev/null; then
if btrfs subvolume list "$temp_mount" 2>/dev/null | grep -qE '(@|@home|@var|@boot|@root|root)'; then
umount "$temp_mount" 2>/dev/null
rmdir "$temp_mount" 2>/dev/null
return 0
fi
umount "$temp_mount" 2>/dev/null
fi
rmdir "$temp_mount" 2>/dev/null
while read -r part; do
if [[ -n "$part" ]]; then
local part_fs=$(lsblk -no FSTYPE "/dev/$part" 2>/dev/null)
if [[ "$part_fs" == "btrfs" ]]; then
local mount_point=$(lsblk -no MOUNTPOINT "/dev/$part" 2>/dev/null)
if [[ "$mount_point" == "/" || "$mount_point" == "/boot" || "$mount_point" == "/home" || "$mount_point" == "/var" ]]; then
return 0
fi
fi
fi
done < <(lsblk -ln -o NAME "$disk" | tail -n +2)
fi
local disk_uuid=$(blkid -s UUID -o value "$disk" 2>/dev/null)
local part_uuids=()
while read -r part; do
if [[ -n "$part" ]]; then
local uuid=$(blkid -s UUID -o value "/dev/$part" 2>/dev/null)
if [[ -n "$uuid" ]]; then
part_uuids+=("$uuid")
fi
fi
done < <(lsblk -ln -o NAME "$disk" | tail -n +2)
for uuid in "${part_uuids[@]}" "$disk_uuid"; do
if [[ -n "$uuid" ]] && grep -q "UUID=$uuid" /etc/fstab; then
local mount_point=$(grep "UUID=$uuid" /etc/fstab | awk '{print $2}')
if [[ "$mount_point" == "/" || "$mount_point" == "/boot" || "$mount_point" == "/home" || "$mount_point" == "/var" ]]; then
return 0
fi
fi
done
if grep -q "$disk" /etc/fstab; then
local mount_point=$(grep "$disk" /etc/fstab | awk '{print $2}')
if [[ "$mount_point" == "/" || "$mount_point" == "/boot" || "$mount_point" == "/home" || "$mount_point" == "/var" ]]; then
return 0
fi
fi
local disk_count=$(lsblk -dn -e 7,11 -o PATH | wc -l)
if [[ "$disk_count" -eq 1 ]]; then
return 0
fi
return 1
}
USED_DISKS=$(lsblk -n -o PKNAME,TYPE | grep 'lvm' | awk '{print "/dev/" $1}')
MOUNTED_DISKS=$(lsblk -ln -o NAME,MOUNTPOINT | awk '$2!="" {print "/dev/" $1}')
ZFS_DISKS=""
ZFS_RAW=$(zpool list -v -H 2>/dev/null | awk '{print $1}' | grep -v '^NAME$' | grep -v '^-' | grep -v '^mirror')
for entry in $ZFS_RAW; do
path=""
if [[ "$entry" == wwn-* || "$entry" == ata-* ]]; then
if [ -e "/dev/disk/by-id/$entry" ]; then
path=$(readlink -f "/dev/disk/by-id/$entry")
fi
elif [[ "$entry" == /dev/* ]]; then
path="$entry"
fi
if [ -n "$path" ]; then
base_disk=$(lsblk -no PKNAME "$path" 2>/dev/null)
if [ -n "$base_disk" ]; then
ZFS_DISKS+="/dev/$base_disk"$'\n'
fi
fi
done
ZFS_DISKS=$(echo "$ZFS_DISKS" | sort -u)
LVM_DEVICES=$(
pvs --noheadings -o pv_name 2> >(grep -v 'File descriptor .* leaked') |
while read -r dev; do
[[ -n "$dev" && -e "$dev" ]] && readlink -f "$dev"
done | sort -u
)
FREE_DISKS=()
while read -r DISK; do
[[ "$DISK" =~ /dev/zd ]] && continue
INFO=($(get_disk_info "$DISK"))
MODEL="${INFO[@]::${#INFO[@]}-1}"
SIZE="${INFO[-1]}"
LABEL=""
SHOW_DISK=true
IS_MOUNTED=false
IS_RAID=false
IS_ZFS=false
IS_LVM=false
IS_SYSTEM=false
IS_USB=false
if is_system_disk "$DISK"; then
IS_SYSTEM=true
fi
if is_usb_disk "$DISK"; then
IS_USB=true
fi
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=""
if [[ -n "$DISK" && -e "$DISK" ]]; then
REAL_PATH=$(readlink -f "$DISK")
fi
if [[ -n "$REAL_PATH" ]] && echo "$LVM_DEVICES" | grep -qFx "$REAL_PATH"; then
IS_MOUNTED=true
fi
USED_BY=""
REAL_PATH=""
if [[ -n "$DISK" && -e "$DISK" ]]; then
REAL_PATH=$(readlink -f "$DISK")
fi
CONFIG_DATA=$(grep -vE '^\s*#' /etc/pve/qemu-server/*.conf /etc/pve/lxc/*.conf 2>/dev/null)
if grep -Fq "$REAL_PATH" <<< "$CONFIG_DATA"; then
USED_BY="$(translate "In use")"
else
for SYMLINK in /dev/disk/by-id/*; do
[[ -e "$SYMLINK" ]] || continue
if [[ "$(readlink -f "$SYMLINK")" == "$REAL_PATH" ]]; then
if grep -Fq "$SYMLINK" <<< "$CONFIG_DATA"; then
USED_BY="$(translate "In use")"
break
fi
fi
done
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
if $IS_MOUNTED; then SHOW_DISK=false; fi
if $IS_SYSTEM; 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"
if $IS_USB; then
LABEL+=" USB"
else
LABEL+=" $(translate "Internal")"
fi
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
dialog --title "$(translate "Error")" --msgbox "$(translate "No available disks found on the host.")" 8 60
exit 1
fi
# Building the array for dialog (format: tag item on/off tag item on/off...)
DLG_LIST=()
for ((i=0; i<${#FREE_DISKS[@]}; i+=3)); do
DLG_LIST+=("${FREE_DISKS[i]}" "${FREE_DISKS[i+1]}" "${FREE_DISKS[i+2]}")
done
SELECTED=$(dialog --clear --backtitle "ProxMenux" --title "$(translate "Select Disk")" \
--radiolist "\n$(translate "Select the disk you want to mount on the host:")" 20 90 10 \
"${DLG_LIST[@]}" 2>&1 >/dev/tty)
if [ -z "$SELECTED" ]; then
dialog --title "$(translate "Error")" --msgbox "$(translate "No disk was selected.")" 8 50
exit 1
fi
# ------------------- Partitions and formatting ------------------------
PARTITION=$(lsblk -rno NAME "$SELECTED" | awk -v disk="$(basename "$SELECTED")" '$1 != disk {print $1; exit}')
SKIP_FORMAT=false
DEFAULT_MOUNT="/mnt/backup"
if [ -n "$PARTITION" ]; then
PARTITION="/dev/$PARTITION"
CURRENT_FS=$(lsblk -no FSTYPE "$PARTITION" | xargs)
if [[ "$CURRENT_FS" == "ext4" || "$CURRENT_FS" == "xfs" || "$CURRENT_FS" == "btrfs" ]]; then
SKIP_FORMAT=true
else
dialog --title "$(translate "Unsupported Filesystem")" --yesno \
"$(translate "The partition") $PARTITION $(translate "has an unsupported filesystem ($CURRENT_FS).\nDo you want to format it?")" 10 70
if [ $? -ne 0 ]; then exit 0; fi
fi
else
CURRENT_FS=$(lsblk -no FSTYPE "$SELECTED" | xargs)
if [[ "$CURRENT_FS" == "ext4" || "$CURRENT_FS" == "xfs" || "$CURRENT_FS" == "btrfs" ]]; then
SKIP_FORMAT=true
PARTITION="$SELECTED"
else
dialog --title "$(translate "No Valid Partitions")" --yesno \
"$(translate "The disk has no partitions and no valid filesystem. Do you want to create a new partition and format it?")" 10 70
if [ $? -ne 0 ]; then exit 0; fi
echo -e "$(translate "Creating partition table and partition...")"
parted -s "$SELECTED" mklabel gpt
parted -s "$SELECTED" mkpart primary 0% 100%
sleep 2
partprobe "$SELECTED"
sleep 2
PARTITION=$(lsblk -rno NAME "$SELECTED" | awk -v disk="$(basename "$SELECTED")" '$1 != disk {print $1; exit}')
if [ -n "$PARTITION" ]; then
PARTITION="/dev/$PARTITION"
else
dialog --title "$(translate "Partition Error")" --msgbox \
"$(translate "Failed to create partition on disk") $SELECTED." 8 70
exit 1
fi
fi
fi
if [ "$SKIP_FORMAT" != true ]; then
FORMAT_TYPE=$(dialog --title "$(translate "Select Format Type")" --menu \
"$(translate "Select the filesystem type for") $PARTITION:" 15 60 5 \
"ext4" "$(translate "Extended Filesystem 4 (recommended)")" \
"xfs" "XFS" \
"btrfs" "Btrfs" 2>&1 >/dev/tty)
if [ -z "$FORMAT_TYPE" ]; then
dialog --title "$(translate "Format Cancelled")" --msgbox \
"$(translate "Format operation cancelled. The disk will not be added.")" 8 60
exit 0
fi
dialog --title "$(translate "WARNING")" --yesno \
"$(translate "WARNING: This operation will FORMAT the disk") $PARTITION $(translate "with") $FORMAT_TYPE.\n\n$(translate "ALL DATA ON THIS DISK WILL BE PERMANENTLY LOST!")\n\n$(translate "Are you sure you want to continue")" 15 70
if [ $? -ne 0 ]; then exit 0; fi
echo -e "$(translate "Formatting partition") $PARTITION $(translate "with") $FORMAT_TYPE..."
case "$FORMAT_TYPE" in
"ext4") mkfs.ext4 -F "$PARTITION" ;;
"xfs") mkfs.xfs -f "$PARTITION" ;;
"btrfs") mkfs.btrfs -f "$PARTITION" ;;
esac
if [ $? -ne 0 ]; then
cleanup
dialog --title "$(translate "Format Failed")" --msgbox \
"$(translate "Failed to format partition") $PARTITION $(translate "with") $FORMAT_TYPE." 12 70
exit 1
else
partprobe "$SELECTED"
sleep 2
fi
fi
# ------------------- Mount point and permissions (modular, non-blocking) -------------------
MOUNT_POINT=$(dialog --clear --title "$(translate "Mount Point")" \
--inputbox "$(translate "Enter the mount point for the disk (e.g., /mnt/backup):")" \
10 60 "$DEFAULT_MOUNT" 2>&1 >/dev/tty)
if [ -z "$MOUNT_POINT" ]; then
>&2 echo "$(translate "No mount point was specified.")"
return 1
fi
mkdir -p "$MOUNT_POINT"
UUID=$(blkid -s UUID -o value "$PARTITION")
FS_TYPE=$(lsblk -no FSTYPE "$PARTITION" | xargs)
FSTAB_ENTRY="UUID=$UUID $MOUNT_POINT $FS_TYPE defaults 0 0"
if grep -q "UUID=$UUID" /etc/fstab; then
sed -i "s|^.*UUID=$UUID.*|$FSTAB_ENTRY|" /etc/fstab
else
echo "$FSTAB_ENTRY" >> /etc/fstab
fi
mount "$MOUNT_POINT" 2> >(grep -v "systemd still uses")
if [ $? -eq 0 ]; then
if ! getent group sharedfiles >/dev/null; then
groupadd sharedfiles
fi
chown root:sharedfiles "$MOUNT_POINT"
chmod 2775 "$MOUNT_POINT"
echo "$MOUNT_POINT" > /usr/local/share/proxmenux/last_backup_mount.txt
MOUNT_POINT=$(echo "$MOUNT_POINT" | head -n1 | tr -d '\r\n\t ')
echo "$MOUNT_POINT"
else
>&2 echo "$(translate "Failed to mount the disk at") $MOUNT_POINT"
return 1
fi
}

View File

@ -165,8 +165,8 @@ install_igpu_in_container() {
}
select_container
show_proxmenux_logo
configure_lxc_for_igpu
install_igpu_in_container

View File

@ -279,7 +279,7 @@ apt_upgrade() {
progress=$((i * 10))
tput cup $((row + 3)) 9
printf "[%-50s] %3d%%" "$(printf "#%.0s" $(seq 1 $((progress/2))))" "$progress"
sleep 0.2
done
fi
done
@ -292,7 +292,51 @@ apt_upgrade() {
fi
msg_info "$(translate "Updating PVE application manager, patience...")"
total_steps=$(pveam update 2>&1 | grep -E "^(Downloading|Importing)" | wc -l)
[ $total_steps -eq 0 ] && total_steps=1
tput sc
(
pveam update 2>&1 | while IFS= read -r line; do
if [[ $line == "Downloading"* ]] || [[ $line == "Importing"* ]]; then
file_name=$(echo "$line" | sed -E 's/.* (Downloading|Importing) ([^ ]+).*/\2/')
[ -z "$file_name" ] && file_name="$(translate "Unknown")"
tput rc
tput ed
row=$(( $(tput lines) - 6 ))
tput cup $row 0; echo "$(translate "Updating PVE application manager...")"
tput cup $((row + 1)) 0; echo "──────────────────────────────────────────────"
tput cup $((row + 2)) 0; echo "Downloading: $file_name"
tput cup $((row + 3)) 0; echo "Progress: [ ] 0%"
tput cup $((row + 4)) 0; echo "──────────────────────────────────────────────"
for i in $(seq 1 10); do
progress=$((i * 10))
tput cup $((row + 3)) 9
printf "[%-50s] %3d%%" "$(printf "#%.0s" $(seq 1 $((progress/2))))" "$progress"
sleep 0.2
done
fi
done
)
if [ $? -eq 0 ]; then
tput rc
tput ed
msg_ok "$(translate "PVE application manager updated")"
fi
tput cnorm
# Install additional Proxmox packages
@ -547,45 +591,9 @@ fs.aio-max-nr = 1048576"
skip_apt_languages_() {
msg_info2 "$(translate "Configuring APT to skip downloading additional languages")"
local config_file="/etc/apt/apt.conf.d/99-disable-translations"
local config_content="Acquire::Languages \"none\";"
msg_info "$(translate "Setting APT language configuration...")"
if [ -f "$config_file" ] && grep -q "$config_content" "$config_file"; then
msg_ok "$(translate "APT language configuration updated")"
else
echo -e "$config_content\n" > "$config_file"
msg_ok "$(translate "APT language configuration updated")"
fi
msg_success "$(translate "APT configured to skip downloading additional languages")"
}
skip_apt_languages() {
msg_info2 "$(translate "Configuring APT to skip downloading additional languages")"
local default_locale
if [ -f /etc/default/locale ]; then
default_locale=$(grep '^LANG=' /etc/default/locale | cut -d= -f2)
elif [ -f /etc/environment ]; then
default_locale=$(grep '^LANG=' /etc/environment | cut -d= -f2)
fi
default_locale="${default_locale:-en_US.UTF-8}"
if ! locale -a | grep -qi "^${default_locale//-/_}$"; then
msg_info "$(translate "Generating missing locale:") $default_locale"
echo "$default_locale UTF-8" >> /etc/locale.gen
locale-gen "$default_locale"
msg_ok "$(translate "Locale generated")"
fi
local config_file="/etc/apt/apt.conf.d/99-disable-translations"
local config_content="Acquire::Languages \"none\";"
@ -604,7 +612,6 @@ skip_apt_languages() {
# ==========================================================
@ -735,6 +742,7 @@ packages_list=(
progress=$((i * 10))
tput cup $((row + 3)) 9
printf "[%-50s] %3d%%" "$(printf "#%.0s" $(seq 1 $((progress/2))))" "$progress"
sleep 0.2
done
/usr/bin/env DEBIAN_FRONTEND=noninteractive apt-get -y -o Dpkg::Options::='--force-confdef' install "$package" > /dev/null 2>&1
@ -1710,7 +1718,7 @@ configure_ksmtuned() {
enable_vfio_iommu_() {
enable_vfio_iommu() {
msg_info2 "$(translate "Enabling IOMMU and configuring VFIO for PCI passthrough...")"
NECESSARY_REBOOT=1
@ -1810,92 +1818,6 @@ enable_vfio_iommu_() {
enable_vfio_iommu() {
msg_info2 "$(translate "Enabling IOMMU and configuring VFIO for PCI passthrough...")"
NECESSARY_REBOOT=1
# Detect if system uses ZFS/systemd-boot (Proxmox)
local uses_zfs=false
local cmdline_file="/etc/kernel/cmdline"
if [[ -f "$cmdline_file" ]] && grep -qE 'root=ZFS=|root=ZFS/' "$cmdline_file"; then
uses_zfs=true
fi
# Enable IOMMU
local cpu_info=$(cat /proc/cpuinfo)
local grub_file="/etc/default/grub"
local iommu_param=""
local additional_params="pcie_acs_override=downstream,multifunction nofb nomodeset video=vesafb:off,efifb:off"
if [[ "$cpu_info" == *"GenuineIntel"* ]]; then
msg_info "$(translate "Detected Intel CPU")"
iommu_param="intel_iommu=on"
elif [[ "$cpu_info" == *"AuthenticAMD"* ]]; then
msg_info "$(translate "Detected AMD CPU")"
iommu_param="amd_iommu=on"
else
msg_warning "$(translate "Unknown CPU type. IOMMU might not be properly enabled.")"
return 1
fi
if [[ "$uses_zfs" == true ]]; then
# --- SYSTEMD-BOOT: /etc/kernel/cmdline ---
if grep -q "$iommu_param" "$cmdline_file"; then
msg_ok "$(translate "IOMMU already configured in /etc/kernel/cmdline")"
else
cp "$cmdline_file" "${cmdline_file}.bak"
# sed -i "s|\"$| $iommu_param iommu=pt|" "$cmdline_file"
sed -i "s|\s*$| $iommu_param iommu=pt|" "$cmdline_file"
msg_ok "$(translate "IOMMU parameters added to /etc/kernel/cmdline")"
fi
else
# --- GRUB ---
if grep -q "$iommu_param" "$grub_file"; then
msg_ok "$(translate "IOMMU already enabled in GRUB configuration")"
else
cp "$grub_file" "${grub_file}.bak"
sed -i "/GRUB_CMDLINE_LINUX_DEFAULT=/ s|\"$| $iommu_param iommu=pt\"|" "$grub_file"
msg_ok "$(translate "IOMMU enabled in GRUB configuration")"
fi
fi
# Configure VFIO modules (avoid duplicates)
local modules_file="/etc/modules"
msg_info "$(translate "Checking VFIO modules...")"
local vfio_modules=("vfio" "vfio_iommu_type1" "vfio_pci" "vfio_virqfd")
for module in "${vfio_modules[@]}"; do
grep -q "^$module" "$modules_file" || echo "$module" >> "$modules_file"
done
msg_ok "$(translate "VFIO modules configured.")"
# Blacklist conflicting drivers (avoid duplicates)
local blacklist_file="/etc/modprobe.d/blacklist.conf"
msg_info "$(translate "Checking conflicting drivers blacklist...")"
touch "$blacklist_file"
local blacklist_drivers=("nouveau" "lbm-nouveau" "amdgpu" "radeon" "nvidia" "nvidiafb")
for driver in "${blacklist_drivers[@]}"; do
grep -q "^blacklist $driver" "$blacklist_file" || echo "blacklist $driver" >> "$blacklist_file"
done
if ! grep -q "options nouveau modeset=0" "$blacklist_file"; then
echo "options nouveau modeset=0" >> "$blacklist_file"
fi
msg_ok "$(translate "Conflicting drivers blacklisted successfully.")"
# Propagate the settings
msg_info "$(translate "Updating initramfs, GRUB, and EFI boot, patience...")"
update-initramfs -u -k all > /dev/null 2>&1
if [[ "$uses_zfs" == true ]]; then
proxmox-boot-tool refresh > /dev/null 2>&1
else
update-grub > /dev/null 2>&1
fi
msg_success "$(translate "IOMMU and VFIO setup completed")"
}
# ==========================================================
@ -2038,53 +1960,8 @@ EOF
remove_subscription_banner() {
msg_info2 "$(translate "Removing Proxmox subscription nag banner...")"
local JS_FILE="/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js"
local GZ_FILE="/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js.gz"
local APT_HOOK="/etc/apt/apt.conf.d/no-nag-script"
if [[ ! -f "$APT_HOOK" ]]; then
cat <<'EOF' > "$APT_HOOK"
DPkg::Post-Invoke { "dpkg -V proxmox-widget-toolkit | grep -q '/proxmoxlib\.js$'; if [ $? -eq 1 ]; then { echo 'Removing subscription nag from UI...'; sed -i '/.*data\.status.*{/{s/\!//;s/active/NoMoreNagging/;s/Active/NoMoreNagging/}' /usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js; rm -f /usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js.gz; }; fi"; };
EOF
msg_ok "$(translate "APT hook for nag removal created")"
else
msg_info "$(translate "APT hook for nag removal already exists")"
fi
if [[ -f "$JS_FILE" ]]; then
sed -i '/.*data\.status.*{/{s/\!//;s/active/NoMoreNagging/;s/Active/NoMoreNagging/}' "$JS_FILE"
if [[ -f "$GZ_FILE" ]]; then
rm -f "$GZ_FILE"
msg_info "$(translate "Deleted proxmoxlib.js.gz to force browser refresh")"
fi
touch "$JS_FILE"
msg_ok "$(translate "Patched proxmoxlib.js (banner should disappear after browser refresh)")"
else
msg_error "$(translate "proxmoxlib.js not found. Cannot patch subscription banner.")"
return 1
fi
apt --reinstall install proxmox-widget-toolkit -y > /dev/null 2>&1
msg_success "$(translate "Subscription nag banner removed.")"
}
remove_subscription_banner_() {
msg_info2 "$(translate "Checking Proxmox subscription banner and nag status...")"
local proxmox_js="/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js"
@ -2599,24 +2476,6 @@ EOF
update_pve_appliance_manager() {
msg_info "$(translate "Updating PVE application manager...")"
if pveam update > /dev/null 2>&1; then
msg_ok "$(translate "PVE application manager updated")"
else
msg_warn "$(translate "No updates or failed to fetch templates")"
fi
}
# ==========================================================
# ==========================================================
# Auxiliary help functions
# ==========================================================
@ -2749,17 +2608,20 @@ lvm_repair_check() {
# Main menu function
main_menu() {
local HEADER
if [[ "$LANGUAGE" == "es" ]]; then
HEADER="Seleccione las opciones a configurar:\n\n Descripción | Categoría"
else
HEADER="$(translate "Choose options to configure:")\n\n Description | Category"
fi
local HEADER=$(printf " %-56s %10s" "$(translate "Description")" "Category")
declare -A category_order=(
["Basic Settings"]=1 ["System"]=2 ["Hardware"]=3 ["Virtualization"]=4
["Network"]=5 ["Storage"]=6 ["Security"]=7 ["Customization"]=8
["Monitoring"]=9 ["Performance"]=10 ["Optional"]=11
["Basic Settings"]=1
["System"]=2
["Hardware"]=3
["Virtualization"]=4
["Network"]=5
["Storage"]=6
["Security"]=7
["Customization"]=8
["Monitoring"]=9
["Performance"]=10
["Optional"]=11
)
local options=(
@ -2795,7 +2657,6 @@ main_menu() {
"Monitoring|Install OVH Real Time Monitoring|OVHRTM"
"Performance|Use pigz for faster gzip compression|PIGZ"
"Optional|Install and configure Fastfetch|FASTFETCH"
"Optional|Update Proxmox VE Appliance Manager|PVEAM"
"Optional|Add latest Ceph support|CEPH"
"Optional|Add Proxmox testing repository|REPOTEST"
"Optional|Enable High Availability services|ENABLE_HA"
@ -2808,187 +2669,168 @@ main_menu() {
done | sort -n | cut -d'|' -f2-))
unset IFS
local max_desc_length=0
local temp_descriptions=()
for option in "${sorted_options[@]}"; do
IFS='|' read -r category description function_name <<< "$option"
local desc_translated="$(translate "$description")"
temp_descriptions+=("$desc_translated")
local desc_length=${#desc_translated}
if [ $desc_length -gt $max_desc_length ]; then
max_desc_length=$desc_length
fi
done
if [ $max_desc_length -gt 50 ]; then
max_desc_length=50
fi
local total_width=65
local max_desc_width=50
local category_width=15
local category_position=$((total_width - category_width))
local checklist_items=()
local menu_items=()
local i=1
local desc_index=0
local previous_category=""
for option in "${sorted_options[@]}"; do
IFS='|' read -r category description function_name <<< "$option"
translated_description="$(translate "$description")"
local max_cut=$((category_position - 3))
[[ "$max_cut" -lt 10 ]] && max_cut=10
if [[ ${#translated_description} -gt $max_cut ]]; then
translated_description="${translated_description:0:$((max_cut - 3))}..."
fi
if [[ "$category" != "$previous_category" && "$category" == "Optional" && -n "$previous_category" ]]; then
checklist_items+=("" "==============================================================" "")
menu_items+=("" "================================================================" "")
fi
local desc_translated="${temp_descriptions[$desc_index]}"
desc_index=$((desc_index + 1))
if [ ${#desc_translated} -gt $max_desc_length ]; then
desc_translated="${desc_translated:0:$((max_desc_length-3))}..."
fi
local spaces_needed=$((max_desc_length - ${#desc_translated}))
local padding=""
for ((j=0; j<spaces_needed; j++)); do
padding+=" "
local line="$translated_description"
local spaces_needed=$((category_position - ${#translated_description}))
for ((j = 0; j < spaces_needed; j++)); do
line+=" "
done
local line="${desc_translated}${padding} | ${category}"
line+="$category"
checklist_items+=("$i" "$line" "off")
menu_items+=("$i" "$line" "OFF")
i=$((i + 1))
previous_category="$category"
done
exec 3>&1
selected_indices=$(dialog --clear \
--backtitle "ProxMenux" \
--title "$(translate "Post-Installation Options")" \
--checklist "$HEADER" 22 80 15 \
"${checklist_items[@]}" \
2>&1 1>&3)
cleanup
local dialog_exit=$?
exec 3>&-
local selected_indices=$(whiptail --title "$(translate "ProxMenux Custom Script for Post-Installation")" \
--checklist --separate-output \
"\n$HEADER\n\n$(translate "Choose options to configure:")\n$(translate "Use [SPACE] to select/deselect and [ENTER] to confirm:")" \
20 82 12 \
"${menu_items[@]}" \
3>&1 1>&2 2>&3)
if [[ $dialog_exit -ne 0 || -z "$selected_indices" ]]; then
if [ $? -ne 0 ]; then
echo "User cancelled. Exiting."
exit 0
fi
IFS=$'\n' read -d '' -r -a selected_options <<< "$selected_indices"
declare -A selected_functions
if [ -n "$selected_indices" ]; then
show_proxmenux_logo
msg_title "$SCRIPT_TITLE"
for index in "${selected_options[@]}"; do
option=${sorted_options[$((index - 1))]}
IFS='|' read -r category description function_name <<< "$option"
selected_functions[$function_name]=1
declare -A selected_functions
read -ra indices_array <<< "$selected_indices"
[[ "$function_name" == "FASTFETCH" ]] && selected_functions[MOTD]=0
done
for index in "${indices_array[@]}"; do
if [[ -z "$index" ]] || ! [[ "$index" =~ ^[0-9]+$ ]]; then
continue
fi
local item_index=$(( (index - 1) * 3 + 1 ))
if [[ $item_index -lt ${#checklist_items[@]} ]]; then
local selected_line="${checklist_items[$item_index]}"
if [[ "$selected_line" =~ ^.*(\-\-\-|===+).*$ ]]; then
return 1
fi
fi
option=${sorted_options[$((index - 1))]}
IFS='|' read -r _ description function_name <<< "$option"
selected_functions[$function_name]=1
[[ "$function_name" == "FASTFETCH" ]] && selected_functions[MOTD]=0
done
for index in "${!sorted_options[@]}"; do
option=${sorted_options[$index]}
IFS='|' read -r category description function_name <<< "$option"
if [[ ${selected_functions[$function_name]} -eq 1 ]]; then
case $function_name in
APTUPGRADE) apt_upgrade ;;
TIMESYNC) configure_time_sync ;;
NOAPTLANG) skip_apt_languages ;;
UTILS) install_system_utils ;;
JOURNALD) optimize_journald ;;
LOGROTATE) optimize_logrotate ;;
LIMITS) increase_system_limits ;;
ENTROPY) configure_entropy ;;
MEMORYFIXES) optimize_memory_settings ;;
KEXEC) enable_kexec ;;
KERNELPANIC) configure_kernel_panic ;;
KERNELHEADERS) install_kernel_headers ;;
AMDFIXES) apply_amd_fixes ;;
GUESTAGENT) install_guest_agent ;;
VFIO_IOMMU) enable_vfio_iommu ;;
KSMTUNED) configure_ksmtuned ;;
APTIPV4) force_apt_ipv4 ;;
NET) apply_network_optimizations ;;
OPENVSWITCH) install_openvswitch ;;
TCPFASTOPEN) enable_tcp_fast_open ;;
ZFSARC) optimize_zfs_arc ;;
ZFSAUTOSNAPSHOT) install_zfs_auto_snapshot ;;
VZDUMP) optimize_vzdump ;;
DISABLERPC) disable_rpc ;;
FAIL2BAN) install_fail2ban ;;
LYNIS) install_lynis ;;
BASHRC) customize_bashrc ;;
MOTD) setup_motd ;;
NOSUBBANNER) remove_subscription_banner ;;
OVHRTM) install_ovh_rtm ;;
PIGZ) configure_pigz ;;
FASTFETCH) configure_fastfetch ;;
CEPH) install_ceph ;;
REPOTEST) add_repo_test ;;
ENABLE_HA) enable_ha ;;
FIGURINE) configure_figurine ;;
*) echo "Option $function_name not implemented yet" ;;
esac
fi
done
clear
show_proxmenux_logo
msg_title "$SCRIPT_TITLE"
for option in "${sorted_options[@]}"; do
IFS='|' read -r _ description function_name <<< "$option"
if [[ ${selected_functions[$function_name]} -eq 1 ]]; then
case $function_name in
APTUPGRADE) apt_upgrade ;;
TIMESYNC) configure_time_sync ;;
NOAPTLANG) skip_apt_languages ;;
UTILS) install_system_utils ;;
JOURNALD) optimize_journald ;;
LOGROTATE) optimize_logrotate ;;
LIMITS) increase_system_limits ;;
ENTROPY) configure_entropy ;;
MEMORYFIXES) optimize_memory_settings ;;
KEXEC) enable_kexec ;;
KERNELPANIC) configure_kernel_panic ;;
KERNELHEADERS) install_kernel_headers ;;
AMDFIXES) apply_amd_fixes ;;
GUESTAGENT) install_guest_agent ;;
VFIO_IOMMU) enable_vfio_iommu ;;
KSMTUNED) configure_ksmtuned ;;
APTIPV4) force_apt_ipv4 ;;
NET) apply_network_optimizations ;;
OPENVSWITCH) install_openvswitch ;;
TCPFASTOPEN) enable_tcp_fast_open ;;
ZFSARC) optimize_zfs_arc ;;
ZFSAUTOSNAPSHOT) install_zfs_auto_snapshot ;;
VZDUMP) optimize_vzdump ;;
DISABLERPC) disable_rpc ;;
FAIL2BAN) install_fail2ban ;;
LYNIS) install_lynis ;;
BASHRC) customize_bashrc ;;
MOTD) setup_motd ;;
NOSUBBANNER) remove_subscription_banner ;;
OVHRTM) install_ovh_rtm ;;
PIGZ) configure_pigz ;;
FASTFETCH) configure_fastfetch ;;
CEPH) install_ceph ;;
REPOTEST) add_repo_test ;;
ENABLE_HA) enable_ha ;;
FIGURINE) configure_figurine ;;
PVEAM) update_pve_appliance_manager ;;
*) echo "Option $function_name not implemented yet" ;;
esac
fi
done
if [[ "$NECESSARY_REBOOT" -eq 1 ]]; then
whiptail --title "Reboot Required" \
--yesno "$(translate "Some changes require a reboot to take effect. Do you want to restart now?")" 10 60
if [[ $? -eq 0 ]]; then
msg_info "$(translate "Removing no longer required packages and purging old cached updates...")"
apt-get -y autoremove >/dev/null 2>&1
apt-get -y autoclean >/dev/null 2>&1
msg_ok "$(translate "Cleanup finished")"
msg_success "$(translate "Press Enter to continue...")"
read -r
msg_warn "$(translate "Rebooting the system...")"
reboot
if [ "$NECESSARY_REBOOT" -eq 1 ]; then
whiptail --title "Reboot Required" --yesno "$(translate "Some changes require a reboot to take effect. Do you want to restart now?")" 10 60
if [ $? -eq 0 ]; then
msg_info "$(translate "Removing no longer required packages and purging old cached updates...")"
/usr/bin/env DEBIAN_FRONTEND=noninteractive apt-get -y -o Dpkg::Options::='--force-confdef' autoremove >/dev/null 2>&1
/usr/bin/env DEBIAN_FRONTEND=noninteractive apt-get -y -o Dpkg::Options::='--force-confdef' autoclean >/dev/null 2>&1
msg_ok "$(translate "Cleanup finished")"
msg_success "$(translate "Press Enter to continue...")"
read -r
msg_warn "$(translate "Rebooting the system...")"
reboot
else
msg_info "$(translate "Removing no longer required packages and purging old cached updates...")"
apt-get -y autoremove >/dev/null 2>&1
apt-get -y autoclean >/dev/null 2>&1
msg_ok "$(translate "Cleanup finished")"
msg_info2 "$(translate "You can reboot later manually.")"
msg_success "$(translate "Press Enter to continue...")"
read -r
exit 0
msg_info "$(translate "Removing no longer required packages and purging old cached updates...")"
/usr/bin/env DEBIAN_FRONTEND=noninteractive apt-get -y -o Dpkg::Options::='--force-confdef' autoremove >/dev/null 2>&1
/usr/bin/env DEBIAN_FRONTEND=noninteractive apt-get -y -o Dpkg::Options::='--force-confdef' autoclean >/dev/null 2>&1
msg_ok "$(translate "Cleanup finished")"
msg_info2 "$(translate "You can reboot later manually.")"
msg_success "$(translate "Press Enter to continue...")"
read -r
exit 0
fi
fi
msg_success "$(translate "All changes applied. No reboot required.")"
msg_success "$(translate "Press Enter to return to menu...")"
read -r
clear
fi
msg_success "$(translate "All changes applied. No reboot required.")"
msg_success "$(translate "Press Enter to return to menu...")"
read -r
else
exit 0
fi
}
if [[ "$LANGUAGE" != "en" ]]; then
show_proxmenux_logo
msg_lang "$(translate "Generating automatic translations...")"
fi
main_menu

View File

@ -6,15 +6,15 @@
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : MIT (https://raw.githubusercontent.com/MacRimi/ProxMenux/main/LICENSE)
# Version : 1.1
# Last Updated: 29/05/2025
# Version : 1.0
# Last Updated: 28/01/2025
# ==========================================================
# Description:
# This script automates the process of importing disk images into Proxmox VE virtual machines (VMs),
# making it easy to attach pre-existing disk files without manual configuration.
#
# Before running the script, ensure that disk images are available in /var/lib/vz/template/images/.
# The script scans this directory for compatible formats (.img, .qcow2, .vmdk, .raw) and lists the available files.
# The script scans this directory for compatible formats (.img, .qcow2, .vmdk) and lists the available files.
#
# Using an interactive menu, you can:
# - Select a VM to attach the imported disk.
@ -32,163 +32,211 @@ BASE_DIR="/usr/local/share/proxmenux"
UTILS_FILE="$BASE_DIR/utils.sh"
VENV_PATH="/opt/googletrans-env"
[[ -f "$UTILS_FILE" ]] && source "$UTILS_FILE"
if [[ -f "$UTILS_FILE" ]]; then
source "$UTILS_FILE"
fi
load_language
initialize_cache
# Configuration ============================================
show_proxmenux_logo
# ==========================================================
# Path where disk images are stored
IMAGES_DIR="/var/lib/vz/template/images/"
detect_image_dir() {
for store in $(pvesm status -content images | awk 'NR>1 {print $1}'); do
path=$(pvesm path "${store}:template" 2>/dev/null)
if [[ -d "$path" ]]; then
for ext in raw img qcow2 vmdk; do
if compgen -G "$path/*.$ext" > /dev/null; then
echo "$path"
return 0
fi
done
for sub in images iso; do
dir="$path/$sub"
if [[ -d "$dir" ]]; then
for ext in raw img qcow2 vmdk; do
if compgen -G "$dir/*.$ext" > /dev/null; then
echo "$dir"
return 0
fi
done
fi
done
fi
done
for fallback in /var/lib/vz/template/images /var/lib/vz/template/iso; do
if [[ -d "$fallback" ]]; then
for ext in raw img qcow2 vmdk; do
if compgen -G "$fallback/*.$ext" > /dev/null; then
echo "$fallback"
return 0
fi
done
fi
done
return 1
}
IMAGES_DIR=$(detect_image_dir)
if [[ -z "$IMAGES_DIR" ]]; then
dialog --title "$(translate 'No Images Found')" \
--msgbox "$(translate 'Could not find any directory containing disk images')\n\n$(translate 'Make sure there is at least one file with extension .img, .qcow2, .vmdk or .raw')" 15 60
exit 1
# Initial setup
if [ ! -d "$IMAGES_DIR" ]; then
msg_info "$(translate 'Creating images directory')"
mkdir -p "$IMAGES_DIR"
chmod 755 "$IMAGES_DIR"
msg_ok "$(translate 'Images directory created:') $IMAGES_DIR"
fi
IMAGES=$(ls -A "$IMAGES_DIR" | grep -E "\.(img|qcow2|vmdk|raw)$")
# Check if there are any images in the directory
IMAGES=$(ls -A "$IMAGES_DIR" | grep -E "\.(img|qcow2|vmdk)$")
if [ -z "$IMAGES" ]; then
dialog --title "$(translate 'No Disk Images Found')" \
--msgbox "$(translate 'No compatible disk images found in:')\n\n$IMAGES_DIR\n\n$(translate 'Supported formats: .img, .qcow2, .vmdk, .raw')" 15 60
exit 1
whiptail --title "$(translate 'No Images Found')" \
--msgbox "$(translate 'No images available for import in:')\n\n$IMAGES_DIR\n\n$(translate 'Supported formats: .img, .qcow2, .vmdk')\n\n$(translate 'Please add some images and try again.')" 15 60
exit 1
fi
# === Select VM
# Display initial message
whiptail --title "$(translate 'Import Disk Image')" --msgbox "$(translate 'Make sure the disk images you want to import are located in:')\n\n$IMAGES_DIR\n\n$(translate 'Supported formats: .img, .qcow2, .vmdk.')" 15 60
# 1. Select VM
msg_info "$(translate 'Getting VM list')"
VM_LIST=$(qm list | awk 'NR>1 {print $1" "$2}')
[[ -z "$VM_LIST" ]] && { msg_error "$(translate 'No VMs available in the system')"; exit 1; }
if [ -z "$VM_LIST" ]; then
msg_error "$(translate 'No VMs available in the system')"
exit 1
fi
msg_ok "$(translate 'VM list obtained')"
VMID=$(whiptail --title "$(translate 'Select VM')" \
--menu "$(translate 'Select the VM where you want to import the disk image:')" 20 70 10 $VM_LIST 3>&1 1>&2 2>&3)
[[ -z "$VMID" ]] && exit 1
VMID=$(whiptail --title "$(translate 'Select VM')" --menu "$(translate 'Select the VM where you want to import the disk image:')" 15 60 8 $VM_LIST 3>&1 1>&2 2>&3)
if [ -z "$VMID" ]; then
# msg_error "$(translate 'No VM selected')"
exit 1
fi
# === Select storage
# 2. Select storage volume
msg_info "$(translate 'Getting storage volumes')"
STORAGE_LIST=$(pvesm status -content images | awk 'NR>1 {print $1}')
[[ -z "$STORAGE_LIST" ]] && { msg_error "$(translate 'No storage volumes available')"; exit 1; }
if [ -z "$STORAGE_LIST" ]; then
msg_error "$(translate 'No storage volumes available')"
exit 1
fi
msg_ok "$(translate 'Storage volumes obtained')"
# Create an array of storage options for whiptail
STORAGE_OPTIONS=()
while read -r storage; do STORAGE_OPTIONS+=("$storage" ""); done <<< "$STORAGE_LIST"
STORAGE=$(whiptail --title "$(translate 'Select Storage')" \
--menu "$(translate 'Select the storage volume for disk import:')" 20 70 10 "${STORAGE_OPTIONS[@]}" 3>&1 1>&2 2>&3)
[[ -z "$STORAGE" ]] && exit 1
while read -r storage; do
STORAGE_OPTIONS+=("$storage" "")
done <<< "$STORAGE_LIST"
STORAGE=$(whiptail --title "$(translate 'Select Storage')" --menu "$(translate 'Select the storage volume for disk import:')" 15 60 8 "${STORAGE_OPTIONS[@]}" 3>&1 1>&2 2>&3)
if [ -z "$STORAGE" ]; then
# msg_error "$(translate 'No storage selected')"
exit 1
fi
# === Select images
# 3. Select disk images
msg_info "$(translate 'Scanning disk images')"
if [ -z "$IMAGES" ]; then
msg_warn "$(translate 'No compatible disk images found in') $IMAGES_DIR"
exit 0
fi
msg_ok "$(translate 'Disk images found')"
IMAGE_OPTIONS=()
while read -r img; do IMAGE_OPTIONS+=("$img" "" "OFF"); done <<< "$IMAGES"
SELECTED_IMAGES=$(whiptail --title "$(translate 'Select Disk Images')" \
--checklist "$(translate 'Select the disk images to import:')" 20 70 12 "${IMAGE_OPTIONS[@]}" 3>&1 1>&2 2>&3)
[[ -z "$SELECTED_IMAGES" ]] && exit 1
while read -r img; do
IMAGE_OPTIONS+=("$img" "" "OFF")
done <<< "$IMAGES"
SELECTED_IMAGES=$(whiptail --title "$(translate 'Select Disk Images')" --checklist "$(translate 'Select the disk images to import:')" 20 60 10 "${IMAGE_OPTIONS[@]}" 3>&1 1>&2 2>&3)
if [ -z "$SELECTED_IMAGES" ]; then
# msg_error "$(translate 'No images selected')"
exit 1
fi
# === Import each selected image
# 4. Import each selected image
for IMAGE in $SELECTED_IMAGES; do
IMAGE=$(echo "$IMAGE" | tr -d '"')
INTERFACE=$(whiptail --title "$(translate 'Interface Type')" --menu "$(translate 'Select the interface type for the image:') $IMAGE" 15 40 4 \
"sata" "SATA" "scsi" "SCSI" "virtio" "VirtIO" "ide" "IDE" 3>&1 1>&2 2>&3)
[[ -z "$INTERFACE" ]] && { msg_error "$(translate 'No interface type selected for') $IMAGE"; continue; }
FULL_PATH="$IMAGES_DIR/$IMAGE"
msg_info "$(translate 'Importing image:') $IMAGE"
TEMP_DISK_FILE=$(mktemp)
# Remove quotes from selected image
IMAGE=$(echo "$IMAGE" | tr -d '"')
qm importdisk "$VMID" "$FULL_PATH" "$STORAGE" 2>&1 | while read -r line; do
if [[ "$line" =~ transferred ]]; then
PERCENT=$(echo "$line" | grep -oP "\(\d+\.\d+%\)" | tr -d '()%')
echo -ne "\r${TAB}${BL}-$(translate 'Importing image:') $IMAGE-${CL} ${PERCENT}%"
elif [[ "$line" =~ successfully\ imported\ disk ]]; then
echo "$line" | grep -oP "(?<=successfully imported disk ').*(?=')" > "$TEMP_DISK_FILE"
# 5. Select interface type for each image
INTERFACE=$(whiptail --title "$(translate 'Interface Type')" --menu "$(translate 'Select the interface type for the image:') $IMAGE" 15 40 4 \
"sata" "SATA" \
"scsi" "SCSI" \
"virtio" "VirtIO" \
"ide" "IDE" 3>&1 1>&2 2>&3)
if [ -z "$INTERFACE" ]; then
msg_error "$(translate 'No interface type selected for') $IMAGE"
continue
fi
done
echo -ne "\n"
IMPORT_STATUS=${PIPESTATUS[0]}
FULL_PATH="$IMAGES_DIR/$IMAGE"
# Show initial message
msg_info "$(translate 'Importing image:')"
# Temporary file to capture the imported disk
TEMP_DISK_FILE=$(mktemp)
# Execute the command and process its output in real-time
qm importdisk "$VMID" "$FULL_PATH" "$STORAGE" 2>&1 | while read -r line; do
if [[ "$line" =~ transferred ]]; then
if [ "$IMPORT_STATUS" -eq 0 ]; then
msg_ok "$(translate 'Image imported successfully')"
IMPORTED_DISK=$(cat "$TEMP_DISK_FILE")
rm -f "$TEMP_DISK_FILE"
# Extract the progress percentage
PERCENT=$(echo "$line" | grep -oP "\(\d+\.\d+%\)" | tr -d '()%')
if [ -n "$IMPORTED_DISK" ]; then
EXISTING_DISKS=$(qm config "$VMID" | grep -oP "${INTERFACE}\d+" | sort -n)
NEXT_SLOT=0
[[ -n "$EXISTING_DISKS" ]] && NEXT_SLOT=$(( $(echo "$EXISTING_DISKS" | tail -n1 | sed "s/${INTERFACE}//") + 1 ))
# Show progress with custom format without translation
echo -ne "\r${TAB}${YW}-$(translate 'Importing image:') $IMAGE-${CL} ${PERCENT}%"
SSD_OPTION=""
if [ "$INTERFACE" != "virtio" ]; then
whiptail --yesno "$(translate 'Do you want to use SSD emulation for this disk?')" 10 60 && SSD_OPTION=",ssd=1"
fi
elif [[ "$line" =~ successfully\ imported\ disk ]]; then
msg_info "$(translate 'Configuring disk')"
if qm set "$VMID" --${INTERFACE}${NEXT_SLOT} "$IMPORTED_DISK${SSD_OPTION}" &>/dev/null; then
msg_ok "$(translate 'Image') $IMAGE $(translate 'configured as') ${INTERFACE}${NEXT_SLOT}"
whiptail --yesno "$(translate 'Do you want to make this disk bootable?')" 10 60 && {
msg_info "$(translate 'Configuring disk as bootable')"
if qm set "$VMID" --boot c --bootdisk ${INTERFACE}${NEXT_SLOT} &>/dev/null; then
msg_ok "$(translate 'Disk configured as bootable')"
else
msg_error "$(translate 'Could not configure the disk as bootable')"
fi
}
else
msg_error "$(translate 'Could not configure disk') ${INTERFACE}${NEXT_SLOT} $(translate 'for VM') $VMID"
fi
# Extract the imported disk name and save it to the temporary file
echo "$line" | grep -oP "(?<=successfully imported disk ').*(?=')" > "$TEMP_DISK_FILE"
fi
done
echo -ne "\n"
IMPORT_STATUS=${PIPESTATUS[0]} # Capture the exit status of the main command
if [ $IMPORT_STATUS -eq 0 ]; then
msg_ok "$(translate 'Image imported successfully')"
# Read the imported disk from the temporary file
IMPORTED_DISK=$(cat "$TEMP_DISK_FILE")
rm -f "$TEMP_DISK_FILE" # Delete the temporary file
if [ -n "$IMPORTED_DISK" ]; then
# Find the next available disk slot
EXISTING_DISKS=$(qm config "$VMID" | grep -oP "${INTERFACE}\d+" | sort -n)
if [ -z "$EXISTING_DISKS" ]; then
# If there are no existing disks, start from 0
NEXT_SLOT=0
else
# If there are existing disks, take the last one and add 1
LAST_SLOT=$(echo "$EXISTING_DISKS" | tail -n1 | sed "s/${INTERFACE}//")
NEXT_SLOT=$((LAST_SLOT + 1))
fi
# Ask if SSD emulation is desired (only for non-VirtIO interfaces)
if [ "$INTERFACE" != "virtio" ]; then
if (whiptail --title "$(translate 'SSD Emulation')" --yesno "$(translate 'Do you want to use SSD emulation for this disk?')" 10 60); then
SSD_OPTION=",ssd=1"
else
SSD_OPTION=""
fi
else
SSD_OPTION=""
fi
msg_info "$(translate 'Configuring disk')"
# Configure the disk in the VM
if qm set "$VMID" --${INTERFACE}${NEXT_SLOT} "$IMPORTED_DISK${SSD_OPTION}" &>/dev/null; then
msg_ok "$(translate 'Image') $IMAGE $(translate 'configured as') ${INTERFACE}${NEXT_SLOT}"
# Ask if the disk should be bootable
if (whiptail --title "$(translate 'Make Bootable')" --yesno "$(translate 'Do you want to make this disk bootable?')" 10 60); then
msg_info "$(translate 'Configuring disk as bootable')"
if qm set "$VMID" --boot c --bootdisk ${INTERFACE}${NEXT_SLOT} &>/dev/null; then
msg_ok "$(translate 'Disk configured as bootable')"
else
msg_error "$(translate 'Could not configure the disk as bootable')"
fi
fi
else
msg_error "$(translate 'Could not configure disk') ${INTERFACE}${NEXT_SLOT} $(translate 'for VM') $VMID"
fi
else
msg_error "$(translate 'Could not find the imported disk')"
fi
else
msg_error "$(translate 'Could not find the imported disk')"
msg_error "$(translate 'Could not import') $IMAGE"
fi
else
msg_error "$(translate 'Could not import') $IMAGE"
fi
done
msg_ok "$(translate 'All selected images have been processed')"
msg_success "$(translate "Press Enter to return to menu...")"
read -r
sleep 2

View File

@ -4,11 +4,10 @@
# ProxMenu - A menu-driven script for Proxmox VE management
# ==========================================================
# Author : MacRimi
# Revision : @Blaspt (USB passthrough via udev rule with persistent /dev/coral)
# Copyright : (c) 2024 MacRimi
# License : MIT (https://raw.githubusercontent.com/MacRimi/ProxMenux/main/LICENSE)
# Version : 1.1
# Last Updated: 16/05/2025
# Version : 1.0
# Last Updated: 28/01/2025
# ==========================================================
# Description:
# This script automates the configuration and installation of
@ -18,10 +17,13 @@
# - Installs necessary drivers inside the container
# - Manages required system and container restarts
#
# Supports Coral USB and Coral M.2 (PCIe) devices.
# Includes USB passthrough enhancement using persistent udev alias (/dev/coral).
# The script aims to simplify the process of enabling
# AI-powered video analysis capabilities in containers
# LXC, leveraging hardware acceleration for
# improved performance.
# ==========================================================
# Configuration ============================================
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
BASE_DIR="/usr/local/share/proxmenux"
UTILS_FILE="$BASE_DIR/utils.sh"
@ -36,7 +38,10 @@ initialize_cache
# ==========================================================
# Select LXC container
select_container() {
CONTAINERS=$(pct list | awk 'NR>1 {print $1, $3}' | xargs -n2)
if [ -z "$CONTAINERS" ]; then
msg_error "$(translate 'No containers available in Proxmox.')"
@ -59,12 +64,15 @@ select_container() {
msg_ok "$(translate 'Container selected:') $CONTAINER_ID"
}
# Validate that the selected container is valid
validate_container_id() {
if [ -z "$CONTAINER_ID" ]; then
msg_error "$(translate 'Container ID not defined. Make sure to select a container first.')"
exit 1
fi
# Check if the container is running and stop it before configuration
if pct status "$CONTAINER_ID" | grep -q "running"; then
msg_info "$(translate 'Stopping the container before applying configuration...')"
pct stop "$CONTAINER_ID"
@ -72,20 +80,8 @@ validate_container_id() {
fi
}
# Añadir regla udev para Coral USB para persistencia de permisos
add_udev_rule_for_coral_usb() {
RULE_FILE="/etc/udev/rules.d/99-coral-usb.rules"
RULE_CONTENT='SUBSYSTEM=="usb", ATTRS{idVendor}=="18d1", ATTRS{idProduct}=="9302", MODE="0666", TAG+="uaccess"'
if [[ ! -f "$RULE_FILE" ]] || ! grep -qF "$RULE_CONTENT" "$RULE_FILE"; then
echo "$RULE_CONTENT" > "$RULE_FILE"
udevadm control --reload-rules && udevadm trigger
msg_ok "$(translate 'Udev rule for Coral USB added and rules reloaded.')"
else
msg_ok "$(translate 'Udev rule for Coral USB already exists.')"
fi
}
# Configure LXC for Coral TPU and iGPU
configure_lxc_hardware() {
validate_container_id
CONFIG_FILE="/etc/pve/lxc/${CONTAINER_ID}.conf"
@ -94,7 +90,6 @@ configure_lxc_hardware() {
exit 1
fi
# Privileged container
if grep -q "^unprivileged: 1" "$CONFIG_FILE"; then
msg_info "$(translate 'The container is unprivileged. Changing to privileged...')"
sed -i "s/^unprivileged: 1/unprivileged: 0/" "$CONFIG_FILE"
@ -108,12 +103,11 @@ configure_lxc_hardware() {
msg_ok "$(translate 'The container is already privileged.')"
fi
# Enable nesting feature
# Configure iGPU
if ! grep -q "features: nesting=1" "$CONFIG_FILE"; then
echo "features: nesting=1" >> "$CONFIG_FILE"
fi
# iGPU support
if ! grep -q "c 226:0 rwm" "$CONFIG_FILE"; then
echo "lxc.cgroup2.devices.allow: c 226:0 rwm # iGPU" >> "$CONFIG_FILE"
echo "lxc.cgroup2.devices.allow: c 226:128 rwm # iGPU" >> "$CONFIG_FILE"
@ -121,7 +115,6 @@ configure_lxc_hardware() {
echo "lxc.mount.entry: /dev/dri/renderD128 dev/dri/renderD128 none bind,optional,create=file" >> "$CONFIG_FILE"
fi
# Framebuffer support
if ! grep -q "c 29:0 rwm # Framebuffer" "$CONFIG_FILE"; then
echo "lxc.cgroup2.devices.allow: c 29:0 rwm # Framebuffer" >> "$CONFIG_FILE"
fi
@ -130,37 +123,30 @@ configure_lxc_hardware() {
echo "lxc.mount.entry: /dev/fb0 dev/fb0 none bind,optional,create=file" >> "$CONFIG_FILE"
fi
# ----------------------------------------------------------
# Coral USB passthrough (via udev + /dev/coral)
# ----------------------------------------------------------
add_udev_rule_for_coral_usb
# Configure Coral TPU (USB and M.2)
if ! grep -Pq "^lxc.cgroup2.devices.allow: c 189:\* rwm # Coral USB$" "$CONFIG_FILE"; then
echo "lxc.cgroup2.devices.allow: c 189:* rwm # Coral USB" >> "$CONFIG_FILE"
fi
if ! grep -Pq "^lxc.mount.entry: /dev/coral dev/coral none bind,optional,create=file$" "$CONFIG_FILE"; then
echo "lxc.mount.entry: /dev/coral dev/coral none bind,optional,create=file" >> "$CONFIG_FILE"
if ! grep -Pq "^lxc.mount.entry: /dev/bus/usb dev/bus/usb none bind,optional,create=dir$" "$CONFIG_FILE"; then
echo "lxc.mount.entry: /dev/bus/usb dev/bus/usb none bind,optional,create=dir" >> "$CONFIG_FILE"
fi
# ----------------------------------------------------------
# Coral M.2 (PCIe) support
# ----------------------------------------------------------
if lspci | grep -iq "Global Unichip"; then
if ! grep -Pq "^lxc.cgroup2.devices.allow: c 245:0 rwm # Coral M2 Apex$" "$CONFIG_FILE"; then
echo "lxc.cgroup2.devices.allow: c 245:0 rwm # Coral M2 Apex" >> "$CONFIG_FILE"
fi
if ! grep -Pq "^lxc.mount.entry: /dev/apex_0 dev/apex_0 none bind,optional,create=file$" "$CONFIG_FILE"; then
echo "lxc.mount.entry: /dev/apex_0 dev/apex_0 none bind,optional,create=file" >> "$CONFIG_FILE"
fi
if ! grep -Pq "^lxc.mount.entry: /dev/apex_0 dev/apex_0 none bind,optional,create=file$" "$CONFIG_FILE"; then
echo "lxc.mount.entry: /dev/apex_0 dev/apex_0 none bind,optional,create=file" >> "$CONFIG_FILE"
fi
msg_ok "$(translate 'Coral TPU and iGPU configuration added to container') $CONTAINER_ID."
}
# Install Coral TPU drivers in the container
install_coral_in_container() {
msg_info2 "$(translate 'Installing iGPU and Coral TPU drivers inside the container...')"
tput sc
LOG_FILE=$(mktemp)
pct start "$CONTAINER_ID"
CORAL_M2=$(lspci | grep -i "Global Unichip")
@ -202,22 +188,25 @@ install_coral_in_container() {
'" "$LOG_FILE"
if [ $? -eq 0 ]; then
tput rc
tput ed
rm -f "$LOG_FILE"
tput rc
tput ed
rm -f "$LOG_FILE"
msg_ok "$(translate 'iGPU and Coral TPU drivers installed inside the container.')"
else
msg_error "$(translate 'Failed to install iGPU and Coral TPU drivers inside the container.')"
cat "$LOG_FILE"
cat "$LOG_FILE"
rm -f "$LOG_FILE"
exit 1
fi
}
select_container
show_proxmenux_logo
select_container
configure_lxc_hardware
install_coral_in_container
install_coral_in_container
msg_ok "$(translate 'Configuration completed.')"
sleep 2

View File

@ -68,7 +68,6 @@ verify_and_add_repos() {
# Function to install Coral TPU drivers on the host
install_coral_host() {
show_proxmenux_logo
verify_and_add_repos
apt-get install -y git devscripts dh-dkms dkms pve-headers-$(uname -r) >/dev/null 2>&1

View File

@ -64,7 +64,6 @@ change_language() {
"pt" "$(translate "Portuguese")" 3>&1 1>&2 2>&3)
if [ -z "$LANGUAGE" ]; then
clear
msg_error "$(translate "No language selected.")"
return
fi
@ -76,7 +75,7 @@ change_language() {
else
echo "{\"language\": \"$LANGUAGE\"}" > "$CONFIG_FILE"
fi
clear
msg_ok "$(translate "Language changed to") $LANGUAGE"
# Reload the menu

View File

@ -67,8 +67,8 @@ function header_info() {
# MAIN EXECUTION
# ==========================================================
#header_info
#echo -e "\n Loading..."
header_info
echo -e "\n Loading..."
#sleep 1
@ -98,7 +98,7 @@ function start_vm_configuration() {
while true; do
OS_TYPE=$(dialog --backtitle "ProxMenux" \
--title "$(translate "Select System Type")" \
--menu "\n$(translate "Choose the type of virtual system to install:")" 20 70 10 \
--menu "\n$(translate "Choose the type of virtual system to install:")" 18 70 10 \
1 "$(translate "Create") VM System NAS" \
2 "$(translate "Create") VM System Windows" \
3 "$(translate "Create") VM System Linux" \

View File

@ -67,9 +67,9 @@ function header_info() {
# MAIN EXECUTION
# ==========================================================
#header_info
#echo -e "\n Loading..."
#sleep 1
header_info
echo -e "\n Loading..."
sleep 1

View File

@ -25,43 +25,43 @@ initialize_cache
# ==========================================================
while true; do
OPTION=$(dialog --clear --backtitle "ProxMenux" --title "$(translate "GPUs and Coral-TPU Menu")" \
--menu "\n$(translate "Select an option:")" 20 70 8 \
"1" "$(translate "Add HW iGPU acceleration to an LXC")" \
"2" "$(translate "Add Coral TPU to an LXC")" \
"3" "$(translate "Install/Update Coral TPU on the Host")" \
"4" "$(translate "Return to Main Menu")" \
2>&1 >/dev/tty)
OPTION=$(whiptail --title "$(translate "GPUs and Coral-TPU Menu")" --menu "$(translate "Select an option:")" 20 70 8 \
"1" "$(translate "Add HW iGPU acceleration to an LXC")" \
"2" "$(translate "Add Coral TPU to an LXC")" \
"3" "$(translate "Install/Update Coral TPU on the Host")" \
"4" "$(translate "Return to Main Menu")" 3>&1 1>&2 2>&3)
case $OPTION in
1)
show_proxmenux_logo
msg_info2 "$(translate "Running script:") $(translate " HW iGPU acceleration LXC")..."
bash <(curl -s "$REPO_URL/scripts/configure_igpu_lxc.sh")
if [ $? -ne 0 ]; then
clear
show_proxmenux_logo
msg_warn "$(translate "Operation cancelled.")"
sleep 2
fi
;;
2)
show_proxmenux_logo
msg_info2 "$(translate "Running script:") Coral TPU LXC..."
bash <(curl -s "$REPO_URL/scripts/install_coral_lxc.sh")
if [ $? -ne 0 ]; then
clear
show_proxmenux_logo
msg_warn "$(translate "Operation cancelled.")"
sleep 2
fi
;;
3)
show_proxmenux_logo
msg_info2 "$(translate "Running script:") $(translate "Install/Update") Coral..."
bash <(curl -s "$REPO_URL/scripts/install_coral_pve.sh")
if [ $? -ne 0 ]; then
clear
show_proxmenux_logo
msg_warn "$(translate "Operation cancelled.")"
sleep 2
fi
;;
4) exec bash <(curl -s "$REPO_URL/scripts/menus/main_menu.sh") ;;
*) exec bash <(curl -s "$REPO_URL/scripts/menus/main_menu.sh") ;;
esac
done

View File

@ -37,13 +37,13 @@ show_menu() {
dialog --clear \
--backtitle "ProxMenux" \
--title "$(translate "Main ProxMenux")" \
--menu "$(translate "Select an option:")" 20 70 10 \
--menu "$(translate "Select an option:")" 18 70 10 \
1 "$(translate "Settings post-install Proxmox")" \
2 "$(translate "Help and Info Commands")" \
3 "$(translate "Hardware: GPUs and Coral-TPU")" \
4 "$(translate "Create VM from template or script")" \
5 "$(translate "Disk and Storage Manager")" \
6 "$(translate "Proxmox VE Helper Scripts")" \
6 "$(translate "Essential Proxmox VE Helper-Scripts")" \
7 "$(translate "Network")" \
8 "$(translate "Settings")" \
9 "$(translate "Exit")" 2>"$TEMP_FILE"

View File

@ -6,11 +6,11 @@
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : MIT (https://raw.githubusercontent.com/MacRimi/ProxMenux/main/LICENSE)
# Version : 1.1
# Last Updated: 04/06/2025
# Version : 1.0
# Last Updated: 28/01/2025
# ==========================================================
# Description:
# This script provides a simple and efficient way to access and execute Proxmox VE scripts
# This script provides a simple and efficient way to access and execute essential Proxmox VE scripts
# from the Community Scripts project (https://community-scripts.github.io/ProxmoxVE/).
#
# It serves as a convenient tool to run key automation scripts that simplify system management,
@ -31,297 +31,156 @@ fi
load_language
initialize_cache
#show_proxmenux_logo
# ==========================================================
HELPERS_JSON_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/refs/heads/main/json/helpers_cache.json"
METADATA_URL="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/frontend/public/json/metadata.json"
for cmd in curl jq dialog; do
if ! command -v "$cmd" >/dev/null; then
echo "Missing required command: $cmd"
exit 1
fi
done
CACHE_JSON=$(curl -s "$HELPERS_JSON_URL")
META_JSON=$(curl -s "$METADATA_URL")
declare -A CATEGORY_NAMES
while read -r id name; do
CATEGORY_NAMES[$id]="$name"
done < <(echo "$META_JSON" | jq -r '.categories[] | "\(.id)\t\(.name)"')
declare -A CATEGORY_COUNT
for id in $(echo "$CACHE_JSON" | jq -r '.[].categories[]'); do
((CATEGORY_COUNT[$id]++))
done
get_type_label() {
local type="$1"
case "$type" in
ct) echo $'\Z1LXC\Zn' ;;
vm) echo $'\Z4VM\Zn' ;;
pve) echo $'\Z3PVE\Zn' ;;
addon) echo $'\Z2ADDON\Zn' ;;
*) echo $'\Z7GEN\Zn' ;;
esac
}
# Base URL community-scripts
BASE_URL="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc"
BASE_URL2="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main"
download_script() {
local url="$1"
local fallback_pve="${url/misc\/tools\/pve}"
local fallback_addon="${url/misc\/tools\/addon}"
local fallback_copydata="${url/misc\/tools\/copy-data}"
local url="$1"
local fallback_pve="${url/misc/tools\/pve}"
local fallback_addon="${url/misc/tools\/addon}"
local fallback_copydata="${url/misc/tools\/copy-data}"
if curl --silent --head --fail "$url" >/dev/null; then
bash <(curl -s "$url")
elif curl --silent --head --fail "$fallback_pve" >/dev/null; then
bash <(curl -s "$fallback_pve")
elif curl --silent --head --fail "$fallback_addon" >/dev/null; then
bash <(curl -s "$fallback_addon")
elif curl --silent --head --fail "$fallback_copydata" >/dev/null; then
bash <(curl -s "$fallback_copydata")
else
dialog --title "Helper Scripts" --msgbox "Error: Failed to download the script." 12 70
fi
}
RETURN_TO_MAIN=false
format_credentials() {
local script_info="$1"
local credentials_info=""
local has_credentials
has_credentials=$(echo "$script_info" | base64 --decode | jq -r 'has("default_credentials")')
if [[ "$has_credentials" == "true" ]]; then
local username password
username=$(echo "$script_info" | base64 --decode | jq -r '.default_credentials.username // empty')
password=$(echo "$script_info" | base64 --decode | jq -r '.default_credentials.password // empty')
if [[ -n "$username" && -n "$password" ]]; then
credentials_info="Username: $username | Password: $password"
elif [[ -n "$username" ]]; then
credentials_info="Username: $username"
elif [[ -n "$password" ]]; then
credentials_info="Password: $password"
fi
fi
echo "$credentials_info"
}
run_script_by_slug() {
local slug="$1"
local script_info
script_info=$(echo "$CACHE_JSON" | jq -r --arg slug "$slug" '.[] | select(.slug == $slug) | @base64')
decode() {
echo "$1" | base64 --decode | jq -r "$2"
}
local name desc script_url notes
name=$(decode "$script_info" ".name")
desc=$(decode "$script_info" ".desc")
script_url=$(decode "$script_info" ".script_url")
notes=$(decode "$script_info" ".notes | join(\"\n\")")
local notes_dialog=""
if [[ -n "$notes" ]]; then
while IFS= read -r line; do
notes_dialog+="$line\n"
done <<< "$notes"
notes_dialog="${notes_dialog%\\n}"
fi
local credentials
credentials=$(format_credentials "$script_info")
local msg="\Zb\Z4Descripción:\Zn\n$desc"
[[ -n "$notes_dialog" ]] && msg+="\n\n\Zb\Z4Notes:\Zn\n$notes_dialog"
[[ -n "$credentials" ]] && msg+="\n\n\Zb\Z4Default Credentials:\Zn\n$credentials"
dialog --clear --colors --backtitle "ProxMenux" --title "$name" --yesno "$msg\n\nExecute this script?" 22 85
if [[ $? -eq 0 ]]; then
download_script "$script_url"
echo
echo
if [[ -n "$desc" || -n "$notes" || -n "$credentials" ]]; then
echo -e "$TAB\e[1;36mScript Information:\e[0m"
if [[ -n "$notes" ]]; then
echo -e "$TAB\e[1;33mNotes:\e[0m"
while IFS= read -r line; do
[[ -z "$line" ]] && continue
echo -e "$TAB$line"
done <<< "$notes"
echo
fi
if [[ -n "$credentials" ]]; then
echo -e "$TAB\e[1;32mDefault Credentials:\e[0m"
echo "$TAB$credentials"
echo
fi
fi
msg_success "Press Enter to return to the main menu..."
read -r
RETURN_TO_MAIN=true
fi
}
search_and_filter_scripts() {
local search_term=""
while true; do
search_term=$(dialog --inputbox "Enter search term (leave empty to show all scripts):" \
8 65 "$search_term" 3>&1 1>&2 2>&3)
[[ $? -ne 0 ]] && return
local filtered_json
if [[ -z "$search_term" ]]; then
filtered_json="$CACHE_JSON"
if curl --silent --head --fail "$url" >/dev/null; then
bash <(curl -s "$url")
elif curl --silent --head --fail "$fallback_pve" >/dev/null; then
bash <(curl -s "$fallback_pve")
elif curl --silent --head --fail "$fallback_addon" >/dev/null; then
bash <(curl -s "$fallback_addon")
elif curl --silent --head --fail "$fallback_copydata" >/dev/null; then
bash <(curl -s "$fallback_copydata")
else
local search_lower
search_lower=$(echo "$search_term" | tr '[:upper:]' '[:lower:]')
filtered_json=$(echo "$CACHE_JSON" | jq --arg term "$search_lower" '
[.[] | select(
(.name | ascii_downcase | contains($term)) or
(.desc | ascii_downcase | contains($term))
)]')
fi
local count
count=$(echo "$filtered_json" | jq length)
if [[ $count -eq 0 ]]; then
dialog --msgbox "No scripts found for: '$search_term'\n\nTry a different search term." 8 50
continue
msg_error "$(translate 'Error: Failed to download the script.')\033[0m"
msg_error "\n$(translate 'Tried URLs:')\n- $url\n- $fallback_pve\n- $fallback_addons\n- $fallback_copydata\n"
msg_success "$(translate "Press Enter to return to menu...")"
read -r
fi
}
# Array with script names, URLs, categories, and descriptions
scripts=(
"Proxmox VE LXC IP-Tag|Containers|$BASE_URL/add-lxc-iptag.sh|Description:\n\nThis script automatically adds IP address as tags to LXC containers using a Systemd service.\n\nThe service also updates the tags if a LXC IP address is changed. Configuration: nano /opt/lxc-iptag/iptag.conf. iptag.service must be restarted after change.\n\n\The Proxmox Node must contain ipcalc and net-tools. apt-get install -y ipcalc net-tools"
"Add Netbird to LXC|Networking|$BASE_URL/add-netbird-lxc.sh|Description:\n\nNetBird combines a configuration-free peer-to-peer private network and a centralized access control system in a single platform, making it easy to create secure private networks for your organization or home.\n\nAfter the script finishes, reboot the LXC then run netbird up in the LXC console.\n\n\The script only works in Debian/Ubuntu, not in Alpine!"
"Add Tailscale to LXC|Networking|$BASE_URL/add-tailscale-lxc.sh|Description:\n\nTailscale is a software-defined networking solution that enables secure communication between devices over the internet.\n\nIt creates a virtual private network (VPN) that enables devices to communicate with each other as if they were on the same local network.\n\n\After the script finishes, reboot the LXC then run tailscale up in the LXC console."
"Proxmox VE LXC Cleaner|Maintenance|$BASE_URL/clean-lxcs.sh|Description:\n\nThis script provides options to delete logs and cache, and repopulate apt lists for Ubuntu and Debian systems."
"Proxmox VE Host Backup|Security|$BASE_URL/host-backup.sh|Description:\n\nThis script serves as a versatile backup utility, enabling users to specify both the backup path and the directory they want to work in.\n\nThis flexibility empowers users to select the specific files and directories they wish to back up, making it compatible with a wide range of hosts, not limited to Proxmox.\n\nA backup is rendered ineffective when it remains stored on the host"
"Add hardware Acceleration LXC|Containers|$BASE_URL/hw-acceleration.sh|Description:\n\nEnables hardware acceleration IGPU for LXC containers."
"Proxmox Clean Orphaned LVM|Maintenance|$BASE_URL/clean-orphaned-lvm.sh|Description:\n\nThis script helps Proxmox users identify and remove orphaned LVM volumes that are no longer associated with any VM or LXC container.\n\nIt scans all LVM volumes, detects unused ones, and provides an interactive prompt to delete them safely.\n\nSystem-critical volumes like root, swap, and data are excluded to prevent accidental deletion."
"Install Crowdsec|Security|$BASE_URL/crowdsec.sh|Description:\n\nCrowdSec is a free and open-source intrusion prevention system (IPS) designed to provide network security against malicious traffic.\n\nIt is a collaborative IPS that analyzes behaviors and responses to attacks by sharing signals across a community of users."
"Proxmox VE LXC Filesystem Trim|Maintenance|$BASE_URL/fstrim.sh|Description:\n\nThis maintains SSD performance by managing unused blocks.\n\nThin-provisioned storage systems also require management to prevent unnecessary storage use.\n\nVMs automate fstrim, while LXC containers need manual or automated fstrim processes for optimal performance.\n\nThis is designed to work with SSDs on ext4 filesystems only."
"Install Glances|Monitoring|$BASE_URL/glances.sh|Description:\n\nGlances is an open-source system cross-platform monitoring tool.\n\nIt allows real-time monitoring of various aspects of your system such as CPU, memory, disk, network usage etc."
"Proxmox VE Kernel Clean|Maintenance|$BASE_URL/kernel-clean.sh|Description:\n\nCleaning unused kernel images is beneficial for reducing the length of the GRUB menu and freeing up disk space.\n\nBy removing old, unused kernels, the system is able to conserve disk space and streamline the boot process."
"Proxmox VE Kernel Pin|System|$BASE_URL/kernel-pin.sh|Description:\n\nKernel Pin is an essential tool for effortlessly managing kernel pinning and unpinning."
"Container LXC Deletion|Containers|$BASE_URL/lxc-delete.sh|Description:\n\nThis script helps manage and delete LXC containers on a Proxmox VE server.\n\nIt lists all available containers, allowing the user to select one or more for deletion through an interactive menu.\n\nRunning containers are automatically stopped before deletion, and the user is asked to confirm each action.\n\nThe script ensures a controlled and efficient container management process."
"Proxmox VE Processor Microcode|System|$BASE_URL/microcode.sh|Description:\n\nProcessor Microcode is a layer of low-level software that runs on the processor and provides patches or updates to its firmware.\n\nMicrocode updates can fix hardware bugs, improve performance, and enhance security features of the processor."
"Proxmox VE Netdata|Monitoring|$BASE_URL/netdata.sh|Description:\n\nNetdata is an open-source, real-time performance monitoring tool designed to provide insights into the performance and health of systems and applications.\n\nIt is often used by system administrators, DevOps professionals, and developers to monitor and troubleshoot issues on servers and other devices."
"Install Olivetin|Applications|$BASE_URL/olivetin.sh|Description:\n\nOliveTin provides a secure and straightforward way to execute pre-determined shell commands through a web-based interface.\n\nConfiguration Path: /etc/OliveTin/config.yaml"
"Proxmox VE Post Install|System|$BASE_URL/post-pve-install.sh|Description:\n\nThis script provides options for managing Proxmox VE repositories, including disabling the Enterprise Repo, adding or correcting PVE sources, enabling the No-Subscription Repo, adding the test Repo, disabling the subscription nag, updating Proxmox VE, and rebooting the system.\n\nExecute within the Proxmox shell.\n\n\It is recommended to answer yes (y) to all options presented during the process."
"Proxmox VE CPU Scaling Governor|System|$BASE_URL/scaling-governor.sh|Description:\n\nThe CPU scaling governor determines how the CPU frequency is adjusted based on the workload, with the goal of either conserving power or improving performance.\n\nBy scaling the frequency up or down, the operating system can optimize the CPU usage and conserve energy when possible. Generic Scaling Governors."
"Proxmox VE Cron LXC Updater|Maintenance|$BASE_URL/cron-update-lxcs.sh|Description:\n\nThis script will add/remove a crontab schedule that updates all LXCs every Sunday at midnight. To exclude LXCs from updating, edit the crontab using crontab -e and add CTID as shown in the example below:\n\n0 0 * * 0 PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin /bin/bash -c \"\$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/tools/pve/update-lxcs-cron.sh)\" -s 103 111 >>/var/log/update-lxcs-cron.log 2>/dev/null"
"Proxmox VE LXC Updater|Maintenance|$BASE_URL/update-lxcs.sh|Description:\n\nThis script has been created to simplify and speed up the process of updating all LXC containers across various Linux distributions, such as Ubuntu, Debian, Devuan, Alpine Linux, CentOS-Rocky-Alma, Fedora, and ArchLinux.\n\nDesigned to automatically skip templates and specific containers during the update, enhancing its convenience and usability."
"Proxmox Backup Server|Security|$BASE_URL2/ct/proxmox-backup-server.sh|Description:\n\nProxmox Backup Server is an enterprise backup solution, for backing up and restoring VMs, containers, and physical hosts. By supporting incremental, fully deduplicated backups, Proxmox Backup Server significantly reduces network load and saves valuable storage space.\n\n\nSet a root password if using autologin. This will be the PBS password. passwd root"
)
show_menu() {
declare -A category_order
category_order["System"]=1
category_order["Maintenance"]=2
category_order["Containers"]=3
category_order["Applications"]=4
category_order["Monitoring"]=5
category_order["Networking"]=6
category_order["Security"]=7
custom_sort() {
while IFS='|' read -r name category url description; do
category=$(echo "$category" | xargs)
order=${category_order[$category]:-999}
printf "%d|%s|%s|%s|%s\n" "$order" "$name" "$category" "$url" "$description"
done | sort -n | cut -d'|' -f2-
}
while true; do
declare -A index_to_slug
local menu_items=()
local i=1
while IFS=$'\t' read -r slug name type; do
index_to_slug[$i]="$slug"
local label
label=$(get_type_label "$type")
local padded_name
padded_name=$(printf "%-42s" "$name")
local entry="$padded_name $label"
menu_items+=("$i" "$entry")
((i++))
done < <(echo "$filtered_json" | jq -r '
sort_by(.name)[] | [.slug, .name, .type] | @tsv')
menu_items+=("" "")
menu_items+=("new_search" "New Search")
menu_items+=("show_all" "Show All Scripts")
local title="Search Results"
if [[ -n "$search_term" ]]; then
title="Search Results for: '$search_term' ($count found)"
else
title="All Available Scripts ($count total)"
fi
local selected
selected=$(dialog --colors --backtitle "ProxMenux" \
--title "$title" \
--menu "Select a script or action:" \
22 75 15 "${menu_items[@]}" 3>&1 1>&2 2>&3)
if [[ $? -ne 0 ]]; then
return
fi
case "$selected" in
"new_search")
break
;;
"show_all")
search_term=""
filtered_json="$CACHE_JSON"
count=$(echo "$filtered_json" | jq length)
continue
;;
"back"|"")
return
;;
*)
if [[ -n "${index_to_slug[$selected]}" ]]; then
run_script_by_slug "${index_to_slug[$selected]}"
[[ "$RETURN_TO_MAIN" == true ]] && { RETURN_TO_MAIN=false; return; }
fi
;;
esac
IFS=$'\n' sorted_scripts=($(printf "%s\n" "${scripts[@]}" | custom_sort))
unset IFS
HEADER=$(printf " %-57s %-20s" "$(translate "Name")" "$(translate "Category")")
menu_items=()
for script in "${sorted_scripts[@]}"; do
IFS='|' read -r name category url description <<< "$script"
translated_category=$(translate "$category")
padded_name=$(printf "%-57s" "$name")
menu_items+=("$padded_name" "$translated_category")
done
menu_items+=("$(translate "Return to Main Menu")" "")
cleanup
script_selection=$(whiptail --title "$(translate "Essential Proxmox VE Helper-Scripts")" \
--menu "\n$HEADER\n\n$(translate "Select a script to execute")" 25 78 16 \
"${menu_items[@]}" 3>&1 1>&2 2>&3)
if [ -n "$script_selection" ]; then
script_selection=$(echo "$script_selection" | xargs)
if [ "$script_selection" = "$(translate "Return to Main Menu")" ]; then
whiptail --title "Proxmox VE Helper-Scripts" \
--msgbox "$(translate "Visit the website to discover more scripts, stay updated with the latest updates, and support the project:\n\nhttps://community-scripts.github.io/ProxmoxVE")" 15 70
exec bash <(curl -s "$REPO_URL/scripts/menus/main_menu.sh")
fi
for script in "${sorted_scripts[@]}"; do
IFS='|' read -r name category url description <<< "$script"
if [ "$name" = "$script_selection" ]; then
selected_url="$url"
selected_description=$(translate "$description")
break
fi
done
if [ -n "$selected_url" ]; then
if whiptail --title "$(translate "Script Information")" \
--yes-button "$(translate "Accept")" \
--no-button "$(translate "Cancel")" \
--yesno "$selected_description" 20 78; then
#msg_info2 "$(translate "Executing script:") $script_selection"
#sleep 2
download_script "$selected_url"
msg_ok "$(translate "Script completed.")"
msg_success "$(translate "Press Enter to return to the main menu...")"
read -r
clear
else
msg_info2 "$(translate "Script execution cancelled.")"
sleep 2
fi
else
echo "$(translate "Error: Could not find the selected script URL.")"
read -rp "$(translate "Press Enter to continue...")"
fi
else
exec bash <(curl -s "$REPO_URL/scripts/menus/main_menu.sh")
fi
done
done
}
while true; do
MENU_ITEMS=()
MENU_ITEMS+=("search" "Search/Filter Scripts")
MENU_ITEMS+=("" "")
for id in $(printf "%s\n" "${!CATEGORY_COUNT[@]}" | sort -n); do
name="${CATEGORY_NAMES[$id]:-Category $id}"
count="${CATEGORY_COUNT[$id]}"
padded_name=$(printf "%-35s" "$name")
padded_count=$(printf "(%2d)" "$count")
MENU_ITEMS+=("$id" "$padded_name $padded_count")
done
SELECTED=$(dialog --backtitle "ProxMenux" --title "Proxmox VE Helper-Scripts" --menu \
"Select a category or search for scripts:" 20 70 14 \
"${MENU_ITEMS[@]}" 3>&1 1>&2 2>&3) || {
dialog --clear --title "Proxmox VE Helper-Scripts" \
--msgbox "\n\n$(translate "Visit the website to discover more scripts, stay updated with the latest updates, and support the project:")\n\nhttps://community-scripts.github.io/ProxmoxVE" 15 70
#clear
exec bash <(curl -s "$REPO_URL/scripts/menus/main_menu.sh")
}
if [[ "$SELECTED" == "search" ]]; then
search_and_filter_scripts
continue
fi
while true; do
declare -A INDEX_TO_SLUG
SCRIPTS=()
i=1
while IFS=$'\t' read -r slug name type; do
INDEX_TO_SLUG[$i]="$slug"
label=$(get_type_label "$type")
padded_name=$(printf "%-42s" "$name")
entry="$padded_name $label"
SCRIPTS+=("$i" "$entry")
((i++))
done < <(echo "$CACHE_JSON" | jq -r --argjson id "$SELECTED" \
'[.[] | select(.categories | index($id)) | {slug, name, type}] | sort_by(.name)[] | [.slug, .name, .type] | @tsv')
SCRIPT_INDEX=$(dialog --colors --backtitle "ProxMenux" --title "Scripts in ${CATEGORY_NAMES[$SELECTED]}" --menu \
"Choose a script to execute:" 20 70 14 \
"${SCRIPTS[@]}" 3>&1 1>&2 2>&3) || break
SCRIPT_SELECTED="${INDEX_TO_SLUG[$SCRIPT_INDEX]}"
run_script_by_slug "$SCRIPT_SELECTED"
[[ "$RETURN_TO_MAIN" == true ]] && { RETURN_TO_MAIN=false; break; }
done
done
if [[ "$LANGUAGE" != "en" ]]; then
show_proxmenux_logo
msg_lang "$(translate "Generating automatic translations...")"
fi
show_menu

View File

@ -1,186 +0,0 @@
#!/bin/bash
# ==========================================================
# ProxMenu - A menu-driven script for Proxmox VE management
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : MIT (https://raw.githubusercontent.com/MacRimi/ProxMenux/main/LICENSE)
# Version : 1.0
# Last Updated: 28/01/2025
# ==========================================================
# Description:
# This script provides a simple and efficient way to access and execute essential Proxmox VE scripts
# from the Community Scripts project (https://community-scripts.github.io/ProxmoxVE/).
#
# It serves as a convenient tool to run key automation scripts that simplify system management,
# continuing the great work and legacy of tteck in making Proxmox VE more accessible.
# A streamlined solution for executing must-have tools in Proxmox VE.
# ==========================================================
# Configuration ============================================
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
BASE_DIR="/usr/local/share/proxmenux"
UTILS_FILE="$BASE_DIR/utils.sh"
VENV_PATH="/opt/googletrans-env"
if [[ -f "$UTILS_FILE" ]]; then
source "$UTILS_FILE"
fi
load_language
initialize_cache
#show_proxmenux_logo
# ==========================================================
# Base URL community-scripts
BASE_URL="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc"
BASE_URL2="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main"
download_script() {
local url="$1"
local fallback_pve="${url/misc/tools\/pve}"
local fallback_addon="${url/misc/tools\/addon}"
local fallback_copydata="${url/misc/tools\/copy-data}"
if curl --silent --head --fail "$url" >/dev/null; then
bash <(curl -s "$url")
elif curl --silent --head --fail "$fallback_pve" >/dev/null; then
bash <(curl -s "$fallback_pve")
elif curl --silent --head --fail "$fallback_addon" >/dev/null; then
bash <(curl -s "$fallback_addon")
elif curl --silent --head --fail "$fallback_copydata" >/dev/null; then
bash <(curl -s "$fallback_copydata")
else
msg_error "$(translate 'Error: Failed to download the script.')\033[0m"
msg_error "\n$(translate 'Tried URLs:')\n- $url\n- $fallback_pve\n- $fallback_addons\n- $fallback_copydata\n"
msg_success "$(translate "Press Enter to return to menu...")"
read -r
fi
}
# Array with script names, URLs, categories, and descriptions
scripts=(
"Proxmox VE LXC IP-Tag|Containers|$BASE_URL/add-lxc-iptag.sh|Description:\n\nThis script automatically adds IP address as tags to LXC containers using a Systemd service.\n\nThe service also updates the tags if a LXC IP address is changed. Configuration: nano /opt/lxc-iptag/iptag.conf. iptag.service must be restarted after change.\n\n\The Proxmox Node must contain ipcalc and net-tools. apt-get install -y ipcalc net-tools"
"Add Netbird to LXC|Networking|$BASE_URL/add-netbird-lxc.sh|Description:\n\nNetBird combines a configuration-free peer-to-peer private network and a centralized access control system in a single platform, making it easy to create secure private networks for your organization or home.\n\nAfter the script finishes, reboot the LXC then run netbird up in the LXC console.\n\n\The script only works in Debian/Ubuntu, not in Alpine!"
"Add Tailscale to LXC|Networking|$BASE_URL/add-tailscale-lxc.sh|Description:\n\nTailscale is a software-defined networking solution that enables secure communication between devices over the internet.\n\nIt creates a virtual private network (VPN) that enables devices to communicate with each other as if they were on the same local network.\n\n\After the script finishes, reboot the LXC then run tailscale up in the LXC console."
"Proxmox VE LXC Cleaner|Maintenance|$BASE_URL/clean-lxcs.sh|Description:\n\nThis script provides options to delete logs and cache, and repopulate apt lists for Ubuntu and Debian systems."
"Proxmox VE Host Backup|Security|$BASE_URL/host-backup.sh|Description:\n\nThis script serves as a versatile backup utility, enabling users to specify both the backup path and the directory they want to work in.\n\nThis flexibility empowers users to select the specific files and directories they wish to back up, making it compatible with a wide range of hosts, not limited to Proxmox.\n\nA backup is rendered ineffective when it remains stored on the host"
"Add hardware Acceleration LXC|Containers|$BASE_URL/hw-acceleration.sh|Description:\n\nEnables hardware acceleration IGPU for LXC containers."
"Proxmox Clean Orphaned LVM|Maintenance|$BASE_URL/clean-orphaned-lvm.sh|Description:\n\nThis script helps Proxmox users identify and remove orphaned LVM volumes that are no longer associated with any VM or LXC container.\n\nIt scans all LVM volumes, detects unused ones, and provides an interactive prompt to delete them safely.\n\nSystem-critical volumes like root, swap, and data are excluded to prevent accidental deletion."
"Install Crowdsec|Security|$BASE_URL/crowdsec.sh|Description:\n\nCrowdSec is a free and open-source intrusion prevention system (IPS) designed to provide network security against malicious traffic.\n\nIt is a collaborative IPS that analyzes behaviors and responses to attacks by sharing signals across a community of users."
"Proxmox VE LXC Filesystem Trim|Maintenance|$BASE_URL/fstrim.sh|Description:\n\nThis maintains SSD performance by managing unused blocks.\n\nThin-provisioned storage systems also require management to prevent unnecessary storage use.\n\nVMs automate fstrim, while LXC containers need manual or automated fstrim processes for optimal performance.\n\nThis is designed to work with SSDs on ext4 filesystems only."
"Install Glances|Monitoring|$BASE_URL/glances.sh|Description:\n\nGlances is an open-source system cross-platform monitoring tool.\n\nIt allows real-time monitoring of various aspects of your system such as CPU, memory, disk, network usage etc."
"Proxmox VE Kernel Clean|Maintenance|$BASE_URL/kernel-clean.sh|Description:\n\nCleaning unused kernel images is beneficial for reducing the length of the GRUB menu and freeing up disk space.\n\nBy removing old, unused kernels, the system is able to conserve disk space and streamline the boot process."
"Proxmox VE Kernel Pin|System|$BASE_URL/kernel-pin.sh|Description:\n\nKernel Pin is an essential tool for effortlessly managing kernel pinning and unpinning."
"Container LXC Deletion|Containers|$BASE_URL/lxc-delete.sh|Description:\n\nThis script helps manage and delete LXC containers on a Proxmox VE server.\n\nIt lists all available containers, allowing the user to select one or more for deletion through an interactive menu.\n\nRunning containers are automatically stopped before deletion, and the user is asked to confirm each action.\n\nThe script ensures a controlled and efficient container management process."
"Proxmox VE Processor Microcode|System|$BASE_URL/microcode.sh|Description:\n\nProcessor Microcode is a layer of low-level software that runs on the processor and provides patches or updates to its firmware.\n\nMicrocode updates can fix hardware bugs, improve performance, and enhance security features of the processor."
"Proxmox VE Netdata|Monitoring|$BASE_URL/netdata.sh|Description:\n\nNetdata is an open-source, real-time performance monitoring tool designed to provide insights into the performance and health of systems and applications.\n\nIt is often used by system administrators, DevOps professionals, and developers to monitor and troubleshoot issues on servers and other devices."
"Install Olivetin|Applications|$BASE_URL/olivetin.sh|Description:\n\nOliveTin provides a secure and straightforward way to execute pre-determined shell commands through a web-based interface.\n\nConfiguration Path: /etc/OliveTin/config.yaml"
"Proxmox VE Post Install|System|$BASE_URL/post-pve-install.sh|Description:\n\nThis script provides options for managing Proxmox VE repositories, including disabling the Enterprise Repo, adding or correcting PVE sources, enabling the No-Subscription Repo, adding the test Repo, disabling the subscription nag, updating Proxmox VE, and rebooting the system.\n\nExecute within the Proxmox shell.\n\n\It is recommended to answer yes (y) to all options presented during the process."
"Proxmox VE CPU Scaling Governor|System|$BASE_URL/scaling-governor.sh|Description:\n\nThe CPU scaling governor determines how the CPU frequency is adjusted based on the workload, with the goal of either conserving power or improving performance.\n\nBy scaling the frequency up or down, the operating system can optimize the CPU usage and conserve energy when possible. Generic Scaling Governors."
"Proxmox VE Cron LXC Updater|Maintenance|$BASE_URL/cron-update-lxcs.sh|Description:\n\nThis script will add/remove a crontab schedule that updates all LXCs every Sunday at midnight. To exclude LXCs from updating, edit the crontab using crontab -e and add CTID as shown in the example below:\n\n0 0 * * 0 PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin /bin/bash -c \"\$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/tools/pve/update-lxcs-cron.sh)\" -s 103 111 >>/var/log/update-lxcs-cron.log 2>/dev/null"
"Proxmox VE LXC Updater|Maintenance|$BASE_URL/update-lxcs.sh|Description:\n\nThis script has been created to simplify and speed up the process of updating all LXC containers across various Linux distributions, such as Ubuntu, Debian, Devuan, Alpine Linux, CentOS-Rocky-Alma, Fedora, and ArchLinux.\n\nDesigned to automatically skip templates and specific containers during the update, enhancing its convenience and usability."
"Proxmox Backup Server|Security|$BASE_URL2/ct/proxmox-backup-server.sh|Description:\n\nProxmox Backup Server is an enterprise backup solution, for backing up and restoring VMs, containers, and physical hosts. By supporting incremental, fully deduplicated backups, Proxmox Backup Server significantly reduces network load and saves valuable storage space.\n\n\nSet a root password if using autologin. This will be the PBS password. passwd root"
)
show_menu() {
declare -A category_order
category_order["System"]=1
category_order["Maintenance"]=2
category_order["Containers"]=3
category_order["Applications"]=4
category_order["Monitoring"]=5
category_order["Networking"]=6
category_order["Security"]=7
custom_sort() {
while IFS='|' read -r name category url description; do
category=$(echo "$category" | xargs)
order=${category_order[$category]:-999}
printf "%d|%s|%s|%s|%s\n" "$order" "$name" "$category" "$url" "$description"
done | sort -n | cut -d'|' -f2-
}
while true; do
IFS=$'\n' sorted_scripts=($(printf "%s\n" "${scripts[@]}" | custom_sort))
unset IFS
HEADER=$(printf " %-57s %-20s" "$(translate "Name")" "$(translate "Category")")
menu_items=()
for script in "${sorted_scripts[@]}"; do
IFS='|' read -r name category url description <<< "$script"
translated_category=$(translate "$category")
padded_name=$(printf "%-57s" "$name")
menu_items+=("$padded_name" "$translated_category")
done
menu_items+=("$(translate "Return to Main Menu")" "")
cleanup
script_selection=$(whiptail --title "$(translate "Essential Proxmox VE Helper-Scripts")" \
--menu "\n$HEADER\n\n$(translate "Select a script to execute")" 25 78 16 \
"${menu_items[@]}" 3>&1 1>&2 2>&3)
if [ -n "$script_selection" ]; then
script_selection=$(echo "$script_selection" | xargs)
if [ "$script_selection" = "$(translate "Return to Main Menu")" ]; then
whiptail --title "Proxmox VE Helper-Scripts" \
--msgbox "$(translate "Visit the website to discover more scripts, stay updated with the latest updates, and support the project:\n\nhttps://community-scripts.github.io/ProxmoxVE")" 15 70
exec bash <(curl -s "$REPO_URL/scripts/menus/main_menu.sh")
fi
for script in "${sorted_scripts[@]}"; do
IFS='|' read -r name category url description <<< "$script"
if [ "$name" = "$script_selection" ]; then
selected_url="$url"
selected_description=$(translate "$description")
break
fi
done
if [ -n "$selected_url" ]; then
if whiptail --title "$(translate "Script Information")" \
--yes-button "$(translate "Accept")" \
--no-button "$(translate "Cancel")" \
--yesno "$selected_description" 20 78; then
#msg_info2 "$(translate "Executing script:") $script_selection"
#sleep 2
download_script "$selected_url"
msg_ok "$(translate "Script completed.")"
msg_success "$(translate "Press Enter to return to the main menu...")"
read -r
clear
else
msg_info2 "$(translate "Script execution cancelled.")"
sleep 2
fi
else
echo "$(translate "Error: Could not find the selected script URL.")"
read -rp "$(translate "Press Enter to continue...")"
fi
else
exec bash <(curl -s "$REPO_URL/scripts/menus/main_menu.sh")
fi
done
}
if [[ "$LANGUAGE" != "en" ]]; then
show_proxmenux_logo
msg_lang "$(translate "Generating automatic translations...")"
fi
show_menu

View File

@ -6,10 +6,11 @@
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : MIT (https://raw.githubusercontent.com/MacRimi/ProxMenux/main/LICENSE)
# Version : 1.1
# Last Updated: 28/05/2025
# Version : 1.0
# Last Updated: 24/02/2025
# ==========================================================
# Configuration ============================================
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
BASE_DIR="/usr/local/share/proxmenux"
UTILS_FILE="$BASE_DIR/utils.sh"
@ -18,24 +19,21 @@ VENV_PATH="/opt/googletrans-env"
if [[ -f "$UTILS_FILE" ]]; then
source "$UTILS_FILE"
fi
load_language
initialize_cache
#show_proxmenux_logo
# ==========================================================
confirm_and_run() {
local name="$1"
local command="$2"
dialog --clear --title "$(translate "Confirmation")" \
--yesno "\n\n$(translate "Do you want to run the post-installation script from") $name?" 10 70
response=$?
clear
if [ $response -eq 0 ]; then
if whiptail --title "$(translate "Confirmation")" \
--yesno "$(translate "Do you want to run the post-installation script from") $name?" \
10 70; then
eval "$command"
echo
echo ""
msg_success "$(translate 'Press ENTER to continue...')"
read -r _
else
@ -44,75 +42,62 @@ confirm_and_run() {
fi
}
scripts_es=(
"Script post-install personalizable |ProxMenux|bash <(curl -s $REPO_URL/scripts/customizable_post_install.sh)"
"Script post-install Proxmox VE |Helper-Scripts|bash -c \"\$(wget -qLO - https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/tools/pve/post-pve-install.sh); msg_success \\\"\$(translate 'Press ENTER to continue...')\\\"; read -r _\""
"Script post-iinstall xshok-proxmox|fork xshok-proxmox|confirm_and_run \"Xshok\" \"wget https://raw.githubusercontent.com/MacRimi/xshok-proxmox/master/install-post.sh -c -O install-post.sh && bash install-post.sh && rm install-post.sh\""
"Desinstalar herramientas|ProxMenux|bash <(curl -s $REPO_URL/scripts/uninstall-tools.sh)"
)
scripts_all_langs=(
# Define scripts array
scripts=(
"Customizable script post-installation|ProxMenux|bash <(curl -s $REPO_URL/scripts/customizable_post_install.sh)"
"Proxmox VE Post Install|Helper-Scripts|bash -c \"\$(wget -qLO - https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/tools/pve/post-pve-install.sh); msg_success \\\"\$(translate 'Press ENTER to continue...')\\\"; read -r _\""
"Xshok-proxmox Post install|fork xshok-proxmox|confirm_and_run \"Xshok\" \"wget https://raw.githubusercontent.com/MacRimi/xshok-proxmox/master/install-post.sh -c -O install-post.sh && bash install-post.sh && rm install-post.sh\""
"xshok-proxmox Post install|fork xshok-proxmox|confirm_and_run \"Xshok\" \"wget https://raw.githubusercontent.com/MacRimi/xshok-proxmox/master/install-post.sh -c -O install-post.sh && bash install-post.sh && rm install-post.sh\""
"Uninstall Tools|ProxMenux|bash <(curl -s $REPO_URL/scripts/uninstall-tools.sh)"
)
show_menu() {
while true; do
local HEADER
local current_scripts=()
if [[ "$LANGUAGE" == "es" ]]; then
HEADER="\n Seleccione un script post-instalación:\n\n Descripción │ Fuente"
current_scripts=("${scripts_es[@]}")
else
HEADER="\n$(translate " Select a post-installation script:")\n\n Description │ Source"
current_scripts=("${scripts_all_langs[@]}")
fi
HEADER=$(printf " %-52s %-20s" "$(translate "Name")" "$(translate "Repository")")
menu_items=()
for i in "${!current_scripts[@]}"; do
IFS='|' read -r name repository command <<< "${current_scripts[$i]}"
for i in "${!scripts[@]}"; do
IFS='|' read -r name repository command <<< "${scripts[$i]}"
number=$((i+1))
local display_name="$name"
[[ "$LANGUAGE" != "es" ]] && display_name="$(translate "$name")"
formatted_line=$(printf "%-47s │ %s" "$display_name" "$repository")
menu_items+=("$number" "$formatted_line")
padded_option=$(printf "%2d %-50s" "$number" "$(translate "$name")")
menu_items+=("$padded_option" "$repository")
done
menu_items+=("$(( ${#current_scripts[@]}+1 ))" "$(translate "Return to Main Menu")")
exec 3>&1
script_selection=$(dialog --clear --backtitle "ProxMenux" --title "$(translate "Post-Installation Scripts Menu")" \
--menu "$HEADER" 20 78 $((${#menu_items[@]}/2)) \
"${menu_items[@]}" 2>&1 1>&3)
exit_status=$?
exec 3>&-
menu_items+=("$(printf "%2d %-40s" "$((${#scripts[@]}+1))" "$(translate "Return to Main Menu")")" "")
cleanup
script_selection=$(whiptail --title "$(translate "Post-Installation Scripts Menu")" \
--menu "\n$HEADER" 20 78 $((${#scripts[@]}+1)) \
"${menu_items[@]}" 3>&1 1>&2 2>&3)
if [ $exit_status -ne 0 ]; then
if [ -n "$script_selection" ]; then
selected_number=$(echo "$script_selection" | awk '{print $1}')
if [ "$selected_number" = "$((${#scripts[@]}+1))" ]; then
#show_proxmenux_logo
exec bash <(curl -s "$REPO_URL/scripts/menus/main_menu.sh")
fi
index=$((selected_number - 1))
if [ $index -ge 0 ] && [ $index -lt ${#scripts[@]} ]; then
IFS='|' read -r name repository command <<< "${scripts[$index]}"
eval "$command"
fi
else
#show_proxmenux_logo
exec bash <(curl -s "$REPO_URL/scripts/menus/main_menu.sh")
fi
if [ "$script_selection" = "$(( ${#current_scripts[@]} + 1 ))" ]; then
exec bash <(curl -s "$REPO_URL/scripts/menus/main_menu.sh")
fi
index=$((script_selection - 1))
if [ $index -ge 0 ] && [ $index -lt ${#current_scripts[@]} ]; then
IFS='|' read -r _ _ command <<< "${current_scripts[$index]}"
eval "$command"
fi
done
}
clear
if [[ "$LANGUAGE" != "en" ]]; then
show_proxmenux_logo
msg_lang "$(translate "Generating automatic translations...")"
fi
#if [[ "$LANGUAGE" != "en" ]]; then
# show_proxmenux_logo
# msg_lang "$(translate "Generating automatic translations...")"
#fi
cleanup
show_menu

View File

@ -26,32 +26,36 @@ initialize_cache
while true; do
OPTION=$(dialog --clear --backtitle "ProxMenux" --title "$(translate "Disk and Storage Manager Menu")" \
--menu "\n$(translate "Select an option:")" 20 70 10 \
"1" "$(translate "Add Disk") Passthrough $(translate "to a VM")" \
"2" "$(translate "Add Disk") Passthrough $(translate "to a LXC")" \
"3" "$(translate "Import Disk Image to a VM")" \
"4" "$(translate "Return to Main Menu")" \
2>&1 >/dev/tty)
OPTION=$(whiptail --title "$(translate "Disk and Storage Manager Menu")" --menu "$(translate "Select an option:")" 20 70 10 \
"1" "$(translate "Add Disk Passthrough to a VM")" \
"2" "$(translate "Add Disk") Passthrough $(translate "to a CT")" \
"3" "$(translate "Import Disk Image to a VM")" \
"4" "$(translate "Return to Main Menu")" 3>&1 1>&2 2>&3)
case $OPTION in
1)
clear
#show_proxmenux_logo
msg_info2 "$(translate "Running script: Add Disk Passthrough to a VM")..."
bash <(curl -s "$REPO_URL/scripts/storage/disk-passthrough.sh")
;;
2)
clear
#show_proxmenux_logo
msg_info2 "$(translate "Running script: Add Disk Passthrough to a CT")..."
bash <(curl -s "$REPO_URL/scripts/storage/disk-passthrough_ct.sh")
;;
3)
clear
#show_proxmenux_logo
msg_info2 "$(translate "Running script: Import Disk Image to a VM")..."
bash <(curl -s "$REPO_URL/scripts/storage/import-disk-image.sh")
;;
4)
#show_proxmenux_logo
exec bash <(curl -s "$REPO_URL/scripts/menus/main_menu.sh")
;;
*)
#show_proxmenux_logo
exec bash <(curl -s "$REPO_URL/scripts/menus/main_menu.sh")
;;
esac
done

View File

@ -6,8 +6,8 @@
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : MIT (https://raw.githubusercontent.com/MacRimi/ProxMenux/main/LICENSE)
# Version : 1.1
# Last Updated: 28/06/2025
# Version : 1.0
# Last Updated: 28/01/2025
# ==========================================================
# Description:
# This script allows users to assign physical disks to existing
@ -19,6 +19,7 @@
# - Configures the selected disks for the CT and verifies the assignment.
# ==========================================================
# Configuration ============================================
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
BASE_DIR="/usr/local/share/proxmenux"
@ -28,12 +29,12 @@ VENV_PATH="/opt/googletrans-env"
if [[ -f "$UTILS_FILE" ]]; then
source "$UTILS_FILE"
fi
load_language
initialize_cache
# ==========================================================
get_disk_info() {
local disk=$1
MODEL=$(lsblk -dn -o MODEL "$disk" | xargs)
@ -41,13 +42,15 @@ get_disk_info() {
echo "$MODEL" "$SIZE"
}
CT_LIST=$(pct list | awk 'NR>1 {print $1, $3}')
CT_LIST=$(pct list | awk 'NR>1 {print $1, $3}')
if [ -z "$CT_LIST" ]; then
whiptail --title "$(translate "Error")" --msgbox "$(translate "No CTs available in the system.")" 8 40
exit 1
fi
CTID=$(whiptail --title "$(translate "Select CT for destination disk")" --menu "$(translate "Select the CT to which you want to add disks:")" 15 60 8 $CT_LIST 3>&1 1>&2 2>&3)
if [ -z "$CTID" ]; then
@ -57,32 +60,32 @@ fi
CTID=$(echo "$CTID" | tr -d '"')
clear
show_proxmenux_logo
echo -e
msg_info2 "$(translate "Add Disk") Passthrough $(translate "to a LXC")"
echo -e
msg_ok "$(translate "CT selected successfully.")"
CT_STATUS=$(pct status "$CTID" | awk '{print $2}')
CT_STATUS=$(pct status "$CTID" | awk '{print $2}')
if [ "$CT_STATUS" != "running" ]; then
msg_info "$(translate "Starting CT") $CTID..."
pct start "$CTID"
sleep 2
if [ "$(pct status "$CTID" | awk '{print $2}')" != "running" ]; then
msg_error "$(translate "Failed to start the CT.")"
sleep 2
exit 1
fi
msg_ok "$(translate "CT started successfully.")"
fi
CONF_FILE="/etc/pve/lxc/$CTID.conf"
if grep -q '^unprivileged: 1' "$CONF_FILE"; then
if whiptail --title "$(translate "Privileged Container")" \
--yesno "$(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?")" 12 70; then
msg_info "$(translate "Stopping container") $CTID..."
pct shutdown "$CTID" &>/dev/null
for i in {1..10}; do
@ -91,15 +94,20 @@ if grep -q '^unprivileged: 1' "$CONF_FILE"; then
break
fi
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.")"
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.")"
msg_info "$(translate "Starting container") $CTID..."
pct start "$CTID" &>/dev/null
sleep 2
@ -108,6 +116,7 @@ if grep -q '^unprivileged: 1' "$CONF_FILE"; then
exit 1
fi
msg_ok "$(translate "Container started successfully.")"
else
whiptail --title "$(translate "Aborted")" \
--msgbox "$(translate "Operation cancelled. Cannot continue with an unprivileged container.")" 10 60
@ -115,7 +124,16 @@ if grep -q '^unprivileged: 1' "$CONF_FILE"; then
fi
fi
##########################################
msg_info "$(translate "Detecting available disks...")"
USED_DISKS=$(lsblk -n -o PKNAME,TYPE | grep 'lvm' | awk '{print "/dev/" $1}')
@ -133,6 +151,7 @@ for entry in $ZFS_RAW; do
elif [[ "$entry" == /dev/* ]]; then
path="$entry"
fi
if [ -n "$path" ]; then
base_disk=$(lsblk -no PKNAME "$path" 2>/dev/null)
if [ -n "$base_disk" ]; then
@ -141,36 +160,44 @@ for entry in $ZFS_RAW; do
fi
done
ZFS_DISKS=$(echo "$ZFS_DISKS" | sort -u)
is_disk_in_use() {
local disk="$1"
while read -r part fstype; do
case "$fstype" in
zfs_member|linux_raid_member)
return 0 ;;
esac
if echo "$MOUNTED_DISKS" | grep -q "/dev/$part"; then
return 0
fi
done < <(lsblk -ln -o NAME,FSTYPE "$disk" | tail -n +2)
if echo "$USED_DISKS" | grep -q "$disk" || echo "$ZFS_DISKS" | grep -q "$disk"; then
return 0
fi
return 1
}
FREE_DISKS=()
LVM_DEVICES=$(pvs --noheadings -o pv_name 2> >(grep -v 'File descriptor .* leaked') | xargs -n1 readlink -f | sort -u)
RAID_ACTIVE=$(grep -Po 'md\d+\s*:\s*active\s+raid[0-9]+' /proc/mdstat | awk '{print $1}' | sort -u)
while read -r DISK; do
[[ "$DISK" =~ /dev/zd ]] && continue
INFO=($(get_disk_info "$DISK"))
MODEL="${INFO[@]::${#INFO[@]}-1}"
SIZE="${INFO[-1]}"
LABEL=""
SHOW_DISK=true
IS_MOUNTED=false
IS_RAID=false
IS_ZFS=false
@ -190,9 +217,11 @@ while read -r DISK; do
IS_MOUNTED=true
fi
USED_BY=""
REAL_PATH=$(readlink -f "$DISK")
CONFIG_DATA=$(grep -vE '^\s*#' /etc/pve/qemu-server/*.conf /etc/pve/lxc/*.conf 2>/dev/null)
if grep -Fq "$REAL_PATH" <<< "$CONFIG_DATA"; then
USED_BY="$(translate "In use")"
else
@ -206,6 +235,8 @@ while read -r DISK; do
done
fi
if $IS_RAID && grep -q "$DISK" <<< "$(cat /proc/mdstat)"; then
if grep -q "active raid" /proc/mdstat; then
SHOW_DISK=false
@ -229,6 +260,7 @@ while read -r DISK; do
[[ "$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
@ -243,9 +275,19 @@ fi
msg_ok "$(translate "Available disks detected.")"
######################################################
MAX_WIDTH=$(printf "%s\n" "${FREE_DISKS[@]}" | awk '{print length}' | sort -nr | head -n1)
TOTAL_WIDTH=$((MAX_WIDTH + 20))
if [ $TOTAL_WIDTH -lt 50 ]; then
TOTAL_WIDTH=50
fi
@ -270,10 +312,12 @@ msg_info "$(translate "Processing selected disks...")"
for DISK in $SELECTED; do
DISK=$(echo "$DISK" | tr -d '"')
DISK_INFO=$(get_disk_info "$DISK")
ASSIGNED_TO=""
RUNNING_CTS=""
RUNNING_VMS=""
while read -r CT_ID CT_NAME; do
if [[ "$CT_ID" =~ ^[0-9]+$ ]] && pct config "$CT_ID" | grep -q "$DISK"; then
ASSIGNED_TO+="CT $CT_ID $CT_NAME\n"
@ -284,6 +328,7 @@ for DISK in $SELECTED; do
fi
done < <(pct list | awk 'NR>1 {print $1, $3}')
while read -r VM_ID VM_NAME; do
if [[ "$VM_ID" =~ ^[0-9]+$ ]] && qm config "$VM_ID" | grep -q "$DISK"; then
ASSIGNED_TO+="VM $VM_ID $VM_NAME\n"
@ -309,12 +354,18 @@ for DISK in $SELECTED; do
fi
cleanup
if lsblk "$DISK" | grep -q "raid" || grep -q "${DISK##*/}" /proc/mdstat; then
whiptail --title "$(translate "RAID Detected")" --msgbox "$(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.")" 18 70
clear
exit
fi
MOUNT_POINT=$(whiptail --title "$(translate "Mount Point")" --inputbox "$(translate "Enter the mount point for the disk (e.g., /mnt/disk_passthrough):")" 10 60 "/mnt/disk_passthrough" 3>&1 1>&2 2>&3)
if [ -z "$MOUNT_POINT" ]; then
@ -324,12 +375,18 @@ for DISK in $SELECTED; do
msg_ok "$(translate "Mount point specified: $MOUNT_POINT")"
PARTITION=$(lsblk -rno NAME "$DISK" | awk -v disk="$(basename "$DISK")" '$1 != disk {print $1; exit}')
SKIP_FORMAT=false
if [ -n "$PARTITION" ]; then
PARTITION="/dev/$PARTITION"
CURRENT_FS=$(lsblk -no FSTYPE "$PARTITION" | xargs)
if [[ "$CURRENT_FS" == "ext4" || "$CURRENT_FS" == "xfs" || "$CURRENT_FS" == "btrfs" ]]; then
SKIP_FORMAT=true
msg_ok "$(translate "Detected existing filesystem") $CURRENT_FS $(translate "on") $PARTITION."
@ -340,22 +397,27 @@ for DISK in $SELECTED; do
fi
fi
else
CURRENT_FS=$(lsblk -no FSTYPE "$DISK" | xargs)
if [[ "$CURRENT_FS" == "ext4" || "$CURRENT_FS" == "xfs" || "$CURRENT_FS" == "btrfs" ]]; then
SKIP_FORMAT=true
PARTITION="$DISK"
msg_ok "$(translate "Detected filesystem") $CURRENT_FS $(translate "directly on disk") $DISK."
else
whiptail --title "$(translate "No Valid Partitions")" --yesno "$(translate "The disk has no partitions and no valid filesystem. Do you want to create a new partition and format it?")" 10 70
if [ $? -ne 0 ]; then
continue
fi
echo -e "$(translate "Creating partition table and partition...")"
parted -s "$DISK" mklabel gpt
parted -s "$DISK" mkpart primary 0% 100%
sleep 2
partprobe "$DISK"
sleep 2
PARTITION=$(lsblk -rno NAME "$DISK" | awk -v disk="$(basename "$DISK")" '$1 != disk {print $1; exit}')
if [ -n "$PARTITION" ]; then
PARTITION="/dev/$PARTITION"
@ -366,12 +428,17 @@ for DISK in $SELECTED; do
fi
fi
if [ "$SKIP_FORMAT" != true ]; then
CURRENT_FS=$(lsblk -no FSTYPE "$PARTITION" | xargs)
if [[ "$CURRENT_FS" == "ext4" || "$CURRENT_FS" == "xfs" || "$CURRENT_FS" == "btrfs" ]]; then
SKIP_FORMAT=true
msg_ok "$(translate "Detected existing filesystem") $CURRENT_FS $(translate "on") $PARTITION. $(translate "Skipping format.")"
else
FORMAT_TYPE=$(whiptail --title "$(translate "Select Format Type")" --menu "$(translate "Select the filesystem type for") $DISK_INFO:" 15 60 6 \
"ext4" "$(translate "Extended Filesystem 4 (recommended)")" \
"xfs" "$(translate "XFS Filesystem")" \
@ -383,7 +450,6 @@ for DISK in $SELECTED; do
fi
whiptail --title "$(translate "WARNING")" --yesno "$(translate "WARNING: This operation will FORMAT the disk") $DISK_INFO $(translate "with") $FORMAT_TYPE.\\n\\n$(translate "ALL DATA ON THIS DISK WILL BE PERMANENTLY LOST!")\\n\\n$(translate "Are you sure you want to continue")" 15 70
if [ $? -ne 0 ]; then
whiptail --title "$(translate "Format Cancelled")" --msgbox "$(translate "Format operation cancelled. The disk will not be added.")" 8 60
continue
@ -391,8 +457,13 @@ for DISK in $SELECTED; do
fi
fi
if [ "$SKIP_FORMAT" != true ]; then
echo -e "$(translate "Formatting partition") $PARTITION $(translate "with") $FORMAT_TYPE..."
case "$FORMAT_TYPE" in
"ext4") mkfs.ext4 -F "$PARTITION" ;;
"xfs") mkfs.xfs -f "$PARTITION" ;;
@ -409,55 +480,28 @@ for DISK in $SELECTED; do
fi
fi
INDEX=0
while pct config "$CTID" | grep -q "mp${INDEX}:"; do
((INDEX++))
done
# Determine the filesystem type for mount options
CURRENT_FS=$(lsblk -no FSTYPE "$PARTITION" | xargs)
if [[ -n "$CURRENT_FS" ]]; then
FORMAT_TYPE="$CURRENT_FS"
fi
# Install filesystem tools in container if needed
FS_PKG=""
FS_BIN=""
if [[ "$FORMAT_TYPE" == "xfs" ]]; then
FS_PKG="xfsprogs"
FS_BIN="mkfs.xfs"
elif [[ "$FORMAT_TYPE" == "btrfs" ]]; then
FS_PKG="btrfs-progs"
FS_BIN="mkfs.btrfs"
fi
if [[ -n "$FS_PKG" && -n "$FS_BIN" ]]; 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 pct exec "$CTID" -- sh -c "[ -f /etc/alpine-release ]"; then
pct exec "$CTID" -- sh -c "apk update >/dev/null && apk add --no-progress $FS_PKG >/dev/null"
elif pct exec "$CTID" -- sh -c "[ -f /etc/os-release ] && (grep -qE 'debian|ubuntu' /etc/os-release)"; then
pct exec "$CTID" -- sh -c "apt-get update -qq >/dev/null && apt-get install -y -qq $FS_PKG >/dev/null"
fi
msg_ok "$(translate "Required tools for $FORMAT_TYPE installed in CT $CTID.")"
fi
fi
##############################################################################
CURRENT_FS=$(lsblk -no FSTYPE "$PARTITION" | xargs)
if [ "$CURRENT_FS" == "xfs" ] || [ "$FORMAT_TYPE" == "xfs" ]; then
RESULT=$(pct set "$CTID" -mp${INDEX} "$PARTITION,mp=$MOUNT_POINT,backup=0,ro=0,acl=1" 2>&1)
RESULT=$(pct set "$CTID" -mp${INDEX} "$PARTITION,mp=$MOUNT_POINT,backup=0,ro=0" 2>&1)
else
pct exec "$CTID" -- chmod -R 777 "$MOUNT_POINT"
RESULT=$(pct set "$CTID" -mp${INDEX} "$PARTITION,mp=$MOUNT_POINT,backup=0,ro=0,acl=1" 2>&1)
fi
pct exec "$CTID" -- chmod -R 777 "$MOUNT_POINT" 2>/dev/null || true
##############################################################################
if [ $? -eq 0 ]; then
MESSAGE="$(translate "The disk") $DISK_INFO $(translate "has been successfully added to CT") $CTID $(translate "as a mount point at") $MOUNT_POINT."
if [ -n "$ASSIGNED_TO" ]; then
@ -469,9 +513,10 @@ for DISK in $SELECTED; do
else
ERROR_MESSAGES+="$(translate "Could not add disk") $DISK_INFO $(translate "to CT") $CTID.\\n$(translate "Error:") $RESULT\\n\\n"
fi
done
msg_ok "$(translate "Disk processing completed.")"
if [ -n "$SUCCESS_MESSAGES" ]; then

View File

@ -6,15 +6,15 @@
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : MIT (https://raw.githubusercontent.com/MacRimi/ProxMenux/main/LICENSE)
# Version : 1.1
# Last Updated: 29/05/2025
# Version : 1.0
# Last Updated: 28/01/2025
# ==========================================================
# Description:
# This script automates the process of importing disk images into Proxmox VE virtual machines (VMs),
# making it easy to attach pre-existing disk files without manual configuration.
#
# Before running the script, ensure that disk images are available in /var/lib/vz/template/images/.
# The script scans this directory for compatible formats (.img, .qcow2, .vmdk, .raw) and lists the available files.
# The script scans this directory for compatible formats (.img, .qcow2, .vmdk) and lists the available files.
#
# Using an interactive menu, you can:
# - Select a VM to attach the imported disk.
@ -32,65 +32,40 @@ BASE_DIR="/usr/local/share/proxmenux"
UTILS_FILE="$BASE_DIR/utils.sh"
VENV_PATH="/opt/googletrans-env"
[[ -f "$UTILS_FILE" ]] && source "$UTILS_FILE"
if [[ -f "$UTILS_FILE" ]]; then
source "$UTILS_FILE"
fi
load_language
initialize_cache
# Configuration ============================================
# ==========================================================
# Path where disk images are stored
IMAGES_DIR="/var/lib/vz/template/images/"
detect_image_dir() {
for store in $(pvesm status -content images | awk 'NR>1 {print $1}'); do
path=$(pvesm path "${store}:template" 2>/dev/null)
if [[ -d "$path" ]]; then
for ext in raw img qcow2 vmdk; do
if compgen -G "$path/*.$ext" > /dev/null; then
echo "$path"
return 0
fi
done
for sub in images iso; do
dir="$path/$sub"
if [[ -d "$dir" ]]; then
for ext in raw img qcow2 vmdk; do
if compgen -G "$dir/*.$ext" > /dev/null; then
echo "$dir"
return 0
fi
done
fi
done
fi
done
for fallback in /var/lib/vz/template/images /var/lib/vz/template/iso; do
if [[ -d "$fallback" ]]; then
for ext in raw img qcow2 vmdk; do
if compgen -G "$fallback/*.$ext" > /dev/null; then
echo "$fallback"
return 0
fi
done
fi
done
return 1
}
IMAGES_DIR=$(detect_image_dir)
if [[ -z "$IMAGES_DIR" ]]; then
dialog --title "$(translate 'No Images Found')" \
--msgbox "$(translate 'Could not find any directory containing disk images')\n\n$(translate 'Make sure there is at least one file with extension .img, .qcow2, .vmdk or .raw')" 15 60
exit 1
# Initial setup
if [ ! -d "$IMAGES_DIR" ]; then
msg_info "$(translate 'Creating images directory')"
mkdir -p "$IMAGES_DIR"
chmod 755 "$IMAGES_DIR"
msg_ok "$(translate 'Images directory created:') $IMAGES_DIR"
fi
IMAGES=$(ls -A "$IMAGES_DIR" | grep -E "\.(img|qcow2|vmdk|raw)$")
# Check if there are any images in the directory
IMAGES=$(ls -A "$IMAGES_DIR" | grep -E "\.(img|qcow2|vmdk)$")
if [ -z "$IMAGES" ]; then
dialog --title "$(translate 'No Disk Images Found')" \
--msgbox "$(translate 'No compatible disk images found in:')\n\n$IMAGES_DIR\n\n$(translate 'Supported formats: .img, .qcow2, .vmdk, .raw')" 15 60
exit 1
whiptail --title "$(translate 'No Images Found')" \
--msgbox "$(translate 'No images available for import in:')\n\n$IMAGES_DIR\n\n$(translate 'Supported formats: .img, .qcow2, .vmdk')\n\n$(translate 'Please add some images and try again.')" 15 60
exit 1
fi
# Display initial message
whiptail --title "$(translate 'Import Disk Image')" --msgbox "$(translate 'Make sure the disk images you want to import are located in:')\n\n$IMAGES_DIR\n\n$(translate 'Supported formats: .img, .qcow2, .vmdk.')" 15 60
# 1. Select VM
msg_info "$(translate 'Getting VM list')"
VM_LIST=$(qm list | awk 'NR>1 {print $1" "$2}')
@ -103,7 +78,7 @@ msg_ok "$(translate 'VM list obtained')"
VMID=$(whiptail --title "$(translate 'Select VM')" --menu "$(translate 'Select the VM where you want to import the disk image:')" 15 60 8 $VM_LIST 3>&1 1>&2 2>&3)
if [ -z "$VMID" ]; then
# msg_error "$(translate 'No VM selected')"
exit 1
fi
@ -118,7 +93,7 @@ if [ -z "$STORAGE_LIST" ]; then
fi
msg_ok "$(translate 'Storage volumes obtained')"
# Create an array of storage options for whiptail
STORAGE_OPTIONS=()
while read -r storage; do
STORAGE_OPTIONS+=("$storage" "")
@ -127,7 +102,7 @@ done <<< "$STORAGE_LIST"
STORAGE=$(whiptail --title "$(translate 'Select Storage')" --menu "$(translate 'Select the storage volume for disk import:')" 15 60 8 "${STORAGE_OPTIONS[@]}" 3>&1 1>&2 2>&3)
if [ -z "$STORAGE" ]; then
# msg_error "$(translate 'No storage selected')"
exit 1
fi
@ -149,19 +124,17 @@ done <<< "$IMAGES"
SELECTED_IMAGES=$(whiptail --title "$(translate 'Select Disk Images')" --checklist "$(translate 'Select the disk images to import:')" 20 60 10 "${IMAGE_OPTIONS[@]}" 3>&1 1>&2 2>&3)
if [ -z "$SELECTED_IMAGES" ]; then
# msg_error "$(translate 'No images selected')"
exit 1
fi
# 4. Import each selected image
for IMAGE in $SELECTED_IMAGES; do
# Remove quotes from selected image
IMAGE=$(echo "$IMAGE" | tr -d '"')
# 5. Select interface type for each image
INTERFACE=$(whiptail --title "$(translate 'Interface Type')" --menu "$(translate 'Select the interface type for the image:') $IMAGE" 15 40 4 \
"sata" "SATA" \
"scsi" "SCSI" \
@ -175,63 +148,57 @@ for IMAGE in $SELECTED_IMAGES; do
FULL_PATH="$IMAGES_DIR/$IMAGE"
# Show initial message
msg_info "$(translate 'Importing image:')"
# Temporary file to capture the imported disk
TEMP_DISK_FILE=$(mktemp)
# Execute the command and process its output in real-time
qm importdisk "$VMID" "$FULL_PATH" "$STORAGE" 2>&1 | while read -r line; do
if [[ "$line" =~ transferred ]]; then
PERCENT=$(echo "$line" | grep -oP "\d+\.\d+(?=%)")
echo -ne "\r${TAB}${BL}-$(translate 'Importing image:') $IMAGE-${CL} ${PERCENT}%"
# Extract the progress percentage
PERCENT=$(echo "$line" | grep -oP "\(\d+\.\d+%\)" | tr -d '()%')
# Show progress with custom format without translation
echo -ne "\r${TAB}${YW}-$(translate 'Importing image:') $IMAGE-${CL} ${PERCENT}%"
elif [[ "$line" =~ successfully\ imported\ disk ]]; then
# Extract the imported disk name and save it to the temporary file
echo "$line" | grep -oP "(?<=successfully imported disk ').*(?=')" > "$TEMP_DISK_FILE"
fi
done
echo -ne "\n"
IMPORT_STATUS=${PIPESTATUS[0]}
IMPORT_STATUS=${PIPESTATUS[0]} # Capture the exit status of the main command
if [ $IMPORT_STATUS -eq 0 ]; then
msg_ok "$(translate 'Image imported successfully')"
# Read the imported disk from the temporary file
IMPORTED_DISK=$(cat "$TEMP_DISK_FILE")
rm -f "$TEMP_DISK_FILE"
if [ -z "$IMPORTED_DISK" ]; then
STORAGE_TYPE=$(pvesm status -storage "$STORAGE" | awk 'NR>1 {print $2}')
if [[ "$STORAGE_TYPE" == "btrfs" || "$STORAGE_TYPE" == "dir" || "$STORAGE_TYPE" == "nfs" ]]; then
UNUSED_LINE=$(qm config "$VMID" | grep -E '^unused[0-9]+:')
IMPORTED_ID=$(echo "$UNUSED_LINE" | cut -d: -f1)
IMPORTED_DISK=$(echo "$UNUSED_LINE" | cut -d: -f2- | xargs)
else
IMPORTED_DISK=$(qm config "$VMID" | grep -E 'unused[0-9]+' | tail -1 | cut -d: -f2- | xargs)
IMPORTED_ID=$(qm config "$VMID" | grep -E 'unused[0-9]+' | tail -1 | cut -d: -f1)
fi
fi
rm -f "$TEMP_DISK_FILE" # Delete the temporary file
if [ -n "$IMPORTED_DISK" ]; then
# Find the next available disk slot
EXISTING_DISKS=$(qm config "$VMID" | grep -oP "${INTERFACE}\d+" | sort -n)
if [ -z "$EXISTING_DISKS" ]; then
# If there are no existing disks, start from 0
NEXT_SLOT=0
else
# If there are existing disks, take the last one and add 1
LAST_SLOT=$(echo "$EXISTING_DISKS" | tail -n1 | sed "s/${INTERFACE}//")
NEXT_SLOT=$((LAST_SLOT + 1))
fi
# Ask if SSD emulation is desired (only for non-VirtIO interfaces)
if [ "$INTERFACE" != "virtio" ]; then
if (whiptail --title "$(translate 'SSD Emulation')" --yesno "$(translate 'Do you want to use SSD emulation for this disk?')" 10 60); then
SSD_OPTION=",ssd=1"
@ -242,18 +209,14 @@ for IMAGE in $SELECTED_IMAGES; do
SSD_OPTION=""
fi
msg_info "$(translate 'Configuring disk')"
# Configure the disk in the VM
if qm set "$VMID" --${INTERFACE}${NEXT_SLOT} "$IMPORTED_DISK${SSD_OPTION}" &>/dev/null; then
msg_ok "$(translate 'Image') $IMAGE $(translate 'configured as') ${INTERFACE}${NEXT_SLOT}"
if [[ -n "$IMPORTED_ID" ]]; then
qm set "$VMID" -delete "$IMPORTED_ID" >/dev/null 2>&1
fi
# Ask if the disk should be bootable
if (whiptail --title "$(translate 'Make Bootable')" --yesno "$(translate 'Do you want to make this disk bootable?')" 10 60); then
msg_info "$(translate 'Configuring disk as bootable')"
@ -265,22 +228,14 @@ for IMAGE in $SELECTED_IMAGES; do
fi
else
msg_error "$(translate 'Could not configure disk') ${INTERFACE}${NEXT_SLOT} $(translate 'for VM') $VMID"
echo "DEBUG: Tried to configure: --${INTERFACE}${NEXT_SLOT} \"$IMPORTED_DISK${SSD_OPTION}\""
echo "DEBUG: VM config after import:"
qm config "$VMID" | grep -E "(unused|${INTERFACE})"
fi
else
msg_error "$(translate 'Could not find the imported disk')"
echo "DEBUG: VM config after import:"
qm config "$VMID"
fi
else
msg_error "$(translate 'Could not import') $IMAGE"
fi
done
msg_ok "$(translate 'All selected images have been processed')"
msg_success "$(translate "Press Enter to return to menu...")"
read -r
sleep 2

View File

@ -1,446 +0,0 @@
#!/bin/bash
# ==========================================================
# ProxMenu - Mount independent disk on Proxmox host
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : MIT
# Version : 1.3-dialog
# Last Updated: 13/12/2024
# ==========================================================
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
BASE_DIR="/usr/local/share/proxmenux"
UTILS_FILE="$BASE_DIR/utils.sh"
VENV_PATH="/opt/googletrans-env"
if [[ -f "$UTILS_FILE" ]]; then
source "$UTILS_FILE"
fi
load_language
initialize_cache
mount_disk_host_bk() {
get_disk_info() {
local disk=$1
MODEL=$(lsblk -dn -o MODEL "$disk" | xargs)
SIZE=$(lsblk -dn -o SIZE "$disk" | xargs)
echo "$MODEL" "$SIZE"
}
is_usb_disk() {
local disk=$1
local disk_name=$(basename "$disk")
if readlink -f "/sys/block/$disk_name/device" 2>/dev/null | grep -q "usb"; then
return 0
fi
if udevadm info --query=property --name="$disk" 2>/dev/null | grep -q "ID_BUS=usb"; then
return 0
fi
return 1
}
is_system_disk() {
local disk=$1
local disk_name=$(basename "$disk")
local system_mounts=$(df -h | grep -E '^\S+\s+\S+\s+\S+\s+\S+\s+\S+\s+(/|/boot|/usr|/var|/home)$' | awk '{print $1}')
for mount_dev in $system_mounts; do
local mount_disk=""
if [[ "$mount_dev" =~ ^/dev/mapper/ ]]; then
local vg_name=$(lvs --noheadings -o vg_name "$mount_dev" 2>/dev/null | xargs)
if [[ -n "$vg_name" ]]; then
local pvs_list=$(pvs --noheadings -o pv_name -S vg_name="$vg_name" 2>/dev/null | xargs)
for pv in $pvs_list; do
if [[ -n "$pv" && -e "$pv" ]]; then
mount_disk=$(lsblk -no PKNAME "$pv" 2>/dev/null)
if [[ -n "$mount_disk" && "/dev/$mount_disk" == "$disk" ]]; then
return 0
fi
fi
done
fi
elif [[ "$mount_dev" =~ ^/dev/[hsv]d[a-z][0-9]* || "$mount_dev" =~ ^/dev/nvme[0-9]+n[0-9]+p[0-9]+ ]]; then
mount_disk=$(lsblk -no PKNAME "$mount_dev" 2>/dev/null)
if [[ -n "$mount_disk" && "/dev/$mount_disk" == "$disk" ]]; then
return 0
fi
fi
done
local fs_type=$(lsblk -no FSTYPE "$disk" 2>/dev/null | head -1)
if [[ "$fs_type" == "btrfs" ]]; then
local temp_mount=$(mktemp -d)
if mount -o ro "$disk" "$temp_mount" 2>/dev/null; then
if btrfs subvolume list "$temp_mount" 2>/dev/null | grep -qE '(@|@home|@var|@boot|@root|root)'; then
umount "$temp_mount" 2>/dev/null
rmdir "$temp_mount" 2>/dev/null
return 0
fi
umount "$temp_mount" 2>/dev/null
fi
rmdir "$temp_mount" 2>/dev/null
while read -r part; do
if [[ -n "$part" ]]; then
local part_fs=$(lsblk -no FSTYPE "/dev/$part" 2>/dev/null)
if [[ "$part_fs" == "btrfs" ]]; then
local mount_point=$(lsblk -no MOUNTPOINT "/dev/$part" 2>/dev/null)
if [[ "$mount_point" == "/" || "$mount_point" == "/boot" || "$mount_point" == "/home" || "$mount_point" == "/var" ]]; then
return 0
fi
fi
fi
done < <(lsblk -ln -o NAME "$disk" | tail -n +2)
fi
local disk_uuid=$(blkid -s UUID -o value "$disk" 2>/dev/null)
local part_uuids=()
while read -r part; do
if [[ -n "$part" ]]; then
local uuid=$(blkid -s UUID -o value "/dev/$part" 2>/dev/null)
if [[ -n "$uuid" ]]; then
part_uuids+=("$uuid")
fi
fi
done < <(lsblk -ln -o NAME "$disk" | tail -n +2)
for uuid in "${part_uuids[@]}" "$disk_uuid"; do
if [[ -n "$uuid" ]] && grep -q "UUID=$uuid" /etc/fstab; then
local mount_point=$(grep "UUID=$uuid" /etc/fstab | awk '{print $2}')
if [[ "$mount_point" == "/" || "$mount_point" == "/boot" || "$mount_point" == "/home" || "$mount_point" == "/var" ]]; then
return 0
fi
fi
done
if grep -q "$disk" /etc/fstab; then
local mount_point=$(grep "$disk" /etc/fstab | awk '{print $2}')
if [[ "$mount_point" == "/" || "$mount_point" == "/boot" || "$mount_point" == "/home" || "$mount_point" == "/var" ]]; then
return 0
fi
fi
local disk_count=$(lsblk -dn -e 7,11 -o PATH | wc -l)
if [[ "$disk_count" -eq 1 ]]; then
return 0
fi
return 1
}
msg_info "$(translate "Detecting available disks...")"
USED_DISKS=$(lsblk -n -o PKNAME,TYPE | grep 'lvm' | awk '{print "/dev/" $1}')
MOUNTED_DISKS=$(lsblk -ln -o NAME,MOUNTPOINT | awk '$2!="" {print "/dev/" $1}')
ZFS_DISKS=""
ZFS_RAW=$(zpool list -v -H 2>/dev/null | awk '{print $1}' | grep -v '^NAME$' | grep -v '^-' | grep -v '^mirror')
for entry in $ZFS_RAW; do
path=""
if [[ "$entry" == wwn-* || "$entry" == ata-* ]]; then
if [ -e "/dev/disk/by-id/$entry" ]; then
path=$(readlink -f "/dev/disk/by-id/$entry")
fi
elif [[ "$entry" == /dev/* ]]; then
path="$entry"
fi
if [ -n "$path" ]; then
base_disk=$(lsblk -no PKNAME "$path" 2>/dev/null)
if [ -n "$base_disk" ]; then
ZFS_DISKS+="/dev/$base_disk"$'\n'
fi
fi
done
ZFS_DISKS=$(echo "$ZFS_DISKS" | sort -u)
LVM_DEVICES=$(
pvs --noheadings -o pv_name 2> >(grep -v 'File descriptor .* leaked') |
while read -r dev; do
[[ -n "$dev" && -e "$dev" ]] && readlink -f "$dev"
done | sort -u
)
FREE_DISKS=()
while read -r DISK; do
[[ "$DISK" =~ /dev/zd ]] && continue
INFO=($(get_disk_info "$DISK"))
MODEL="${INFO[@]::${#INFO[@]}-1}"
SIZE="${INFO[-1]}"
LABEL=""
SHOW_DISK=true
IS_MOUNTED=false
IS_RAID=false
IS_ZFS=false
IS_LVM=false
IS_SYSTEM=false
IS_USB=false
if is_system_disk "$DISK"; then
IS_SYSTEM=true
fi
if is_usb_disk "$DISK"; then
IS_USB=true
fi
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=""
if [[ -n "$DISK" && -e "$DISK" ]]; then
REAL_PATH=$(readlink -f "$DISK")
fi
if [[ -n "$REAL_PATH" ]] && echo "$LVM_DEVICES" | grep -qFx "$REAL_PATH"; then
IS_MOUNTED=true
fi
USED_BY=""
REAL_PATH=""
if [[ -n "$DISK" && -e "$DISK" ]]; then
REAL_PATH=$(readlink -f "$DISK")
fi
CONFIG_DATA=$(grep -vE '^\s*#' /etc/pve/qemu-server/*.conf /etc/pve/lxc/*.conf 2>/dev/null)
if grep -Fq "$REAL_PATH" <<< "$CONFIG_DATA"; then
USED_BY="$(translate "In use")"
else
for SYMLINK in /dev/disk/by-id/*; do
[[ -e "$SYMLINK" ]] || continue
if [[ "$(readlink -f "$SYMLINK")" == "$REAL_PATH" ]]; then
if grep -Fq "$SYMLINK" <<< "$CONFIG_DATA"; then
USED_BY="$(translate "In use")"
break
fi
fi
done
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
if $IS_MOUNTED; then SHOW_DISK=false; fi
if $IS_SYSTEM; 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"
if $IS_USB; then
LABEL+=" USB"
else
LABEL+=" $(translate "Internal")"
fi
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
dialog --title "$(translate "Error")" --msgbox "$(translate "No available disks found on the host.")" 8 60
clear
exit 1
fi
msg_ok "$(translate "Available disks detected.")"
# Building the array for dialog (format: tag item on/off tag item on/off...)
DLG_LIST=()
for ((i=0; i<${#FREE_DISKS[@]}; i+=3)); do
DLG_LIST+=("${FREE_DISKS[i]}" "${FREE_DISKS[i+1]}" "${FREE_DISKS[i+2]}")
done
SELECTED=$(dialog --clear --backtitle "ProxMenux" --title "$(translate "Select Disk")" \
--radiolist "\n$(translate "Select the disk you want to mount on the host:")" 20 90 10 \
"${DLG_LIST[@]}" 2>&1 >/dev/tty)
if [ -z "$SELECTED" ]; then
dialog --title "$(translate "Error")" --msgbox "$(translate "No disk was selected.")" 8 50
clear
exit 1
fi
msg_ok "$(translate "Disk selected successfully:") $SELECTED"
# ------------------- Partitions and formatting ------------------------
PARTITION=$(lsblk -rno NAME "$SELECTED" | awk -v disk="$(basename "$SELECTED")" '$1 != disk {print $1; exit}')
SKIP_FORMAT=false
DEFAULT_MOUNT="/mnt/backup"
if [ -n "$PARTITION" ]; then
PARTITION="/dev/$PARTITION"
CURRENT_FS=$(lsblk -no FSTYPE "$PARTITION" | xargs)
if [[ "$CURRENT_FS" == "ext4" || "$CURRENT_FS" == "xfs" || "$CURRENT_FS" == "btrfs" ]]; then
SKIP_FORMAT=true
msg_ok "$(translate "Detected existing filesystem") $CURRENT_FS $(translate "on") $PARTITION."
else
dialog --title "$(translate "Unsupported Filesystem")" --yesno \
"$(translate "The partition") $PARTITION $(translate "has an unsupported filesystem ($CURRENT_FS).\nDo you want to format it?")" 10 70
if [ $? -ne 0 ]; then exit 0; fi
fi
else
CURRENT_FS=$(lsblk -no FSTYPE "$SELECTED" | xargs)
if [[ "$CURRENT_FS" == "ext4" || "$CURRENT_FS" == "xfs" || "$CURRENT_FS" == "btrfs" ]]; then
SKIP_FORMAT=true
PARTITION="$SELECTED"
msg_ok "$(translate "Detected filesystem") $CURRENT_FS $(translate "directly on disk") $SELECTED."
else
dialog --title "$(translate "No Valid Partitions")" --yesno \
"$(translate "The disk has no partitions and no valid filesystem. Do you want to create a new partition and format it?")" 10 70
if [ $? -ne 0 ]; then exit 0; fi
echo -e "$(translate "Creating partition table and partition...")"
parted -s "$SELECTED" mklabel gpt
parted -s "$SELECTED" mkpart primary 0% 100%
sleep 2
partprobe "$SELECTED"
sleep 2
PARTITION=$(lsblk -rno NAME "$SELECTED" | awk -v disk="$(basename "$SELECTED")" '$1 != disk {print $1; exit}')
if [ -n "$PARTITION" ]; then
PARTITION="/dev/$PARTITION"
else
dialog --title "$(translate "Partition Error")" --msgbox \
"$(translate "Failed to create partition on disk") $SELECTED." 8 70
exit 1
fi
fi
fi
if [ "$SKIP_FORMAT" != true ]; then
FORMAT_TYPE=$(dialog --title "$(translate "Select Format Type")" --menu \
"$(translate "Select the filesystem type for") $PARTITION:" 15 60 5 \
"ext4" "$(translate "Extended Filesystem 4 (recommended)")" \
"xfs" "XFS" \
"btrfs" "Btrfs" 2>&1 >/dev/tty)
if [ -z "$FORMAT_TYPE" ]; then
dialog --title "$(translate "Format Cancelled")" --msgbox \
"$(translate "Format operation cancelled. The disk will not be added.")" 8 60
exit 0
fi
dialog --title "$(translate "WARNING")" --yesno \
"$(translate "WARNING: This operation will FORMAT the disk") $PARTITION $(translate "with") $FORMAT_TYPE.\n\n$(translate "ALL DATA ON THIS DISK WILL BE PERMANENTLY LOST!")\n\n$(translate "Are you sure you want to continue")" 15 70
if [ $? -ne 0 ]; then exit 0; fi
echo -e "$(translate "Formatting partition") $PARTITION $(translate "with") $FORMAT_TYPE..."
case "$FORMAT_TYPE" in
"ext4") mkfs.ext4 -F "$PARTITION" ;;
"xfs") mkfs.xfs -f "$PARTITION" ;;
"btrfs") mkfs.btrfs -f "$PARTITION" ;;
esac
if [ $? -ne 0 ]; then
cleanup
dialog --title "$(translate "Format Failed")" --msgbox \
"$(translate "Failed to format partition") $PARTITION $(translate "with") $FORMAT_TYPE." 12 70
exit 1
else
msg_ok "$(translate "Partition") $PARTITION $(translate "successfully formatted with") $FORMAT_TYPE."
partprobe "$SELECTED"
sleep 2
fi
fi
# ------------------- Mount point and permissions -------------------
MOUNT_POINT=$(dialog --title "$(translate "Mount Point")" \
--inputbox "$(translate "Enter the mount point for the disk (e.g., /mnt/backup):")" \
10 60 "$DEFAULT_MOUNT" 2>&1 >/dev/tty)
if [ -z "$MOUNT_POINT" ]; then
dialog --title "$(translate "Error")" --msgbox "$(translate "No mount point was specified.")" 8 40
exit 1
fi
msg_ok "$(translate "Mount point specified:") $MOUNT_POINT"
mkdir -p "$MOUNT_POINT"
UUID=$(blkid -s UUID -o value "$PARTITION")
FS_TYPE=$(lsblk -no FSTYPE "$PARTITION" | xargs)
FSTAB_ENTRY="UUID=$UUID $MOUNT_POINT $FS_TYPE defaults 0 0"
if grep -q "UUID=$UUID" /etc/fstab; then
sed -i "s|^.*UUID=$UUID.*|$FSTAB_ENTRY|" /etc/fstab
msg_ok "$(translate "fstab entry updated for") $UUID"
else
echo "$FSTAB_ENTRY" >> /etc/fstab
msg_ok "$(translate "fstab entry added for") $UUID"
fi
mount "$MOUNT_POINT" 2> >(grep -v "systemd still uses")
if [ $? -eq 0 ]; then
if ! getent group sharedfiles >/dev/null; then
groupadd sharedfiles
msg_ok "$(translate "Group 'sharedfiles' created")"
else
msg_ok "$(translate "Group 'sharedfiles' already exists")"
fi
chown root:sharedfiles "$MOUNT_POINT"
chmod 2775 "$MOUNT_POINT"
dialog --title "$(translate "Success")" --msgbox "$(translate "The disk has been successfully mounted at") $MOUNT_POINT" 8 60
echo "$MOUNT_POINT" > /usr/local/share/proxmenux/last_backup_mount.txt
msg_ok "$(translate "Disk mounted at") $MOUNT_POINT"
msg_success "$(translate "Press Enter to return to menu...")"
read -r
else
dialog --title "$(translate "Mount Error")" --msgbox "$(translate "Failed to mount the disk at") $MOUNT_POINT" 8 60
msg_success "$(translate "Press Enter to return to menu...")"
read -r
exit 1
fi
}

View File

@ -1,240 +0,0 @@
#!/usr/bin/env bash
# ==========================================================
# ProxMenux - UUP Dump ISO Creator
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : MIT (https://raw.githubusercontent.com/MacRimi/ProxMenux/main/LICENSE)
# Version : 1.0
# Last Updated: 07/05/2025
# ==========================================================
# Description:
# This script is part of the ProxMenux tools for Proxmox VE.
# It allows downloading and converting official Windows ISO images
# from UUP Dump using a shared link (with ID, pack, and edition).
#
# Key features:
# - Automatically installs and verifies required dependencies (aria2c, cabextract, wimlib-imagex…)
# - Downloads the selected Windows edition from UUP Dump using aria2
# - Converts the downloaded files into a bootable ISO
# - Stores the resulting ISO in the default template path (/var/lib/vz/template/iso)
# - Provides a graphical prompt via whiptail for user-friendly usage
#
# This tool simplifies the creation of official Windows ISOs
# for use in virtual machines within Proxmox VE.
# ==========================================================
BASE_DIR="/usr/local/share/proxmenux"
UTILS_FILE="$BASE_DIR/utils.sh"
VENV_PATH="/opt/googletrans-env"
if [[ -f "$UTILS_FILE" ]]; then
source "$UTILS_FILE"
fi
load_language
initialize_cache
clear
show_proxmenux_logo
# ==========================================================
detect_iso_dir() {
for store in $(pvesm status -content iso | awk 'NR>1 {print $1}'); do
for ext in iso img; do
volid=$(pvesm list "$store" --content iso | awk -v ext="$ext" 'NR>1 && $2 ~ ext {print $1; exit}')
if [[ -n "$volid" ]]; then
path=$(pvesm path "$volid" 2>/dev/null)
dir=$(dirname "$path")
[[ -d "$dir" ]] && echo "$dir" && return 0
fi
done
done
if [[ -d /var/lib/vz/template/iso ]]; then
echo "/var/lib/vz/template/iso"
return 0
fi
return 1
}
function run_uupdump_creator() {
local DEPS=(curl aria2 cabextract wimtools genisoimage chntpw)
local CMDS=(curl aria2c cabextract wimlib-imagex genisoimage chntpw)
local MISSING=()
local FAILED=()
for i in "${!CMDS[@]}"; do
if ! command -v "${CMDS[$i]}" &>/dev/null; then
MISSING+=("${DEPS[$i]}")
fi
done
if [[ ${#MISSING[@]} -gt 0 ]]; then
msg_info "$(translate "Installing dependencies: ${MISSING[*]}")"
apt-get update -qq >/dev/null 2>&1
if ! apt-get install -y "${MISSING[@]}" >/dev/null 2>&1; then
msg_error "$(translate "Failed to install: ${MISSING[*]}")"
exit 1
fi
fi
for i in "${!CMDS[@]}"; do
if ! command -v "${CMDS[$i]}" &>/dev/null; then
FAILED+=("${CMDS[$i]}")
fi
done
if [[ ${#FAILED[@]} -eq 0 ]]; then
msg_ok "$(translate "All dependencies installed and verified.")"
else
msg_error "$(translate "Missing commands after installation: ${FAILED[*]}")"
exit 1
fi
ISO_DIR=$(detect_iso_dir)
if [[ -z "$ISO_DIR" ]]; then
msg_error "$(translate "Could not determine a valid ISO storage directory.")"
exit 1
fi
mkdir -p "$ISO_DIR"
TMP_DIR=$(dialog --inputbox "Enter temporary folder path (default: /root/uup-temp):" 10 60 "/root/uup-temp" 3>&1 1>&2 2>&3)
if [[ $? -ne 0 || -z "$TMP_DIR" ]]; then
TMP_DIR="/root/uup-temp"
fi
OUT_DIR="$ISO_DIR"
CONVERTER="/root/uup-converter"
mkdir -p "$TMP_DIR" "$OUT_DIR"
cd "$TMP_DIR" || exit 1
UUP_URL=$(whiptail --inputbox "$(translate "Paste the UUP Dump URL here")" 10 90 3>&1 1>&2 2>&3)
if [[ $? -ne 0 || -z "$UUP_URL" ]]; then
msg_warn "$(translate "Cancelled by user or empty URL.")"
return 1
fi
if [[ ! "$UUP_URL" =~ id=.+\&pack=.+\&edition=.+ ]]; then
msg_error "$(translate "The URL does not contain the required parameters (id, pack, edition).")"
sleep 2
return 1
fi
BUILD_ID=$(echo "$UUP_URL" | grep -oP 'id=\K[^&]+')
LANG=$(echo "$UUP_URL" | grep -oP 'pack=\K[^&]+')
EDITION=$(echo "$UUP_URL" | grep -oP 'edition=\K[^&]+')
ARCH="amd64"
echo -e "\n${BGN}=============== UUP Dump Creator ===============${CL}"
echo -e " ${BGN}🆔 ID:${CL} ${DGN}$BUILD_ID${CL}"
echo -e " ${BGN}🌐 Language:${CL} ${DGN}$LANG${CL}"
echo -e " ${BGN}💿 Edition:${CL} ${DGN}$EDITION${CL}"
echo -e " ${BGN}🖥️ Architecture:${CL} ${DGN}$ARCH${CL}"
echo -e "${BGN}===============================================${CL}\n"
if [[ ! -f "$CONVERTER/convert.sh" ]]; then
echo "📦 $(translate "Downloading UUP converter...")"
mkdir -p "$CONVERTER"
cd "$CONVERTER" || exit 1
wget -q https://git.uupdump.net/uup-dump/converter/archive/refs/heads/master.tar.gz -O converter.tar.gz
tar -xzf converter.tar.gz --strip-components=1
chmod +x convert.sh
cd "$TMP_DIR" || exit 1
fi
cat > uup_download_linux.sh <<EOF
#!/bin/bash
mkdir -p files
echo "https://git.uupdump.net/uup-dump/converter/archive/refs/heads/master.tar.gz" > files/converter_multi
for prog in aria2c cabextract wimlib-imagex chntpw; do
which \$prog &>/dev/null || { echo "\$prog not found."; exit 1; }
done
which genisoimage &>/dev/null || which mkisofs &>/dev/null || { echo "genisoimage/mkisofs not found."; exit 1; }
destDir="UUPs"
tempScript="aria2_script.\$RANDOM.txt"
aria2c --no-conf --console-log-level=warn --log-level=info --log="aria2_download.log" \
-x16 -s16 -j2 --allow-overwrite=true --auto-file-renaming=false -d"files" -i"files/converter_multi" || exit 1
aria2c --no-conf --console-log-level=warn --log-level=info --log="aria2_download.log" \
-o"\$tempScript" --allow-overwrite=true --auto-file-renaming=false \
"https://uupdump.net/get.php?id=$BUILD_ID&pack=$LANG&edition=$EDITION&aria2=2" || exit 1
grep '#UUPDUMP_ERROR:' "\$tempScript" && { echo "❌ Error generating UUP download list."; exit 1; }
aria2c --no-conf --console-log-level=warn --log-level=info --log="aria2_download.log" \
-x16 -s16 -j5 -c -R -d"\$destDir" -i"\$tempScript" || exit 1
EOF
chmod +x uup_download_linux.sh
# ==========================
./uup_download_linux.sh
# ==========================
UUP_FOLDER=$(find "$TMP_DIR" -type d -name "UUPs" | head -n1)
[[ -z "$UUP_FOLDER" ]] && msg_error "$(translate "No UUP folder found.")" && exit 1
echo -e "\n${GN}=======================================${CL}"
echo -e " 💿 ${GN}Starting ISO conversion...${CL}"
echo -e "${GN}=======================================${CL}\n"
"$CONVERTER/convert.sh" wim "$UUP_FOLDER" 1
ISO_FILE=$(find "$TMP_DIR" "$CONVERTER" "$UUP_FOLDER" -maxdepth 1 -iname "*.iso" | head -n1)
if [[ -f "$ISO_FILE" ]]; then
mv "$ISO_FILE" "$OUT_DIR/"
msg_ok "$(translate "ISO created successfully:") $OUT_DIR/$(basename "$ISO_FILE")"
msg_ok "$(translate "Cleaning temporary files...")"
rm -rf "$TMP_DIR" "$CONVERTER"
export OS_TYPE="windows"
export LANGUAGE=C
export LANG=C
export LC_ALL=C
load_language
initialize_cache
msg_success "$(translate "Press Enter to return to menu...")"
read -r
else
msg_warn "$(translate "No ISO was generated.")"
rm -rf "$TMP_DIR" "$CONVERTER"
export LANGUAGE=C
export LANG=C
export LC_ALL=C
load_language
initialize_cache
msg_success "$(translate "Press Enter to return to menu...")"
read -r
return 1
fi
}

View File

@ -1,327 +0,0 @@
#!/bin/bash
# ==========================================================
# ProxMenu - A menu-driven script for Proxmox VE management
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : MIT (https://raw.githubusercontent.com/MacRimi/ProxMenux/main/LICENSE)
# Version : 1.1
# Last Updated: 04/06/2025
# ==========================================================
# Description:
# This script provides a simple and efficient way to access and execute Proxmox VE scripts
# from the Community Scripts project (https://community-scripts.github.io/ProxmoxVE/).
#
# It serves as a convenient tool to run key automation scripts that simplify system management,
# continuing the great work and legacy of tteck in making Proxmox VE more accessible.
# A streamlined solution for executing must-have tools in Proxmox VE.
# ==========================================================
# Configuration ============================================
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
BASE_DIR="/usr/local/share/proxmenux"
UTILS_FILE="$BASE_DIR/utils.sh"
VENV_PATH="/opt/googletrans-env"
if [[ -f "$UTILS_FILE" ]]; then
source "$UTILS_FILE"
fi
load_language
initialize_cache
# ==========================================================
HELPERS_JSON_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/refs/heads/main/json/helpers_cache.json"
METADATA_URL="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/frontend/public/json/metadata.json"
for cmd in curl jq dialog; do
if ! command -v "$cmd" >/dev/null; then
echo "Missing required command: $cmd"
exit 1
fi
done
CACHE_JSON=$(curl -s "$HELPERS_JSON_URL")
META_JSON=$(curl -s "$METADATA_URL")
declare -A CATEGORY_NAMES
while read -r id name; do
CATEGORY_NAMES[$id]="$name"
done < <(echo "$META_JSON" | jq -r '.categories[] | "\(.id)\t\(.name)"')
declare -A CATEGORY_COUNT
for id in $(echo "$CACHE_JSON" | jq -r '.[].categories[]'); do
((CATEGORY_COUNT[$id]++))
done
get_type_label() {
local type="$1"
case "$type" in
ct) echo $'\Z1LXC\Zn' ;;
vm) echo $'\Z4VM\Zn' ;;
pve) echo $'\Z3PVE\Zn' ;;
addon) echo $'\Z2ADDON\Zn' ;;
*) echo $'\Z7GEN\Zn' ;;
esac
}
download_script() {
local url="$1"
local fallback_pve="${url/misc\/tools\/pve}"
local fallback_addon="${url/misc\/tools\/addon}"
local fallback_copydata="${url/misc\/tools\/copy-data}"
if curl --silent --head --fail "$url" >/dev/null; then
bash <(curl -s "$url")
elif curl --silent --head --fail "$fallback_pve" >/dev/null; then
bash <(curl -s "$fallback_pve")
elif curl --silent --head --fail "$fallback_addon" >/dev/null; then
bash <(curl -s "$fallback_addon")
elif curl --silent --head --fail "$fallback_copydata" >/dev/null; then
bash <(curl -s "$fallback_copydata")
else
dialog --title "Helper Scripts" --msgbox "Error: Failed to download the script." 12 70
fi
}
RETURN_TO_MAIN=false
format_credentials() {
local script_info="$1"
local credentials_info=""
local has_credentials
has_credentials=$(echo "$script_info" | base64 --decode | jq -r 'has("default_credentials")')
if [[ "$has_credentials" == "true" ]]; then
local username password
username=$(echo "$script_info" | base64 --decode | jq -r '.default_credentials.username // empty')
password=$(echo "$script_info" | base64 --decode | jq -r '.default_credentials.password // empty')
if [[ -n "$username" && -n "$password" ]]; then
credentials_info="Username: $username | Password: $password"
elif [[ -n "$username" ]]; then
credentials_info="Username: $username"
elif [[ -n "$password" ]]; then
credentials_info="Password: $password"
fi
fi
echo "$credentials_info"
}
run_script_by_slug() {
local slug="$1"
local script_info
script_info=$(echo "$CACHE_JSON" | jq -r --arg slug "$slug" '.[] | select(.slug == $slug) | @base64')
decode() {
echo "$1" | base64 --decode | jq -r "$2"
}
local name desc script_url notes
name=$(decode "$script_info" ".name")
desc=$(decode "$script_info" ".desc")
script_url=$(decode "$script_info" ".script_url")
notes=$(decode "$script_info" ".notes | join(\"\n\")")
local notes_dialog=""
if [[ -n "$notes" ]]; then
while IFS= read -r line; do
notes_dialog+="$line\n"
done <<< "$notes"
notes_dialog="${notes_dialog%\\n}"
fi
local credentials
credentials=$(format_credentials "$script_info")
local msg="\Zb\Z4Descripción:\Zn\n$desc"
[[ -n "$notes_dialog" ]] && msg+="\n\n\Zb\Z4Notes:\Zn\n$notes_dialog"
[[ -n "$credentials" ]] && msg+="\n\n\Zb\Z4Default Credentials:\Zn\n$credentials"
dialog --clear --colors --backtitle "ProxMenux" --title "$name" --yesno "$msg\n\nExecute this script?" 22 85
if [[ $? -eq 0 ]]; then
download_script "$script_url"
echo
echo
if [[ -n "$desc" || -n "$notes" || -n "$credentials" ]]; then
echo -e "$TAB\e[1;36mScript Information:\e[0m"
if [[ -n "$notes" ]]; then
echo -e "$TAB\e[1;33mNotes:\e[0m"
while IFS= read -r line; do
[[ -z "$line" ]] && continue
echo -e "$TAB$line"
done <<< "$notes"
echo
fi
if [[ -n "$credentials" ]]; then
echo -e "$TAB\e[1;32mDefault Credentials:\e[0m"
echo "$TAB$credentials"
echo
fi
fi
msg_success "Press Enter to return to the main menu..."
read -r
RETURN_TO_MAIN=true
fi
}
search_and_filter_scripts() {
local search_term=""
while true; do
search_term=$(dialog --inputbox "Enter search term (leave empty to show all scripts):" \
8 65 "$search_term" 3>&1 1>&2 2>&3)
[[ $? -ne 0 ]] && return
local filtered_json
if [[ -z "$search_term" ]]; then
filtered_json="$CACHE_JSON"
else
local search_lower
search_lower=$(echo "$search_term" | tr '[:upper:]' '[:lower:]')
filtered_json=$(echo "$CACHE_JSON" | jq --arg term "$search_lower" '
[.[] | select(
(.name | ascii_downcase | contains($term)) or
(.desc | ascii_downcase | contains($term))
)]')
fi
local count
count=$(echo "$filtered_json" | jq length)
if [[ $count -eq 0 ]]; then
dialog --msgbox "No scripts found for: '$search_term'\n\nTry a different search term." 8 50
continue
fi
while true; do
declare -A index_to_slug
local menu_items=()
local i=1
while IFS=$'\t' read -r slug name type; do
index_to_slug[$i]="$slug"
local label
label=$(get_type_label "$type")
local padded_name
padded_name=$(printf "%-42s" "$name")
local entry="$padded_name $label"
menu_items+=("$i" "$entry")
((i++))
done < <(echo "$filtered_json" | jq -r '
sort_by(.name)[] | [.slug, .name, .type] | @tsv')
menu_items+=("" "")
menu_items+=("new_search" "New Search")
menu_items+=("show_all" "Show All Scripts")
local title="Search Results"
if [[ -n "$search_term" ]]; then
title="Search Results for: '$search_term' ($count found)"
else
title="All Available Scripts ($count total)"
fi
local selected
selected=$(dialog --colors --backtitle "ProxMenux" \
--title "$title" \
--menu "Select a script or action:" \
22 75 15 "${menu_items[@]}" 3>&1 1>&2 2>&3)
if [[ $? -ne 0 ]]; then
return
fi
case "$selected" in
"new_search")
break
;;
"show_all")
search_term=""
filtered_json="$CACHE_JSON"
count=$(echo "$filtered_json" | jq length)
continue
;;
"back"|"")
return
;;
*)
if [[ -n "${index_to_slug[$selected]}" ]]; then
run_script_by_slug "${index_to_slug[$selected]}"
[[ "$RETURN_TO_MAIN" == true ]] && { RETURN_TO_MAIN=false; return; }
fi
;;
esac
done
done
}
while true; do
MENU_ITEMS=()
MENU_ITEMS+=("search" "Search/Filter Scripts")
MENU_ITEMS+=("" "")
for id in $(printf "%s\n" "${!CATEGORY_COUNT[@]}" | sort -n); do
name="${CATEGORY_NAMES[$id]:-Category $id}"
count="${CATEGORY_COUNT[$id]}"
padded_name=$(printf "%-35s" "$name")
padded_count=$(printf "(%2d)" "$count")
MENU_ITEMS+=("$id" "$padded_name $padded_count")
done
SELECTED=$(dialog --backtitle "ProxMenux" --title "Proxmox VE Helper-Scripts" --menu \
"Select a category or search for scripts:" 20 70 14 \
"${MENU_ITEMS[@]}" 3>&1 1>&2 2>&3) || {
dialog --title "Proxmox VE Helper-Scripts" \
--msgbox "\n\n$(translate "Visit the website to discover more scripts, stay updated with the latest updates, and support the project:")\n\nhttps://community-scripts.github.io/ProxmoxVE" 15 70
clear
break
}
if [[ "$SELECTED" == "search" ]]; then
search_and_filter_scripts
continue
fi
while true; do
declare -A INDEX_TO_SLUG
SCRIPTS=()
i=1
while IFS=$'\t' read -r slug name type; do
INDEX_TO_SLUG[$i]="$slug"
label=$(get_type_label "$type")
padded_name=$(printf "%-42s" "$name")
entry="$padded_name $label"
SCRIPTS+=("$i" "$entry")
((i++))
done < <(echo "$CACHE_JSON" | jq -r --argjson id "$SELECTED" \
'[.[] | select(.categories | index($id)) | {slug, name, type}] | sort_by(.name)[] | [.slug, .name, .type] | @tsv')
SCRIPT_INDEX=$(dialog --colors --backtitle "ProxMenux" --title "Scripts in ${CATEGORY_NAMES[$SELECTED]}" --menu \
"Choose a script to execute:" 20 70 14 \
"${SCRIPTS[@]}" 3>&1 1>&2 2>&3) || break
SCRIPT_SELECTED="${INDEX_TO_SLUG[$SCRIPT_INDEX]}"
run_script_by_slug "$SCRIPT_SELECTED"
[[ "$RETURN_TO_MAIN" == true ]] && { RETURN_TO_MAIN=false; break; }
done
done

View File

@ -106,35 +106,14 @@ if [[ -z "$ISO_DIR" ]]; then
exit 1
fi
mkdir -p "$ISO_DIR"
TMP_DIR="/root/uup-temp"
OUT_DIR="$ISO_DIR"
CONVERTER="/root/uup-converter"
DEFAULT_TMP="/root/uup-temp"
USER_INPUT=$(dialog --inputbox "Enter temporary folder path (default: $DEFAULT_TMP):" 10 60 "$DEFAULT_TMP" 3>&1 1>&2 2>&3)
if [[ $? -ne 0 || -z "$USER_INPUT" ]]; then
USER_INPUT="$DEFAULT_TMP"
fi
#
if [[ "$USER_INPUT" == "$DEFAULT_TMP" ]]; then
TMP_DIR="$USER_INPUT"
CLEAN_ALL=true
else
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
RANDOM_ID=$(head /dev/urandom | tr -dc a-z0-9 | head -c 4)
TMP_DIR="${USER_INPUT%/}/uup-session-${TIMESTAMP}-${RANDOM_ID}"
CLEAN_ALL=false
fi
mkdir -p "$TMP_DIR" || {
msg_error "$(translate "Failed to create temporary directory:") $TMP_DIR"
exit 1
}
OUT_DIR=$(detect_iso_dir)
[[ -z "$OUT_DIR" ]] && msg_error "$(translate "Could not determine a valid ISO directory.")" && exit 1
mkdir -p "$OUT_DIR"
mkdir -p "$TMP_DIR" "$OUT_DIR"
cd "$TMP_DIR" || exit 1
UUP_URL=$(whiptail --inputbox "$(translate "Paste the UUP Dump URL here")" 10 90 3>&1 1>&2 2>&3)
@ -228,12 +207,7 @@ if [[ -f "$ISO_FILE" ]]; then
msg_ok "$(translate "Cleaning temporary files...")"
if [[ "$CLEAN_ALL" == true ]]; then
rm -rf "$TMP_DIR" "$CONVERTER"
else
[[ -d "$TMP_DIR" ]] && rm -rf "$TMP_DIR"
[[ -d "$CONVERTER" ]] && rm -rf "$CONVERTER"
fi
export OS_TYPE="windows"
export LANGUAGE=C
@ -244,15 +218,15 @@ fi
msg_success "$(translate "Press Enter to return to menu...")"
read -r
else
msg_warn "$(translate "No ISO was generated.")"
rm -rf "$TMP_DIR" "$CONVERTER"
export LANGUAGE=C
export LANG=C
export LC_ALL=C
load_language
initialize_cache
msg_success "$(translate "Press Enter to return to menu...")"
read -r
return 1

View File

@ -173,26 +173,11 @@ function configure_vm_advanced() {
BIOS_TYPE=" -bios $BIOS"
# CPU Type
# CPU_CHOICE=$(whiptail --backtitle "ProxMenux" --title "$(translate "CPU Model")" \
# --radiolist "$(translate "Select CPU model")" 10 60 2 \
# "host" "Host (recommended)" ON \
# "kvm64" "Generic KVM64" OFF 3>&1 1>&2 2>&3) || return 1
# [[ "$CPU_CHOICE" == "host" ]] && CPU_TYPE=" -cpu host" || CPU_TYPE=" -cpu kvm64"
CPU_CHOICE=$(whiptail --backtitle "ProxMenux" --title "$(translate "CPU Model")" \
--radiolist "$(translate "Select CPU model")" 17 70 11 \
"host" "Host (recommended)" ON \
"kvm64" "Generic KVM64" OFF \
"kvm32" "Generic KVM32" OFF \
"qemu64" "QEMU 64-bit CPU" OFF \
"qemu32" "QEMU 32-bit CPU" OFF \
"max" "Expose all QEMU CPU features" OFF \
"x86-64-v2" "Nehalem-class (x86-64-v2)" OFF \
"x86-64-v2-AES" "Same as v2 but with AES" OFF \
"x86-64-v3" "Haswell-class (x86-64-v3)" OFF \
"x86-64-v4" "Skylake-class (x86-64-v4)" OFF 3>&1 1>&2 2>&3) || return 1
CPU_TYPE=" -cpu $CPU_CHOICE"
--radiolist "$(translate "Select CPU model")" 10 60 2 \
"host" "Host (recommended)" ON \
"kvm64" "Generic KVM64" OFF 3>&1 1>&2 2>&3) || return 1
[[ "$CPU_CHOICE" == "host" ]] && CPU_TYPE=" -cpu host" || CPU_TYPE=" -cpu kvm64"
# Core Count
CORE_COUNT=$(whiptail --backtitle "ProxMenux" --inputbox "$(translate "Number of CPU cores (default: 2)")" \

View File

@ -11,17 +11,21 @@ export default function CoralTPULXC() {
return (
<div className="max-w-3xl mx-auto">
<h1 className="text-3xl font-bold mb-6">Enable Coral TPU in an LXC</h1>
<p className="mb-4">
This guide explains how to configure Google Coral TPU support for LXC containers in Proxmox VE using <strong>ProxMenux</strong>.
Coral TPU provides dedicated AI acceleration, improving inference performance for machine learning applications. It is particularly useful for video surveillance applications with real-time video analysis, such as <a href='https://frigate.video/' target='_blank' className='text-blue-600 hover:underline'>Frigate</a> or <a href='https://www.ispyconnect.com' target='_blank' className='text-blue-600 hover:underline'>Agent DVR</a> or <a href='https://blueirissoftware.com/' target='_blank' className='text-blue-600 hover:underline'>Blue Iris</a> using <a href='https://www.codeproject.com/ai/index.aspx' target='_blank' className='text-blue-600 hover:underline'>CodeProject.AI</a>.
</p>
<h2 className="text-2xl font-semibold mt-8 mb-4">Overview</h2>
<p className="mb-4">The script automates the complete configuration of Coral TPU support in LXC containers, including USB and M.2 variants. It applies Proxmox-specific container settings, manages device passthrough permissions, and installs required drivers both on the host and inside the container.</p>
<p className="mb-4">The USB variant uses a persistent mapping based on <code>/dev/coral</code> via <code>udev</code> rules, avoiding reliance on dynamic USB paths like <code>/dev/bus/usb/*</code>. This ensures consistent device assignment across reboots and hardware reordering.</p>
<p className="mb-4">The M.2 version is detected automatically and configured only if present.</p>
<p className="mb-4">The script automates the following steps:</p>
<ol className="list-decimal pl-6 space-y-2 mb-6">
<li>Allows selection of an existing LXC container.</li>
<li>Ensures the container is privileged for hardware access.</li>
<li>Configures LXC parameters for Coral TPU and Intel iGPU.</li>
<li>Installs required drivers and dependencies inside the container.</li>
</ol>
<h2 className="text-2xl font-semibold mt-8 mb-4">Implementation Steps</h2>
<Steps>
<Steps.Step title="Select an LXC Container">
@ -35,47 +39,13 @@ export default function CoralTPULXC() {
<li>Sets device permissions for TPU and iGPU.</li>
<li>Configures proper device mounts.</li>
</ul>
<CopyableCode
code={`# Coral USB persistent passthrough example:
/etc/udev/rules.d/99-coral-usb.rules
SUBSYSTEM=="usb", ATTRS{idVendor}=="18d1", ATTRS{idProduct}=="9302", SYMLINK+="coral", MODE="0666"
# LXC config:
lxc.cgroup2.devices.allow: c 189:* rwm
lxc.mount.entry: /dev/coral dev/coral none bind,optional,create=file`}
className="my-4"
/>
<CopyableCode
code={`# Coral M.2 passthrough example (automatically added if detected):
lxc.cgroup2.devices.allow: c 245:0 rwm
lxc.mount.entry: /dev/apex_0 dev/apex_0 none bind,optional,create=file`}
className="my-4"
/>
</Steps.Step>
<Steps.Step title="Install Required Drivers">
<p>The script installs the necessary components inside the container:</p>
<ul className="list-disc pl-6 space-y-1 mt-2">
<li>GPU drivers:</li>
<ul className="list-disc pl-10">
<li><code>va-driver-all</code></li>
<li><code>ocl-icd-libopencl1</code></li>
<li><code>intel-opencl-icd</code></li>
<li><code>vainfo</code></li>
<li><code>intel-gpu-tools</code></li>
</ul>
<li>Coral TPU dependencies:</li>
<ul className="list-disc pl-10">
<li><code>python3</code></li>
<li><code>python3-pip</code></li>
<li><code>python3-venv</code></li>
<li><code>gnupg</code></li>
<li><code>curl</code></li>
</ul>
<li>Coral TPU drivers:</li>
<ul className="list-disc pl-10">
<li><code>libedgetpu1-std</code> (standard performance)</li>
<li><code>libedgetpu1-max</code> (maximum performance, optional)</li>
</ul>
<li>GPU drivers (va-driver-all, intel-opencl-icd).</li>
<li>Coral TPU dependencies (Python, GPG keys, repository setup).</li>
<li>Coral TPU drivers (USB and M.2 support).</li>
</ul>
</Steps.Step>
<Steps.Step title="Select Coral TPU Driver Version">
@ -86,7 +56,7 @@ lxc.mount.entry: /dev/apex_0 dev/apex_0 none bind,optional,create=file`}
</ul>
</Steps.Step>
</Steps>
<h2 className="text-2xl font-semibold mt-8 mb-4">Expected Results</h2>
<ul className="list-disc pl-6 space-y-2 mb-6">
<li>The selected container is correctly configured for TPU and iGPU usage.</li>
@ -94,15 +64,14 @@ lxc.mount.entry: /dev/apex_0 dev/apex_0 none bind,optional,create=file`}
<li>The container will restart as needed during the process.</li>
<li>After completion, applications inside the container can utilize Coral TPU acceleration.</li>
</ul>
<h2 className="text-2xl font-semibold mt-8 mb-4">Important Considerations</h2>
<ul className="list-disc pl-6 space-y-2 mb-6">
<li>The script supports both USB and M.2 Coral TPU devices.</li>
<li>The Proxmox host must have the required Coral TPU and Intel GPU drivers installed.</li>
<li>Additional application-specific configurations may be required inside the container.</li>
<li>Coral USB passthrough uses a persistent device alias <code>/dev/coral</code> created by a udev rule. This improves stability and avoids issues with changing USB port identifiers.</li>
<li>Coral M.2 devices are detected dynamically using <code>lspci</code> and configured only if present.</li>
</ul>
</div>
)
}

View File

@ -10,79 +10,58 @@ export default function InstallCoralTPUHost() {
return (
<div className="max-w-3xl mx-auto">
<h1 className="text-3xl font-bold mb-6">Install Coral TPU on the Host</h1>
<p className="mb-4">
<strong>Before using Coral TPU inside an LXC container, the drivers must first be installed on the Proxmox VE host. This script automates that process, ensuring the necessary setup is completed.</strong>
<br /><br />
This guide explains how to install and configure Google Coral TPU drivers on a Proxmox VE host using <strong>ProxMenux</strong>. This setup enables hardware acceleration for AI-based applications that leverage Coral TPU.
<p className="mb-4"><strong>Before using Coral TPU inside an LXC container, the drivers must first be installed on the Proxmox VE host. This script automates that process, ensuring the necessary setup is completed.</strong><br/><br/>
This guide explains how to install and configure Google Coral TPU drivers on a Proxmox VE host using <strong>ProxMenux</strong>.
This setup enables hardware acceleration for AI-based applications that leverage Coral TPU.
</p>
<h2 className="text-2xl font-semibold mt-8 mb-4">Overview</h2>
<p className="mb-4">The script automates the following steps:</p>
<ol className="list-decimal pl-6 space-y-2 mb-6">
<li>Prompts for confirmation before proceeding with installation.</li>
<li>Verifies and configures necessary repositories on the host.</li>
<li>Installs required build dependencies and kernel headers for driver compilation.</li>
<li>Installs required dependencies for driver compilation.</li>
<li>Clones the Coral TPU driver repository and builds the drivers.</li>
<li>Installs the compiled Coral TPU drivers.</li>
<li>Prompts for a system restart to apply changes.</li>
</ol>
<h2 className="text-2xl font-semibold mt-8 mb-4">Implementation Steps</h2>
<Steps>
<Steps.Step title="Pre-Installation Confirmation">
<p>The script prompts the user for confirmation before proceeding, as a system restart is required after installation.</p>
</Steps.Step>
<Steps.Step title="Repository Configuration">
<p>The script verifies and configures required repositories:</p>
<ul className="list-disc pl-6 space-y-1 mt-2">
<li>Adds the <strong>pve-no-subscription</strong> repository if not present.</li>
<li>Adds <strong>non-free-firmware</strong> repositories for required packages.</li>
<li>Runs <code>apt-get update</code> to fetch the latest package lists.</li>
<li>Runs an update to fetch the latest package lists.</li>
</ul>
</Steps.Step>
<Steps.Step title="Driver Installation">
<p>The script installs and compiles the required Coral TPU drivers:</p>
<p>The script installs and compiles the required drivers:</p>
<ul className="list-disc pl-6 space-y-1 mt-2">
<li>Installs the following packages:</li>
<ul className="list-disc pl-10">
<li><code>git</code></li>
<li><code>devscripts</code></li>
<li><code>dh-dkms</code></li>
<li><code>dkms</code></li>
<li><code>pve-headers-$(uname -r)</code> (Proxmox kernel headers)</li>
</ul>
<li>Clones the Coral TPU driver source from:</li>
<ul className="list-disc pl-10">
<li><code>https://github.com/google/gasket-driver</code></li>
</ul>
<li>Builds the driver using <code>debuild</code> and installs it using <code>dpkg -i</code>.</li>
<li>Installs dependencies such as <strong>git, dkms, devscripts</strong>, and kernel headers.</li>
<li>Clones the <strong>gasket-driver</strong> repository from Google.</li>
<li>Builds the Coral TPU driver packages.</li>
<li>Installs the compiled drivers on the host.</li>
</ul>
<CopyableCode
code={`# Commands used to build and install Coral TPU driver on host
apt install -y git devscripts dh-dkms dkms pve-headers-$(uname -r)
git clone https://github.com/google/gasket-driver.git
cd gasket-driver
debuild -us -uc -tc -b
dpkg -i ../gasket-dkms_*.deb`}
className="my-4"
/>
</Steps.Step>
<Steps.Step title="Post-Installation Confirmation">
<p>The script prompts the user to restart the server to apply the changes.</p>
</Steps.Step>
</Steps>
<h2 className="text-2xl font-semibold mt-8 mb-4">Expected Results</h2>
<ul className="list-disc pl-6 space-y-2 mb-6">
<li>The Coral TPU drivers are installed successfully on the Proxmox VE host.</li>
<li>Required repositories and dependencies are configured properly.</li>
<li>A system restart is performed to complete the installation.</li>
</ul>
</div>
)
}

View File

@ -10,31 +10,6 @@ interface ChangelogEntry {
title: string
}
// Function to clean and format markdown content for RSS
function formatContentForRSS(content: string): string {
return (
content
// Convert ### headers to bold text
.replace(/^### (.+)$/gm, "**$1**")
// Convert ** bold ** to simple bold
.replace(/\*\*(.*?)\*\*/g, "$1")
// Clean code blocks - remove ``` and format nicely
.replace(/```[\s\S]*?```/g, (match) => {
const code = match.replace(/```/g, "").trim()
return `\n${code}\n`
})
// Convert - bullet points to •
.replace(/^- /gm, "• ")
// Clean up multiple newlines
.replace(/\n{3,}/g, "\n\n")
// Remove backslashes used for line breaks
.replace(/\\\s*$/gm, "")
// Clean up extra spaces
.replace(/\s+/g, " ")
.trim()
)
}
async function parseChangelog(): Promise<ChangelogEntry[]> {
try {
const changelogPath = path.join(process.cwd(), "..", "CHANGELOG.md")
@ -46,67 +21,45 @@ async function parseChangelog(): Promise<ChangelogEntry[]> {
const fileContents = fs.readFileSync(changelogPath, "utf8")
const entries: ChangelogEntry[] = []
// Split by ## headers (both versions and dates)
const lines = fileContents.split("\n")
let currentEntry: Partial<ChangelogEntry> | null = null
let contentLines: string[] = []
// Split by any heading (## or ###) to catch all changes, not just versions
const sections = fileContents.split(/^(##\s+.*$)/gm).filter((section) => section.trim())
for (const line of lines) {
// Check for version header: ## [1.1.1] - 2025-03-21
const versionMatch = line.match(/^##\s+\[([^\]]+)\]\s*-\s*(\d{4}-\d{2}-\d{2})/)
for (let i = 0; i < sections.length - 1; i += 2) {
const headerLine = sections[i]
const content = sections[i + 1] || ""
// Check for date-only header: ## 2025-05-13
const dateMatch = line.match(/^##\s+(\d{4}-\d{2}-\d{2})$/)
// Check if it's a version header (## [version] - date)
const versionMatch = headerLine.match(/##\s+\[([^\]]+)\]\s*-\s*(\d{4}-\d{2}-\d{2})/)
if (versionMatch || dateMatch) {
// Save previous entry if exists
if (currentEntry && contentLines.length > 0) {
const rawContent = contentLines.join("\n").trim()
currentEntry.content = formatContentForRSS(rawContent)
if (currentEntry.version && currentEntry.date && currentEntry.title) {
entries.push(currentEntry as ChangelogEntry)
}
}
if (versionMatch) {
const version = versionMatch[1]
const date = versionMatch[2]
// Start new entry
if (versionMatch) {
const version = versionMatch[1]
const date = versionMatch[2]
currentEntry = {
version,
date,
url: `https://macrimi.github.io/ProxMenux/changelog#${version}`,
title: `ProxMenux ${version}`,
}
} else if (dateMatch) {
entries.push({
version,
date,
content: content.trim(),
url: `https://macrimi.github.io/ProxMenux/changelog#${version}`,
title: `ProxMenux ${version}`,
})
} else {
// Check for date-only headers (## 2025-05-13)
const dateMatch = headerLine.match(/##\s+(\d{4}-\d{2}-\d{2})/)
if (dateMatch) {
const date = dateMatch[1]
currentEntry = {
entries.push({
version: date,
date,
content: content.trim(),
url: `https://macrimi.github.io/ProxMenux/changelog#${date}`,
title: `ProxMenux Update ${date}`,
}
}
contentLines = []
} else if (currentEntry && line.trim()) {
// Add content lines (skip empty lines at the beginning)
if (contentLines.length > 0 || line.trim() !== "") {
contentLines.push(line)
})
}
}
}
// Don't forget the last entry
if (currentEntry && contentLines.length > 0) {
const rawContent = contentLines.join("\n").trim()
currentEntry.content = formatContentForRSS(rawContent)
if (currentEntry.version && currentEntry.date && currentEntry.title) {
entries.push(currentEntry as ChangelogEntry)
}
}
return entries.slice(0, 20) // Latest 20 entries
return entries.slice(0, 15) // Latest 15 entries
} catch (error) {
console.error("Error parsing changelog:", error)
return []
@ -134,7 +87,7 @@ export async function GET() {
(entry) => `
<item>
<title>${entry.title}</title>
<description><![CDATA[${entry.content.length > 1000 ? entry.content.substring(0, 1000) + "..." : entry.content}]]></description>
<description><![CDATA[${entry.content.substring(0, 500)}${entry.content.length > 500 ? "..." : ""}]]></description>
<link>${entry.url}</link>
<guid isPermaLink="true">${entry.url}</guid>
<pubDate>${new Date(entry.date).toUTCString()}</pubDate>

View File

@ -81,6 +81,7 @@ export const sidebarItems: MenuItem[] = [
{
title: "Network",
submenu: [
{ title: "Repair Network", href: "/docs/network/repair-network" },
{ title: "Verify Network", href: "/docs/network/verify-network" },
{ title: "Show IP Information", href: "/docs/network/show-ip-information" },
],

Binary file not shown.

Before

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB