diff --git a/.docs/TODO.md b/.docs/TODO.md index 2f9c459..703222c 100644 --- a/.docs/TODO.md +++ b/.docs/TODO.md @@ -80,7 +80,7 @@ ### Specialized Hardware/System Scripts - [ ] Host/FanControl/DellIPMIFanControl.sh - IPMI-specific, hardware dependent - [ ] Host/FanControl/EnablePWMFanControl.sh - PWM control, hardware dependent -- [ ] Host/Hardware/EnableCPUScalingGoverner.sh - Kernel parameter manipulation +- [ ] Host/Hardware/EnableCPUScalingGovernor.sh - Kernel parameter manipulation - [ ] Host/Bulk/FirstTimeProxmoxSetup.sh - Complex initial setup wizard ### Interactive Menu Scripts diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 15d30a4..7652ed7 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -57,6 +57,12 @@ jobs: run: | echo "Running repository checks..." ./.check/_RunChecks.sh + + - name: Run unit tests + run: | + echo "Running unit test suite..." + chmod +x Utilities/RunAllTests.sh + cd Utilities && bash RunAllTests.sh --unit-only - name: Check results run: | diff --git a/CHANGELOG.md b/CHANGELOG.md index fd06658..e1310e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,76 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2.2.0] - 2026-03-02 + +Security hardening, performance optimizations, and GUI improvements + +### Security +- **SSH Password Exposure** - Switched all `sshpass -p` calls to `sshpass -e` (environment variable) + - Passwords no longer visible in `ps aux` process listing + - Applied to all 4 sites in SSH.sh (`__wait_for_ssh__`, `__ssh_exec__`, `__scp_send__`, `__scp_fetch__`) + - SSHPASS environment variable is unset immediately after each command +- **Container Password Exposure** - Changed `__ct_change_password__` to pipe credentials via stdin + - Previously embedded password in `bash -c` command string (visible in /proc) + - Now pipes directly to `pct exec -- chpasswd` +- **Guacamole Token Security** - Token file now created with restricted permissions + - Directory created with `mkdir -p -m 700`, token file set to `chmod 600` + - Prevents other system users from reading authentication tokens +- **Guacamole API Credentials** - Switched to `--data-urlencode` for curl authentication + - Prevents special characters in passwords (e.g., `&`, `=`) from breaking API calls +- **Eval Removal** - Replaced `eval` with safer alternatives across 10 sites in 6 files + - Command execution contexts now use `bash -c` instead of `eval "$cmd"` + - ArgumentParser.sh uses `declare -g` instead of `eval` for variable assignment +- **ArgumentParser Blocklist** - Extended reserved variable name list + - Added high-risk names (HOSTNAME, RANDOM, SECONDS, GROUPS, etc.) to prevent overwrites + +### Fixed +- **Filename Typo** - Renamed `EnableCPUScalingGoverner.sh` to `EnableCPUScalingGovernor.sh` + - Updated all references in CHANGELOG.md, .docs/TODO.md, and internal SCRIPT_NAME +- **CreateFromISO Structure** - Moved `set -euo pipefail` after header comment block + - Added shellcheck source directive for sourced utility files +- **RemoveStorage Race Condition** - Cached VM/CT config per iteration + - Added `|| continue` to skip VMs/CTs deleted between list and config check +- **Locale-Dependent Parsing** - Fixed AWK decimal parsing in CreateFromISO.sh + - Added `LC_NUMERIC=C` and comma-to-dot conversion for European locale compatibility +- **GUI Unicode Symbols** - Replaced all Unicode checkmarks/crosses with plain text + +### Changed +- **GUI Breadcrumb Navigation** - Path display now shows `cc_pve > Storage > Ceph` style +- **GUI Script Descriptions** - Menu listings show inline description extracted from script headers +- **GUI Log Level Hint** - "Type 'l' to change log level" only shown in remote execution mode +- **SSH Error Context** - Connection failures now display the SSH error reason at all 7 failure sites +- **SSH Keepalive** - Added `ServerAliveInterval=5` and `ServerAliveCountMax=3` to SSH and SCP +- **Multi-Node Recovery** - Execution summary now lists per-node results with retry option + - Shows `OK: node1 node2` and `FAIL: node3` after multi-remote execution + - Prompts to retry only the failed nodes +- **CreateFromISO ArgumentParser Migration** - Replaced `getopts` with `__parse_args__` + - Arguments now use `--vm-name`, `--iso-url`, `--vm-storage` style flags + - All 8 arguments optional with interactive fallback preserved + +### Added +- **CI Unit Tests** - Added unit test stage to `.github/workflows/checks.yml` + - Runs `Utilities/RunAllTests.sh` after static analysis checks +- **BulkOperations Source Guards** - Defensive guards on source calls in BulkOperations.sh +- **GUI Update Safety Guard** - Validates BASE_DIR before cleanup in `update_scripts()` +- **Documentation** - Added `Manuals/README.md` table of contents and Documentation section in main README + +### Performance +- **FindVMIDFromIP Caching** - Config fetched once per VMID instead of 3 times (~67% fewer API calls) +- **Double-Sed Consolidation** - Merged 9 paired `sed | sed` calls into single `sed -e ... -e ...` + - Applied to BulkConfigureNetworkBandwidth, BulkConfigureDiskIOPS, BulkConfigureDiskBandwidth +- **Bash Builtins** - Replaced `echo | tr` subprocesses with native `${var^^}` case conversion + - Applied to FindVMIDFromIP, BulkCloneSetIP_Proxmox, BulkReconfigureMacAddresses, Conversion.sh, ChangeAllMACPrefix.sh +- **Carriage Return Removal** - Replaced `echo | tr -d '\r'` with `${var//$'\r'/}` in GUI.sh + +### Technical Details +- `sshpass -e` reads from `SSHPASS` environment variable; inline assignment (`SSHPASS=x cmd`) used where possible +- `declare -g` requires Bash 4.2+ +- `eval` retained in TestFramework.sh (dynamic function stubs) and RemoteExecutor.sh (SSH parameter expansion) - both legitimate uses +- Multi-node retry uses recursive `__execute_remote_script__` call with filtered target list +- FindVMIDFromIP caches both JSON and plain-text config formats per VMID for reuse +- `--data-urlencode` sends each parameter separately, preventing URL parameter injection + ## [2.1.9] - 2026-02-24 Remote execution cancellation, live output streaming, and custom port support improvements @@ -315,7 +385,7 @@ Implementation of ArgumentParser across the codebase and improved scripting stan - Now use `__parse_args__` declarative parsing - Fixed `__prompt_yes_no__` -> `__prompt_user_yn__` calls - **ArgumentParser integration** - - Host/Hardware/EnableCPUScalingGoverner.sh + - Host/Hardware/EnableCPUScalingGovernor.sh - RemoteManagement/ConfigureOverSSH/Proxmox/BulkDisableAutoStart.sh - RemoteManagement/ConfigureOverSSH/Proxmox/BulkUnmountISOs.sh - Storage/Ceph/SetScrubInterval.sh: (quality improvement with proper documentation/standards) diff --git a/GUI.sh b/GUI.sh index e5a9e51..4b41450 100755 --- a/GUI.sh +++ b/GUI.sh @@ -493,12 +493,13 @@ configure_single_remote() { echo "Testing connection..." if ! __test_remote_connection__ "$manual_ip" "$manual_pass" "$manual_user" "$manual_port"; then echo - __line_rgb__ "✗ Connection failed! Please check IP, username, port, and password." 255 0 0 + __line_rgb__ "Connection failed! Please check IP, username, port, and password." 255 0 0 + [[ -n "${LAST_SSH_ERROR:-}" ]] && echo "Reason: $LAST_SSH_ERROR" echo "Press Enter to try again..." read -r continue fi - echo "✓ Connection successful!" + echo "Connection successful!" sleep 1 __clear_remote_targets__ @@ -597,12 +598,13 @@ configure_single_remote() { echo "Testing connection..." if ! __test_remote_connection__ "$manual_ip" "$manual_pass" "$manual_user" "$manual_port"; then echo - __line_rgb__ "✗ Connection failed! Please check IP, username, and password." 255 0 0 + __line_rgb__ "Connection failed! Please check IP, username, and password." 255 0 0 + [[ -n "${LAST_SSH_ERROR:-}" ]] && echo "Reason: $LAST_SSH_ERROR" echo "Press Enter to try again..." read -r continue fi - echo "✓ Connection successful!" + echo "Connection successful!" sleep 1 __clear_remote_targets__ @@ -639,12 +641,13 @@ configure_single_remote() { echo "Testing connection to $selected_name..." if ! __test_remote_connection__ "$selected_ip" "$node_pass" "$selected_username" "$selected_port"; then echo - __line_rgb__ "✗ Connection failed! Please check password." 255 0 0 + __line_rgb__ "Connection failed! Please check password." 255 0 0 + [[ -n "${LAST_SSH_ERROR:-}" ]] && echo "Reason: $LAST_SSH_ERROR" echo "Press Enter to try again..." read -r continue fi - echo "✓ Connection successful!" + echo "Connection successful!" sleep 1 __clear_remote_targets__ @@ -860,10 +863,11 @@ configure_multi_saved() { echo "Testing connection to $node_name..." if ! __test_remote_connection__ "$node_ip" "$node_pass" "$node_username" "$node_port"; then echo - __line_rgb__ "✗ Connection to $node_name failed! Skipping this node." 255 0 0 + __line_rgb__ "Connection to $node_name failed! Skipping this node." 255 0 0 + [[ -n "${LAST_SSH_ERROR:-}" ]] && echo "Reason: $LAST_SSH_ERROR" continue fi - echo "✓ Connection to $node_name successful!" + echo "Connection to $node_name successful!" NODE_PASSWORDS["$node_name"]="$node_pass" else @@ -893,12 +897,13 @@ configure_multi_saved() { echo "Testing password on first node ($first_name)..." if ! __test_remote_connection__ "$first_ip" "$shared_pass" "$first_username" "$first_port"; then echo - __line_rgb__ "✗ Password test failed on $first_name!" 255 0 0 + __line_rgb__ "Password test failed on $first_name!" 255 0 0 + [[ -n "${LAST_SSH_ERROR:-}" ]] && echo "Reason: $LAST_SSH_ERROR" echo "Please try again..." sleep 2 continue fi - echo "✓ Password works on $first_name" + echo "Password works on $first_name" sleep 1 for target in "${REMOTE_TARGETS[@]}"; do @@ -919,18 +924,20 @@ configure_multi_saved() { echo "Testing connection to $node_name..." if ! __test_remote_connection__ "$node_ip" "$node_pass" "$node_username" "$node_port"; then echo - __line_rgb__ "✗ Connection to $node_name failed! Please try again." 255 0 0 + __line_rgb__ "Connection to $node_name failed! Please try again." 255 0 0 + [[ -n "${LAST_SSH_ERROR:-}" ]] && echo "Reason: $LAST_SSH_ERROR" # Loop back to retry this specific node read -rsp "Enter password for $node_name ($node_ip): " node_pass echo echo "Testing connection to $node_name..." if ! __test_remote_connection__ "$node_ip" "$node_pass" "$node_username" "$node_port"; then echo - __line_rgb__ "✗ Still failed! Skipping $node_name." 255 0 0 + __line_rgb__ "Still failed! Skipping $node_name." 255 0 0 + [[ -n "${LAST_SSH_ERROR:-}" ]] && echo "Reason: $LAST_SSH_ERROR" continue fi fi - echo "✓ Connection to $node_name successful!" + echo "Connection to $node_name successful!" NODE_PASSWORDS["$node_name"]="$node_pass" done @@ -1213,7 +1220,7 @@ run_script_local() { __line_rgb__ "=== Running: $(display_path "$script_path") $param_line ===" 200 200 0 IFS=' ' read -r -a param_array <<<"$param_line" - param_line=$(echo "$param_line" | tr -d '\r') + param_line="${param_line//$'\r'/}" mkdir -p .log touch .log/out.log @@ -1413,6 +1420,15 @@ update_scripts() { echo "Updating files..." + # Safety check: verify BASE_DIR looks like the repo before deleting + if [[ ! -f "$BASE_DIR/GUI.sh" || ! -d "$BASE_DIR/Utilities" ]]; then + echo "Error: BASE_DIR ($BASE_DIR) does not appear to be the ProxmoxScripts repository" + echo "Aborting update to prevent accidental file deletion" + rm -rf "$TEMP_DIR" + sleep 3 + return + fi + # Remove old files but preserve .current_branch find "$BASE_DIR" -mindepth 1 ! -name ".current_branch" ! -name ".git" -delete 2>/dev/null || true @@ -1524,8 +1540,13 @@ navigate() { while true; do clear show_ascii_art - echo -n "CURRENT DIRECTORY: " - __line_rgb__ "./$(display_path "$current_dir")" 0 255 0 + + # Show breadcrumb-style path + local rel_path + rel_path="$(display_path "$current_dir")" + local breadcrumb="${rel_path//\// > }" + echo -n "PATH: " + __line_rgb__ "$breadcrumb" 0 255 0 echo echo "Folders and scripts:" echo "----------------------------------------" @@ -1544,7 +1565,7 @@ navigate() { ((index += 1)) done - # List scripts + # List scripts with one-line descriptions for s in "${scripts[@]}"; do local sname sname="$(basename "$s")" @@ -1555,8 +1576,41 @@ navigate() { continue fi fi - - __line_rgb__ "$index) $sname" 100 200 100 + + # Extract first description line from script header + local desc="" + local past_name=false + while IFS= read -r line; do + [[ "$line" =~ ^#!/ ]] && continue + if [[ "$line" =~ ^#[[:space:]]*$ ]]; then + continue + elif [[ "$line" =~ ^#[[:space:]]+(.+)$ ]]; then + local content="${BASH_REMATCH[1]}" + # Skip the script filename line + if [[ "$content" == *.sh ]]; then + past_name=true + continue + fi + if [[ "$past_name" == true ]]; then + desc="$content" + break + fi + else + break + fi + done <"$s" + + if [[ -n "$desc" ]]; then + # Truncate long descriptions + if [[ ${#desc} -gt 60 ]]; then + desc="${desc:0:57}..." + fi + echo -ne "\033[38;2;100;200;100m$index) $sname\033[0m" + echo " - $desc" + else + __line_rgb__ "$index) $sname" 100 200 100 + fi + menu_map[$index]="$s" ((index += 1)) done @@ -1565,7 +1619,9 @@ navigate() { echo "----------------------------------------" echo echo "Type 'h' to show script comments." - echo "Type 'l' to change log level (remote execution only)." + if [[ "$EXECUTION_MODE" != "local" ]]; then + echo "Type 'l' to change log level." + fi show_common_footer "b" "e" echo echo "----------------------------------------" diff --git a/Host/Hardware/EnableCPUScalingGoverner.sh b/Host/Hardware/EnableCPUScalingGovernor.sh similarity index 96% rename from Host/Hardware/EnableCPUScalingGoverner.sh rename to Host/Hardware/EnableCPUScalingGovernor.sh index fb61178..ee75348 100755 --- a/Host/Hardware/EnableCPUScalingGoverner.sh +++ b/Host/Hardware/EnableCPUScalingGovernor.sh @@ -1,6 +1,6 @@ #!/bin/bash # -# EnableCPUScalingGoverner.sh +# EnableCPUScalingGovernor.sh # # A script to manage CPU frequency scaling governor on a Proxmox (or general Linux) system. # Supports three major actions: @@ -9,11 +9,11 @@ # 3. configure - Adjust CPU governor ("performance", "balanced", or "powersave") with optional min/max frequencies. # # Usage: -# EnableCPUScalingGoverner.sh install -# EnableCPUScalingGoverner.sh install performance -m 1.2GHz -M 3.0GHz -# EnableCPUScalingGoverner.sh remove -# EnableCPUScalingGoverner.sh configure balanced -# EnableCPUScalingGoverner.sh configure powersave --min 800MHz +# EnableCPUScalingGovernor.sh install +# EnableCPUScalingGovernor.sh install performance -m 1.2GHz -M 3.0GHz +# EnableCPUScalingGovernor.sh remove +# EnableCPUScalingGovernor.sh configure balanced +# EnableCPUScalingGovernor.sh configure powersave --min 800MHz # # Arguments: # action - Action to perform: install, remove, or configure @@ -53,7 +53,7 @@ trap '__handle_err__ $LINENO "$BASH_COMMAND"' ERR # Globals / Defaults ############################################################################### -SCRIPT_NAME="EnableCPUScalingGoverner.sh" +SCRIPT_NAME="EnableCPUScalingGovernor.sh" TARGET_PATH="/usr/local/bin/${SCRIPT_NAME}" BALANCED_FALLBACK="ondemand" diff --git a/LXC/Operations/BulkAddToPool.sh b/LXC/Operations/BulkAddToPool.sh new file mode 100755 index 0000000..577228f --- /dev/null +++ b/LXC/Operations/BulkAddToPool.sh @@ -0,0 +1,73 @@ +#!/bin/bash +# +# BulkAddToPool.sh +# +# Adds LXC containers to a Proxmox VE resource pool. +# Uses BulkOperations framework for cluster-wide execution. +# +# Usage: +# BulkAddToPool.sh +# +# Arguments: +# start_ctid - Starting container ID +# end_ctid - Ending container ID +# pool - Target resource pool name +# +# Examples: +# BulkAddToPool.sh 200 230 production +# BulkAddToPool.sh 100 100 testing +# +# Function Index: +# - main +# + +set -euo pipefail + +# shellcheck source=Utilities/Prompts.sh +source "${UTILITYPATH}/Prompts.sh" +# shellcheck source=Utilities/Communication.sh +source "${UTILITYPATH}/Communication.sh" +# shellcheck source=Utilities/ArgumentParser.sh +source "${UTILITYPATH}/ArgumentParser.sh" +# shellcheck source=Utilities/BulkOperations.sh +source "${UTILITYPATH}/BulkOperations.sh" + +trap '__handle_err__ $LINENO "$BASH_COMMAND"' ERR + +# Parse arguments +__parse_args__ "start_ctid:ctid end_ctid:ctid pool:pool" "$@" + +# --- main -------------------------------------------------------------------- +main() { + __check_root__ + __check_proxmox__ + + add_to_pool_callback() { + local ctid="$1" + pvesh set "/pools/${POOL}" --vms "$ctid" + } + + __bulk_ct_operation__ --name "Add Containers to Pool (${POOL})" --report "$START_CTID" "$END_CTID" add_to_pool_callback + + __bulk_summary__ + + [[ $BULK_FAILED -gt 0 ]] && exit 1 + __ok__ "Containers added to pool '${POOL}' successfully!" +} + +main + +############################################################################### +# Script notes: +############################################################################### +# Last checked: 2026-02-27 +# +# Changes: +# - 2026-02-27: Initial creation +# +# Fixes: +# - +# +# Known issues: +# - +# diff --git a/Networking/FindVMIDFromIP.sh b/Networking/FindVMIDFromIP.sh index dfde091..3c9fbee 100755 --- a/Networking/FindVMIDFromIP.sh +++ b/Networking/FindVMIDFromIP.sh @@ -115,7 +115,7 @@ get_mac_from_ip() { extract_vmid_from_mac() { local mac="$1" local mac_upper - mac_upper=$(echo "$mac" | tr '[:lower:]' '[:upper:]') + mac_upper="${mac^^}" # Check if MAC starts with BC: if [[ ! "$mac_upper" =~ ^BC: ]]; then @@ -154,14 +154,19 @@ find_direct_vmid() { vm_ids=$(pvesh get /nodes/"$node"/qemu --output-format=json 2>/dev/null | jq -r '.[] | .vmid' || true) for vmid in $vm_ids; do + # Cache config (JSON and plain text) once per VM + local vm_config_json vm_config_plain + vm_config_json=$(pvesh get /nodes/"$node"/qemu/"$vmid"/config --output-format=json 2>/dev/null || echo "{}") + vm_config_plain=$(pvesh get /nodes/"$node"/qemu/"$vmid"/config 2>/dev/null || true) + local vm_name + vm_name=$(echo "$vm_config_json" | jq -r '.name // "N/A"') + # Check guest agent local guest_ips guest_ips=$(pvesh get /nodes/"$node"/qemu/"$vmid"/agent/network-get-interfaces --output-format=json 2>/dev/null | \ jq -r '.result[] | select(."ip-addresses") | ."ip-addresses"[] | select(."ip-address") | ."ip-address"' 2>/dev/null || true) if echo "$guest_ips" | grep -q "^${ip}$"; then - local vm_name - vm_name=$(pvesh get /nodes/"$node"/qemu/"$vmid"/config --output-format=json 2>/dev/null | jq -r '.name // "N/A"') echo "" __ok__ "Found VM ${vmid} on node ${node}" echo " Type: VM (qemu)" @@ -172,12 +177,9 @@ find_direct_vmid() { # Check config local config_ips - config_ips=$(pvesh get /nodes/"$node"/qemu/"$vmid"/config 2>/dev/null | \ - grep -oP 'ip=\K[0-9.]+' || true) + config_ips=$(echo "$vm_config_plain" | grep -oP 'ip=\K[0-9.]+' || true) if echo "$config_ips" | grep -q "^${ip}$"; then - local vm_name - vm_name=$(pvesh get /nodes/"$node"/qemu/"$vmid"/config --output-format=json 2>/dev/null | jq -r '.name // "N/A"') echo "" __ok__ "Found VM ${vmid} on node ${node} (from config)" echo " Type: VM (qemu)" @@ -192,14 +194,19 @@ find_direct_vmid() { ct_ids=$(pvesh get /nodes/"$node"/lxc --output-format=json 2>/dev/null | jq -r '.[] | .vmid' || true) for ctid in $ct_ids; do + # Cache config (JSON and plain text) once per CT + local ct_config_json ct_config_plain + ct_config_json=$(pvesh get /nodes/"$node"/lxc/"$ctid"/config --output-format=json 2>/dev/null || echo "{}") + ct_config_plain=$(pvesh get /nodes/"$node"/lxc/"$ctid"/config 2>/dev/null || true) + local ct_name + ct_name=$(echo "$ct_config_json" | jq -r '.hostname // "N/A"') + # Check interfaces local ct_ips ct_ips=$(pvesh get /nodes/"$node"/lxc/"$ctid"/interfaces --output-format=json 2>/dev/null | \ jq -r '.[] | select(.inet) | .inet' 2>/dev/null | cut -d'/' -f1 || true) if echo "$ct_ips" | grep -q "^${ip}$"; then - local ct_name - ct_name=$(pvesh get /nodes/"$node"/lxc/"$ctid"/config --output-format=json 2>/dev/null | jq -r '.hostname // "N/A"') echo "" __ok__ "Found CT ${ctid} on node ${node}" echo " Type: LXC" @@ -209,12 +216,9 @@ find_direct_vmid() { fi # Check config - config_ips=$(pvesh get /nodes/"$node"/lxc/"$ctid"/config 2>/dev/null | \ - grep -oP 'ip=\K[0-9.]+' || true) + config_ips=$(echo "$ct_config_plain" | grep -oP 'ip=\K[0-9.]+' || true) if echo "$config_ips" | grep -q "^${ip}$"; then - local ct_name - ct_name=$(pvesh get /nodes/"$node"/lxc/"$ctid"/config --output-format=json 2>/dev/null | jq -r '.hostname // "N/A"') echo "" __ok__ "Found CT ${ctid} on node ${node} (from config)" echo " Type: LXC" diff --git a/README.md b/README.md index 804fe90..ba4c63e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Proxmox VE Management Scripts -[![Version](https://img.shields.io/badge/version-2.1.6-blue.svg)](https://github.com/coelacant1/ProxmoxScripts/releases) +[![Version](https://img.shields.io/badge/version-2.2.0-blue.svg)](https://github.com/coelacant1/ProxmoxScripts/releases) [![Repository Checks](https://github.com/coelacant1/ProxmoxScripts/actions/workflows/checks.yml/badge.svg)](https://github.com/coelacant1/ProxmoxScripts/actions/workflows/checks.yml) [![Deploy static content to Pages](https://github.com/coelacant1/ProxmoxScripts/actions/workflows/static.yml/badge.svg?branch=main)](https://github.com/coelacant1/ProxmoxScripts/actions/workflows/static.yml) [![Release on .sh changes](https://github.com/coelacant1/ProxmoxScripts/actions/workflows/release.yml/badge.svg?branch=main)](https://github.com/coelacant1/ProxmoxScripts/actions/workflows/release.yml) @@ -233,6 +233,17 @@ Example for description and example commands in each script in this repository: vm_offset - An integer value to offset the VM IDs to avoid conflicts. target_network - The network bridge on the target server to connect the VMs. +## Documentation + +Comprehensive guides are available in the [Manuals/](Manuals/) directory and via the GUI (`h` or `?`): + +- **[Getting Started](Manuals/getting-started.txt)** - Setup, prerequisites, and first-run instructions +- **[GUI Overview](Manuals/gui-overview.txt)** - Complete interface guide +- **[Execution Modes](Manuals/execution-modes.txt)** - Local, single-remote, and multi-remote modes +- **[Node Management](Manuals/node-management.txt)** - Adding and configuring Proxmox nodes +- **[SSH Proxy Configuration](Manuals/ssh-proxy-configuration.txt)** - Setting up SSH proxies and jump hosts +- **[Troubleshooting](Manuals/troubleshooting.txt)** - Common issues and solutions + ## Contributing Please read the [CONTRIBUTING.md](CONTRIBUTING.md) guide for: diff --git a/Resources/ChangeAllMACPrefix.sh b/Resources/ChangeAllMACPrefix.sh index 2068eb4..2634098 100755 --- a/Resources/ChangeAllMACPrefix.sh +++ b/Resources/ChangeAllMACPrefix.sh @@ -99,7 +99,8 @@ main() { # Validate and normalize prefix local prefix_upper - prefix_upper="$(echo "$prefix" | tr '[:lower:]' '[:upper:]' | xargs)" + prefix_upper="${prefix^^}" + prefix_upper="${prefix_upper// /}" if [[ ! "$prefix_upper" =~ ^([0-9A-F]{2}):([0-9A-F]{2}):([0-9A-F]{2})$ ]]; then __err__ "Invalid MAC prefix: $prefix" diff --git a/Resources/InteractiveRestore.sh b/Resources/InteractiveRestore.sh index 60a720e..9866792 100755 --- a/Resources/InteractiveRestore.sh +++ b/Resources/InteractiveRestore.sh @@ -200,7 +200,7 @@ restore_container() { __info__ "Executing: ${cmd}" - if eval "$cmd" 2>&1; then + if bash -c "$cmd" 2>&1; then __ok__ "Container ${ctid} restored successfully" return 0 else diff --git a/Storage/AddStorage.sh b/Storage/AddStorage.sh index 9db672e..c31d577 100755 --- a/Storage/AddStorage.sh +++ b/Storage/AddStorage.sh @@ -145,7 +145,7 @@ add_nfs_storage() { [[ -n "$NODES" ]] && cmd+=" --nodes ${NODES}" [[ -n "$OPTIONS" ]] && cmd+=" --options ${OPTIONS}" - if eval "$cmd"; then + if bash -c "$cmd"; then __ok__ "NFS storage '${STORAGE_ID}' added successfully" return 0 else @@ -168,7 +168,7 @@ add_smb_storage() { [[ -n "$CONTENT" ]] && cmd+=" --content ${CONTENT}" [[ -n "$NODES" ]] && cmd+=" --nodes ${NODES}" - if eval "$cmd"; then + if bash -c "$cmd"; then __ok__ "${STORAGE_TYPE^^} storage '${STORAGE_ID}' added successfully" return 0 else @@ -191,7 +191,7 @@ add_pbs_storage() { [[ -n "$FINGERPRINT" ]] && cmd+=" --fingerprint ${FINGERPRINT}" [[ -n "$NODES" ]] && cmd+=" --nodes ${NODES}" - if eval "$cmd"; then + if bash -c "$cmd"; then __ok__ "PBS storage '${STORAGE_ID}' added successfully" return 0 else diff --git a/Storage/RemoveStorage.sh b/Storage/RemoveStorage.sh index 2ed86f9..3842e28 100755 --- a/Storage/RemoveStorage.sh +++ b/Storage/RemoveStorage.sh @@ -62,7 +62,9 @@ check_storage_usage() { # Check VMs for vmid in $(qm list 2>/dev/null | tail -n +2 | awk '{print $1}'); do - if qm config "$vmid" 2>/dev/null | grep -q ":${STORAGE_ID}:"; then + local vm_config + vm_config=$(qm config "$vmid" 2>/dev/null) || continue + if echo "$vm_config" | grep -q ":${STORAGE_ID}:"; then in_use=true usage_details+=" VM ${vmid}\n" fi @@ -70,7 +72,9 @@ check_storage_usage() { # Check LXC containers for ctid in $(pct list 2>/dev/null | tail -n +2 | awk '{print $1}'); do - if pct config "$ctid" 2>/dev/null | grep -q ":${STORAGE_ID}:"; then + local ct_config + ct_config=$(pct config "$ctid" 2>/dev/null) || continue + if echo "$ct_config" | grep -q ":${STORAGE_ID}:"; then in_use=true usage_details+=" CT ${ctid}\n" fi diff --git a/ThirdParty/ApacheGuacamole/GetGuacamoleAuthenticationToken.sh b/ThirdParty/ApacheGuacamole/GetGuacamoleAuthenticationToken.sh index af6eabe..d7c2820 100755 --- a/ThirdParty/ApacheGuacamole/GetGuacamoleAuthenticationToken.sh +++ b/ThirdParty/ApacheGuacamole/GetGuacamoleAuthenticationToken.sh @@ -44,11 +44,12 @@ main() { __info__ "Requesting authentication token from Guacamole" __info__ "Server: $GUAC_URL" - mkdir -p "$(dirname "$TOKEN_PATH")" + mkdir -p -m 700 "$(dirname "$TOKEN_PATH")" local token_response if ! token_response=$(curl -s -X POST \ - -d "username=${GUAC_USER}&password=${GUAC_PASS}" \ + --data-urlencode "username=${GUAC_USER}" \ + --data-urlencode "password=${GUAC_PASS}" \ "${GUAC_URL}/api/tokens" 2>&1); then __err__ "Failed to connect to Guacamole server" exit 1 @@ -64,6 +65,7 @@ main() { fi echo "$auth_token" >"$TOKEN_PATH" + chmod 600 "$TOKEN_PATH" __ok__ "Authentication token retrieved successfully!" __info__ "Token saved to: $TOKEN_PATH" diff --git a/Utilities/ArgumentParser.sh b/Utilities/ArgumentParser.sh index 9f316ca..51fa39c 100755 --- a/Utilities/ArgumentParser.sh +++ b/Utilities/ArgumentParser.sh @@ -168,6 +168,8 @@ __parse_args__() { PATH HOME USER SHELL PWD OLDPWD IFS LANG TZ TERM TMPDIR UID EUID GID PPID BASH_VERSION BASH_VERSINFO FUNCNAME LINENO + HOSTNAME LOGNAME MAIL EDITOR + TEMP TMP GROUPS ) # Arrays to hold spec details @@ -227,9 +229,9 @@ __parse_args__() { # Initialize variable if [[ "$type" == "flag" || "$type" == "bool" ]]; then - eval "${var_name}=false" + declare -g "${var_name}=false" else - eval "${var_name}='${default}'" + declare -g "${var_name}=${default}" fi else # Positional argument @@ -307,7 +309,7 @@ __parse_args__() { # Handle boolean flags if [[ "$flag_type" == "flag" || "$flag_type" == "bool" ]]; then - eval "${flag_name}=true" + declare -g "${flag_name}=true" __argparser_log__ "DEBUG" "Set boolean flag: $flag_name=true" shift continue @@ -329,7 +331,7 @@ __parse_args__() { return 1 fi - eval "${flag_name}='${flag_value}'" + declare -g "${flag_name}=${flag_value}" __argparser_log__ "DEBUG" "Set flag: $flag_name='$flag_value'" else # Positional argument @@ -352,7 +354,7 @@ __parse_args__() { return 1 fi - eval "${pos_name}='${arg}'" + declare -g "${pos_name}=${arg}" __argparser_log__ "DEBUG" "Set positional: $pos_name='$arg'" ((positional_index += 1)) || true shift @@ -371,7 +373,7 @@ __parse_args__() { fi local pos_name="${POSITIONAL_NAMES[$i]^^}" - eval "${pos_name}='${default}'" + declare -g "${pos_name}=${default}" done # Special handling for VMID ranges diff --git a/Utilities/BulkOperations.sh b/Utilities/BulkOperations.sh index 10b0fee..1f14553 100755 --- a/Utilities/BulkOperations.sh +++ b/Utilities/BulkOperations.sh @@ -48,8 +48,12 @@ __bulk_log__() { } # Source dependencies -source "${UTILITYPATH}/Operations.sh" -source "${UTILITYPATH}/Communication.sh" +if [[ -n "${UTILITYPATH:-}" && -f "${UTILITYPATH}/Operations.sh" ]]; then + source "${UTILITYPATH}/Operations.sh" +fi +if [[ -n "${UTILITYPATH:-}" && -f "${UTILITYPATH}/Communication.sh" ]]; then + source "${UTILITYPATH}/Communication.sh" +fi # Global state for bulk operations declare -g BULK_TOTAL=0 diff --git a/Utilities/Conversion.sh b/Utilities/Conversion.sh index c8c8b6a..2c6bd70 100755 --- a/Utilities/Conversion.sh +++ b/Utilities/Conversion.sh @@ -183,7 +183,7 @@ __vmid_to_mac_prefix__() { done local upperPrefix - upperPrefix=$(echo "$prefix" | tr '[:lower:]' '[:upper:]') + upperPrefix="${prefix^^}" local result="$upperPrefix" local segment diff --git a/Utilities/Logger.sh b/Utilities/Logger.sh index d5f6ed5..76e319f 100755 --- a/Utilities/Logger.sh +++ b/Utilities/Logger.sh @@ -195,7 +195,7 @@ __log_command__() { __log__ "DEBUG" "Executing: $cmd" "CMD" # Execute command and capture exit code - eval "$cmd" + bash -c "$cmd" local exit_code=$? if [[ $exit_code -eq 0 ]]; then diff --git a/Utilities/Operations.sh b/Utilities/Operations.sh index 54aedb1..4bf924a 100755 --- a/Utilities/Operations.sh +++ b/Utilities/Operations.sh @@ -587,7 +587,7 @@ __ct_stop__() { __api_log__ "DEBUG" "Executing: $cmd" - if eval "$cmd" 2>/dev/null; then + if bash -c "$cmd" 2>/dev/null; then __api_log__ "INFO" "Successfully stopped CT $ctid" return 0 else @@ -1371,7 +1371,7 @@ __node_exec__() { __api_log__ "DEBUG" "Executing locally on $node" local output local exit_code - output=$(eval "$command" 2>&1) + output=$(bash -c "$command" 2>&1) exit_code=$? if [[ -n "$output" ]]; then echo "$output" @@ -1556,7 +1556,7 @@ __ct_set_memory__() { local cmd="pct set $ctid --memory $memory" [[ -n "$swap" ]] && cmd+=" --swap $swap" - if eval "$cmd" 2>/dev/null; then + if bash -c "$cmd" 2>/dev/null; then __api_log__ "INFO" "Successfully set memory for CT $ctid" return 0 else @@ -1851,7 +1851,7 @@ __ct_change_password__() { return 1 fi - if __ct_exec__ "$ctid" "echo '${username}:${password}' | chpasswd" 2>/dev/null; then + if echo "${username}:${password}" | pct exec "$ctid" -- chpasswd 2>/dev/null; then __api_log__ "INFO" "Successfully changed password for user $username in CT $ctid" return 0 else diff --git a/Utilities/RemoteExecutor.sh b/Utilities/RemoteExecutor.sh index 816c08b..d782d58 100755 --- a/Utilities/RemoteExecutor.sh +++ b/Utilities/RemoteExecutor.sh @@ -151,10 +151,13 @@ __test_remote_connection__() { local username="$3" local port="$4" - # Try simple echo command to test connection - if __ssh_exec__ "$node_ip" "$node_pass" "$username" "$port" "echo test" &>/dev/null; then + # Try simple echo command to test connection, capture stderr for diagnostics + LAST_SSH_ERROR="" + local ssh_err + if ssh_err=$(__ssh_exec__ "$node_ip" "$node_pass" "$username" "$port" "echo test" 2>&1 >/dev/null); then return 0 else + LAST_SSH_ERROR="$ssh_err" return 1 fi } @@ -171,9 +174,9 @@ __ssh_exec__() { local command="$*" if [[ "$USE_SSH_KEYS" == "true" ]] || [[ -z "$node_pass" ]]; then - ssh -o StrictHostKeyChecking=no -o ConnectTimeout=5 -p "$port" "${username}@${node_ip}" "$command" + ssh -o StrictHostKeyChecking=no -o ConnectTimeout=5 -o ServerAliveInterval=5 -o ServerAliveCountMax=3 -p "$port" "${username}@${node_ip}" "$command" else - sshpass -p "$node_pass" ssh -o StrictHostKeyChecking=no -o ConnectTimeout=5 -p "$port" "${username}@${node_ip}" "$command" + sshpass -p "$node_pass" ssh -o StrictHostKeyChecking=no -o ConnectTimeout=5 -o ServerAliveInterval=5 -o ServerAliveCountMax=3 -p "$port" "${username}@${node_ip}" "$command" fi } @@ -379,7 +382,7 @@ __execute_on_remote_node__() { REMOTE_CURRENT_NODE_PORT="$port" REMOTE_CURRENT_PID_FILE="$remote_pid_file" - # Stop spinner — stream live output directly to terminal + # Stop spinner - stream live output directly to terminal __ok__ "Executing script on $node_name (live output)..." echo echo "--- Remote Output ($node_name) ---" @@ -465,6 +468,8 @@ __execute_remote_script__() { local success_count=0 local fail_count=0 + declare -a FAILED_NODES=() + declare -a SUCCEEDED_NODES=() # Reset interrupt flag REMOTE_INTERRUPTED=0 @@ -491,6 +496,7 @@ __execute_remote_script__() { IFS=':' read -r node_name node_ip <<<"$target" || { echo "Failed to parse target: $target" ((fail_count += 1)) + FAILED_NODES+=("$node_name") continue } @@ -501,6 +507,7 @@ __execute_remote_script__() { else echo "No password configured for $node_name" ((fail_count += 1)) + FAILED_NODES+=("$node_name") continue fi @@ -513,8 +520,10 @@ __execute_remote_script__() { # Execute on this node (always continue to next node regardless of result) if __execute_on_remote_node__ "$node_name" "$node_ip" "$node_pass" "$node_username" "$node_port" "$script_path" "$script_relative" "$script_dir_relative" "$param_line"; then ((success_count += 1)) + SUCCEEDED_NODES+=("$node_name") else ((fail_count += 1)) + FAILED_NODES+=("$node_name") fi # Check again after execution in case interrupt happened during execution @@ -537,8 +546,44 @@ __execute_remote_script__() { echo echo "========================================" echo "Summary: $success_count successful, $fail_count failed" + if [[ ${#SUCCEEDED_NODES[@]} -gt 0 ]]; then + echo " OK: ${SUCCEEDED_NODES[*]}" + fi + if [[ ${#FAILED_NODES[@]} -gt 0 ]]; then + echo " FAIL: ${FAILED_NODES[*]}" + fi echo "========================================" + # Offer retry for failed nodes + if [[ ${#FAILED_NODES[@]} -gt 0 && ${#REMOTE_TARGETS[@]} -gt 1 ]]; then + echo + read -rp "Retry failed nodes? (y/n): " retry_choice + if [[ "$retry_choice" == "y" || "$retry_choice" == "Y" ]]; then + # Build a temporary target list from failed nodes only + local -a ORIGINAL_TARGETS=("${REMOTE_TARGETS[@]}") + REMOTE_TARGETS=() + for node_name in "${FAILED_NODES[@]}"; do + for target in "${ORIGINAL_TARGETS[@]}"; do + if [[ "${target%%:*}" == "$node_name" ]]; then + REMOTE_TARGETS+=("$target") + break + fi + done + done + + echo + echo "Retrying ${#REMOTE_TARGETS[@]} failed node(s)..." + echo + + # Recursive retry + __execute_remote_script__ "$script_path" "$display_path_result" "$script_relative" "$script_dir_relative" "$param_line" + + # Restore original targets + REMOTE_TARGETS=("${ORIGINAL_TARGETS[@]}") + return + fi + fi + # Export for use by GUI.sh export LAST_SCRIPT="$display_path_result" export LAST_OUTPUT="Remote execution on ${#REMOTE_TARGETS[@]} node(s): $success_count OK, $fail_count FAIL" diff --git a/Utilities/SSH.sh b/Utilities/SSH.sh index 2b82fba..e9977fb 100755 --- a/Utilities/SSH.sh +++ b/Utilities/SSH.sh @@ -62,7 +62,7 @@ __wait_for_ssh__() { for attempt in $(seq 1 "$maxAttempts"); do __ssh_log__ "DEBUG" "SSH connection attempt $attempt/$maxAttempts to $host" - if sshpass -p "$sshPassword" ssh -o BatchMode=no -o ConnectTimeout=5 -o StrictHostKeyChecking=no \ + if SSHPASS="$sshPassword" sshpass -e ssh -o BatchMode=no -o ConnectTimeout=5 -o StrictHostKeyChecking=no \ "$sshUsername@$host" exit 2>/dev/null; then echo "SSH is up on \"$host\"" __ssh_log__ "INFO" "SSH connection successful to $host" @@ -216,7 +216,8 @@ __ssh_exec__() { local -a sshCmd=() if [ -n "$password" ]; then - sshCmd+=(sshpass -p "$password") + export SSHPASS="$password" + sshCmd+=(sshpass -e) fi sshCmd+=(ssh) @@ -229,7 +230,7 @@ __ssh_exec__() { sshCmd+=(-p "$port") fi - sshCmd+=(-o "BatchMode=no" -o "ConnectTimeout=$connectTimeout") + sshCmd+=(-o "BatchMode=no" -o "ConnectTimeout=$connectTimeout" -o "ServerAliveInterval=5" -o "ServerAliveCountMax=3") if [ "$useStrict" -eq 0 ]; then sshCmd+=(-o "StrictHostKeyChecking=no" -o "UserKnownHostsFile=/dev/null") @@ -248,6 +249,7 @@ __ssh_exec__() { __ssh_log__ "DEBUG" "Executing: ${sshCmd[*]}" "${sshCmd[@]}" local exit_code=$? + unset SSHPASS __ssh_log__ "DEBUG" "SSH command completed with exit code: $exit_code" return $exit_code } @@ -367,7 +369,8 @@ __scp_send__() { local -a scpCmd=() if [ -n "$password" ]; then - scpCmd+=(sshpass -p "$password") + export SSHPASS="$password" + scpCmd+=(sshpass -e) fi scpCmd+=(scp -q) @@ -403,6 +406,7 @@ __scp_send__() { __ssh_log__ "DEBUG" "Executing SCP send: ${scpCmd[*]}" "${scpCmd[@]}" local exit_code=$? + unset SSHPASS __ssh_log__ "DEBUG" "SCP send completed with exit code: $exit_code" return $exit_code } @@ -516,7 +520,8 @@ __scp_fetch__() { local -a scpCmd=() if [ -n "$password" ]; then - scpCmd+=(sshpass -p "$password") + export SSHPASS="$password" + scpCmd+=(sshpass -e) fi scpCmd+=(scp -q) @@ -558,6 +563,7 @@ __scp_fetch__() { __ssh_log__ "DEBUG" "Executing SCP fetch: ${scpCmd[*]}" "${scpCmd[@]}" local exit_code=$? + unset SSHPASS __ssh_log__ "DEBUG" "SCP fetch completed with exit code: $exit_code" return $exit_code } diff --git a/Utilities/_Utilities.md b/Utilities/_Utilities.md index 1f06dd8..9de20c1 100644 --- a/Utilities/_Utilities.md +++ b/Utilities/_Utilities.md @@ -1,6 +1,6 @@ # ProxmoxScripts Utility Functions Reference -**Auto-generated documentation** - Last updated: 2026-02-25 12:57:55 +**Auto-generated documentation** - Last updated: 2026-03-02 14:20:02 --- diff --git a/VirtualMachines/CloudInit/BulkMoveCloudInit.sh b/VirtualMachines/CloudInit/BulkMoveCloudInit.sh index 8093d82..41d4e14 100755 --- a/VirtualMachines/CloudInit/BulkMoveCloudInit.sh +++ b/VirtualMachines/CloudInit/BulkMoveCloudInit.sh @@ -175,7 +175,7 @@ main() { [[ -n "$sshkeys_option" ]] && cmd="$cmd $sshkeys_option" local result=0 - if ! eval "$cmd" 2>/dev/null; then + if ! bash -c "$cmd" 2>/dev/null; then result=1 fi diff --git a/VirtualMachines/CreateFromISO.sh b/VirtualMachines/CreateFromISO.sh index 5d9b303..4048f62 100755 --- a/VirtualMachines/CreateFromISO.sh +++ b/VirtualMachines/CreateFromISO.sh @@ -1,5 +1,4 @@ #!/bin/bash -set -euo pipefail # # CreateFromISO.sh # @@ -7,8 +6,8 @@ set -euo pipefail # and creates a Proxmox VM with user-specified or tier-based parameters. # # Usage: -# CreateFromISO.sh -n Win10 -L "http://example.com/windows10.iso" -# CreateFromISO.sh -n Win10 -L "http://example.com/windows10.iso" -s "local-lvm" -d 32 -b uefi -p t0h -v vmbr1 +# CreateFromISO.sh --vm-name Win10 --iso-url "http://example.com/windows10.iso" +# CreateFromISO.sh --vm-name Win10 --iso-url "http://example.com/windows10.iso" --vm-storage "local-lvm" --disk-gib 32 --bios-type uefi --tier t0h --bridge-name vmbr1 # # Tier Profiles (memory in GiB, cores): # t0h: 64GB, 20 cores, host CPU @@ -34,7 +33,12 @@ set -euo pipefail # Dependencies beyond default Proxmox 8: None (curl is included by default). # +set -euo pipefail + +# shellcheck source=Utilities/Prompts.sh source "${UTILITYPATH}/Prompts.sh" +# shellcheck source=Utilities/ArgumentParser.sh +source "${UTILITYPATH}/ArgumentParser.sh" ############################################################################### # pick_largest_storage_for_content: returns the storage ID with the most free space @@ -56,7 +60,7 @@ function pick_largest_storage_for_content { local numericAvail if [[ "$storeAvail" =~ G$ ]]; then local withoutG="${storeAvail%G}" - numericAvail=$(awk -v val="$withoutG" 'BEGIN {printf "%.0f", val*1024}') + numericAvail=$(LC_NUMERIC=C awk -v val="${withoutG//,/.}" 'BEGIN {printf "%.0f", val*1024}') else # If no 'G' suffix, assume bytes and convert to MiB numericAvail=$((storeAvail / 1024 / 1024)) @@ -343,31 +347,8 @@ function pick_iso_local_or_remote { __check_root__ __check_proxmox__ -# Parse optional arguments for non-interactive: -# -n / -N => VM_NAME -# -l / -L => ISO_URL -# -s / -S => VM_STORAGE -# -d / -D => DISK_GIB -# -b / -B => BIOS_TYPE -# -p / -P => TIER -# -v / -V => BRIDGE_NAME -# -i / -I => VM_ID -while getopts ":n:N:l:L:s:S:d:D:b:B:p:P:v:V:i:I:" opt; do - case "${opt}" in - n | N) VM_NAME="${OPTARG}" ;; - l | L) ISO_URL="${OPTARG}" ;; - s | S) VM_STORAGE="${OPTARG}" ;; - d | D) DISK_GIB="${OPTARG}" ;; - b | B) BIOS_TYPE="${OPTARG}" ;; - p | P) TIER="${OPTARG}" ;; - v | V) BRIDGE_NAME="${OPTARG}" ;; - i | I) VM_ID="${OPTARG}" ;; - *) - echo "Unknown option -${opt}" >&2 - exit 1 - ;; - esac -done +# Parse optional arguments for non-interactive mode (all optional; interactive fallback below) +__parse_args__ "--vm-name:string:? --iso-url:string:? --vm-storage:string:? --disk-gib:string:? --bios-type:string:? --tier:string:? --bridge-name:string:? --vm-id:string:?" "$@" ############################################################################### # Interactive mode if essential arguments are missing diff --git a/VirtualMachines/Hardware/BulkConfigureDiskBandwidth.sh b/VirtualMachines/Hardware/BulkConfigureDiskBandwidth.sh index 7882bc7..4407829 100755 --- a/VirtualMachines/Hardware/BulkConfigureDiskBandwidth.sh +++ b/VirtualMachines/Hardware/BulkConfigureDiskBandwidth.sh @@ -163,25 +163,25 @@ main() { # Update read bandwidth limit if [[ -n "$MBPS_RD" ]]; then - new_config=$(echo "$new_config" | sed "s/,mbps_rd=[^,]*//" | sed "s/mbps_rd=[^,]*,//") + new_config=$(echo "$new_config" | sed -e "s/,mbps_rd=[^,]*//" -e "s/mbps_rd=[^,]*,//") [[ "$MBPS_RD" != "0" ]] && new_config="${new_config},mbps_rd=${MBPS_RD}" fi # Update write bandwidth limit if [[ -n "$MBPS_WR" ]]; then - new_config=$(echo "$new_config" | sed "s/,mbps_wr=[^,]*//" | sed "s/mbps_wr=[^,]*,//") + new_config=$(echo "$new_config" | sed -e "s/,mbps_wr=[^,]*//" -e "s/mbps_wr=[^,]*,//") [[ "$MBPS_WR" != "0" ]] && new_config="${new_config},mbps_wr=${MBPS_WR}" fi # Update read burst bandwidth limit if [[ -n "$MBPS_RD_MAX" ]]; then - new_config=$(echo "$new_config" | sed "s/,mbps_rd_max=[^,]*//" | sed "s/mbps_rd_max=[^,]*,//") + new_config=$(echo "$new_config" | sed -e "s/,mbps_rd_max=[^,]*//" -e "s/mbps_rd_max=[^,]*,//") [[ "$MBPS_RD_MAX" != "0" ]] && new_config="${new_config},mbps_rd_max=${MBPS_RD_MAX}" fi # Update write burst bandwidth limit if [[ -n "$MBPS_WR_MAX" ]]; then - new_config=$(echo "$new_config" | sed "s/,mbps_wr_max=[^,]*//" | sed "s/mbps_wr_max=[^,]*,//") + new_config=$(echo "$new_config" | sed -e "s/,mbps_wr_max=[^,]*//" -e "s/mbps_wr_max=[^,]*,//") [[ "$MBPS_WR_MAX" != "0" ]] && new_config="${new_config},mbps_wr_max=${MBPS_WR_MAX}" fi diff --git a/VirtualMachines/Hardware/BulkConfigureDiskIOPS.sh b/VirtualMachines/Hardware/BulkConfigureDiskIOPS.sh index a54a2a0..192d925 100755 --- a/VirtualMachines/Hardware/BulkConfigureDiskIOPS.sh +++ b/VirtualMachines/Hardware/BulkConfigureDiskIOPS.sh @@ -164,25 +164,25 @@ main() { # Update read IOPS limit if [[ -n "$IOPS_RD" ]]; then - new_config=$(echo "$new_config" | sed "s/,iops_rd=[^,]*//" | sed "s/iops_rd=[^,]*,//") + new_config=$(echo "$new_config" | sed -e "s/,iops_rd=[^,]*//" -e "s/iops_rd=[^,]*,//") [[ "$IOPS_RD" != "0" ]] && new_config="${new_config},iops_rd=${IOPS_RD}" fi # Update write IOPS limit if [[ -n "$IOPS_WR" ]]; then - new_config=$(echo "$new_config" | sed "s/,iops_wr=[^,]*//" | sed "s/iops_wr=[^,]*,//") + new_config=$(echo "$new_config" | sed -e "s/,iops_wr=[^,]*//" -e "s/iops_wr=[^,]*,//") [[ "$IOPS_WR" != "0" ]] && new_config="${new_config},iops_wr=${IOPS_WR}" fi # Update read burst IOPS limit if [[ -n "$IOPS_RD_MAX" ]]; then - new_config=$(echo "$new_config" | sed "s/,iops_rd_max=[^,]*//" | sed "s/iops_rd_max=[^,]*,//") + new_config=$(echo "$new_config" | sed -e "s/,iops_rd_max=[^,]*//" -e "s/iops_rd_max=[^,]*,//") [[ "$IOPS_RD_MAX" != "0" ]] && new_config="${new_config},iops_rd_max=${IOPS_RD_MAX}" fi # Update write burst IOPS limit if [[ -n "$IOPS_WR_MAX" ]]; then - new_config=$(echo "$new_config" | sed "s/,iops_wr_max=[^,]*//" | sed "s/iops_wr_max=[^,]*,//") + new_config=$(echo "$new_config" | sed -e "s/,iops_wr_max=[^,]*//" -e "s/iops_wr_max=[^,]*,//") [[ "$IOPS_WR_MAX" != "0" ]] && new_config="${new_config},iops_wr_max=${IOPS_WR_MAX}" fi diff --git a/VirtualMachines/Hardware/BulkConfigureNetworkBandwidth.sh b/VirtualMachines/Hardware/BulkConfigureNetworkBandwidth.sh index 86aadf6..e346ba5 100755 --- a/VirtualMachines/Hardware/BulkConfigureNetworkBandwidth.sh +++ b/VirtualMachines/Hardware/BulkConfigureNetworkBandwidth.sh @@ -107,7 +107,7 @@ main() { local new_config="$current_config" # Remove existing rate parameter - new_config=$(echo "$new_config" | sed "s/,rate=[^,]*//" | sed "s/rate=[^,]*,//") + new_config=$(echo "$new_config" | sed -e "s/,rate=[^,]*//" -e "s/rate=[^,]*,//") # Add new rate if non-zero [[ "$RATE" != "0" ]] && new_config="${new_config},rate=${RATE}" diff --git a/VirtualMachines/Operations/BulkAddToPool.sh b/VirtualMachines/Operations/BulkAddToPool.sh new file mode 100755 index 0000000..a92f98b --- /dev/null +++ b/VirtualMachines/Operations/BulkAddToPool.sh @@ -0,0 +1,73 @@ +#!/bin/bash +# +# BulkAddToPool.sh +# +# Adds virtual machines to a Proxmox VE resource pool. +# Uses BulkOperations framework for cluster-wide execution. +# +# Usage: +# BulkAddToPool.sh +# +# Arguments: +# start_vmid - Starting VM ID +# end_vmid - Ending VM ID +# pool - Target resource pool name +# +# Examples: +# BulkAddToPool.sh 400 430 production +# BulkAddToPool.sh 100 100 testing +# +# Function Index: +# - main +# + +set -euo pipefail + +# shellcheck source=Utilities/Prompts.sh +source "${UTILITYPATH}/Prompts.sh" +# shellcheck source=Utilities/Communication.sh +source "${UTILITYPATH}/Communication.sh" +# shellcheck source=Utilities/ArgumentParser.sh +source "${UTILITYPATH}/ArgumentParser.sh" +# shellcheck source=Utilities/BulkOperations.sh +source "${UTILITYPATH}/BulkOperations.sh" + +trap '__handle_err__ $LINENO "$BASH_COMMAND"' ERR + +# Parse arguments +__parse_args__ "start_vmid:vmid end_vmid:vmid pool:pool" "$@" + +# --- main -------------------------------------------------------------------- +main() { + __check_root__ + __check_proxmox__ + + add_to_pool_callback() { + local vmid="$1" + pvesh set "/pools/${POOL}" --vms "$vmid" + } + + __bulk_vm_operation__ --name "Add VMs to Pool (${POOL})" --report "$START_VMID" "$END_VMID" add_to_pool_callback + + __bulk_summary__ + + [[ $BULK_FAILED -gt 0 ]] && exit 1 + __ok__ "VMs added to pool '${POOL}' successfully!" +} + +main + +############################################################################### +# Script notes: +############################################################################### +# Last checked: 2026-02-27 +# +# Changes: +# - 2026-02-27: Initial creation +# +# Fixes: +# - +# +# Known issues: +# - +# diff --git a/VirtualMachines/Operations/BulkCloneSetIP_Proxmox.sh b/VirtualMachines/Operations/BulkCloneSetIP_Proxmox.sh index 435dc1f..56a2420 100755 --- a/VirtualMachines/Operations/BulkCloneSetIP_Proxmox.sh +++ b/VirtualMachines/Operations/BulkCloneSetIP_Proxmox.sh @@ -87,7 +87,7 @@ if [[ ! "$NEW_PREFIX" =~ ^([0-9A-Fa-f]{2}):([0-9A-Fa-f]{2}):([0-9A-Fa-f]{2})$ ]] exit 1 fi -UPREFIX="$(echo "$NEW_PREFIX" | tr '[:lower:]' '[:upper:]')" +UPREFIX="${NEW_PREFIX^^}" echo "Setting datacenter mac_prefix to $UPREFIX ..." if [[ ! -d /etc/pve ]]; then diff --git a/VirtualMachines/Operations/BulkReconfigureMacAddresses.sh b/VirtualMachines/Operations/BulkReconfigureMacAddresses.sh index 2fa4415..2b41650 100755 --- a/VirtualMachines/Operations/BulkReconfigureMacAddresses.sh +++ b/VirtualMachines/Operations/BulkReconfigureMacAddresses.sh @@ -91,7 +91,7 @@ if [[ ! "$NEW_PREFIX" =~ ^([0-9A-Fa-f]{2}):([0-9A-Fa-f]{2}):([0-9A-Fa-f]{2})$ ]] exit 1 fi -UPREFIX="$(echo "$NEW_PREFIX" | tr '[:lower:]' '[:upper:]')" +UPREFIX="${NEW_PREFIX^^}" echo "Setting datacenter mac_prefix to $UPREFIX ..." if [[ ! -d /etc/pve ]]; then diff --git a/VirtualMachines/Operations/BulkRemoteMigrate.sh b/VirtualMachines/Operations/BulkRemoteMigrate.sh index b6db834..57f5c07 100755 --- a/VirtualMachines/Operations/BulkRemoteMigrate.sh +++ b/VirtualMachines/Operations/BulkRemoteMigrate.sh @@ -75,7 +75,7 @@ main() { local api_token="apitoken=${TARGET_TOKEN}" local migrate_cmd="qm remote-migrate ${vmid} ${target_vmid} '${api_token},host=${TARGET_HOST},fingerprint=${FINGERPRINT}' --target-bridge ${TARGET_NETWORK} --target-storage ${TARGET_STORAGE} --online" - if eval "$migrate_cmd" 2>/dev/null; then + if bash -c "$migrate_cmd" 2>/dev/null; then return 0 else return 1