This commit is contained in:
kristof 2025-06-15 19:42:01 +02:00
parent d336f36929
commit 3dbd0d0aea
3 changed files with 266 additions and 113 deletions

61
tools/extract_kernel.sh Executable file
View File

@ -0,0 +1,61 @@
#!/bin/bash
# Extract kernel and initrd from Ubuntu cloud image
# This script ensures kernel boot works automatically
set -e
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
log() {
echo -e "${GREEN}[$(date +'%Y-%m-%d %H:%M:%S')] $1${NC}"
}
error() {
echo -e "${RED}[$(date +'%Y-%m-%d %H:%M:%S')] ERROR: $1${NC}"
exit 1
}
BASE_SUBVOL="/var/lib/vms/base"
BASE_IMAGE_PATH="$BASE_SUBVOL/ubuntu-24.04-server-cloudimg-amd64.raw"
KERNEL_PATH="$BASE_SUBVOL/vmlinuz"
INITRD_PATH="$BASE_SUBVOL/initrd.img"
# Check if kernel and initrd already exist
if [ -f "$KERNEL_PATH" ] && [ -f "$INITRD_PATH" ]; then
log "Kernel and initrd already extracted"
exit 0
fi
# Check if base image exists
if [ ! -f "$BASE_IMAGE_PATH" ]; then
error "Base image not found at $BASE_IMAGE_PATH"
fi
log "Extracting kernel and initrd from base image..."
# Create temporary mount point
TEMP_MOUNT=$(mktemp -d)
trap "umount '$TEMP_MOUNT' 2>/dev/null || true; losetup -d /dev/loop1 2>/dev/null || true; rmdir '$TEMP_MOUNT'" EXIT
# Mount the image
losetup -P /dev/loop1 "$BASE_IMAGE_PATH"
# Mount the boot partition (partition 16 contains kernel/initrd)
mount /dev/loop1p16 "$TEMP_MOUNT"
# Copy kernel and initrd
if [ -f "$TEMP_MOUNT/vmlinuz-6.8.0-60-generic" ] && [ -f "$TEMP_MOUNT/initrd.img-6.8.0-60-generic" ]; then
cp "$TEMP_MOUNT/vmlinuz-6.8.0-60-generic" "$KERNEL_PATH"
cp "$TEMP_MOUNT/initrd.img-6.8.0-60-generic" "$INITRD_PATH"
log "✓ Kernel and initrd extracted successfully"
else
error "Kernel or initrd not found in boot partition"
fi
# Cleanup is handled by trap
log "Kernel extraction completed"

View File

@ -1,7 +1,7 @@
#!/bin/bash
# Ubuntu VM Management Script
# Usage: ubuntu_vm_manage.sh <command> [vm_name]
# Usage: ubuntu_vm_manage.sh <command> [vm_number]
set -e
@ -36,65 +36,73 @@ info() {
show_usage() {
echo "Ubuntu VM Management Script"
echo ""
echo "Usage: $0 <command> [vm_name]"
echo "Usage: $0 <command> [vm_number]"
echo ""
echo "Commands:"
echo " list - List all VMs and their status"
echo " status <vm_name> - Show detailed status of a specific VM"
echo " console <vm_name> - Connect to VM console (serial)"
echo " ssh <vm_name> - SSH to VM (requires network setup)"
echo " stop <vm_name> - Stop a running VM"
echo " start <vm_name> - Start a stopped VM"
echo " delete <vm_name> - Delete a VM completely"
echo " ip <vm_name> - Show VM IP address"
echo " logs <vm_name> - Show VM logs"
echo " status <vm_number> - Show detailed status of a specific VM"
echo " console <vm_number> - Connect to VM console (serial)"
echo " ssh <vm_number> - SSH to VM (requires network setup)"
echo " stop <vm_number> - Stop a running VM"
echo " start <vm_number> - Start a stopped VM"
echo " delete <vm_number> - Delete a VM completely"
echo " ip <vm_number> - Show VM IP address"
echo " logs <vm_number> - Show VM logs"
echo ""
echo "Examples:"
echo " $0 list"
echo " $0 status test-vm"
echo " $0 console test-vm"
echo " $0 ssh test-vm"
echo " $0 status 1"
echo " $0 console 1"
echo " $0 ssh 1"
}
list_vms() {
log "Listing all VMs..."
echo ""
printf "%-15s %-10s %-8s %-15s %-20s\n" "VM NAME" "STATUS" "PID" "MEMORY" "STARTED"
printf "%-15s %-10s %-8s %-15s %-20s\n" "-------" "------" "---" "------" "-------"
printf "%-8s %-15s %-15s %-10s %-8s %-15s %-20s\n" "VM #" "IP ADDRESS" "VM NAME" "STATUS" "PID" "MEMORY" "STARTED"
printf "%-8s %-15s %-15s %-10s %-8s %-15s %-20s\n" "----" "----------" "-------" "------" "---" "------" "-------"
if [ ! -d "$VMS_SUBVOL" ]; then
warn "No VMs directory found at $VMS_SUBVOL"
return
fi
for vm_dir in "$VMS_SUBVOL"/*; do
for vm_dir in "$VMS_SUBVOL"/vm*; do
if [ -d "$vm_dir" ]; then
vm_name=$(basename "$vm_dir")
vm_info_file="$vm_dir/vm-info.txt"
if [ -f "$vm_info_file" ]; then
# Safely source the file with error handling
if source "$vm_info_file" 2>/dev/null; then
# Check if VM is running
if [ -n "$VM_PID" ] && kill -0 "$VM_PID" 2>/dev/null; then
status="${GREEN}RUNNING${NC}"
pid="$VM_PID"
vm_dirname=$(basename "$vm_dir")
# Extract number from vm directory name (vm1, vm2, etc.)
vm_number=${vm_dirname#vm}
if [[ "$vm_number" =~ ^[0-9]+$ ]]; then
vm_ip="192.168.100.$vm_number"
vm_info_file="$vm_dir/vm-info.txt"
if [ -f "$vm_info_file" ]; then
# Safely source the file with error handling
if source "$vm_info_file" 2>/dev/null; then
# Check if VM is running
if [ -n "$VM_PID" ] && kill -0 "$VM_PID" 2>/dev/null; then
status="${GREEN}RUNNING${NC}"
pid="$VM_PID"
else
status="${RED}STOPPED${NC}"
pid="N/A"
fi
# Handle missing or malformed STARTED field
if [ -z "$STARTED" ]; then
STARTED="Unknown"
fi
# Use VM_NAME from config, fallback to vm number
display_name="${VM_NAME:-vm$vm_number}"
printf "%-8s %-15s %-15s %-18s %-8s %-15s %-20s\n" "$vm_number" "$vm_ip" "$display_name" "$(printf "%b" "$status")" "$pid" "${MEMORY_MB}MB" "$STARTED"
else
status="${RED}STOPPED${NC}"
pid="N/A"
printf "%-8s %-15s %-15s %-18s %-8s %-15s %-20s\n" "$vm_number" "$vm_ip" "vm$vm_number" "$(printf "%b" "${RED}ERROR${NC}")" "N/A" "N/A" "Config Error"
fi
# Handle missing or malformed STARTED field
if [ -z "$STARTED" ]; then
STARTED="Unknown"
fi
printf "%-15s %-18s %-8s %-15s %-20s\n" "$vm_name" "$(printf "%b" "$status")" "$pid" "${MEMORY_MB}MB" "$STARTED"
else
printf "%-15s %-18s %-8s %-15s %-20s\n" "$vm_name" "$(printf "%b" "${RED}ERROR${NC}")" "N/A" "N/A" "Config Error"
printf "%-8s %-15s %-15s %-18s %-8s %-15s %-20s\n" "$vm_number" "$vm_ip" "vm$vm_number" "$(printf "%b" "${YELLOW}UNKNOWN${NC}")" "N/A" "N/A" "N/A"
fi
else
printf "%-15s %-18s %-8s %-15s %-20s\n" "$vm_name" "$(printf "%b" "${YELLOW}UNKNOWN${NC}")" "N/A" "N/A" "N/A"
fi
fi
done
@ -102,23 +110,30 @@ list_vms() {
}
show_vm_status() {
local vm_name="$1"
local vm_dir="$VMS_SUBVOL/$vm_name"
local vm_number="$1"
local vm_dir="$VMS_SUBVOL/vm$vm_number"
local vm_info_file="$vm_dir/vm-info.txt"
# Validate VM number
if ! [[ "$vm_number" =~ ^[0-9]+$ ]]; then
error "VM number must be a number"
fi
if [ ! -d "$vm_dir" ]; then
error "VM '$vm_name' not found"
error "VM number '$vm_number' not found"
fi
if [ ! -f "$vm_info_file" ]; then
error "VM info file not found for '$vm_name'"
error "VM info file not found for VM number '$vm_number'"
fi
source "$vm_info_file"
log "VM Status for: $vm_name"
log "VM Status for VM number: $vm_number"
echo ""
echo "VM Name: $VM_NAME"
echo "VM Number: $vm_number"
echo "VM Name: ${VM_NAME:-vm$vm_number}"
echo "Static IP: 192.168.100.$vm_number"
echo "Memory: ${MEMORY_MB}MB"
echo "CPU Cores: $CPU_CORES"
echo "Started: $STARTED"
@ -152,21 +167,26 @@ show_vm_status() {
}
connect_console() {
local vm_name="$1"
local vm_dir="$VMS_SUBVOL/$vm_name"
local vm_number="$1"
local vm_dir="$VMS_SUBVOL/vm$vm_number"
local vm_info_file="$vm_dir/vm-info.txt"
# Validate VM number
if ! [[ "$vm_number" =~ ^[0-9]+$ ]]; then
error "VM number must be a number"
fi
if [ ! -f "$vm_info_file" ]; then
error "VM '$vm_name' not found"
error "VM number '$vm_number' not found"
fi
source "$vm_info_file"
if [ -z "$VM_PID" ] || ! kill -0 "$VM_PID" 2>/dev/null; then
error "VM '$vm_name' is not running"
error "VM number '$vm_number' is not running"
fi
info "Connecting to console for VM '$vm_name'"
info "Connecting to console for VM number '$vm_number'"
info "Press Ctrl+A then X to exit console"
echo ""
@ -186,39 +206,50 @@ connect_console() {
}
ssh_to_vm() {
local vm_name="$1"
local vm_number="$1"
local vm_ip="192.168.100.$vm_number"
warn "SSH connection requires:"
warn "1. VM to be fully booted"
warn "2. Network bridge configured with IP"
warn "3. VM to have received IP via DHCP"
echo ""
info "To set up networking:"
info "1. Configure bridge IP: ip addr add 192.168.100.1/24 dev br0"
info "2. Enable IP forwarding: echo 1 > /proc/sys/net/ipv4/ip_forward"
info "3. Set up NAT: iptables -t nat -A POSTROUTING -s 192.168.100.0/24 -j MASQUERADE"
echo ""
# Validate VM number
if ! [[ "$vm_number" =~ ^[0-9]+$ ]]; then
error "VM number must be a number"
fi
info "Attempting SSH connection to VM number $vm_number at $vm_ip"
info "Default login: ubuntu / ubuntu"
info "SSH command: ssh ubuntu@<vm-ip>"
echo ""
# Try to SSH directly
if command -v sshpass &>/dev/null; then
info "Using sshpass for automatic login..."
sshpass -p 'ubuntu' ssh -o StrictHostKeyChecking=no ubuntu@"$vm_ip"
else
info "Manual SSH connection (enter password 'ubuntu'):"
ssh -o StrictHostKeyChecking=no ubuntu@"$vm_ip"
fi
}
stop_vm() {
local vm_name="$1"
local vm_dir="$VMS_SUBVOL/$vm_name"
local vm_number="$1"
local vm_dir="$VMS_SUBVOL/vm$vm_number"
local vm_info_file="$vm_dir/vm-info.txt"
# Validate VM number
if ! [[ "$vm_number" =~ ^[0-9]+$ ]]; then
error "VM number must be a number"
fi
if [ ! -f "$vm_info_file" ]; then
error "VM '$vm_name' not found"
error "VM number '$vm_number' not found"
fi
source "$vm_info_file"
if [ -z "$VM_PID" ] || ! kill -0 "$VM_PID" 2>/dev/null; then
warn "VM '$vm_name' is not running"
warn "VM number '$vm_number' is not running"
return
fi
log "Stopping VM '$vm_name' (PID: $VM_PID)..."
log "Stopping VM number '$vm_number' (PID: $VM_PID)..."
# Graceful shutdown first
kill -TERM "$VM_PID" 2>/dev/null || true
@ -242,15 +273,20 @@ stop_vm() {
rm -f "$VM_SOCKET"
fi
log "VM '$vm_name' stopped"
log "VM number '$vm_number' stopped"
}
delete_vm() {
local vm_name="$1"
local vm_dir="$VMS_SUBVOL/$vm_name"
local vm_number="$1"
local vm_dir="$VMS_SUBVOL/vm$vm_number"
# Validate VM number
if ! [[ "$vm_number" =~ ^[0-9]+$ ]]; then
error "VM number must be a number"
fi
if [ ! -d "$vm_dir" ]; then
error "VM '$vm_name' not found"
error "VM number '$vm_number' not found"
fi
# Stop VM first if running
@ -258,45 +294,50 @@ delete_vm() {
source "$vm_dir/vm-info.txt"
if [ -n "$VM_PID" ] && kill -0 "$VM_PID" 2>/dev/null; then
log "Stopping VM before deletion..."
stop_vm "$vm_name"
stop_vm "$vm_number"
fi
fi
# Confirm deletion
echo -e "${RED}WARNING: This will permanently delete VM '$vm_name' and all its data!${NC}"
echo -e "${RED}WARNING: This will permanently delete VM number '$vm_number' and all its data!${NC}"
read -p "Are you sure? (yes/no): " confirm
if [ "$confirm" = "yes" ]; then
log "Deleting VM '$vm_name'..."
log "Deleting VM number '$vm_number'..."
btrfs subvolume delete "$vm_dir"
log "VM '$vm_name' deleted successfully"
log "VM number '$vm_number' deleted successfully"
else
info "Deletion cancelled"
fi
}
start_vm() {
local vm_name="$1"
local vm_dir="$VMS_SUBVOL/$vm_name"
local vm_number="$1"
local vm_dir="$VMS_SUBVOL/vm$vm_number"
local vm_info_file="$vm_dir/vm-info.txt"
# Validate VM number
if ! [[ "$vm_number" =~ ^[0-9]+$ ]]; then
error "VM number must be a number"
fi
if [ ! -d "$vm_dir" ]; then
error "VM '$vm_name' not found"
error "VM number '$vm_number' not found"
fi
if [ ! -f "$vm_info_file" ]; then
error "VM info file not found for '$vm_name'"
error "VM info file not found for VM number '$vm_number'"
fi
source "$vm_info_file"
# Check if VM is already running
if [ -n "$VM_PID" ] && kill -0 "$VM_PID" 2>/dev/null; then
warn "VM '$vm_name' is already running (PID: $VM_PID)"
warn "VM number '$vm_number' is already running (PID: $VM_PID)"
return
fi
log "Starting VM '$vm_name'..."
log "Starting VM number '$vm_number'..."
# Create TAP interface
if ! ip link show "$TAP_NAME" &>/dev/null; then
@ -309,12 +350,17 @@ start_vm() {
# Remove existing socket if it exists
rm -f "$VM_SOCKET"
# Start Cloud Hypervisor in background
# Start Cloud Hypervisor in background with kernel boot
KERNEL_PATH="/var/lib/vms/base/vmlinuz"
INITRD_PATH="/var/lib/vms/base/initrd.img"
cloud-hypervisor \
--api-socket "$VM_SOCKET" \
--memory "size=${MEMORY_MB}M" \
--cpus "boot=$CPU_CORES" \
--kernel "/var/lib/vms/base/hypervisor-fw" \
--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=52:54:00:$(printf '%02x:%02x:%02x' $((RANDOM%256)) $((RANDOM%256)) $((RANDOM%256)))" \
--serial tty \
@ -326,10 +372,10 @@ start_vm() {
# Update VM info file with new PID
sed -i "s/VM_PID=.*/VM_PID=$NEW_VM_PID/" "$vm_info_file"
log "VM '$vm_name' started with PID $NEW_VM_PID"
log "VM number '$vm_number' started with PID $NEW_VM_PID"
log "VM socket: $VM_SOCKET"
log "TAP interface: $TAP_NAME"
log "To connect to console: ./ubuntu_vm_manage.sh console $vm_name"
log "To connect to console: ./ubuntu_vm_manage.sh console $vm_number"
}
# Main script logic
@ -339,50 +385,65 @@ if [ $# -eq 0 ]; then
fi
COMMAND="$1"
VM_NAME="$2"
VM_NUMBER="$2"
case "$COMMAND" in
"list")
list_vms
;;
"status")
if [ -z "$VM_NAME" ]; then
error "VM name required for status command"
if [ -z "$VM_NUMBER" ]; then
error "VM number required for status command"
fi
show_vm_status "$VM_NAME"
show_vm_status "$VM_NUMBER"
;;
"console")
if [ -z "$VM_NAME" ]; then
error "VM name required for console command"
if [ -z "$VM_NUMBER" ]; then
error "VM number required for console command"
fi
connect_console "$VM_NAME"
connect_console "$VM_NUMBER"
;;
"ssh")
if [ -z "$VM_NAME" ]; then
error "VM name required for ssh command"
if [ -z "$VM_NUMBER" ]; then
error "VM number required for ssh command"
fi
ssh_to_vm "$VM_NAME"
ssh_to_vm "$VM_NUMBER"
;;
"stop")
if [ -z "$VM_NAME" ]; then
error "VM name required for stop command"
if [ -z "$VM_NUMBER" ]; then
error "VM number required for stop command"
fi
stop_vm "$VM_NAME"
stop_vm "$VM_NUMBER"
;;
"start")
if [ -z "$VM_NAME" ]; then
error "VM name required for start command"
if [ -z "$VM_NUMBER" ]; then
error "VM number required for start command"
fi
start_vm "$VM_NAME"
start_vm "$VM_NUMBER"
;;
"delete")
if [ -z "$VM_NAME" ]; then
error "VM name required for delete command"
if [ -z "$VM_NUMBER" ]; then
error "VM number required for delete command"
fi
delete_vm "$VM_NAME"
delete_vm "$VM_NUMBER"
;;
"ip"|"logs")
warn "Command '$COMMAND' not yet implemented"
"ip")
if [ -z "$VM_NUMBER" ]; then
error "VM number required for ip command"
fi
echo "VM $VM_NUMBER IP address: 192.168.100.$VM_NUMBER"
;;
"logs")
if [ -z "$VM_NUMBER" ]; then
error "VM number required for logs command"
fi
log_file="/tmp/cloud-hypervisor-vm$VM_NUMBER.log"
if [ -f "$log_file" ]; then
log "Showing logs for VM number $VM_NUMBER:"
tail -f "$log_file"
else
warn "Log file not found: $log_file"
fi
;;
*)
error "Unknown command: $COMMAND"

View File

@ -383,10 +383,36 @@ if [ ! -f "$FIRMWARE_PATH" ]; then
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
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..."
@ -625,14 +651,19 @@ log "Generated MAC address for VM: $VM_MAC"
log "Launching Cloud Hypervisor..."
# Try to start Cloud Hypervisor and capture any error output
log "Starting Cloud Hypervisor with command:"
log "cloud-hypervisor --api-socket $VM_SOCKET --memory size=${MEMORY_MB}M --cpus boot=$CPU_CORES --firmware $FIRMWARE_PATH --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"
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"
# Try with API socket first, fall back without it if needed
cloud-hypervisor \
--memory "size=${MEMORY_MB}M" \
--cpus "boot=$CPU_CORES" \
--firmware "$FIRMWARE_PATH" \
--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" \