#!/bin/bash # Ubuntu VM Start Script with Cloud Hypervisor and Btrfs Thin Provisioning # Usage: ubuntu_vm_start.sh $vm_number $name $mem $cores set -e # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color # Configuration BASE_IMAGE_NAME="ubuntu-24.04-server-cloudimg-amd64" BASE_IMAGE_URL="https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img" FIRMWARE_URL="https://github.com/cloud-hypervisor/rust-hypervisor-firmware/releases/download/0.5.0/hypervisor-fw" VM_BASE_DIR="/var/lib/vms" BTRFS_MOUNT_POINT="/var/lib/vms" BASE_SUBVOL="$BTRFS_MOUNT_POINT/base" VMS_SUBVOL="$BTRFS_MOUNT_POINT/vms" # Network configuration BRIDGE_NAME="br0" BRIDGE_IP="192.168.100.1/24" NETWORK="192.168.100.0/24" log() { echo -e "${GREEN}[$(date +'%Y-%m-%d %H:%M:%S')] $1${NC}" } warn() { echo -e "${YELLOW}[$(date +'%Y-%m-%d %H:%M:%S')] WARNING: $1${NC}" } error() { echo -e "${RED}[$(date +'%Y-%m-%d %H:%M:%S')] ERROR: $1${NC}" # If VM_NUMBER is set and we're in VM creation phase, clean up if [ -n "$VM_NUMBER" ] && [ -n "$VM_PID" ]; then cleanup_failed_vm "$VM_NUMBER" fi exit 1 } info() { echo -e "${BLUE}[$(date +'%Y-%m-%d %H:%M:%S')] INFO: $1${NC}" } # Test functions test_step() { local step_name="$1" local test_command="$2" info "Testing: $step_name" if eval "$test_command"; then log "✓ Test passed: $step_name" return 0 else error "✗ Test failed: $step_name" return 1 fi } test_file_exists() { local file_path="$1" local description="$2" test_step "$description" "[ -f '$file_path' ]" } test_directory_exists() { local dir_path="$1" local description="$2" test_step "$description" "[ -d '$dir_path' ]" } test_command_exists() { local command="$1" local description="$2" test_step "$description" "command -v '$command' &> /dev/null" } test_network_interface() { local interface="$1" local description="$2" test_step "$description" "ip link show '$interface' &>/dev/null" } test_process_running() { local pid="$1" local description="$2" test_step "$description" "kill -0 '$pid' 2>/dev/null" } # Cleanup function for failed VM creation cleanup_failed_vm() { local vm_number="$1" warn "VM creation failed, cleaning up..." # Call the delete script to clean up local delete_script="$(dirname "$0")/ubuntu_vm_delete.sh" if [ -f "$delete_script" ]; then log "Running cleanup script: $delete_script" "$delete_script" "$vm_number" || warn "Cleanup script failed, manual cleanup may be required" else warn "Delete script not found at $delete_script, manual cleanup required" fi } # Generate a proper password hash for 'ubuntu' generate_password_hash() { # Generate salt and hash for password 'ubuntu' python3 -c "import crypt; print(crypt.crypt('ubuntu', crypt.mksalt(crypt.METHOD_SHA512)))" } # Wait for VM to boot and verify static IP wait_for_vm_boot() { local vm_name="$1" local expected_ip="$2" local max_wait=180 # 3 minutes, increased from 120 local count=0 log "Waiting for VM '$vm_name' to boot with static IP $expected_ip..." while [ $count -lt $max_wait ]; do # Check if VM process is still running if ! kill -0 "$VM_PID" 2>/dev/null; then error "VM process died while waiting for boot. Check log: $VM_LOG_FILE" fi # Try to ping the expected static IP if ping -c 2 -W 3 "$expected_ip" >/dev/null 2>&1; then log "VM is responding at static IP address: $expected_ip" echo "$expected_ip" return 0 fi # Also check ARP table for our MAC address local vm_ip=$(arp -a | grep "$VM_MAC" | sed 's/.*(\([^)]*\)).*/\1/' | head -1) if [ -n "$vm_ip" ] && [ "$vm_ip" = "$expected_ip" ]; then log "VM MAC address found in ARP table with expected IP: $expected_ip" echo "$expected_ip" return 0 fi sleep 3 count=$((count + 3)) if [ $((count % 15)) -eq 0 ]; then info "Still waiting for VM to boot... ($count/$max_wait seconds)" info "VM process PID $VM_PID is still running" info "Expected static IP: $expected_ip" # Show recent log entries if [ -f "$VM_LOG_FILE" ]; then info "Recent VM log entries:" tail -3 "$VM_LOG_FILE" 2>/dev/null || true fi fi done warn "VM did not respond at expected IP $expected_ip within $max_wait seconds" warn "VM may still be booting - check manually with: ping $expected_ip" return 1 } # Test IP connectivity test_ip_connectivity() { local vm_ip="$1" local max_attempts=10 local attempt=1 log "Testing IP connectivity to $vm_ip..." while [ $attempt -le $max_attempts ]; do info "Ping attempt $attempt/$max_attempts to $vm_ip" # Test ping connectivity with timeout if ping -c 3 -W 2 "$vm_ip" >/dev/null 2>&1; then log "✓ IP connectivity successful to $vm_ip" return 0 fi sleep 3 attempt=$((attempt + 1)) done error "✗ IP connectivity failed after $max_attempts attempts to $vm_ip" return 1 } # Test SSH connectivity test_ssh_connection() { local vm_ip="$1" local max_attempts=10 local attempt=1 log "Testing SSH connectivity to $vm_ip..." while [ $attempt -le $max_attempts ]; do info "SSH attempt $attempt/$max_attempts" # Test SSH connection with timeout if timeout 10 sshpass -p 'ubuntu' ssh -o StrictHostKeyChecking=no -o ConnectTimeout=5 ubuntu@"$vm_ip" 'echo "SSH connection successful"' 2>/dev/null; then log "✓ SSH connection successful to ubuntu@$vm_ip" return 0 fi sleep 5 attempt=$((attempt + 1)) done error "✗ SSH connection failed after $max_attempts attempts" return 1 } # Check if running as root if [ "$EUID" -ne 0 ]; then error "This script must be run as root for btrfs operations" fi # Parse arguments if [ $# -ne 4 ]; then error "Usage: $0 " fi VM_NUMBER="$1" VM_NAME="$2" MEMORY_MB="$3" CPU_CORES="$4" # Validate arguments if ! [[ "$VM_NUMBER" =~ ^[0-9]+$ ]]; then error "VM number must be a number" fi if [ "$VM_NUMBER" -lt 1 ] || [ "$VM_NUMBER" -gt 200 ]; then error "VM number must be between 1 and 200" fi if ! [[ "$MEMORY_MB" =~ ^[0-9]+$ ]]; then error "Memory must be a number (in MB)" fi if ! [[ "$CPU_CORES" =~ ^[0-9]+$ ]]; then error "CPU cores must be a number" fi if [[ "$VM_NAME" =~ [^a-zA-Z0-9_-] ]]; then error "VM name can only contain alphanumeric characters, hyphens, and underscores" fi # Calculate static IP address based on VM number VM_STATIC_IP="192.168.100.$VM_NUMBER" log "Starting VM: $VM_NAME (number $VM_NUMBER) with ${MEMORY_MB}MB RAM and $CPU_CORES CPU cores" log "VM will be assigned static IP: $VM_STATIC_IP" # Comprehensive prerequisite checks log "Performing prerequisite checks..." # Check if cloud-hypervisor is available test_command_exists "cloud-hypervisor" "Cloud Hypervisor installation" # Check if qemu-img is available (for image conversion) if ! command -v qemu-img &> /dev/null; then warn "qemu-img not found. Installing qemu-utils..." apt update && apt install -y qemu-utils test_command_exists "qemu-img" "QEMU tools installation" fi # Check for required tools test_command_exists "curl" "curl installation" test_command_exists "btrfs" "btrfs tools installation" test_command_exists "ip" "iproute2 tools installation" # Check if ethtool is available (for TAP interface optimization) if ! command -v ethtool &> /dev/null; then log "Installing ethtool for network interface optimization..." apt update && apt install -y ethtool test_command_exists "ethtool" "ethtool installation" fi # Check if sshpass is available (for SSH testing) if ! command -v sshpass &> /dev/null; then log "Installing sshpass for SSH testing..." apt update && apt install -y sshpass test_command_exists "sshpass" "sshpass installation" fi # Check if python3 is available (for password hashing) test_command_exists "python3" "Python3 installation" # Check if genisoimage or mkisofs is available if ! command -v genisoimage &> /dev/null && ! command -v mkisofs &> /dev/null; then log "Installing genisoimage for cloud-init ISO creation..." apt update && apt install -y genisoimage fi log "✓ All prerequisites checked" # Create base directory structure log "Setting up storage structure..." mkdir -p "$VM_BASE_DIR" test_directory_exists "$VM_BASE_DIR" "VM base directory creation" # Check if the base directory is on btrfs FILESYSTEM_TYPE=$(stat -f -c %T "$VM_BASE_DIR" 2>/dev/null) if [ "$FILESYSTEM_TYPE" != "btrfs" ]; then error "Base directory $VM_BASE_DIR is not on a btrfs filesystem (detected: $FILESYSTEM_TYPE). Please create a btrfs filesystem first." fi log "✓ Btrfs filesystem detected at $VM_BASE_DIR" # Create base and vms subvolumes if they don't exist if [ ! -d "$BASE_SUBVOL" ]; then log "Creating base subvolume at $BASE_SUBVOL" btrfs subvolume create "$BASE_SUBVOL" test_directory_exists "$BASE_SUBVOL" "Base subvolume creation" else log "✓ Base subvolume already exists" fi if [ ! -d "$VMS_SUBVOL" ]; then log "Creating VMs subvolume at $VMS_SUBVOL" btrfs subvolume create "$VMS_SUBVOL" test_directory_exists "$VMS_SUBVOL" "VMs subvolume creation" else log "✓ VMs subvolume already exists" fi # Verify subvolumes are properly created test_step "Base subvolume verification" "btrfs subvolume show '$BASE_SUBVOL' &>/dev/null" test_step "VMs subvolume verification" "btrfs subvolume show '$VMS_SUBVOL' &>/dev/null" # Define paths BASE_IMAGE_PATH="$BASE_SUBVOL/${BASE_IMAGE_NAME}.raw" FIRMWARE_PATH="$BASE_SUBVOL/hypervisor-fw" VM_SUBVOL_PATH="$VMS_SUBVOL/vm$VM_NUMBER" VM_IMAGE_PATH="$VM_SUBVOL_PATH/vm$VM_NUMBER.raw" CLOUD_INIT_PATH="$VM_SUBVOL_PATH/cloud-init.img" # Download and prepare base image if it doesn't exist log "Preparing base image and firmware..." if [ ! -f "$BASE_IMAGE_PATH" ]; then log "Base image not found. Downloading Ubuntu cloud image..." # Download the qcow2 image TEMP_QCOW2="/tmp/${BASE_IMAGE_NAME}.img" if ! curl -L --fail --progress-bar -o "$TEMP_QCOW2" "$BASE_IMAGE_URL"; then error "Failed to download Ubuntu cloud image from $BASE_IMAGE_URL" fi test_file_exists "$TEMP_QCOW2" "Ubuntu cloud image download" log "Converting qcow2 image to raw format..." qemu-img convert -p -f qcow2 -O raw "$TEMP_QCOW2" "$BASE_IMAGE_PATH" test_file_exists "$BASE_IMAGE_PATH" "Base image conversion" # Verify the converted image image_info=$(qemu-img info "$BASE_IMAGE_PATH" 2>/dev/null) if echo "$image_info" | grep -q "file format: raw"; then log "✓ Base image successfully converted to raw format" else error "Base image conversion verification failed" fi # Cleanup temporary file rm -f "$TEMP_QCOW2" log "✓ Base image created at $BASE_IMAGE_PATH" else log "✓ Base image already exists at $BASE_IMAGE_PATH" test_file_exists "$BASE_IMAGE_PATH" "Base image verification" fi # Download firmware if it doesn't exist if [ ! -f "$FIRMWARE_PATH" ]; then log "Downloading Cloud Hypervisor firmware..." if ! curl -L --fail --progress-bar -o "$FIRMWARE_PATH" "$FIRMWARE_URL"; then error "Failed to download firmware from $FIRMWARE_URL" fi test_file_exists "$FIRMWARE_PATH" "Firmware download" chmod +x "$FIRMWARE_PATH" test_step "Firmware executable check" "[ -x '$FIRMWARE_PATH' ]" log "✓ Firmware downloaded to $FIRMWARE_PATH" else log "✓ Firmware already exists at $FIRMWARE_PATH" test_file_exists "$FIRMWARE_PATH" "Firmware verification" fi # Extract kernel and initrd from base image log "Extracting kernel and initrd for kernel boot..." EXTRACT_SCRIPT="$(dirname "$0")/extract_kernel.sh" if [ -f "$EXTRACT_SCRIPT" ]; then "$EXTRACT_SCRIPT" else warn "Kernel extraction script not found, attempting manual extraction..." # Fallback manual extraction KERNEL_PATH="$BASE_SUBVOL/vmlinuz" INITRD_PATH="$BASE_SUBVOL/initrd.img" if [ ! -f "$KERNEL_PATH" ] || [ ! -f "$INITRD_PATH" ]; then log "Extracting kernel and initrd manually..." TEMP_MOUNT=$(mktemp -d) losetup -P /dev/loop1 "$BASE_IMAGE_PATH" mount /dev/loop1p16 "$TEMP_MOUNT" cp "$TEMP_MOUNT/vmlinuz-6.8.0-60-generic" "$KERNEL_PATH" 2>/dev/null || true cp "$TEMP_MOUNT/initrd.img-6.8.0-60-generic" "$INITRD_PATH" 2>/dev/null || true umount "$TEMP_MOUNT" losetup -d /dev/loop1 rmdir "$TEMP_MOUNT" fi fi test_file_exists "$BASE_SUBVOL/vmlinuz" "Kernel extraction" test_file_exists "$BASE_SUBVOL/initrd.img" "Initrd extraction" # Create VM subvolume by cloning from base log "Setting up VM-specific storage..." if [ -d "$VM_SUBVOL_PATH" ]; then warn "VM subvolume $VM_NAME already exists. Removing it..." if btrfs subvolume show "$VM_SUBVOL_PATH" &>/dev/null; then btrfs subvolume delete "$VM_SUBVOL_PATH" else rm -rf "$VM_SUBVOL_PATH" fi test_step "VM subvolume cleanup" "[ ! -d '$VM_SUBVOL_PATH' ]" fi log "Creating VM subvolume by cloning base subvolume..." btrfs subvolume snapshot "$BASE_SUBVOL" "$VM_SUBVOL_PATH" test_directory_exists "$VM_SUBVOL_PATH" "VM subvolume creation" test_step "VM subvolume verification" "btrfs subvolume show '$VM_SUBVOL_PATH' &>/dev/null" # Copy the base image to VM subvolume (this will be a CoW copy initially) log "Creating VM disk image (thin provisioned)..." cp --reflink=always "$BASE_IMAGE_PATH" "$VM_IMAGE_PATH" test_file_exists "$VM_IMAGE_PATH" "VM disk image creation" # Verify the image copy vm_image_size=$(stat -c%s "$VM_IMAGE_PATH" 2>/dev/null) base_image_size=$(stat -c%s "$BASE_IMAGE_PATH" 2>/dev/null) if [ "$vm_image_size" = "$base_image_size" ]; then log "✓ VM disk image successfully created (size: $vm_image_size bytes)" else error "VM disk image size mismatch (VM: $vm_image_size, Base: $base_image_size)" fi # Create cloud-init image for first boot log "Creating cloud-init configuration..." # Generate a random MAC address for the VM (used in cloud-init and hypervisor) VM_MAC="52:54:00:$(printf '%02x:%02x:%02x' $((RANDOM%256)) $((RANDOM%256)) $((RANDOM%256)))" log "Generated MAC address for VM: $VM_MAC" # Generate proper password hash for 'ubuntu' PASSWORD_HASH=$(generate_password_hash) test_step "Password hash generation" "[ -n '$PASSWORD_HASH' ]" cat > "/tmp/user-data" << EOF #cloud-config users: - name: ubuntu sudo: ALL=(ALL) NOPASSWD:ALL shell: /bin/bash lock_passwd: false passwd: $PASSWORD_HASH groups: sudo home: /home/ubuntu # Enable SSH with password authentication ssh_pwauth: true disable_root: false chpasswd: expire: false # SSH configuration ssh_authorized_keys: [] # Network configuration with static IP network: config: disabled write_files: - path: /etc/netplan/50-cloud-init.yaml content: | network: version: 2 ethernets: ens3: dhcp4: false addresses: - $VM_STATIC_IP/24 # Use variable for IP # gateway4: 192.168.100.1 # Deprecated routes: # Use modern routes syntax - to: default via: 192.168.100.1 nameservers: addresses: - 8.8.8.8 - 1.1.1.1 # Package updates and installs package_update: true package_upgrade: false packages: - openssh-server - curl - wget - git - htop - vim - net-tools # Ensure SSH service is enabled and started # Also configure static IP as backup runcmd: - systemctl enable ssh - systemctl start ssh - systemctl status ssh - netplan apply - chmod 0600 /etc/netplan/50-cloud-init.yaml || echo "Failed to chmod /etc/netplan/50-cloud-init.yaml" - sleep 2 # Allow netplan apply to settle #- ip addr flush dev ens3 #- ip addr add $VM_STATIC_IP/24 dev ens3 #- ip route add default via 192.168.100.1 - ip addr show ens3 - ip route show - ping 192.168.100.2 -c 3 - echo "WERE THERE" # Final message final_message: "Cloud-init setup complete. VM is ready for SSH access!" EOF test_file_exists "/tmp/user-data" "Cloud-init user-data creation" # Create meta-data file cat > "/tmp/meta-data" << EOF instance-id: $VM_NAME local-hostname: $VM_NAME EOF test_file_exists "/tmp/meta-data" "Cloud-init meta-data creation" # Create cloud-init ISO log "Creating cloud-init ISO..." if command -v genisoimage &> /dev/null; then genisoimage -output "$CLOUD_INIT_PATH" -volid cidata -joliet -rock /tmp/user-data /tmp/meta-data elif command -v mkisofs &> /dev/null; then mkisofs -o "$CLOUD_INIT_PATH" -V cidata -J -r /tmp/user-data /tmp/meta-data else error "Neither genisoimage nor mkisofs found. Please install genisoimage or cdrtools." fi test_file_exists "$CLOUD_INIT_PATH" "Cloud-init ISO creation" # Verify the ISO was created properly iso_size=$(stat -c%s "$CLOUD_INIT_PATH" 2>/dev/null) if [ "$iso_size" -gt 0 ]; then log "✓ Cloud-init ISO created successfully (size: $iso_size bytes)" else error "Cloud-init ISO creation failed or resulted in empty file" fi # Cleanup temporary files rm -f /tmp/user-data /tmp/meta-data log "✓ Cloud-init ISO created at $CLOUD_INIT_PATH" # Resize the VM disk to give it more space (optional, expand to 20GB) log "Resizing VM disk to 20GB..." qemu-img resize "$VM_IMAGE_PATH" 20G # Verify disk resize new_size=$(qemu-img info "$VM_IMAGE_PATH" | grep "virtual size" | awk '{print $3}') if echo "$new_size" | grep -q "20"; then log "✓ VM disk successfully resized to 20GB" else warn "VM disk resize verification failed, but continuing..." fi # Create network configuration TAP_NAME="tap-$VM_NAME" log "Setting up network configuration..." # Check if bridge exists, create if not if ! ip link show "$BRIDGE_NAME" &>/dev/null; then log "Creating bridge interface $BRIDGE_NAME..." ip link add name "$BRIDGE_NAME" type bridge ip link set dev "$BRIDGE_NAME" up # Configure bridge with IP address for VM network log "Configuring bridge IP address..." ip addr add "$BRIDGE_IP" dev "$BRIDGE_NAME" test_network_interface "$BRIDGE_NAME" "Bridge interface creation" test_step "Bridge IP configuration" "ip addr show '$BRIDGE_NAME' | grep -q '192.168.100.1'" else log "✓ Bridge interface $BRIDGE_NAME already exists" # Ensure bridge has IP configured if ! ip addr show "$BRIDGE_NAME" | grep -q "192.168.100.1"; then log "Adding IP address to existing bridge..." ip addr add "$BRIDGE_IP" dev "$BRIDGE_NAME" 2>/dev/null || true fi fi # Create TAP interface for the VM TAP_NAME="tap-vm$VM_NUMBER" log "Creating TAP interface $TAP_NAME..." # Remove existing TAP interface if it exists if ip link show "$TAP_NAME" &>/dev/null; then warn "TAP interface $TAP_NAME already exists, removing it..." ip link delete "$TAP_NAME" 2>/dev/null || true sleep 1 # Give time for cleanup fi # Create TAP interface with proper configuration for Cloud Hypervisor ip tuntap add dev "$TAP_NAME" mode tap user root test_network_interface "$TAP_NAME" "TAP interface creation" # Set TAP interface up ip link set dev "$TAP_NAME" up sleep 1 # Give the interface a moment to come up test_step "TAP interface up" "ip link show '$TAP_NAME' | grep -q 'UP'" # Attach to bridge ip link set dev "$TAP_NAME" master "$BRIDGE_NAME" sleep 1 # Give the bridge attachment a moment to complete test_step "TAP interface bridge attachment" "ip link show '$TAP_NAME' | grep -q 'master'" # Disable offloading features that can cause issues with Cloud Hypervisor ethtool -K "$TAP_NAME" tx off rx off tso off gso off gro off lro off 2>/dev/null || warn "Could not disable TAP interface offloading (ethtool not available)" log "✓ Network interfaces configured successfully" # Ensure basic networking is set up (simplified version of setup_vm_network.sh) log "Ensuring basic VM networking is configured..." # Enable IP forwarding echo 1 > /proc/sys/net/ipv4/ip_forward # Set up basic NAT rules (remove existing first to avoid duplicates) iptables -t nat -D POSTROUTING -s "$NETWORK" -j MASQUERADE 2>/dev/null || true iptables -D FORWARD -i "$BRIDGE_NAME" -j ACCEPT 2>/dev/null || true iptables -D FORWARD -o "$BRIDGE_NAME" -j ACCEPT 2>/dev/null || true # Add new rules iptables -t nat -A POSTROUTING -s "$NETWORK" -j MASQUERADE iptables -A FORWARD -i "$BRIDGE_NAME" -j ACCEPT iptables -A FORWARD -o "$BRIDGE_NAME" -j ACCEPT log "✓ Basic NAT and forwarding rules configured" # Check if dnsmasq is running for DHCP if ! systemctl is-active --quiet dnsmasq 2>/dev/null; then warn "dnsmasq is not running. VMs may not get IP addresses automatically." warn "Consider running: sudo ./setup_vm_network.sh" fi # Start the VM with Cloud Hypervisor log "Starting VM $VM_NAME..." VM_SOCKET="/tmp/cloud-hypervisor-vm$VM_NUMBER.sock" VM_LOG_FILE="/tmp/cloud-hypervisor-vm$VM_NUMBER.log" # Remove existing socket and log file if they exist rm -f "$VM_SOCKET" "$VM_LOG_FILE" # Start Cloud Hypervisor in background with error handling log "Launching Cloud Hypervisor..." # Try to start Cloud Hypervisor and capture any error output log "Starting Cloud Hypervisor with kernel boot:" log "cloud-hypervisor --memory size=${MEMORY_MB}M --cpus boot=$CPU_CORES --kernel $KERNEL_PATH --initramfs $INITRD_PATH --cmdline 'root=LABEL=cloudimg-rootfs ro console=tty1 console=ttyS0' --disk path=$VM_IMAGE_PATH path=$CLOUD_INIT_PATH,readonly=on --net tap=$TAP_NAME,mac=$VM_MAC --serial file=$VM_LOG_FILE --console off --event-monitor path=${VM_LOG_FILE}.events" # Use kernel boot instead of firmware boot to properly pass root device KERNEL_PATH="$BASE_SUBVOL/vmlinuz" INITRD_PATH="$BASE_SUBVOL/initrd.img" cloud-hypervisor \ --api-socket "$VM_SOCKET" \ --memory "size=${MEMORY_MB}M" \ --cpus "boot=$CPU_CORES" \ --kernel "$KERNEL_PATH" \ --initramfs "$INITRD_PATH" \ --cmdline "root=LABEL=cloudimg-rootfs ro console=tty1 console=ttyS0" \ --disk "path=$VM_IMAGE_PATH" "path=$CLOUD_INIT_PATH,readonly=on" \ --net "tap=$TAP_NAME,mac=$VM_MAC" \ --serial tty \ --console off \ --event-monitor "path=${VM_LOG_FILE}.events" & VM_PID=$! # Check if the process started successfully if [ -z "$VM_PID" ]; then error "Failed to get VM process ID" fi # Verify VM process started sleep 2 if ! test_process_running "$VM_PID" "VM process startup"; then error "VM process failed to start or died immediately. Check log: $VM_LOG_FILE" fi log "✓ VM $VM_NAME started successfully with PID $VM_PID" log "VM socket: $VM_SOCKET" log "VM log file: $VM_LOG_FILE" log "TAP interface: $TAP_NAME" log "Bridge interface: $BRIDGE_NAME" log "VM MAC address: $VM_MAC" # Wait for VM to initialize and check if it's running properly log "Waiting for VM to initialize..." init_wait_count=0 while [ $init_wait_count -lt 10 ]; do sleep 1 init_wait_count=$((init_wait_count + 1)) # Check if VM process is still running if ! kill -0 "$VM_PID" 2>/dev/null; then error "VM process died during initialization. Check log: $VM_LOG_FILE" fi if [ $((init_wait_count % 3)) -eq 0 ]; then info "VM initializing... ($init_wait_count/10 seconds)" fi done log "✓ VM initialization completed" # Save VM information for management log "Saving VM configuration..." VM_INFO_FILE="$VM_SUBVOL_PATH/vm-info.txt" cat > "$VM_INFO_FILE" << EOF VM_NUMBER=$VM_NUMBER VM_NAME=$VM_NAME VM_PID=$VM_PID VM_SOCKET=$VM_SOCKET TAP_NAME=$TAP_NAME BRIDGE_NAME=$BRIDGE_NAME MEMORY_MB=$MEMORY_MB CPU_CORES=$CPU_CORES VM_IMAGE_PATH=$VM_IMAGE_PATH CLOUD_INIT_PATH=$CLOUD_INIT_PATH VM_MAC=$VM_MAC VM_LOG_FILE=$VM_LOG_FILE VM_STATIC_IP=$VM_STATIC_IP STARTED="$(date '+%Y-%m-%d %H:%M:%S')" EOF test_file_exists "$VM_INFO_FILE" "VM info file creation" log "✓ VM information saved to $VM_INFO_FILE" # Function to cleanup on exit (only for interactive mode) cleanup_on_exit() { log "Cleaning up VM $VM_NAME..." if [ -n "$VM_PID" ] && kill -0 "$VM_PID" 2>/dev/null; then kill "$VM_PID" wait "$VM_PID" 2>/dev/null fi ip link delete "$TAP_NAME" 2>/dev/null || true rm -f "$VM_SOCKET" } # Test VM boot and connectivity log "Testing VM boot and connectivity..." # Show the static IP that will be used log "VM $VM_NAME will use static IP address: $VM_STATIC_IP" # Wait for VM to boot and verify static IP VM_IP=$(wait_for_vm_boot "$VM_NAME" "$VM_STATIC_IP") if [ $? -ne 0 ] || [ -z "$VM_IP" ]; then error "VM failed to boot or respond at static IP $VM_STATIC_IP. Check log: $VM_LOG_FILE" fi log "✓ VM booted successfully and is using IP: $VM_IP" # Test IP connectivity first log "Testing IP connectivity before SSH..." if test_ip_connectivity "$VM_IP"; then log "✓ IP connectivity test passed for $VM_IP" else error "IP connectivity test failed for $VM_IP" fi # Test SSH connectivity if test_ssh_connection "$VM_IP"; then log "🎉 SUCCESS: VM $VM_NAME is fully operational!" log "✓ VM is running with PID $VM_PID" log "✓ VM has IP address: $VM_IP" log "✓ SSH is working: ssh ubuntu@$VM_IP (password: ubuntu)" log "✓ VM info saved to: $VM_INFO_FILE" # Display relevant IP configuration lines from VM log if [ -f "$VM_LOG_FILE" ]; then info "Relevant IP configuration from VM log ($VM_LOG_FILE):" grep -E "inet .*ens3|default via" "$VM_LOG_FILE" | tail -n 5 || true fi echo "" info "VM $VM_NAME is ready for use!" info "Connect via SSH: ssh ubuntu@$VM_IP" info "Default password: ubuntu (please change after first login)" info "To stop the VM: sudo $(dirname "$0")/ubuntu_vm_delete.sh $VM_NUMBER" echo "" # Don't set trap for successful VMs - let them run log "VM $VM_NAME will continue running in the background." log "Use 'sudo $(dirname "$0")/ubuntu_vm_delete.sh $VM_NUMBER' to stop and delete it." else error "SSH connectivity test failed. VM will be deleted for retry." fi # If we reach here, the VM is working properly log "VM startup and testing completed successfully!"