Last active
October 25, 2025 12:30
-
-
Save AryanXPatel/6082a31e56fc45f45cb2a377176cd532 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/bash | |
| set -euo pipefail | |
| # ============================= | |
| # Enhanced Multi-VM Manager | |
| # ============================= | |
| # Function to display header | |
| display_header() { | |
| clear | |
| cat << "EOF" | |
| ======================================================================== | |
| ___ ____ | |
| / | / __ \ | |
| / /| | / /_/ / | |
| / ___ |/ ____/ | |
| /_/ |_/_/ | |
| POWERED BY AP | |
| ======================================================================== | |
| EOF | |
| echo | |
| } | |
| # Function to display colored output | |
| print_status() { | |
| local type=$1 | |
| local message=$2 | |
| case $type in | |
| "INFO") echo -e "\033[1;34m[INFO]\033[0m $message" ;; | |
| "WARN") echo -e "\033[1;33m[WARN]\033[0m $message" ;; | |
| "ERROR") echo -e "\033[1;31m[ERROR]\033[0m $message" ;; | |
| "SUCCESS") echo -e "\033[1;32m[SUCCESS]\033[0m $message" ;; | |
| "INPUT") echo -e "\033[1;36m[INPUT]\033[0m $message" ;; | |
| *) echo "[$type] $message" ;; | |
| esac | |
| } | |
| # Function to validate input | |
| validate_input() { | |
| local type=$1 | |
| local value=$2 | |
| case $type in | |
| "number") | |
| if ! [[ "$value" =~ ^[0-9]+$ ]]; then | |
| print_status "ERROR" "Must be a number" | |
| return 1 | |
| fi | |
| ;; | |
| "size") | |
| if ! [[ "$value" =~ ^[0-9]+[GgMm]$ ]]; then | |
| print_status "ERROR" "Must be a size with unit (e.g., 100G, 512M)" | |
| return 1 | |
| fi | |
| ;; | |
| "port") | |
| if ! [[ "$value" =~ ^[0-9]+$ ]] || [ "$value" -lt 23 ] || [ "$value" -gt 65535 ]; then | |
| print_status "ERROR" "Must be a valid port number (23-65535)" | |
| return 1 | |
| fi | |
| ;; | |
| "name") | |
| if ! [[ "$value" =~ ^[a-zA-Z0-9_-]+$ ]]; then | |
| print_status "ERROR" "VM name can only contain letters, numbers, hyphens, and underscores" | |
| return 1 | |
| fi | |
| ;; | |
| "username") | |
| if ! [[ "$value" =~ ^[a-z_][a-z0-9_-]*$ ]]; then | |
| print_status "ERROR" "Username must start with a letter or underscore, and contain only letters, numbers, hyphens, and underscores" | |
| return 1 | |
| fi | |
| ;; | |
| esac | |
| return 0 | |
| } | |
| # Function to check dependencies | |
| check_dependencies() { | |
| local deps=("qemu-system-x86_64" "wget" "cloud-localds" "qemu-img") | |
| local missing_deps=() | |
| for dep in "${deps[@]}"; do | |
| if ! command -v "$dep" &> /dev/null; then | |
| missing_deps+=("$dep") | |
| fi | |
| done | |
| if [ ${#missing_deps[@]} -ne 0 ]; then | |
| print_status "ERROR" "Missing dependencies: ${missing_deps[*]}" | |
| print_status "INFO" "On Ubuntu/Debian, try: sudo apt install qemu-system cloud-image-utils wget" | |
| exit 1 | |
| fi | |
| } | |
| # Function to cleanup temporary files | |
| cleanup() { | |
| if [ -f "user-data" ]; then rm -f "user-data"; fi | |
| if [ -f "meta-data" ]; then rm -f "meta-data"; fi | |
| } | |
| # Function to get all VM configurations | |
| get_vm_list() { | |
| find "$VM_DIR" -name "*.conf" -exec basename {} .conf \; 2>/dev/null | sort | |
| } | |
| # Function to load VM configuration | |
| load_vm_config() { | |
| local vm_name=$1 | |
| local config_file="$VM_DIR/$vm_name.conf" | |
| if [[ -f "$config_file" ]]; then | |
| # Clear previous variables | |
| unset VM_NAME OS_TYPE CODENAME IMG_URL HOSTNAME USERNAME PASSWORD | |
| unset DISK_SIZE MEMORY CPUS SSH_PORT GUI_MODE PORT_FORWARDS IMG_FILE SEED_FILE CREATED | |
| source "$config_file" | |
| return 0 | |
| else | |
| print_status "ERROR" "Configuration for VM '$vm_name' not found" | |
| return 1 | |
| fi | |
| } | |
| # Function to save VM configuration | |
| save_vm_config() { | |
| local config_file="$VM_DIR/$VM_NAME.conf" | |
| cat > "$config_file" <<EOF | |
| VM_NAME="$VM_NAME" | |
| OS_TYPE="$OS_TYPE" | |
| CODENAME="$CODENAME" | |
| IMG_URL="$IMG_URL" | |
| HOSTNAME="$HOSTNAME" | |
| USERNAME="$USERNAME" | |
| PASSWORD="$PASSWORD" | |
| DISK_SIZE="$DISK_SIZE" | |
| MEMORY="$MEMORY" | |
| CPUS="$CPUS" | |
| SSH_PORT="$SSH_PORT" | |
| GUI_MODE="$GUI_MODE" | |
| PORT_FORWARDS="$PORT_FORWARDS" | |
| IMG_FILE="$IMG_FILE" | |
| SEED_FILE="$SEED_FILE" | |
| CREATED="$CREATED" | |
| EOF | |
| print_status "SUCCESS" "Configuration saved to $config_file" | |
| } | |
| # Function to create new VM | |
| create_new_vm() { | |
| print_status "INFO" "Creating a new VM" | |
| # OS Selection | |
| print_status "INFO" "Select an OS to set up:" | |
| local os_options=() | |
| local i=1 | |
| for os in "${!OS_OPTIONS[@]}"; do | |
| echo " $i) $os" | |
| os_options[$i]="$os" | |
| ((i++)) | |
| done | |
| while true; do | |
| read -p "$(print_status "INPUT" "Enter your choice (1-${#OS_OPTIONS[@]}): ")" choice | |
| if [[ "$choice" =~ ^[0-9]+$ ]] && [ "$choice" -ge 1 ] && [ "$choice" -le ${#OS_OPTIONS[@]} ]; then | |
| local os="${os_options[$choice]}" | |
| IFS='|' read -r OS_TYPE CODENAME IMG_URL DEFAULT_HOSTNAME DEFAULT_USERNAME DEFAULT_PASSWORD <<< "${OS_OPTIONS[$os]}" | |
| break | |
| else | |
| print_status "ERROR" "Invalid selection. Try again." | |
| fi | |
| done | |
| # Custom Inputs with validation | |
| while true; do | |
| read -p "$(print_status "INPUT" "Enter VM name (default: $DEFAULT_HOSTNAME): ")" VM_NAME | |
| VM_NAME="${VM_NAME:-$DEFAULT_HOSTNAME}" | |
| if validate_input "name" "$VM_NAME"; then | |
| # Check if VM name already exists | |
| if [[ -f "$VM_DIR/$VM_NAME.conf" ]]; then | |
| print_status "ERROR" "VM with name '$VM_NAME' already exists" | |
| else | |
| break | |
| fi | |
| fi | |
| done | |
| while true; do | |
| read -p "$(print_status "INPUT" "Enter hostname (default: $VM_NAME): ")" HOSTNAME | |
| HOSTNAME="${HOSTNAME:-$VM_NAME}" | |
| if validate_input "name" "$HOSTNAME"; then | |
| break | |
| fi | |
| done | |
| while true; do | |
| read -p "$(print_status "INPUT" "Enter username (default: $DEFAULT_USERNAME): ")" USERNAME | |
| USERNAME="${USERNAME:-$DEFAULT_USERNAME}" | |
| if validate_input "username" "$USERNAME"; then | |
| break | |
| fi | |
| done | |
| while true; do | |
| read -s -p "$(print_status "INPUT" "Enter password (default: $DEFAULT_PASSWORD): ")" PASSWORD | |
| PASSWORD="${PASSWORD:-$DEFAULT_PASSWORD}" | |
| echo | |
| if [ -n "$PASSWORD" ]; then | |
| break | |
| else | |
| print_status "ERROR" "Password cannot be empty" | |
| fi | |
| done | |
| while true; do | |
| read -p "$(print_status "INPUT" "Disk size (default: 20G): ")" DISK_SIZE | |
| DISK_SIZE="${DISK_SIZE:-20G}" | |
| if validate_input "size" "$DISK_SIZE"; then | |
| break | |
| fi | |
| done | |
| while true; do | |
| read -p "$(print_status "INPUT" "Memory in MB (default: 2048): ")" MEMORY | |
| MEMORY="${MEMORY:-2048}" | |
| if validate_input "number" "$MEMORY"; then | |
| break | |
| fi | |
| done | |
| while true; do | |
| read -p "$(print_status "INPUT" "Number of CPUs (default: 2): ")" CPUS | |
| CPUS="${CPUS:-2}" | |
| if validate_input "number" "$CPUS"; then | |
| break | |
| fi | |
| done | |
| while true; do | |
| read -p "$(print_status "INPUT" "SSH Port (default: 2222): ")" SSH_PORT | |
| SSH_PORT="${SSH_PORT:-2222}" | |
| if validate_input "port" "$SSH_PORT"; then | |
| # Check if port is already in use | |
| if ss -tln 2>/dev/null | grep -q ":$SSH_PORT "; then | |
| print_status "ERROR" "Port $SSH_PORT is already in use" | |
| else | |
| break | |
| fi | |
| fi | |
| done | |
| while true; do | |
| read -p "$(print_status "INPUT" "Enable GUI mode? (y/n, default: n): ")" gui_input | |
| GUI_MODE=false | |
| gui_input="${gui_input:-n}" | |
| if [[ "$gui_input" =~ ^[Yy]$ ]]; then | |
| GUI_MODE=true | |
| break | |
| elif [[ "$gui_input" =~ ^[Nn]$ ]]; then | |
| break | |
| else | |
| print_status "ERROR" "Please answer y or n" | |
| fi | |
| done | |
| # Additional network options | |
| read -p "$(print_status "INPUT" "Additional port forwards (e.g., 8080:80, press Enter for none): ")" PORT_FORWARDS | |
| IMG_FILE="$VM_DIR/$VM_NAME.img" | |
| SEED_FILE="$VM_DIR/$VM_NAME-seed.iso" | |
| CREATED="$(date)" | |
| # Download and setup VM image | |
| setup_vm_image | |
| # Save configuration | |
| save_vm_config | |
| } | |
| # Function to setup VM image | |
| setup_vm_image() { | |
| print_status "INFO" "Downloading and preparing image..." | |
| # Create VM directory if it doesn't exist | |
| mkdir -p "$VM_DIR" | |
| # Check if image already exists | |
| if [[ -f "$IMG_FILE" ]]; then | |
| print_status "INFO" "Image file already exists. Skipping download." | |
| else | |
| print_status "INFO" "Downloading image from $IMG_URL..." | |
| if ! wget --progress=bar:force "$IMG_URL" -O "$IMG_FILE.tmp"; then | |
| print_status "ERROR" "Failed to download image from $IMG_URL" | |
| exit 1 | |
| fi | |
| mv "$IMG_FILE.tmp" "$IMG_FILE" | |
| fi | |
| # Resize the disk image if needed | |
| if ! qemu-img resize "$IMG_FILE" "$DISK_SIZE" 2>/dev/null; then | |
| print_status "WARN" "Failed to resize disk image. Creating new image with specified size..." | |
| # Create a new image with the specified size | |
| rm -f "$IMG_FILE" | |
| qemu-img create -f qcow2 -F qcow2 -b "$IMG_FILE" "$IMG_FILE.tmp" "$DISK_SIZE" 2>/dev/null || \ | |
| qemu-img create -f qcow2 "$IMG_FILE" "$DISK_SIZE" | |
| if [ -f "$IMG_FILE.tmp" ]; then | |
| mv "$IMG_FILE.tmp" "$IMG_FILE" | |
| fi | |
| fi | |
| # cloud-init configuration | |
| cat > user-data <<EOF | |
| #cloud-config | |
| hostname: $HOSTNAME | |
| ssh_pwauth: true | |
| disable_root: false | |
| users: | |
| - name: $USERNAME | |
| sudo: ALL=(ALL) NOPASSWD:ALL | |
| shell: /bin/bash | |
| password: $(openssl passwd -6 "$PASSWORD" | tr -d '\n') | |
| chpasswd: | |
| list: | | |
| root:$PASSWORD | |
| $USERNAME:$PASSWORD | |
| expire: false | |
| EOF | |
| cat > meta-data <<EOF | |
| instance-id: iid-$VM_NAME | |
| local-hostname: $HOSTNAME | |
| EOF | |
| if ! cloud-localds "$SEED_FILE" user-data meta-data; then | |
| print_status "ERROR" "Failed to create cloud-init seed image" | |
| exit 1 | |
| fi | |
| print_status "SUCCESS" "VM '$VM_NAME' created successfully." | |
| } | |
| # Function to start a VM | |
| start_vm() { | |
| local vm_name=$1 | |
| if load_vm_config "$vm_name"; then | |
| print_status "INFO" "Starting VM: $vm_name" | |
| print_status "INFO" "SSH: ssh -p $SSH_PORT $USERNAME@localhost" | |
| print_status "INFO" "Password: $PASSWORD" | |
| # Check if image file exists | |
| if [[ ! -f "$IMG_FILE" ]]; then | |
| print_status "ERROR" "VM image file not found: $IMG_FILE" | |
| return 1 | |
| fi | |
| # Check if seed file exists | |
| if [[ ! -f "$SEED_FILE" ]]; then | |
| print_status "WARN" "Seed file not found, recreating..." | |
| setup_vm_image | |
| fi | |
| # Base QEMU command | |
| local qemu_cmd=( | |
| qemu-system-x86_64 | |
| -machine accel=tcg | |
| -cpu max | |
| -m "$MEMORY" | |
| -smp "$CPUS" | |
| -drive "file=$IMG_FILE,format=qcow2,if=virtio" | |
| -drive "file=$SEED_FILE,format=raw,if=virtio" | |
| -boot order=c | |
| ) | |
| # Combine SSH + any additional forwards | |
| local hostfwd_args=",hostfwd=tcp::${SSH_PORT}-:22" | |
| if [[ -n "$PORT_FORWARDS" ]]; then | |
| IFS=',' read -ra forwards <<< "$PORT_FORWARDS" | |
| for forward in "${forwards[@]}"; do | |
| IFS=':' read -r host_port guest_port <<< "$forward" | |
| hostfwd_args+=",hostfwd=tcp::${host_port}-:${guest_port}" | |
| done | |
| fi | |
| # Attach one NIC with all forwards | |
| qemu_cmd+=( | |
| -netdev "user,id=n0${hostfwd_args}" | |
| -device virtio-net-pci,netdev=n0 | |
| ) | |
| # Add GUI or console mode | |
| if [[ "$GUI_MODE" == true ]]; then | |
| qemu_cmd+=(-vga virtio -display gtk,gl=on) | |
| else | |
| qemu_cmd+=(-nographic -serial mon:stdio) | |
| fi | |
| # Add performance enhancements | |
| qemu_cmd+=( | |
| -device virtio-balloon-pci | |
| -object rng-random,filename=/dev/urandom,id=rng0 | |
| -device virtio-rng-pci,rng=rng0 | |
| ) | |
| print_status "INFO" "Starting QEMU..." | |
| "${qemu_cmd[@]}" | |
| print_status "INFO" "VM $vm_name has been shut down" | |
| fi | |
| } | |
| # Function to delete a VM | |
| delete_vm() { | |
| local vm_name=$1 | |
| print_status "WARN" "This will permanently delete VM '$vm_name' and all its data!" | |
| read -p "$(print_status "INPUT" "Are you sure? (y/N): ")" -n 1 -r | |
| echo | |
| if [[ $REPLY =~ ^[Yy]$ ]]; then | |
| if load_vm_config "$vm_name"; then | |
| rm -f "$IMG_FILE" "$SEED_FILE" "$VM_DIR/$vm_name.conf" | |
| print_status "SUCCESS" "VM '$vm_name' has been deleted" | |
| fi | |
| else | |
| print_status "INFO" "Deletion cancelled" | |
| fi | |
| } | |
| # Function to show VM info | |
| show_vm_info() { | |
| local vm_name=$1 | |
| if load_vm_config "$vm_name"; then | |
| echo | |
| print_status "INFO" "VM Information: $vm_name" | |
| echo "==========================================" | |
| echo "OS: $OS_TYPE" | |
| echo "Hostname: $HOSTNAME" | |
| echo "Username: $USERNAME" | |
| echo "Password: $PASSWORD" | |
| echo "SSH Port: $SSH_PORT" | |
| echo "Memory: $MEMORY MB" | |
| echo "CPUs: $CPUS" | |
| echo "Disk: $DISK_SIZE" | |
| echo "GUI Mode: $GUI_MODE" | |
| echo "Port Forwards: ${PORT_FORWARDS:-None}" | |
| echo "Created: $CREATED" | |
| echo "Image File: $IMG_FILE" | |
| echo "Seed File: $SEED_FILE" | |
| echo "==========================================" | |
| echo | |
| read -p "$(print_status "INPUT" "Press Enter to continue...")" | |
| fi | |
| } | |
| # Function to check if VM is running | |
| is_vm_running() { | |
| local vm_name=$1 | |
| if pgrep -f "qemu-system-x86_64.*$vm_name" >/dev/null; then | |
| return 0 | |
| else | |
| return 1 | |
| fi | |
| } | |
| # Function to stop a running VM | |
| stop_vm() { | |
| local vm_name=$1 | |
| if load_vm_config "$vm_name"; then | |
| if is_vm_running "$vm_name"; then | |
| print_status "INFO" "Stopping VM: $vm_name" | |
| pkill -f "qemu-system-x86_64.*$IMG_FILE" | |
| sleep 2 | |
| if is_vm_running "$vm_name"; then | |
| print_status "WARN" "VM did not stop gracefully, forcing termination..." | |
| pkill -9 -f "qemu-system-x86_64.*$IMG_FILE" | |
| fi | |
| print_status "SUCCESS" "VM $vm_name stopped" | |
| else | |
| print_status "INFO" "VM $vm_name is not running" | |
| fi | |
| fi | |
| } | |
| # Function to edit VM configuration | |
| edit_vm_config() { | |
| local vm_name=$1 | |
| if load_vm_config "$vm_name"; then | |
| print_status "INFO" "Editing VM: $vm_name" | |
| while true; do | |
| echo "What would you like to edit?" | |
| echo " 1) Hostname" | |
| echo " 2) Username" | |
| echo " 3) Password" | |
| echo " 4) SSH Port" | |
| echo " 5) GUI Mode" | |
| echo " 6) Port Forwards" | |
| echo " 7) Memory (RAM)" | |
| echo " 8) CPU Count" | |
| echo " 9) Disk Size" | |
| echo " 0) Back to main menu" | |
| read -p "$(print_status "INPUT" "Enter your choice: ")" edit_choice | |
| case $edit_choice in | |
| 1) | |
| while true; do | |
| read -p "$(print_status "INPUT" "Enter new hostname (current: $HOSTNAME): ")" new_hostname | |
| new_hostname="${new_hostname:-$HOSTNAME}" | |
| if validate_input "name" "$new_hostname"; then | |
| HOSTNAME="$new_hostname" | |
| break | |
| fi | |
| done | |
| ;; | |
| 2) | |
| while true; do | |
| read -p "$(print_status "INPUT" "Enter new username (current: $USERNAME): ")" new_username | |
| new_username="${new_username:-$USERNAME}" | |
| if validate_input "username" "$new_username"; then | |
| USERNAME="$new_username" | |
| break | |
| fi | |
| done | |
| ;; | |
| 3) | |
| while true; do | |
| read -s -p "$(print_status "INPUT" "Enter new password (current: ****): ")" new_password | |
| new_password="${new_password:-$PASSWORD}" | |
| echo | |
| if [ -n "$new_password" ]; then | |
| PASSWORD="$new_password" | |
| break | |
| else | |
| print_status "ERROR" "Password cannot be empty" | |
| fi | |
| done | |
| ;; | |
| 4) | |
| while true; do | |
| read -p "$(print_status "INPUT" "Enter new SSH port (current: $SSH_PORT): ")" new_ssh_port | |
| new_ssh_port="${new_ssh_port:-$SSH_PORT}" | |
| if validate_input "port" "$new_ssh_port"; then | |
| # Check if port is already in use | |
| if [ "$new_ssh_port" != "$SSH_PORT" ] && ss -tln 2>/dev/null | grep -q ":$new_ssh_port "; then | |
| print_status "ERROR" "Port $new_ssh_port is already in use" | |
| else | |
| SSH_PORT="$new_ssh_port" | |
| break | |
| fi | |
| fi | |
| done | |
| ;; | |
| 5) | |
| while true; do | |
| read -p "$(print_status "INPUT" "Enable GUI mode? (y/n, current: $GUI_MODE): ")" gui_input | |
| gui_input="${gui_input:-}" | |
| if [[ "$gui_input" =~ ^[Yy]$ ]]; then | |
| GUI_MODE=true | |
| break | |
| elif [[ "$gui_input" =~ ^[Nn]$ ]]; then | |
| GUI_MODE=false | |
| break | |
| elif [ -z "$gui_input" ]; then | |
| # Keep current value if user just pressed Enter | |
| break | |
| else | |
| print_status "ERROR" "Please answer y or n" | |
| fi | |
| done | |
| ;; | |
| 6) | |
| read -p "$(print_status "INPUT" "Additional port forwards (current: ${PORT_FORWARDS:-None}): ")" new_port_forwards | |
| PORT_FORWARDS="${new_port_forwards:-$PORT_FORWARDS}" | |
| ;; | |
| 7) | |
| while true; do | |
| read -p "$(print_status "INPUT" "Enter new memory in MB (current: $MEMORY): ")" new_memory | |
| new_memory="${new_memory:-$MEMORY}" | |
| if validate_input "number" "$new_memory"; then | |
| MEMORY="$new_memory" | |
| break | |
| fi | |
| done | |
| ;; | |
| 8) | |
| while true; do | |
| read -p "$(print_status "INPUT" "Enter new CPU count (current: $CPUS): ")" new_cpus | |
| new_cpus="${new_cpus:-$CPUS}" | |
| if validate_input "number" "$new_cpus"; then | |
| CPUS="$new_cpus" | |
| break | |
| fi | |
| done | |
| ;; | |
| 9) | |
| while true; do | |
| read -p "$(print_status "INPUT" "Enter new disk size (current: $DISK_SIZE): ")" new_disk_size | |
| new_disk_size="${new_disk_size:-$DISK_SIZE}" | |
| if validate_input "size" "$new_disk_size"; then | |
| DISK_SIZE="$new_disk_size" | |
| break | |
| fi | |
| done | |
| ;; | |
| 0) | |
| return 0 | |
| ;; | |
| *) | |
| print_status "ERROR" "Invalid selection" | |
| continue | |
| ;; | |
| esac | |
| # Recreate seed image with new configuration if user/password/hostname changed | |
| if [[ "$edit_choice" -eq 1 || "$edit_choice" -eq 2 || "$edit_choice" -eq 3 ]]; then | |
| print_status "INFO" "Updating cloud-init configuration..." | |
| setup_vm_image | |
| fi | |
| # Save configuration | |
| save_vm_config | |
| read -p "$(print_status "INPUT" "Continue editing? (y/N): ")" continue_editing | |
| if [[ ! "$continue_editing" =~ ^[Yy]$ ]]; then | |
| break | |
| fi | |
| done | |
| fi | |
| } | |
| # Function to resize VM disk | |
| resize_vm_disk() { | |
| local vm_name=$1 | |
| if load_vm_config "$vm_name"; then | |
| print_status "INFO" "Current disk size: $DISK_SIZE" | |
| while true; do | |
| read -p "$(print_status "INPUT" "Enter new disk size (e.g., 50G): ")" new_disk_size | |
| if validate_input "size" "$new_disk_size"; then | |
| if [[ "$new_disk_size" == "$DISK_SIZE" ]]; then | |
| print_status "INFO" "New disk size is the same as current size. No changes made." | |
| return 0 | |
| fi | |
| # Check if new size is smaller than current (not recommended) | |
| local current_size_num=${DISK_SIZE%[GgMm]} | |
| local new_size_num=${new_disk_size%[GgMm]} | |
| local current_unit=${DISK_SIZE: -1} | |
| local new_unit=${new_disk_size: -1} | |
| # Convert both to MB for comparison | |
| if [[ "$current_unit" =~ [Gg] ]]; then | |
| current_size_num=$((current_size_num * 1024)) | |
| fi | |
| if [[ "$new_unit" =~ [Gg] ]]; then | |
| new_size_num=$((new_size_num * 1024)) | |
| fi | |
| if [[ $new_size_num -lt $current_size_num ]]; then | |
| print_status "WARN" "Shrinking disk size is not recommended and may cause data loss!" | |
| read -p "$(print_status "INPUT" "Are you sure you want to continue? (y/N): ")" confirm_shrink | |
| if [[ ! "$confirm_shrink" =~ ^[Yy]$ ]]; then | |
| print_status "INFO" "Disk resize cancelled." | |
| return 0 | |
| fi | |
| fi | |
| # Resize the disk | |
| print_status "INFO" "Resizing disk to $new_disk_size..." | |
| if qemu-img resize "$IMG_FILE" "$new_disk_size"; then | |
| DISK_SIZE="$new_disk_size" | |
| save_vm_config | |
| print_status "SUCCESS" "Disk resized successfully to $new_disk_size" | |
| else | |
| print_status "ERROR" "Failed to resize disk" | |
| return 1 | |
| fi | |
| break | |
| fi | |
| done | |
| fi | |
| } | |
| # Function to show VM performance metrics | |
| show_vm_performance() { | |
| local vm_name=$1 | |
| if load_vm_config "$vm_name"; then | |
| if is_vm_running "$vm_name"; then | |
| print_status "INFO" "Performance metrics for VM: $vm_name" | |
| echo "==========================================" | |
| # Get QEMU process ID | |
| local qemu_pid=$(pgrep -f "qemu-system-x86_64.*$IMG_FILE") | |
| if [[ -n "$qemu_pid" ]]; then | |
| # Show process stats | |
| echo "QEMU Process Stats:" | |
| ps -p "$qemu_pid" -o pid,%cpu,%mem,sz,rss,vsz,cmd --no-headers | |
| echo | |
| # Show memory usage | |
| echo "Memory Usage:" | |
| free -h | |
| echo | |
| # Show disk usage | |
| echo "Disk Usage:" | |
| df -h "$IMG_FILE" 2>/dev/null || du -h "$IMG_FILE" | |
| else | |
| print_status "ERROR" "Could not find QEMU process for VM $vm_name" | |
| fi | |
| else | |
| print_status "INFO" "VM $vm_name is not running" | |
| echo "Configuration:" | |
| echo " Memory: $MEMORY MB" | |
| echo " CPUs: $CPUS" | |
| echo " Disk: $DISK_SIZE" | |
| fi | |
| echo "==========================================" | |
| read -p "$(print_status "INPUT" "Press Enter to continue...")" | |
| fi | |
| } | |
| # Main menu function | |
| main_menu() { | |
| while true; do | |
| display_header | |
| local vms=($(get_vm_list)) | |
| local vm_count=${#vms[@]} | |
| if [ $vm_count -gt 0 ]; then | |
| print_status "INFO" "Found $vm_count existing VM(s):" | |
| for i in "${!vms[@]}"; do | |
| local status="Stopped" | |
| if is_vm_running "${vms[$i]}"; then | |
| status="Running" | |
| fi | |
| printf " %2d) %s (%s)\n" $((i+1)) "${vms[$i]}" "$status" | |
| done | |
| echo | |
| fi | |
| echo "Main Menu:" | |
| echo " 1) Create a new VM" | |
| if [ $vm_count -gt 0 ]; then | |
| echo " 2) Start a VM" | |
| echo " 3) Stop a VM" | |
| echo " 4) Show VM info" | |
| echo " 5) Edit VM configuration" | |
| echo " 6) Delete a VM" | |
| echo " 7) Resize VM disk" | |
| echo " 8) Show VM performance" | |
| fi | |
| echo " 0) Exit" | |
| echo | |
| read -p "$(print_status "INPUT" "Enter your choice: ")" choice | |
| case $choice in | |
| 1) | |
| create_new_vm | |
| ;; | |
| 2) | |
| if [ $vm_count -gt 0 ]; then | |
| read -p "$(print_status "INPUT" "Enter VM number to start: ")" vm_num | |
| if [[ "$vm_num" =~ ^[0-9]+$ ]] && [ "$vm_num" -ge 1 ] && [ "$vm_num" -le $vm_count ]; then | |
| start_vm "${vms[$((vm_num-1))]}" | |
| else | |
| print_status "ERROR" "Invalid selection" | |
| fi | |
| fi | |
| ;; | |
| 3) | |
| if [ $vm_count -gt 0 ]; then | |
| read -p "$(print_status "INPUT" "Enter VM number to stop: ")" vm_num | |
| if [[ "$vm_num" =~ ^[0-9]+$ ]] && [ "$vm_num" -ge 1 ] && [ "$vm_num" -le $vm_count ]; then | |
| stop_vm "${vms[$((vm_num-1))]}" | |
| else | |
| print_status "ERROR" "Invalid selection" | |
| fi | |
| fi | |
| ;; | |
| 4) | |
| if [ $vm_count -gt 0 ]; then | |
| read -p "$(print_status "INPUT" "Enter VM number to show info: ")" vm_num | |
| if [[ "$vm_num" =~ ^[0-9]+$ ]] && [ "$vm_num" -ge 1 ] && [ "$vm_num" -le $vm_count ]; then | |
| show_vm_info "${vms[$((vm_num-1))]}" | |
| else | |
| print_status "ERROR" "Invalid selection" | |
| fi | |
| fi | |
| ;; | |
| 5) | |
| if [ $vm_count -gt 0 ]; then | |
| read -p "$(print_status "INPUT" "Enter VM number to edit: ")" vm_num | |
| if [[ "$vm_num" =~ ^[0-9]+$ ]] && [ "$vm_num" -ge 1 ] && [ "$vm_num" -le $vm_count ]; then | |
| edit_vm_config "${vms[$((vm_num-1))]}" | |
| else | |
| print_status "ERROR" "Invalid selection" | |
| fi | |
| fi | |
| ;; | |
| 6) | |
| if [ $vm_count -gt 0 ]; then | |
| read -p "$(print_status "INPUT" "Enter VM number to delete: ")" vm_num | |
| if [[ "$vm_num" =~ ^[0-9]+$ ]] && [ "$vm_num" -ge 1 ] && [ "$vm_num" -le $vm_count ]; then | |
| delete_vm "${vms[$((vm_num-1))]}" | |
| else | |
| print_status "ERROR" "Invalid selection" | |
| fi | |
| fi | |
| ;; | |
| 7) | |
| if [ $vm_count -gt 0 ]; then | |
| read -p "$(print_status "INPUT" "Enter VM number to resize disk: ")" vm_num | |
| if [[ "$vm_num" =~ ^[0-9]+$ ]] && [ "$vm_num" -ge 1 ] && [ "$vm_num" -le $vm_count ]; then | |
| resize_vm_disk "${vms[$((vm_num-1))]}" | |
| else | |
| print_status "ERROR" "Invalid selection" | |
| fi | |
| fi | |
| ;; | |
| 8) | |
| if [ $vm_count -gt 0 ]; then | |
| read -p "$(print_status "INPUT" "Enter VM number to show performance: ")" vm_num | |
| if [[ "$vm_num" =~ ^[0-9]+$ ]] && [ "$vm_num" -ge 1 ] && [ "$vm_num" -le $vm_count ]; then | |
| show_vm_performance "${vms[$((vm_num-1))]}" | |
| else | |
| print_status "ERROR" "Invalid selection" | |
| fi | |
| fi | |
| ;; | |
| 0) | |
| print_status "INFO" "Goodbye!" | |
| exit 0 | |
| ;; | |
| *) | |
| print_status "ERROR" "Invalid option" | |
| ;; | |
| esac | |
| read -p "$(print_status "INPUT" "Press Enter to continue...")" | |
| done | |
| } | |
| # Set trap to cleanup on exit | |
| trap cleanup EXIT | |
| # Check dependencies | |
| check_dependencies | |
| # Initialize paths | |
| VM_DIR="${VM_DIR:-$HOME/vms}" | |
| mkdir -p "$VM_DIR" | |
| # Supported OS list | |
| declare -A OS_OPTIONS=( | |
| ["Ubuntu 22.04"]="ubuntu|jammy|https://cloud-images.ubuntu.com/jammy/current/jammy-server-cloudimg-amd64.img|ubuntu22|ubuntu|ubuntu" | |
| ["Ubuntu 24.04"]="ubuntu|noble|https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img|ubuntu24|ubuntu|ubuntu" | |
| ["Debian 11"]="debian|bullseye|https://cloud.debian.org/images/cloud/bullseye/latest/debian-11-generic-amd64.qcow2|debian11|debian|debian" | |
| ["Debian 12"]="debian|bookworm|https://cloud.debian.org/images/cloud/bookworm/latest/debian-12-generic-amd64.qcow2|debian12|debian|debian" | |
| ["Fedora 40"]="fedora|40|https://download.fedoraproject.org/pub/fedora/linux/releases/40/Cloud/x86_64/images/Fedora-Cloud-Base-40-1.14.x86_64.qcow2|fedora40|fedora|fedora" | |
| ["CentOS Stream 9"]="centos|stream9|https://cloud.centos.org/centos/9-stream/x86_64/images/CentOS-Stream-GenericCloud-9-latest.x86_64.qcow2|centos9|centos|centos" | |
| ["AlmaLinux 9"]="almalinux|9|https://repo.almalinux.org/almalinux/9/cloud/x86_64/images/AlmaLinux-9-GenericCloud-latest.x86_64.qcow2|almalinux9|alma|alma" | |
| ["Rocky Linux 9"]="rockylinux|9|https://download.rockylinux.org/pub/rocky/9/images/x86_64/Rocky-9-GenericCloud.latest.x86_64.qcow2|rocky9|rocky|rocky" | |
| ) | |
| # Start the main menu | |
| main_menu |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment