diff --git a/.check/UpdateFunctionIndex.py b/.check/UpdateFunctionIndex.py index ffa1d20..8750fca 100644 --- a/.check/UpdateFunctionIndex.py +++ b/.check/UpdateFunctionIndex.py @@ -145,10 +145,12 @@ def process_file(filepath): # Combine the updated comment block with the rest of the file new_contents = updated_block + remainder - # Write back to the file + # Write back to the file, preserving original permissions try: + original_mode = os.stat(filepath).st_mode with open(filepath, "w", encoding="utf-8") as f: f.writelines(new_contents) + os.chmod(filepath, original_mode) return True except Exception as e: print(f"\nCould not write to file '{filepath}': {e}") diff --git a/.check/VerifySourceCalls.py b/.check/VerifySourceCalls.py index d87cdf9..597a459 100644 --- a/.check/VerifySourceCalls.py +++ b/.check/VerifySourceCalls.py @@ -396,9 +396,11 @@ def fix_script( if insertion_index == len(lines) and lines_to_insert: new_lines.extend(lines_to_insert) - # 4) Write updated file + # 4) Write updated file, preserving original permissions + original_mode = os.stat(script_path).st_mode with open(script_path, "w", encoding="utf-8") as f: f.writelines(new_lines) + os.chmod(script_path, original_mode) def should_remove_source_line(line, remove_lines, remove_lines_dot): """ diff --git a/.check/_RunChecks.sh b/.check/_RunChecks.sh old mode 100644 new mode 100755 diff --git a/.docs/GenerateContentDiff.sh b/.docs/GenerateContentDiff.sh old mode 100644 new mode 100755 diff --git a/.docs/UpdatePVEGuide.sh b/.docs/UpdatePVEGuide.sh old mode 100644 new mode 100755 diff --git a/.site/serve.sh b/.site/serve.sh old mode 100644 new mode 100755 diff --git a/CCPVE.sh b/CCPVE.sh old mode 100644 new mode 100755 diff --git a/CHANGELOG.md b/CHANGELOG.md index 574d574..fd06658 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,30 @@ 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.1.9] - 2026-02-24 + +Remote execution cancellation, live output streaming, and custom port support improvements + +### Fixed +- **Script Cancellation** - Ctrl+C now kills the remote script process via SSH + - Tracks remote PID and sends targeted SIGTERM/SIGKILL on interrupt + - Replaced unsafe `killall`/`pkill` with process-specific cleanup +- **ArgumentParser Hyphenated Flags** - Flag names with hyphens now map correctly to variable names + - Converts hyphens to underscores in variable names (e.g. `--my-flag` -> `MY_FLAG`) +- **Custom Port Passthrough** - Fixed port not being passed through in manual node entry and IP/VMID range flows + +### Changed +- **Live Remote Output** - Remote script output now streams directly to terminal + - Replaced deferred log download with real-time `tee`-based streaming + - Output log still saved locally for review +- **Git File Modes** - All `.sh` files tracked as executable (100755) + +### Technical Details +- Added `REMOTE_CURRENT_*` tracking globals and `remote_pid_file` for targeted cleanup in `__remote_cleanup__` +- `__add_remote_target__` and `__clear_remote_targets__` now handle port parameter +- Manual node entry prompts for SSH port in GUI single-remote, multi-IP, and multi-VMID flows +- Node selection prompts for SSH port in `__select_nodes__` + ## [2.1.8] - 2026-01-08 Critical bug fixes for bulk operations and performance optimizations diff --git a/Cluster/AddNodes.sh b/Cluster/AddNodes.sh old mode 100644 new mode 100755 diff --git a/Cluster/CreateCluster.sh b/Cluster/CreateCluster.sh old mode 100644 new mode 100755 diff --git a/Cluster/DeleteCluster.sh b/Cluster/DeleteCluster.sh old mode 100644 new mode 100755 diff --git a/Cluster/RemoveClusterNode.sh b/Cluster/RemoveClusterNode.sh old mode 100644 new mode 100755 diff --git a/Firewall/BulkAddFirewallLXCVM.sh b/Firewall/BulkAddFirewallLXCVM.sh old mode 100644 new mode 100755 diff --git a/Firewall/EnableFirewallSetup.sh b/Firewall/EnableFirewallSetup.sh old mode 100644 new mode 100755 diff --git a/GUI.sh b/GUI.sh old mode 100644 new mode 100755 index 3bc2c88..e5a9e51 --- a/GUI.sh +++ b/GUI.sh @@ -467,15 +467,17 @@ configure_single_remote() { read -rp "Enter node name: " manual_name read -rp "Enter username (default: root): " manual_user manual_user="${manual_user:-root}" + read -rp "Enter SSH port (default: 22): " manual_port + manual_port="${manual_port:-22}" # Check for SSH key auth automatically (test since not in nodes.json) - local has_keys=$(__has_ssh_keys__ "$manual_ip" "$manual_user" "") + local has_keys=$(__has_ssh_keys__ "$manual_ip" "$manual_user" "" "$manual_port") if [[ "$has_keys" == "true" ]]; then echo __line_rgb__ "SSH key authentication detected" 0 255 0 export USE_SSH_KEYS=true __clear_remote_targets__ - __add_remote_target__ "$manual_name" "$manual_ip" "" "$manual_user" + __add_remote_target__ "$manual_name" "$manual_ip" "" "$manual_user" "$manual_port" __set_execution_mode__ "single-remote" __success__ "Using SSH key authentication (no password needed)" sleep 1 @@ -489,9 +491,9 @@ configure_single_remote() { # Test connection before proceeding echo "Testing connection..." - if ! __test_remote_connection__ "$manual_ip" "$manual_pass" "$manual_user" "22"; then + 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, port, and password." 255 0 0 echo "Press Enter to try again..." read -r continue @@ -500,7 +502,7 @@ configure_single_remote() { sleep 1 __clear_remote_targets__ - __add_remote_target__ "$manual_name" "$manual_ip" "$manual_pass" "$manual_user" + __add_remote_target__ "$manual_name" "$manual_ip" "$manual_pass" "$manual_user" "$manual_port" __set_execution_mode__ "single-remote" return 0 fi @@ -513,17 +515,22 @@ configure_single_remote() { node_ip=$(__get_node_ip__ "$node_name") local node_username node_username=$(__get_node_username__ "$node_name") + local node_port + node_port=$(__get_node_port__ "$node_name") # Check if SSH keys work for this node (use cache) local key_indicator="" - local has_keys=$(__has_ssh_keys__ "$node_ip" "$node_username" "$node_name") + local has_keys=$(__has_ssh_keys__ "$node_ip" "$node_username" "$node_name" "$node_port") if [[ "$has_keys" == "true" ]]; then key_indicator=" [SSH Key]" else key_indicator=" [No Key]" fi - __line_rgb__ " $i) $node_name ($node_username@$node_ip) $key_indicator" 0 200 200 + local port_indicator="" + [[ "$node_port" != "22" ]] && port_indicator=":${node_port}" + + __line_rgb__ " $i) $node_name ($node_username@$node_ip${port_indicator}) $key_indicator" 0 200 200 node_menu[$i]="$node_name:$node_ip" ((i += 1)) done < <(__get_available_nodes__) @@ -564,15 +571,17 @@ configure_single_remote() { read -rp "Enter node name: " manual_name read -rp "Enter username (default: root): " manual_user manual_user="${manual_user:-root}" + read -rp "Enter SSH port (default: 22): " manual_port + manual_port="${manual_port:-22}" # Check for SSH key auth automatically (use cache) - local has_keys=$(__has_ssh_keys__ "$manual_ip" "$manual_user" "") + local has_keys=$(__has_ssh_keys__ "$manual_ip" "$manual_user" "" "$manual_port") if [[ "$has_keys" == "true" ]]; then echo __line_rgb__ "SSH key authentication detected" 0 255 0 export USE_SSH_KEYS=true __clear_remote_targets__ - __add_remote_target__ "$manual_name" "$manual_ip" "" "$manual_user" + __add_remote_target__ "$manual_name" "$manual_ip" "" "$manual_user" "$manual_port" __set_execution_mode__ "single-remote" __success__ "Using SSH key authentication (no password needed)" sleep 1 @@ -586,7 +595,7 @@ configure_single_remote() { # Test connection before proceeding echo "Testing connection..." - if ! __test_remote_connection__ "$manual_ip" "$manual_pass" "$manual_user" "22"; then + 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 echo "Press Enter to try again..." @@ -597,22 +606,24 @@ configure_single_remote() { sleep 1 __clear_remote_targets__ - __add_remote_target__ "$manual_name" "$manual_ip" "$manual_pass" "$manual_user" + __add_remote_target__ "$manual_name" "$manual_ip" "$manual_pass" "$manual_user" "$manual_port" __set_execution_mode__ "single-remote" return 0 elif [[ -n "$node_choice" && -n "${node_menu[$node_choice]:-}" ]]; then IFS=':' read -r selected_name selected_ip <<<"${node_menu[$node_choice]}" local selected_username selected_username=$(__get_node_username__ "$selected_name") + local selected_port + selected_port=$(__get_node_port__ "$selected_name") # Check for SSH key auth automatically (use cache) - local has_keys=$(__has_ssh_keys__ "$selected_ip" "$selected_username" "$selected_name") + local has_keys=$(__has_ssh_keys__ "$selected_ip" "$selected_username" "$selected_name" "$selected_port") if [[ "$has_keys" == "true" ]]; then echo __line_rgb__ "SSH key authentication detected" 0 255 0 export USE_SSH_KEYS=true __clear_remote_targets__ - __add_remote_target__ "$selected_name" "$selected_ip" "" "$selected_username" + __add_remote_target__ "$selected_name" "$selected_ip" "" "$selected_username" "$selected_port" __set_execution_mode__ "single-remote" __success__ "Using SSH key authentication (no password needed)" sleep 1 @@ -624,10 +635,6 @@ configure_single_remote() { read -rsp "Enter password for $selected_name: " node_pass echo - # Get port for this node - local selected_port - selected_port=$(__get_node_port__ "$selected_name") - # Test connection before proceeding echo "Testing connection to $selected_name..." if ! __test_remote_connection__ "$selected_ip" "$node_pass" "$selected_username" "$selected_port"; then @@ -641,7 +648,7 @@ configure_single_remote() { sleep 1 __clear_remote_targets__ - __add_remote_target__ "$selected_name" "$selected_ip" "$node_pass" "$selected_username" + __add_remote_target__ "$selected_name" "$selected_ip" "$node_pass" "$selected_username" "$selected_port" __set_execution_mode__ "single-remote" return 0 else @@ -970,6 +977,8 @@ configure_multi_ip_range() { echo read -rp "Enter username for all nodes in range (default: root): " shared_user shared_user="${shared_user:-root}" + read -rp "Enter SSH port for all nodes in range (default: 22): " shared_port + shared_port="${shared_port:-22}" read -rsp "Enter password for all nodes in range: " shared_pass echo @@ -981,6 +990,7 @@ configure_multi_ip_range() { REMOTE_TARGETS+=("$node_name:$ip") NODE_PASSWORDS["$node_name"]="$shared_pass" NODE_USERNAMES["$node_name"]="$shared_user" + NODE_PORTS["$node_name"]="$shared_port" done echo @@ -1027,6 +1037,8 @@ configure_multi_vmid_range() { echo read -rp "Enter username for all target nodes (default: root): " shared_user shared_user="${shared_user:-root}" + read -rp "Enter SSH port for all target nodes (default: 22): " shared_port + shared_port="${shared_port:-22}" read -rsp "Enter password for all target nodes: " shared_pass echo @@ -1049,6 +1061,7 @@ configure_multi_vmid_range() { REMOTE_TARGETS+=("$node_name:$ip") NODE_PASSWORDS["$node_name"]="$shared_pass" NODE_USERNAMES["$node_name"]="$shared_user" + NODE_PORTS["$node_name"]="$shared_port" echo " Found VMID $vmid: $ip" ((found_count += 1)) fi diff --git a/HighAvailability/AddResources.sh b/HighAvailability/AddResources.sh old mode 100644 new mode 100755 diff --git a/HighAvailability/CreateHAGroup.sh b/HighAvailability/CreateHAGroup.sh old mode 100644 new mode 100755 diff --git a/HighAvailability/DisableHAClusterWide.sh b/HighAvailability/DisableHAClusterWide.sh old mode 100644 new mode 100755 diff --git a/HighAvailability/DisableHighAvailability.sh b/HighAvailability/DisableHighAvailability.sh old mode 100644 new mode 100755 diff --git a/Host/Bulk/FirstTimeProxmoxSetup.sh b/Host/Bulk/FirstTimeProxmoxSetup.sh old mode 100644 new mode 100755 diff --git a/Host/Bulk/ProxmoxEnableMicrocode.sh b/Host/Bulk/ProxmoxEnableMicrocode.sh old mode 100644 new mode 100755 diff --git a/Host/Bulk/SetTimeZone.sh b/Host/Bulk/SetTimeZone.sh old mode 100644 new mode 100755 diff --git a/Host/Bulk/UpgradeAllServers.sh b/Host/Bulk/UpgradeAllServers.sh old mode 100644 new mode 100755 diff --git a/Host/Bulk/UpgradeRepositories.sh b/Host/Bulk/UpgradeRepositories.sh old mode 100644 new mode 100755 diff --git a/Host/FanControl/DellIPMIFanControl.sh b/Host/FanControl/DellIPMIFanControl.sh old mode 100644 new mode 100755 diff --git a/Host/FanControl/EnablePWMFanControl.sh b/Host/FanControl/EnablePWMFanControl.sh old mode 100644 new mode 100755 diff --git a/Host/FixDPKGLock.sh b/Host/FixDPKGLock.sh old mode 100644 new mode 100755 diff --git a/Host/Hardware/EnableCPUScalingGoverner.sh b/Host/Hardware/EnableCPUScalingGoverner.sh old mode 100644 new mode 100755 diff --git a/Host/Hardware/EnableGPUPassthroughVM.sh b/Host/Hardware/EnableGPUPassthroughVM.sh old mode 100644 new mode 100755 diff --git a/Host/Hardware/EnableIOMMU.sh b/Host/Hardware/EnableIOMMU.sh old mode 100644 new mode 100755 diff --git a/Host/Hardware/EnablePCIPassthroughLXC.sh b/Host/Hardware/EnablePCIPassthroughLXC.sh old mode 100644 new mode 100755 diff --git a/Host/Hardware/EnableX3DOptimization.sh b/Host/Hardware/EnableX3DOptimization.sh old mode 100644 new mode 100755 diff --git a/Host/Hardware/OnlineMemoryTest.sh b/Host/Hardware/OnlineMemoryTest.sh old mode 100644 new mode 100755 diff --git a/Host/Hardware/OptimizeNestedVirtualization.sh b/Host/Hardware/OptimizeNestedVirtualization.sh old mode 100644 new mode 100755 diff --git a/Host/HostInfo.sh b/Host/HostInfo.sh old mode 100644 new mode 100755 diff --git a/Host/QuickDiagnostic.sh b/Host/QuickDiagnostic.sh old mode 100644 new mode 100755 diff --git a/Host/RemoveLocalLVMAndExpand.sh b/Host/RemoveLocalLVMAndExpand.sh old mode 100644 new mode 100755 diff --git a/Host/SeparateNode.sh b/Host/SeparateNode.sh old mode 100644 new mode 100755 diff --git a/Host/Storage/ExpandEXT4Partition.sh b/Host/Storage/ExpandEXT4Partition.sh old mode 100644 new mode 100755 diff --git a/LXC/Hardware/BulkSetCPU.sh b/LXC/Hardware/BulkSetCPU.sh old mode 100644 new mode 100755 diff --git a/LXC/Hardware/BulkSetMemory.sh b/LXC/Hardware/BulkSetMemory.sh old mode 100644 new mode 100755 diff --git a/LXC/Networking/BulkAddSSHKey.sh b/LXC/Networking/BulkAddSSHKey.sh old mode 100644 new mode 100755 diff --git a/LXC/Networking/BulkChangeDNS.sh b/LXC/Networking/BulkChangeDNS.sh old mode 100644 new mode 100755 diff --git a/LXC/Networking/BulkChangeIP.sh b/LXC/Networking/BulkChangeIP.sh old mode 100644 new mode 100755 diff --git a/LXC/Networking/BulkChangeNetwork.sh b/LXC/Networking/BulkChangeNetwork.sh old mode 100644 new mode 100755 diff --git a/LXC/Networking/BulkChangeUserPass.sh b/LXC/Networking/BulkChangeUserPass.sh old mode 100644 new mode 100755 diff --git a/LXC/Operations/BulkClone.sh b/LXC/Operations/BulkClone.sh old mode 100644 new mode 100755 diff --git a/LXC/Operations/BulkDeleteAllLocal.sh b/LXC/Operations/BulkDeleteAllLocal.sh old mode 100644 new mode 100755 diff --git a/LXC/Operations/BulkDeleteRange.sh b/LXC/Operations/BulkDeleteRange.sh old mode 100644 new mode 100755 diff --git a/LXC/Operations/BulkStart.sh b/LXC/Operations/BulkStart.sh old mode 100644 new mode 100755 diff --git a/LXC/Operations/BulkStop.sh b/LXC/Operations/BulkStop.sh old mode 100644 new mode 100755 diff --git a/LXC/Operations/BulkUnlock.sh b/LXC/Operations/BulkUnlock.sh old mode 100644 new mode 100755 diff --git a/LXC/Options/BulkStartAtBoot.sh b/LXC/Options/BulkStartAtBoot.sh old mode 100644 new mode 100755 diff --git a/LXC/Options/BulkToggleProtectionMode.sh b/LXC/Options/BulkToggleProtectionMode.sh old mode 100644 new mode 100755 diff --git a/LXC/Storage/BulkChangeStorage.sh b/LXC/Storage/BulkChangeStorage.sh old mode 100644 new mode 100755 diff --git a/LXC/Storage/BulkMoveVolume.sh b/LXC/Storage/BulkMoveVolume.sh old mode 100644 new mode 100755 diff --git a/LXC/Storage/BulkResizeStorage.sh b/LXC/Storage/BulkResizeStorage.sh old mode 100644 new mode 100755 diff --git a/LXC/UpdateAll.sh b/LXC/UpdateAll.sh old mode 100644 new mode 100755 diff --git a/Networking/AddNetworkBond.sh b/Networking/AddNetworkBond.sh old mode 100644 new mode 100755 diff --git a/Networking/BulkPrintVMIDMacAddresses.sh b/Networking/BulkPrintVMIDMacAddresses.sh old mode 100644 new mode 100755 diff --git a/Networking/BulkSetDNS.sh b/Networking/BulkSetDNS.sh old mode 100644 new mode 100755 diff --git a/Networking/FindVMFromMacAddress.sh b/Networking/FindVMFromMacAddress.sh old mode 100644 new mode 100755 diff --git a/Networking/FindVMIDFromIP.sh b/Networking/FindVMIDFromIP.sh old mode 100644 new mode 100755 diff --git a/Networking/HostIPerfTest.sh b/Networking/HostIPerfTest.sh old mode 100644 new mode 100755 diff --git a/Networking/UpdateNetworkInterfaceNames.sh b/Networking/UpdateNetworkInterfaceNames.sh old mode 100644 new mode 100755 diff --git a/Networking/UplinkSpeedTest.sh b/Networking/UplinkSpeedTest.sh old mode 100644 new mode 100755 diff --git a/Resources/BulkAddIPToNote.sh b/Resources/BulkAddIPToNote.sh old mode 100644 new mode 100755 diff --git a/Resources/ChangeAllMACPrefix.sh b/Resources/ChangeAllMACPrefix.sh old mode 100644 new mode 100755 diff --git a/Resources/ExportProxmoxResources.sh b/Resources/ExportProxmoxResources.sh old mode 100644 new mode 100755 diff --git a/Resources/FindLinkedClone.sh b/Resources/FindLinkedClone.sh old mode 100644 new mode 100755 diff --git a/Resources/InteractiveRestore.sh b/Resources/InteractiveRestore.sh old mode 100644 new mode 100755 diff --git a/Security/PenetrationTest.sh b/Security/PenetrationTest.sh old mode 100644 new mode 100755 diff --git a/Security/PortScan.sh b/Security/PortScan.sh old mode 100644 new mode 100755 diff --git a/Storage/AddStorage.sh b/Storage/AddStorage.sh old mode 100644 new mode 100755 diff --git a/Storage/Benchmark.sh b/Storage/Benchmark.sh old mode 100644 new mode 100755 diff --git a/Storage/Ceph/Cluster/CreateOSDs.sh b/Storage/Ceph/Cluster/CreateOSDs.sh old mode 100644 new mode 100755 diff --git a/Storage/Ceph/Cluster/RestartManagers.sh b/Storage/Ceph/Cluster/RestartManagers.sh old mode 100644 new mode 100755 diff --git a/Storage/Ceph/Cluster/RestartMetadata.sh b/Storage/Ceph/Cluster/RestartMetadata.sh old mode 100644 new mode 100755 diff --git a/Storage/Ceph/Cluster/RestartMonitors.sh b/Storage/Ceph/Cluster/RestartMonitors.sh old mode 100644 new mode 100755 diff --git a/Storage/Ceph/Cluster/RestartOSDs.sh b/Storage/Ceph/Cluster/RestartOSDs.sh old mode 100644 new mode 100755 diff --git a/Storage/Ceph/Cluster/StartStoppedOSDs.sh b/Storage/Ceph/Cluster/StartStoppedOSDs.sh old mode 100644 new mode 100755 diff --git a/Storage/Ceph/Host/EditCrushmap.sh b/Storage/Ceph/Host/EditCrushmap.sh old mode 100644 new mode 100755 diff --git a/Storage/Ceph/Host/RestartAllDaemons.sh b/Storage/Ceph/Host/RestartAllDaemons.sh old mode 100644 new mode 100755 diff --git a/Storage/Ceph/Host/RestartManager.sh b/Storage/Ceph/Host/RestartManager.sh old mode 100644 new mode 100755 diff --git a/Storage/Ceph/Host/RestartMetadata.sh b/Storage/Ceph/Host/RestartMetadata.sh old mode 100644 new mode 100755 diff --git a/Storage/Ceph/Host/RestartMonitor.sh b/Storage/Ceph/Host/RestartMonitor.sh old mode 100644 new mode 100755 diff --git a/Storage/Ceph/Host/RestartOSDs.sh b/Storage/Ceph/Host/RestartOSDs.sh old mode 100644 new mode 100755 diff --git a/Storage/Ceph/Host/SingleDrive.sh b/Storage/Ceph/Host/SingleDrive.sh old mode 100644 new mode 100755 diff --git a/Storage/Ceph/Host/WipeDisk.sh b/Storage/Ceph/Host/WipeDisk.sh old mode 100644 new mode 100755 diff --git a/Storage/Ceph/Pools/AllowPoolSize1.sh b/Storage/Ceph/Pools/AllowPoolSize1.sh old mode 100644 new mode 100755 diff --git a/Storage/Ceph/Pools/SetPoolMinSize1.sh b/Storage/Ceph/Pools/SetPoolMinSize1.sh old mode 100644 new mode 100755 diff --git a/Storage/Ceph/Pools/SetPoolSize1.sh b/Storage/Ceph/Pools/SetPoolSize1.sh old mode 100644 new mode 100755 diff --git a/Storage/Ceph/SetScrubInterval.sh b/Storage/Ceph/SetScrubInterval.sh old mode 100644 new mode 100755 diff --git a/Storage/Ceph/SparsifyDisk.sh b/Storage/Ceph/SparsifyDisk.sh old mode 100644 new mode 100755 diff --git a/Storage/DiskDeleteBulk.sh b/Storage/DiskDeleteBulk.sh old mode 100644 new mode 100755 diff --git a/Storage/DiskDeleteWithSnapshot.sh b/Storage/DiskDeleteWithSnapshot.sh old mode 100644 new mode 100755 diff --git a/Storage/FilesystemTrimAll.sh b/Storage/FilesystemTrimAll.sh old mode 100644 new mode 100755 diff --git a/Storage/OptimizeSpindown.sh b/Storage/OptimizeSpindown.sh old mode 100644 new mode 100755 diff --git a/Storage/PassthroughStorageToLXC.sh b/Storage/PassthroughStorageToLXC.sh old mode 100644 new mode 100755 diff --git a/Storage/RemoveStorage.sh b/Storage/RemoveStorage.sh old mode 100644 new mode 100755 diff --git a/Storage/UpdateStaleMount.sh b/Storage/UpdateStaleMount.sh old mode 100644 new mode 100755 diff --git a/ThirdParty/ApacheGuacamole/BulkDeleteConnectionGuacamole.sh b/ThirdParty/ApacheGuacamole/BulkDeleteConnectionGuacamole.sh old mode 100644 new mode 100755 diff --git a/ThirdParty/ApacheGuacamole/DebugConnectionTree.sh b/ThirdParty/ApacheGuacamole/DebugConnectionTree.sh old mode 100644 new mode 100755 diff --git a/ThirdParty/ApacheGuacamole/GetGuacamoleAuthenticationToken.sh b/ThirdParty/ApacheGuacamole/GetGuacamoleAuthenticationToken.sh old mode 100644 new mode 100755 diff --git a/ThirdParty/ApacheGuacamole/RDP/BulkAddRDPConnectionGuacamole.sh b/ThirdParty/ApacheGuacamole/RDP/BulkAddRDPConnectionGuacamole.sh old mode 100644 new mode 100755 diff --git a/ThirdParty/ApacheGuacamole/RDP/BulkAddSFTPServer.sh b/ThirdParty/ApacheGuacamole/RDP/BulkAddSFTPServer.sh old mode 100644 new mode 100755 diff --git a/ThirdParty/ApacheGuacamole/RDP/BulkPrintRDPConfiguration.sh b/ThirdParty/ApacheGuacamole/RDP/BulkPrintRDPConfiguration.sh old mode 100644 new mode 100755 diff --git a/ThirdParty/ApacheGuacamole/RDP/BulkRemoveDriveRedirection.sh b/ThirdParty/ApacheGuacamole/RDP/BulkRemoveDriveRedirection.sh old mode 100644 new mode 100755 diff --git a/ThirdParty/ApacheGuacamole/RDP/BulkRemoveRDPConnection.sh b/ThirdParty/ApacheGuacamole/RDP/BulkRemoveRDPConnection.sh old mode 100644 new mode 100755 diff --git a/ThirdParty/ApacheGuacamole/RDP/BulkRemoveSFTPServer.sh b/ThirdParty/ApacheGuacamole/RDP/BulkRemoveSFTPServer.sh old mode 100644 new mode 100755 diff --git a/ThirdParty/ApacheGuacamole/RDP/BulkUpdateDriveRedirection.sh b/ThirdParty/ApacheGuacamole/RDP/BulkUpdateDriveRedirection.sh old mode 100644 new mode 100755 diff --git a/ThirdParty/ApacheGuacamole/RemoveGuacamoleAuthenticationToken.sh b/ThirdParty/ApacheGuacamole/RemoveGuacamoleAuthenticationToken.sh old mode 100644 new mode 100755 diff --git a/Utilities/ArgumentParser.sh b/Utilities/ArgumentParser.sh old mode 100644 new mode 100755 index 161fa66..9f316ca --- a/Utilities/ArgumentParser.sh +++ b/Utilities/ArgumentParser.sh @@ -208,10 +208,11 @@ __parse_args__() { fi FLAG_NAMES["--${name}"]="${name^^}" + FLAG_NAMES["--${name}"]="${FLAG_NAMES["--${name}"]//-/_}" FLAG_TYPES["--${name}"]="$type" # Check for reserved variable name conflicts - local var_name="${name^^}" + local var_name="${FLAG_NAMES["--${name}"]}" for reserved in "${CRITICAL_RESERVED[@]}"; do if [[ "$var_name" == "$reserved" ]]; then __argparser_log__ "ERROR" "CRITICAL: Argument name '$name' conflicts with reserved variable '$reserved'" @@ -222,13 +223,13 @@ __parse_args__() { fi done - __argparser_log__ "DEBUG" "Registered flag: --${name} -> ${name^^} (type: $type, optional: $optional)" + __argparser_log__ "DEBUG" "Registered flag: --${name} -> ${var_name} (type: $type, optional: $optional)" # Initialize variable if [[ "$type" == "flag" || "$type" == "bool" ]]; then - eval "${name^^}=false" + eval "${var_name}=false" else - eval "${name^^}='${default}'" + eval "${var_name}='${default}'" fi else # Positional argument diff --git a/Utilities/BulkOperations.sh b/Utilities/BulkOperations.sh old mode 100644 new mode 100755 diff --git a/Utilities/Cluster.sh b/Utilities/Cluster.sh old mode 100644 new mode 100755 diff --git a/Utilities/Colors.sh b/Utilities/Colors.sh old mode 100644 new mode 100755 diff --git a/Utilities/Communication.sh b/Utilities/Communication.sh old mode 100644 new mode 100755 diff --git a/Utilities/ConfigManager.sh b/Utilities/ConfigManager.sh old mode 100644 new mode 100755 index 1f34fe5..92bdb01 --- a/Utilities/ConfigManager.sh +++ b/Utilities/ConfigManager.sh @@ -103,23 +103,24 @@ __set_execution_mode__() { } # Add remote target -# Args: node_name node_ip password [username] +# Args: node_name node_ip password [username] [port] __add_remote_target__() { local node_name="$1" local node_ip="$2" local password="$3" local username="${4:-$DEFAULT_USERNAME}" + local port="${5:-$DEFAULT_PORT}" REMOTE_TARGETS+=("$node_name:$node_ip") NODE_PASSWORDS["$node_name"]="$password" NODE_USERNAMES["$node_name"]="$username" + NODE_PORTS["$node_name"]="$port" } # Clear all remote targets __clear_remote_targets__() { REMOTE_TARGETS=() NODE_PASSWORDS=() - NODE_USERNAMES=() } # Get node IP by name diff --git a/Utilities/Conversion.sh b/Utilities/Conversion.sh old mode 100644 new mode 100755 diff --git a/Utilities/Discovery.sh b/Utilities/Discovery.sh old mode 100644 new mode 100755 diff --git a/Utilities/Display.sh b/Utilities/Display.sh old mode 100644 new mode 100755 diff --git a/Utilities/Logger.sh b/Utilities/Logger.sh old mode 100644 new mode 100755 diff --git a/Utilities/ManualViewer.sh b/Utilities/ManualViewer.sh old mode 100644 new mode 100755 diff --git a/Utilities/Menu.sh b/Utilities/Menu.sh old mode 100644 new mode 100755 diff --git a/Utilities/Network.sh b/Utilities/Network.sh old mode 100644 new mode 100755 diff --git a/Utilities/NodeSelection.sh b/Utilities/NodeSelection.sh old mode 100644 new mode 100755 index 9552536..ea36664 --- a/Utilities/NodeSelection.sh +++ b/Utilities/NodeSelection.sh @@ -122,8 +122,11 @@ __select_nodes__() { echo read -rp "Enter node IP manually: " manual_ip read -rp "Enter node name: " manual_name + read -rp "Enter SSH port (default: 22): " manual_port + manual_port="${manual_port:-22}" SELECTED_NODES["$manual_name"]="$manual_ip" + NODE_PORTS["$manual_name"]="$manual_port" __get_node_passwords__ "single" "$manual_name" return 0 fi @@ -146,7 +149,10 @@ __select_nodes__() { elif [[ "$node_choice" == "m" ]]; then read -rp "Enter node IP: " manual_ip read -rp "Enter node name: " manual_name + read -rp "Enter SSH port (default: 22): " manual_port + manual_port="${manual_port:-22}" SELECTED_NODES["$manual_name"]="$manual_ip" + NODE_PORTS["$manual_name"]="$manual_port" __get_node_passwords__ "single" "$manual_name" return 0 elif [[ -n "$node_choice" && -n "${node_menu[$node_choice]:-}" ]]; then diff --git a/Utilities/Operations.sh b/Utilities/Operations.sh old mode 100644 new mode 100755 diff --git a/Utilities/Prompts.sh b/Utilities/Prompts.sh old mode 100644 new mode 100755 diff --git a/Utilities/RemoteExecutor.sh b/Utilities/RemoteExecutor.sh old mode 100644 new mode 100755 index cccfea8..816c08b --- a/Utilities/RemoteExecutor.sh +++ b/Utilities/RemoteExecutor.sh @@ -44,6 +44,13 @@ fi # Global flag for interrupt REMOTE_INTERRUPTED=0 +# Track current remote execution for cleanup +REMOTE_CURRENT_NODE_IP="" +REMOTE_CURRENT_NODE_PASS="" +REMOTE_CURRENT_NODE_USER="" +REMOTE_CURRENT_NODE_PORT="" +REMOTE_CURRENT_PID_FILE="" + # Authentication mode: "password" or "key" # Can be set globally or per-node USE_SSH_KEYS="${USE_SSH_KEYS:-false}" @@ -60,12 +67,41 @@ __remote_cleanup__() { # Stop any running spinner __stop_spin__ 2>/dev/null || true - # Kill any background SSH processes (more aggressive) - killall -9 sshpass 2>/dev/null || true - killall -9 ssh 2>/dev/null || true - pkill -9 -P $$ sshpass 2>/dev/null || true - pkill -9 -P $$ ssh 2>/dev/null || true - pkill -9 -P $$ scp 2>/dev/null || true + # Kill the remote script process if we have connection info + if [[ -n "$REMOTE_CURRENT_NODE_IP" && -n "$REMOTE_CURRENT_PID_FILE" ]]; then + __warn__ "Sending kill to remote process..." + # Read remote PID and kill it along with its children + { + if [[ "$USE_SSH_KEYS" == "true" ]] || [[ -z "$REMOTE_CURRENT_NODE_PASS" ]]; then + ssh -o StrictHostKeyChecking=no -o ConnectTimeout=3 \ + -p "$REMOTE_CURRENT_NODE_PORT" \ + "${REMOTE_CURRENT_NODE_USER}@${REMOTE_CURRENT_NODE_IP}" \ + "if [ -f '${REMOTE_CURRENT_PID_FILE}' ]; then pid=\$(cat '${REMOTE_CURRENT_PID_FILE}'); kill -TERM -\$pid 2>/dev/null || kill -TERM \$pid 2>/dev/null; sleep 0.5; kill -9 -\$pid 2>/dev/null || kill -9 \$pid 2>/dev/null; rm -f '${REMOTE_CURRENT_PID_FILE}'; fi" + else + sshpass -p "$REMOTE_CURRENT_NODE_PASS" \ + ssh -o StrictHostKeyChecking=no -o ConnectTimeout=3 \ + -p "$REMOTE_CURRENT_NODE_PORT" \ + "${REMOTE_CURRENT_NODE_USER}@${REMOTE_CURRENT_NODE_IP}" \ + "if [ -f '${REMOTE_CURRENT_PID_FILE}' ]; then pid=\$(cat '${REMOTE_CURRENT_PID_FILE}'); kill -TERM -\$pid 2>/dev/null || kill -TERM \$pid 2>/dev/null; sleep 0.5; kill -9 -\$pid 2>/dev/null || kill -9 \$pid 2>/dev/null; rm -f '${REMOTE_CURRENT_PID_FILE}'; fi" + fi + } 2>/dev/null || true + fi + + # Kill local SSH/SCP child processes of this script + local child_pids + child_pids=$(jobs -p 2>/dev/null) || true + if [[ -n "$child_pids" ]]; then + kill -9 $child_pids 2>/dev/null || true + fi + # Also kill direct child ssh/sshpass processes + local pid + for pid in $(pgrep -P $$ 2>/dev/null || true); do + local cmd + cmd=$(ps -o comm= -p "$pid" 2>/dev/null || true) + if [[ "$cmd" == "ssh" || "$cmd" == "sshpass" || "$cmd" == "scp" ]]; then + kill -9 "$pid" 2>/dev/null || true + fi + done # Give processes a moment to die sleep 0.2 @@ -73,6 +109,10 @@ __remote_cleanup__() { # Cleanup temp files rm -f /tmp/remote_exec_*.tar.gz 2>/dev/null || true + # Reset remote tracking + REMOTE_CURRENT_NODE_IP="" + REMOTE_CURRENT_PID_FILE="" + echo echo "Cleanup complete" } @@ -319,8 +359,7 @@ __execute_on_remote_node__() { return 1 fi - # Execute script remotely - __info__ "Executing script..." + # Execute script remotely with live output streaming __log_info__ "Executing $script_relative on remote with args: $param_line" "REMOTE" __log_debug__ "REMOTE_LOG_LEVEL in RemoteExecutor: $REMOTE_LOG_LEVEL" "REMOTE" @@ -328,45 +367,53 @@ __execute_on_remote_node__() { local exec_id="$$_$(date +%s%N)" local remote_log="/tmp/proxmox_remote_execution_${exec_id}.log" local remote_debug_log="/tmp/proxmox_remote_debug_${exec_id}.log" + local remote_pid_file="/tmp/proxmox_remote_pid_${exec_id}" + local local_remote_log="/tmp/remote_${node_name}_${exec_id}.log" + local local_debug_log="/tmp/remote_${node_name}_${exec_id}.debug.log" local ssh_exit_code=0 - # Execute script and capture output to log file + # Track current remote node for Ctrl+C cleanup + REMOTE_CURRENT_NODE_IP="$node_ip" + REMOTE_CURRENT_NODE_PASS="$node_pass" + REMOTE_CURRENT_NODE_USER="$username" + REMOTE_CURRENT_NODE_PORT="$port" + REMOTE_CURRENT_PID_FILE="$remote_pid_file" + + # Stop spinner — stream live output directly to terminal + __ok__ "Executing script on $node_name (live output)..." + echo + echo "--- Remote Output ($node_name) ---" + + # Execute script: stdout/stderr stream live via SSH (tee saves a local copy) + # The remote command writes its PID to a file so we can kill it on interrupt if [[ -n "$param_line" ]]; then __ssh_exec__ "$node_ip" "$node_pass" "$username" "$port" \ - "export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin NON_INTERACTIVE=1 DEBIAN_FRONTEND=noninteractive UTILITYPATH='$REMOTE_TEMP_DIR/Utilities' LOG_FILE='$remote_debug_log' LOG_LEVEL=$REMOTE_LOG_LEVEL LOG_CONSOLE=0 && cd $REMOTE_TEMP_DIR && { echo '=== Remote Execution Start ==='; echo 'Script: bash $script_relative'; echo 'Arguments: $param_line'; echo 'Working directory: '\$(pwd); echo 'UTILITYPATH: '\$UTILITYPATH; echo 'LOG_FILE: '\$LOG_FILE; echo 'LOG_LEVEL: '\$LOG_LEVEL; echo 'LOG_LEVEL (actual): $REMOTE_LOG_LEVEL'; echo '==================================='; eval bash $script_relative $param_line; echo \$? > ${remote_log}.exit; } >> $remote_log 2>&1" - ssh_exit_code=$? + "export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin NON_INTERACTIVE=1 DEBIAN_FRONTEND=noninteractive UTILITYPATH='$REMOTE_TEMP_DIR/Utilities' LOG_FILE='$remote_debug_log' LOG_LEVEL=$REMOTE_LOG_LEVEL LOG_CONSOLE=0 && cd $REMOTE_TEMP_DIR && { eval bash $script_relative $param_line & SCRIPT_PID=\$!; echo \$SCRIPT_PID > $remote_pid_file; wait \$SCRIPT_PID; EXIT_CODE=\$?; rm -f $remote_pid_file; exit \$EXIT_CODE; }" 2>&1 | tee "$local_remote_log" + ssh_exit_code=${PIPESTATUS[0]} else __ssh_exec__ "$node_ip" "$node_pass" "$username" "$port" \ - "export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin NON_INTERACTIVE=1 DEBIAN_FRONTEND=noninteractive UTILITYPATH='$REMOTE_TEMP_DIR/Utilities' LOG_FILE='$remote_debug_log' LOG_LEVEL=$REMOTE_LOG_LEVEL LOG_CONSOLE=0 && cd $REMOTE_TEMP_DIR && { echo '=== Remote Execution Start ==='; echo 'Script: bash $script_relative'; echo 'Working directory: '\$(pwd); echo 'UTILITYPATH: '\$UTILITYPATH; echo 'LOG_FILE: '\$LOG_FILE; echo 'LOG_LEVEL: '\$LOG_LEVEL; echo 'LOG_LEVEL (actual): $REMOTE_LOG_LEVEL'; echo '==================================='; bash $script_relative; echo \$? > ${remote_log}.exit; } >> $remote_log 2>&1" - ssh_exit_code=$? + "export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin NON_INTERACTIVE=1 DEBIAN_FRONTEND=noninteractive UTILITYPATH='$REMOTE_TEMP_DIR/Utilities' LOG_FILE='$remote_debug_log' LOG_LEVEL=$REMOTE_LOG_LEVEL LOG_CONSOLE=0 && cd $REMOTE_TEMP_DIR && { bash $script_relative & SCRIPT_PID=\$!; echo \$SCRIPT_PID > $remote_pid_file; wait \$SCRIPT_PID; EXIT_CODE=\$?; rm -f $remote_pid_file; exit \$EXIT_CODE; }" 2>&1 | tee "$local_remote_log" + ssh_exit_code=${PIPESTATUS[0]} fi - # Get final exit code from file if available - local temp_exit_file="/tmp/remote_exit_${exec_id}" - if __scp_download__ "$node_ip" "$node_pass" "$username" "$port" "${remote_log}.exit" "$temp_exit_file" 2>/dev/null; then - local script_exit_code=$(cat "$temp_exit_file" 2>/dev/null) - if [[ -n "$script_exit_code" ]] && [[ "$script_exit_code" =~ ^[0-9]+$ ]]; then - ssh_exit_code=$script_exit_code - fi - rm -f "$temp_exit_file" - fi + echo "--- End Remote Output ---" - __ok__ "Script execution complete" + # Clear remote tracking + REMOTE_CURRENT_NODE_IP="" + REMOTE_CURRENT_PID_FILE="" __log_info__ "Script execution completed with exit code: $ssh_exit_code" "REMOTE" - # Download final log file and debug logs - local local_remote_log="/tmp/remote_${node_name}_${exec_id}.log" - local local_debug_log="/tmp/remote_${node_name}_${exec_id}.debug.log" + # Append output log to main log file + if [[ -f "$local_remote_log" ]]; then + cat "$local_remote_log" >>"$LOG_FILE" 2>/dev/null || true + fi + echo "Output log saved to: $local_remote_log" - # Ensure we have the final version of the log - __scp_download__ "$node_ip" "$node_pass" "$username" "$port" "$remote_log" "$local_remote_log" 2>/dev/null || true - # Retrieve debug log if it exists if __scp_download__ "$node_ip" "$node_pass" "$username" "$port" "$remote_debug_log" "$local_debug_log" 2>/dev/null; then __log_info__ "Retrieved debug log from $node_name" "REMOTE" - # Show debug log if it has content (not shown live) if [[ -s "$local_debug_log" ]]; then echo echo "--- Debug Log from $node_name (LOG_LEVEL=$REMOTE_LOG_LEVEL) ---" @@ -375,24 +422,17 @@ __execute_on_remote_node__() { echo fi - # Append debug log to main log file cat "$local_debug_log" >>"$LOG_FILE" 2>/dev/null || true echo "Debug log saved to: $local_debug_log" else __log_debug__ "No debug log available from $node_name" "REMOTE" fi - echo "Output log saved to: $local_remote_log" - if [[ -f "$local_remote_log" ]]; then - cat "$local_remote_log" >>"$LOG_FILE" - fi - # Cleanup - CURRENT_MESSAGE="Cleaning up..." - __update__ "$CURRENT_MESSAGE" + __info__ "Cleaning up..." __log_info__ "Cleaning up remote directory: $REMOTE_TEMP_DIR" "REMOTE" __ssh_exec__ "$node_ip" "$node_pass" "$username" "$port" \ - "rm -rf $REMOTE_TEMP_DIR $remote_log $remote_debug_log ${remote_log}.exit" 2>/dev/null || __log_warn__ "Cleanup failed (non-critical)" "REMOTE" + "rm -rf $REMOTE_TEMP_DIR $remote_log $remote_debug_log ${remote_log}.exit $remote_pid_file" 2>/dev/null || __log_warn__ "Cleanup failed (non-critical)" "REMOTE" __ok__ "Cleanup complete" echo diff --git a/Utilities/RemoteRunAllTests.sh b/Utilities/RemoteRunAllTests.sh old mode 100644 new mode 100755 diff --git a/Utilities/RunAllTests.sh b/Utilities/RunAllTests.sh old mode 100644 new mode 100755 diff --git a/Utilities/SSH.sh b/Utilities/SSH.sh old mode 100644 new mode 100755 diff --git a/Utilities/StateManager.sh b/Utilities/StateManager.sh old mode 100644 new mode 100755 diff --git a/Utilities/TestFramework.sh b/Utilities/TestFramework.sh old mode 100644 new mode 100755 diff --git a/Utilities/_ExampleScript.sh b/Utilities/_ExampleScript.sh old mode 100644 new mode 100755 diff --git a/Utilities/_ExampleStateManagement.sh b/Utilities/_ExampleStateManagement.sh old mode 100644 new mode 100755 diff --git a/Utilities/_TestArgumentParser.sh b/Utilities/_TestArgumentParser.sh old mode 100644 new mode 100755 diff --git a/Utilities/_TestBulkOperations.sh b/Utilities/_TestBulkOperations.sh old mode 100644 new mode 100755 diff --git a/Utilities/_TestCluster.sh b/Utilities/_TestCluster.sh old mode 100644 new mode 100755 diff --git a/Utilities/_TestColors.sh b/Utilities/_TestColors.sh old mode 100644 new mode 100755 diff --git a/Utilities/_TestCommunication.sh b/Utilities/_TestCommunication.sh old mode 100644 new mode 100755 diff --git a/Utilities/_TestConfigManager.sh b/Utilities/_TestConfigManager.sh old mode 100644 new mode 100755 diff --git a/Utilities/_TestConversion.sh b/Utilities/_TestConversion.sh old mode 100644 new mode 100755 diff --git a/Utilities/_TestDiscovery.sh b/Utilities/_TestDiscovery.sh old mode 100644 new mode 100755 diff --git a/Utilities/_TestDisplay.sh b/Utilities/_TestDisplay.sh old mode 100644 new mode 100755 diff --git a/Utilities/_TestIntegrationExample.sh b/Utilities/_TestIntegrationExample.sh old mode 100644 new mode 100755 diff --git a/Utilities/_TestManualViewer.sh b/Utilities/_TestManualViewer.sh old mode 100644 new mode 100755 diff --git a/Utilities/_TestMenu.sh b/Utilities/_TestMenu.sh old mode 100644 new mode 100755 diff --git a/Utilities/_TestNetwork.sh b/Utilities/_TestNetwork.sh old mode 100644 new mode 100755 diff --git a/Utilities/_TestNodeSelection.sh b/Utilities/_TestNodeSelection.sh old mode 100644 new mode 100755 diff --git a/Utilities/_TestOperations.sh b/Utilities/_TestOperations.sh old mode 100644 new mode 100755 diff --git a/Utilities/_TestPrompts.sh b/Utilities/_TestPrompts.sh old mode 100644 new mode 100755 diff --git a/Utilities/_TestQueries.sh b/Utilities/_TestQueries.sh old mode 100644 new mode 100755 diff --git a/Utilities/_TestRemoteExec.sh b/Utilities/_TestRemoteExec.sh old mode 100644 new mode 100755 diff --git a/Utilities/_TestSSH.sh b/Utilities/_TestSSH.sh old mode 100644 new mode 100755 diff --git a/Utilities/_TestStateManager.sh b/Utilities/_TestStateManager.sh old mode 100644 new mode 100755 diff --git a/Utilities/_Utilities.md b/Utilities/_Utilities.md index c28e4f3..1f06dd8 100644 --- a/Utilities/_Utilities.md +++ b/Utilities/_Utilities.md @@ -1,6 +1,6 @@ # ProxmoxScripts Utility Functions Reference -**Auto-generated documentation** - Last updated: 2026-01-09 14:42:42 +**Auto-generated documentation** - Last updated: 2026-02-25 12:57:55 --- diff --git a/VirtualMachines/Backup/BulkBackup.sh b/VirtualMachines/Backup/BulkBackup.sh old mode 100644 new mode 100755 diff --git a/VirtualMachines/CloudInit/BulkAddSSHKey.sh b/VirtualMachines/CloudInit/BulkAddSSHKey.sh old mode 100644 new mode 100755 diff --git a/VirtualMachines/CloudInit/BulkChangeDNS.sh b/VirtualMachines/CloudInit/BulkChangeDNS.sh old mode 100644 new mode 100755 diff --git a/VirtualMachines/CloudInit/BulkChangeIP.sh b/VirtualMachines/CloudInit/BulkChangeIP.sh old mode 100644 new mode 100755 diff --git a/VirtualMachines/CloudInit/BulkChangeUserPass.sh b/VirtualMachines/CloudInit/BulkChangeUserPass.sh old mode 100644 new mode 100755 diff --git a/VirtualMachines/CloudInit/BulkMoveCloudInit.sh b/VirtualMachines/CloudInit/BulkMoveCloudInit.sh old mode 100644 new mode 100755 diff --git a/VirtualMachines/CloudInit/BulkTogglePackageUpgrade.sh b/VirtualMachines/CloudInit/BulkTogglePackageUpgrade.sh old mode 100644 new mode 100755 diff --git a/VirtualMachines/Configuration/VMAddTerminalTTYS0.sh b/VirtualMachines/Configuration/VMAddTerminalTTYS0.sh old mode 100644 new mode 100755 diff --git a/VirtualMachines/ConvertVMToTemplate.sh b/VirtualMachines/ConvertVMToTemplate.sh old mode 100644 new mode 100755 diff --git a/VirtualMachines/CreateFromISO.sh b/VirtualMachines/CreateFromISO.sh old mode 100644 new mode 100755 diff --git a/VirtualMachines/Hardware/BulkConfigureCPU.sh b/VirtualMachines/Hardware/BulkConfigureCPU.sh old mode 100644 new mode 100755 diff --git a/VirtualMachines/Hardware/BulkConfigureDiskBandwidth.sh b/VirtualMachines/Hardware/BulkConfigureDiskBandwidth.sh new file mode 100755 index 0000000..7882bc7 --- /dev/null +++ b/VirtualMachines/Hardware/BulkConfigureDiskBandwidth.sh @@ -0,0 +1,227 @@ +#!/bin/bash +# +# BulkConfigureDiskBandwidth.sh +# +# Configures per-disk bandwidth (MB/s) throttle limits for a range of virtual machines (VMs) +# on a Proxmox VE cluster. Supports read limit, write limit, read max burst, and write max burst. +# Automatically detects which node each VM is on and executes the operation cluster-wide. +# +# Usage: +# BulkConfigureDiskBandwidth.sh 100 110 --mbps-rd 100 +# BulkConfigureDiskBandwidth.sh 100 110 --mbps-rd 100 --mbps-wr 50 +# BulkConfigureDiskBandwidth.sh 100 110 --mbps-rd 200 --mbps-wr 100 --mbps-rd-max 400 --mbps-wr-max 200 +# BulkConfigureDiskBandwidth.sh 100 110 --mbps-rd 100 --disk-id virtio0 +# BulkConfigureDiskBandwidth.sh 400 410 --mbps-rd 0 --mbps-wr 0 --mbps-rd-max 0 --mbps-wr-max 0 +# BulkConfigureDiskBandwidth.sh 100 110 --mbps-rd 100 --mbps-wr 50 --all-disks +# +# Arguments: +# start_id - Starting VM ID in the range +# end_id - Ending VM ID in the range +# --mbps-rd - Read bandwidth limit in MB/s (0=unlimited) +# --mbps-wr - Write bandwidth limit in MB/s (0=unlimited) +# --mbps-rd-max - Read burst bandwidth limit in MB/s (0=unlimited) +# --mbps-wr-max - Write burst bandwidth limit in MB/s (0=unlimited) +# --disk-id - Disk interface ID (default: scsi0, ignored with --all-disks) +# --all-disks - Apply limits to all disks attached to each VM +# +# Disk IDs: scsi0, scsi1, virtio0, virtio1, ide0, ide2, sata0, sata1, etc. +# +# Notes: +# - At least one bandwidth option must be specified +# - Set a value to 0 to remove that limit +# - Burst limits (max) should be >= base limits when both are set +# - Changes take effect immediately for running VMs (no reboot required) +# +# Function Index: +# - validate_custom_options +# - main +# + +set -euo pipefail + +# shellcheck source=Utilities/ArgumentParser.sh +source "${UTILITYPATH}/ArgumentParser.sh" +# shellcheck source=Utilities/Prompts.sh +source "${UTILITYPATH}/Prompts.sh" +# shellcheck source=Utilities/Communication.sh +source "${UTILITYPATH}/Communication.sh" +# shellcheck source=Utilities/BulkOperations.sh +source "${UTILITYPATH}/BulkOperations.sh" +# shellcheck source=Utilities/Cluster.sh +source "${UTILITYPATH}/Cluster.sh" +# shellcheck source=Utilities/Operations.sh +source "${UTILITYPATH}/Operations.sh" + +trap '__handle_err__ $LINENO "$BASH_COMMAND"' ERR + +# --- validate_custom_options ------------------------------------------------- +# @function validate_custom_options +# @description Validates bandwidth-specific configuration options not covered by ArgumentParser. +validate_custom_options() { + # Check that at least one bandwidth option is specified + if [[ -z "$MBPS_RD" && -z "$MBPS_WR" && -z "$MBPS_RD_MAX" && -z "$MBPS_WR_MAX" ]]; then + __err__ "At least one bandwidth option must be specified" + exit 64 + fi + + # Validate disk ID format + if [[ -n "$DISK_ID" ]]; then + if ! [[ "$DISK_ID" =~ ^(scsi|virtio|ide|sata)[0-9]+$ ]]; then + __err__ "Invalid disk ID format: ${DISK_ID}" + __err__ "Valid formats: scsi0, virtio0, ide0, sata0, etc." + exit 64 + fi + fi + + # Warn if burst is less than base limit + if [[ -n "$MBPS_RD" && -n "$MBPS_RD_MAX" && "$MBPS_RD" != "0" && "$MBPS_RD_MAX" != "0" ]]; then + if (( $(echo "$MBPS_RD_MAX < $MBPS_RD" | bc -l) )); then + __warn__ "Read burst (${MBPS_RD_MAX} MB/s) is less than read limit (${MBPS_RD} MB/s)" + fi + fi + + if [[ -n "$MBPS_WR" && -n "$MBPS_WR_MAX" && "$MBPS_WR" != "0" && "$MBPS_WR_MAX" != "0" ]]; then + if (( $(echo "$MBPS_WR_MAX < $MBPS_WR" | bc -l) )); then + __warn__ "Write burst (${MBPS_WR_MAX} MB/s) is less than write limit (${MBPS_WR} MB/s)" + fi + fi +} + +# --- main -------------------------------------------------------------------- +main() { + __check_root__ + __check_proxmox__ + + # Parse arguments using ArgumentParser + __parse_args__ "start_id:vmid end_id:vmid --mbps-rd:number:? --mbps-wr:number:? --mbps-rd-max:number:? --mbps-wr-max:number:? --disk-id:string:scsi0 --all-disks:flag" "$@" + + # Additional custom validation + validate_custom_options + + __info__ "Bulk configure disk bandwidth: VMs ${START_ID} to ${END_ID} (cluster-wide)" + if [[ "$ALL_DISKS" == "true" ]]; then + __info__ "Target: all disks on each VM" + else + __info__ "Disk interface: ${DISK_ID}" + fi + [[ -n "$MBPS_RD" ]] && __info__ " Read limit: ${MBPS_RD} MB/s" + [[ -n "$MBPS_WR" ]] && __info__ " Write limit: ${MBPS_WR} MB/s" + [[ -n "$MBPS_RD_MAX" ]] && __info__ " Read burst: ${MBPS_RD_MAX} MB/s" + [[ -n "$MBPS_WR_MAX" ]] && __info__ " Write burst: ${MBPS_WR_MAX} MB/s" + + # Confirm action + if ! __prompt_user_yn__ "Configure disk bandwidth for VMs ${START_ID}-${END_ID}?"; then + __info__ "Operation cancelled by user" + exit 0 + fi + + # Local callback for bulk operation + configure_bandwidth_callback() { + local vmid="$1" + local node + + node=$(__get_vm_node__ "$vmid") + + if [[ -z "$node" ]]; then + __update__ "VM ${vmid} not found in cluster" + return 1 + fi + + # Determine which disks to configure + local disk_list + if [[ "$ALL_DISKS" == "true" ]]; then + disk_list=$(__node_exec__ "$node" "qm config ${vmid}" 2>/dev/null | grep -E "^(scsi|virtio|ide|sata)[0-9]+:" | cut -d':' -f1 || echo "") + if [[ -z "$disk_list" ]]; then + __update__ "VM ${vmid} has no disks" + return 1 + fi + else + disk_list="$DISK_ID" + fi + + local disk_failed=0 + local disk_count=0 + + while IFS= read -r current_disk; do + [[ -z "$current_disk" ]] && continue + disk_count=$((disk_count + 1)) + + __update__ "Configuring ${current_disk} on VM ${vmid} (node ${node})..." + + # Get current disk configuration + local current_config + current_config=$(__node_exec__ "$node" "qm config ${vmid}" 2>/dev/null | grep "^${current_disk}:" | cut -d' ' -f2- || echo "") + + if [[ -z "$current_config" ]]; then + __update__ "VM ${vmid} has no ${current_disk} disk" + disk_failed=$((disk_failed + 1)) + continue + fi + + # Build new configuration by modifying existing config + local new_config="$current_config" + + # Update read bandwidth limit + if [[ -n "$MBPS_RD" ]]; then + new_config=$(echo "$new_config" | sed "s/,mbps_rd=[^,]*//" | sed "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=[^,]*,//") + [[ "$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=[^,]*,//") + [[ "$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=[^,]*,//") + [[ "$MBPS_WR_MAX" != "0" ]] && new_config="${new_config},mbps_wr_max=${MBPS_WR_MAX}" + fi + + # Apply configuration on correct node + if ! __node_exec__ "$node" "qm set ${vmid} --${current_disk} '${new_config}'" 2>&1; then + disk_failed=$((disk_failed + 1)) + fi + done <<< "$disk_list" + + if [[ $disk_failed -gt 0 ]]; then + __update__ "VM ${vmid}: ${disk_failed}/${disk_count} disk(s) failed" + return 1 + fi + + return 0 + } + + # Use BulkOperations framework + __bulk_vm_operation__ --name "Disk Bandwidth Configuration" --report "$START_ID" "$END_ID" configure_bandwidth_callback + + # Display summary + __bulk_summary__ + + [[ $BULK_FAILED -gt 0 ]] && exit 1 + __ok__ "All disk bandwidth configurations completed successfully!" +} + +main "$@" + +############################################################################### +# Script notes: +############################################################################### +# Last checked: 2026-02-20 +# +# Changes: +# - 2026-02-20: Initial version +# +# Fixes: +# - +# +# Known issues: +# - +# diff --git a/VirtualMachines/Hardware/BulkConfigureDiskIOPS.sh b/VirtualMachines/Hardware/BulkConfigureDiskIOPS.sh new file mode 100755 index 0000000..a54a2a0 --- /dev/null +++ b/VirtualMachines/Hardware/BulkConfigureDiskIOPS.sh @@ -0,0 +1,228 @@ +#!/bin/bash +# +# BulkConfigureDiskIOPS.sh +# +# Configures per-disk IOPS (I/O operations per second) throttle limits for a range of virtual +# machines (VMs) on a Proxmox VE cluster. Supports read limit, write limit, read max burst, +# and write max burst. +# Automatically detects which node each VM is on and executes the operation cluster-wide. +# +# Usage: +# BulkConfigureDiskIOPS.sh 100 110 --iops-rd 1000 +# BulkConfigureDiskIOPS.sh 100 110 --iops-rd 1000 --iops-wr 500 +# BulkConfigureDiskIOPS.sh 100 110 --iops-rd 2000 --iops-wr 1000 --iops-rd-max 4000 --iops-wr-max 2000 +# BulkConfigureDiskIOPS.sh 100 110 --iops-rd 1000 --disk-id virtio0 +# BulkConfigureDiskIOPS.sh 400 410 --iops-rd 0 --iops-wr 0 --iops-rd-max 0 --iops-wr-max 0 +# BulkConfigureDiskIOPS.sh 100 110 --iops-rd 1000 --iops-wr 500 --all-disks +# +# Arguments: +# start_id - Starting VM ID in the range +# end_id - Ending VM ID in the range +# --iops-rd - Read IOPS limit (0=unlimited) +# --iops-wr - Write IOPS limit (0=unlimited) +# --iops-rd-max - Read burst IOPS limit (0=unlimited) +# --iops-wr-max - Write burst IOPS limit (0=unlimited) +# --disk-id - Disk interface ID (default: scsi0, ignored with --all-disks) +# --all-disks - Apply limits to all disks attached to each VM +# +# Disk IDs: scsi0, scsi1, virtio0, virtio1, ide0, ide2, sata0, sata1, etc. +# +# Notes: +# - At least one IOPS option must be specified +# - Set a value to 0 to remove that limit +# - Burst limits (max) should be >= base limits when both are set +# - Changes take effect immediately for running VMs (no reboot required) +# +# Function Index: +# - validate_custom_options +# - main +# + +set -euo pipefail + +# shellcheck source=Utilities/ArgumentParser.sh +source "${UTILITYPATH}/ArgumentParser.sh" +# shellcheck source=Utilities/Prompts.sh +source "${UTILITYPATH}/Prompts.sh" +# shellcheck source=Utilities/Communication.sh +source "${UTILITYPATH}/Communication.sh" +# shellcheck source=Utilities/BulkOperations.sh +source "${UTILITYPATH}/BulkOperations.sh" +# shellcheck source=Utilities/Cluster.sh +source "${UTILITYPATH}/Cluster.sh" +# shellcheck source=Utilities/Operations.sh +source "${UTILITYPATH}/Operations.sh" + +trap '__handle_err__ $LINENO "$BASH_COMMAND"' ERR + +# --- validate_custom_options ------------------------------------------------- +# @function validate_custom_options +# @description Validates IOPS-specific configuration options not covered by ArgumentParser. +validate_custom_options() { + # Check that at least one IOPS option is specified + if [[ -z "$IOPS_RD" && -z "$IOPS_WR" && -z "$IOPS_RD_MAX" && -z "$IOPS_WR_MAX" ]]; then + __err__ "At least one IOPS option must be specified" + exit 64 + fi + + # Validate disk ID format + if [[ -n "$DISK_ID" ]]; then + if ! [[ "$DISK_ID" =~ ^(scsi|virtio|ide|sata)[0-9]+$ ]]; then + __err__ "Invalid disk ID format: ${DISK_ID}" + __err__ "Valid formats: scsi0, virtio0, ide0, sata0, etc." + exit 64 + fi + fi + + # Warn if burst is less than base limit + if [[ -n "$IOPS_RD" && -n "$IOPS_RD_MAX" && "$IOPS_RD" != "0" && "$IOPS_RD_MAX" != "0" ]]; then + if (( IOPS_RD_MAX < IOPS_RD )); then + __warn__ "Read burst (${IOPS_RD_MAX} IOPS) is less than read limit (${IOPS_RD} IOPS)" + fi + fi + + if [[ -n "$IOPS_WR" && -n "$IOPS_WR_MAX" && "$IOPS_WR" != "0" && "$IOPS_WR_MAX" != "0" ]]; then + if (( IOPS_WR_MAX < IOPS_WR )); then + __warn__ "Write burst (${IOPS_WR_MAX} IOPS) is less than write limit (${IOPS_WR} IOPS)" + fi + fi +} + +# --- main -------------------------------------------------------------------- +main() { + __check_root__ + __check_proxmox__ + + # Parse arguments using ArgumentParser + __parse_args__ "start_id:vmid end_id:vmid --iops-rd:number:? --iops-wr:number:? --iops-rd-max:number:? --iops-wr-max:number:? --disk-id:string:scsi0 --all-disks:flag" "$@" + + # Additional custom validation + validate_custom_options + + __info__ "Bulk configure disk IOPS: VMs ${START_ID} to ${END_ID} (cluster-wide)" + if [[ "$ALL_DISKS" == "true" ]]; then + __info__ "Target: all disks on each VM" + else + __info__ "Disk interface: ${DISK_ID}" + fi + [[ -n "$IOPS_RD" ]] && __info__ " Read limit: ${IOPS_RD} IOPS" + [[ -n "$IOPS_WR" ]] && __info__ " Write limit: ${IOPS_WR} IOPS" + [[ -n "$IOPS_RD_MAX" ]] && __info__ " Read burst: ${IOPS_RD_MAX} IOPS" + [[ -n "$IOPS_WR_MAX" ]] && __info__ " Write burst: ${IOPS_WR_MAX} IOPS" + + # Confirm action + if ! __prompt_user_yn__ "Configure disk IOPS for VMs ${START_ID}-${END_ID}?"; then + __info__ "Operation cancelled by user" + exit 0 + fi + + # Local callback for bulk operation + configure_iops_callback() { + local vmid="$1" + local node + + node=$(__get_vm_node__ "$vmid") + + if [[ -z "$node" ]]; then + __update__ "VM ${vmid} not found in cluster" + return 1 + fi + + # Determine which disks to configure + local disk_list + if [[ "$ALL_DISKS" == "true" ]]; then + disk_list=$(__node_exec__ "$node" "qm config ${vmid}" 2>/dev/null | grep -E "^(scsi|virtio|ide|sata)[0-9]+:" | cut -d':' -f1 || echo "") + if [[ -z "$disk_list" ]]; then + __update__ "VM ${vmid} has no disks" + return 1 + fi + else + disk_list="$DISK_ID" + fi + + local disk_failed=0 + local disk_count=0 + + while IFS= read -r current_disk; do + [[ -z "$current_disk" ]] && continue + disk_count=$((disk_count + 1)) + + __update__ "Configuring ${current_disk} on VM ${vmid} (node ${node})..." + + # Get current disk configuration + local current_config + current_config=$(__node_exec__ "$node" "qm config ${vmid}" 2>/dev/null | grep "^${current_disk}:" | cut -d' ' -f2- || echo "") + + if [[ -z "$current_config" ]]; then + __update__ "VM ${vmid} has no ${current_disk} disk" + disk_failed=$((disk_failed + 1)) + continue + fi + + # Build new configuration by modifying existing config + local new_config="$current_config" + + # Update read IOPS limit + if [[ -n "$IOPS_RD" ]]; then + new_config=$(echo "$new_config" | sed "s/,iops_rd=[^,]*//" | sed "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=[^,]*,//") + [[ "$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=[^,]*,//") + [[ "$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=[^,]*,//") + [[ "$IOPS_WR_MAX" != "0" ]] && new_config="${new_config},iops_wr_max=${IOPS_WR_MAX}" + fi + + # Apply configuration on correct node + if ! __node_exec__ "$node" "qm set ${vmid} --${current_disk} '${new_config}'" 2>&1; then + disk_failed=$((disk_failed + 1)) + fi + done <<< "$disk_list" + + if [[ $disk_failed -gt 0 ]]; then + __update__ "VM ${vmid}: ${disk_failed}/${disk_count} disk(s) failed" + return 1 + fi + + return 0 + } + + # Use BulkOperations framework + __bulk_vm_operation__ --name "Disk IOPS Configuration" --report "$START_ID" "$END_ID" configure_iops_callback + + # Display summary + __bulk_summary__ + + [[ $BULK_FAILED -gt 0 ]] && exit 1 + __ok__ "All disk IOPS configurations completed successfully!" +} + +main "$@" + +############################################################################### +# Script notes: +############################################################################### +# Last checked: 2026-02-20 +# +# Changes: +# - 2026-02-20: Initial version +# +# Fixes: +# - +# +# Known issues: +# - +# diff --git a/VirtualMachines/Hardware/BulkConfigureNetwork.sh b/VirtualMachines/Hardware/BulkConfigureNetwork.sh old mode 100644 new mode 100755 diff --git a/VirtualMachines/Hardware/BulkConfigureNetworkBandwidth.sh b/VirtualMachines/Hardware/BulkConfigureNetworkBandwidth.sh new file mode 100755 index 0000000..86aadf6 --- /dev/null +++ b/VirtualMachines/Hardware/BulkConfigureNetworkBandwidth.sh @@ -0,0 +1,155 @@ +#!/bin/bash +# +# BulkConfigureNetworkBandwidth.sh +# +# Configures network bandwidth rate limits on all network interfaces for a range of virtual +# machines (VMs) on a Proxmox VE cluster. Applies the rate limit to every network card (net0, +# net1, etc.) attached to each VM. +# Automatically detects which node each VM is on and executes the operation cluster-wide. +# +# Usage: +# BulkConfigureNetworkBandwidth.sh 100 110 --rate 100 +# BulkConfigureNetworkBandwidth.sh 100 110 --rate 1000 +# BulkConfigureNetworkBandwidth.sh 400 410 --rate 0 +# +# Arguments: +# start_id - Starting VM ID in the range +# end_id - Ending VM ID in the range +# --rate - Rate limit in MB/s (0=unlimited) +# +# Notes: +# - Applies the rate limit to ALL network interfaces on each VM +# - Set rate to 0 to remove the bandwidth limit +# - Changes take effect immediately for running VMs (no reboot required) +# +# Function Index: +# - main +# + +set -euo pipefail + +# shellcheck source=Utilities/ArgumentParser.sh +source "${UTILITYPATH}/ArgumentParser.sh" +# shellcheck source=Utilities/Prompts.sh +source "${UTILITYPATH}/Prompts.sh" +# shellcheck source=Utilities/Communication.sh +source "${UTILITYPATH}/Communication.sh" +# shellcheck source=Utilities/BulkOperations.sh +source "${UTILITYPATH}/BulkOperations.sh" +# shellcheck source=Utilities/Cluster.sh +source "${UTILITYPATH}/Cluster.sh" +# shellcheck source=Utilities/Operations.sh +source "${UTILITYPATH}/Operations.sh" + +trap '__handle_err__ $LINENO "$BASH_COMMAND"' ERR + +# --- main -------------------------------------------------------------------- +main() { + __check_root__ + __check_proxmox__ + + # Parse arguments using ArgumentParser + __parse_args__ "start_id:vmid end_id:vmid --rate:number" "$@" + + __info__ "Bulk configure network bandwidth: VMs ${START_ID} to ${END_ID} (cluster-wide)" + if [[ "$RATE" == "0" ]]; then + __info__ " Rate limit: unlimited (removing limit)" + else + __info__ " Rate limit: ${RATE} MB/s" + fi + + # Confirm action + if ! __prompt_user_yn__ "Configure network bandwidth for VMs ${START_ID}-${END_ID}?"; then + __info__ "Operation cancelled by user" + exit 0 + fi + + # Local callback for bulk operation + configure_net_bandwidth_callback() { + local vmid="$1" + local node + + node=$(__get_vm_node__ "$vmid") + + if [[ -z "$node" ]]; then + __update__ "VM ${vmid} not found in cluster" + return 1 + fi + + # Get all network interfaces for this VM + local net_ids + net_ids=$(__node_exec__ "$node" "qm config ${vmid}" 2>/dev/null | grep "^net[0-9]*:" | cut -d':' -f1 || echo "") + + if [[ -z "$net_ids" ]]; then + __update__ "VM ${vmid} has no network interfaces" + return 1 + fi + + local net_count=0 + local net_failed=0 + + while IFS= read -r net_id; do + [[ -z "$net_id" ]] && continue + net_count=$((net_count + 1)) + + __update__ "Configuring ${net_id} on VM ${vmid} (node ${node})..." + + # Get current config for this interface + local current_config + current_config=$(__node_exec__ "$node" "qm config ${vmid}" 2>/dev/null | grep "^${net_id}:" | cut -d' ' -f2- || echo "") + + if [[ -z "$current_config" ]]; then + net_failed=$((net_failed + 1)) + continue + fi + + # Update rate in existing config + local new_config="$current_config" + + # Remove existing rate parameter + new_config=$(echo "$new_config" | sed "s/,rate=[^,]*//" | sed "s/rate=[^,]*,//") + + # Add new rate if non-zero + [[ "$RATE" != "0" ]] && new_config="${new_config},rate=${RATE}" + + # Apply configuration on correct node + if ! __node_exec__ "$node" "qm set ${vmid} --${net_id} '${new_config}'" 2>&1; then + net_failed=$((net_failed + 1)) + fi + done <<< "$net_ids" + + if [[ $net_failed -gt 0 ]]; then + __update__ "VM ${vmid}: ${net_failed}/${net_count} interfaces failed" + return 1 + fi + + __update__ "VM ${vmid}: configured ${net_count} interface(s)" + return 0 + } + + # Use BulkOperations framework + __bulk_vm_operation__ --name "Network Bandwidth Configuration" --report "$START_ID" "$END_ID" configure_net_bandwidth_callback + + # Display summary + __bulk_summary__ + + [[ $BULK_FAILED -gt 0 ]] && exit 1 + __ok__ "All network bandwidth configurations completed successfully!" +} + +main "$@" + +############################################################################### +# Script notes: +############################################################################### +# Last checked: 2026-02-20 +# +# Changes: +# - 2026-02-20: Initial version +# +# Fixes: +# - +# +# Known issues: +# - +# diff --git a/VirtualMachines/Hardware/BulkSetCPUTypeCoreCount.sh b/VirtualMachines/Hardware/BulkSetCPUTypeCoreCount.sh old mode 100644 new mode 100755 diff --git a/VirtualMachines/Hardware/BulkSetMemoryConfig.sh b/VirtualMachines/Hardware/BulkSetMemoryConfig.sh old mode 100644 new mode 100755 diff --git a/VirtualMachines/Hardware/BulkToggleTabletPointer.sh b/VirtualMachines/Hardware/BulkToggleTabletPointer.sh old mode 100644 new mode 100755 diff --git a/VirtualMachines/Hardware/BulkUnmountISOs.sh b/VirtualMachines/Hardware/BulkUnmountISOs.sh old mode 100644 new mode 100755 diff --git a/VirtualMachines/Operations/BulkClone.sh b/VirtualMachines/Operations/BulkClone.sh old mode 100644 new mode 100755 diff --git a/VirtualMachines/Operations/BulkCloneCloudInit.sh b/VirtualMachines/Operations/BulkCloneCloudInit.sh old mode 100644 new mode 100755 diff --git a/VirtualMachines/Operations/BulkCloneSetIP_Proxmox.sh b/VirtualMachines/Operations/BulkCloneSetIP_Proxmox.sh old mode 100644 new mode 100755 diff --git a/VirtualMachines/Operations/BulkCloneSetIP_Ubuntu.sh b/VirtualMachines/Operations/BulkCloneSetIP_Ubuntu.sh old mode 100644 new mode 100755 diff --git a/VirtualMachines/Operations/BulkCloneSetIP_Windows.sh b/VirtualMachines/Operations/BulkCloneSetIP_Windows.sh old mode 100644 new mode 100755 diff --git a/VirtualMachines/Operations/BulkDelete.sh b/VirtualMachines/Operations/BulkDelete.sh old mode 100644 new mode 100755 diff --git a/VirtualMachines/Operations/BulkDeleteAllLocal.sh b/VirtualMachines/Operations/BulkDeleteAllLocal.sh old mode 100644 new mode 100755 diff --git a/VirtualMachines/Operations/BulkDisableAutoStart.sh b/VirtualMachines/Operations/BulkDisableAutoStart.sh old mode 100644 new mode 100755 diff --git a/VirtualMachines/Operations/BulkHibernate.sh b/VirtualMachines/Operations/BulkHibernate.sh old mode 100644 new mode 100755 diff --git a/VirtualMachines/Operations/BulkMigrate.sh b/VirtualMachines/Operations/BulkMigrate.sh old mode 100644 new mode 100755 diff --git a/VirtualMachines/Operations/BulkReconfigureMacAddresses.sh b/VirtualMachines/Operations/BulkReconfigureMacAddresses.sh old mode 100644 new mode 100755 diff --git a/VirtualMachines/Operations/BulkRemoteMigrate.sh b/VirtualMachines/Operations/BulkRemoteMigrate.sh old mode 100644 new mode 100755 diff --git a/VirtualMachines/Operations/BulkReset.sh b/VirtualMachines/Operations/BulkReset.sh old mode 100644 new mode 100755 diff --git a/VirtualMachines/Operations/BulkResume.sh b/VirtualMachines/Operations/BulkResume.sh old mode 100644 new mode 100755 diff --git a/VirtualMachines/Operations/BulkStart.sh b/VirtualMachines/Operations/BulkStart.sh old mode 100644 new mode 100755 diff --git a/VirtualMachines/Operations/BulkStop.sh b/VirtualMachines/Operations/BulkStop.sh old mode 100644 new mode 100755 diff --git a/VirtualMachines/Operations/BulkSuspend.sh b/VirtualMachines/Operations/BulkSuspend.sh old mode 100644 new mode 100755 diff --git a/VirtualMachines/Operations/BulkUnlock.sh b/VirtualMachines/Operations/BulkUnlock.sh old mode 100644 new mode 100755 diff --git a/VirtualMachines/Options/BulkEnableGuestAgent.sh b/VirtualMachines/Options/BulkEnableGuestAgent.sh old mode 100644 new mode 100755 diff --git a/VirtualMachines/Options/BulkToggleProtectionMode.sh b/VirtualMachines/Options/BulkToggleProtectionMode.sh old mode 100644 new mode 100755 diff --git a/VirtualMachines/Options/BulkToggleStartAtBoot.sh b/VirtualMachines/Options/BulkToggleStartAtBoot.sh old mode 100644 new mode 100755 diff --git a/VirtualMachines/RestoreVM.sh b/VirtualMachines/RestoreVM.sh old mode 100644 new mode 100755 diff --git a/VirtualMachines/Storage/BulkChangeStorage.sh b/VirtualMachines/Storage/BulkChangeStorage.sh old mode 100644 new mode 100755 diff --git a/VirtualMachines/Storage/BulkConfigureDisk.sh b/VirtualMachines/Storage/BulkConfigureDisk.sh old mode 100644 new mode 100755 diff --git a/VirtualMachines/Storage/BulkMoveDisk.sh b/VirtualMachines/Storage/BulkMoveDisk.sh old mode 100644 new mode 100755 diff --git a/VirtualMachines/Storage/BulkResizeStorage.sh b/VirtualMachines/Storage/BulkResizeStorage.sh old mode 100644 new mode 100755