deb-bootc-image-builder/test/vm.py

461 lines
15 KiB
Python

#!/usr/bin/env python3
"""
Virtual machine testing utilities for deb-bootc-image-builder.
This module provides utilities for testing built images in virtual machines,
including:
- VM creation and management
- Image boot testing
- Debian-specific VM configurations
"""
import os
import subprocess
import tempfile
import shutil
import json
import time
import logging
from typing import Dict, List, Any, Optional, Tuple
from pathlib import Path
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class VirtualMachine:
"""Virtual machine for testing Debian images."""
def __init__(self, name: str, image_path: str, vm_type: str = "qemu"):
"""Initialize virtual machine."""
self.name = name
self.image_path = image_path
self.vm_type = vm_type
self.vm_process = None
self.vm_pid = None
self.network_config = None
self.memory = "2G"
self.cpus = 2
self.disk_size = "10G"
# Debian-specific configurations
self.debian_release = "trixie"
self.architecture = "amd64"
self.boot_timeout = 300 # 5 minutes
logger.info(f"Initialized VM {name} with image {image_path}")
def configure_network(self, network_type: str = "user", port_forward: Optional[Dict[str, int]] = None):
"""Configure VM network."""
self.network_config = {
"type": network_type,
"port_forward": port_forward or {}
}
logger.info(f"Configured network: {network_type}")
def set_resources(self, memory: str = "2G", cpus: int = 2, disk_size: str = "10G"):
"""Set VM resource limits."""
self.memory = memory
self.cpus = cpus
self.disk_size = disk_size
logger.info(f"Set resources: {memory} RAM, {cpus} CPUs, {disk_size} disk")
def start(self) -> bool:
"""Start the virtual machine."""
try:
if self.vm_type == "qemu":
return self._start_qemu()
else:
logger.error(f"Unsupported VM type: {self.vm_type}")
return False
except Exception as e:
logger.error(f"Failed to start VM: {e}")
return False
def _start_qemu(self) -> bool:
"""Start QEMU virtual machine."""
cmd = [
"qemu-system-x86_64",
"-name", self.name,
"-m", self.memory,
"-smp", str(self.cpus),
"-drive", f"file={self.image_path},if=virtio,format=qcow2",
"-enable-kvm",
"-display", "none",
"-serial", "stdio",
"-monitor", "none"
]
# Add network configuration
if self.network_config:
if self.network_config["type"] == "user":
cmd.extend(["-net", "user"])
if self.network_config["port_forward"]:
for host_port, guest_port in self.network_config["port_forward"].items():
cmd.extend(["-net", f"user,hostfwd=tcp::{host_port}-:{guest_port}"])
elif self.network_config["type"] == "bridge":
cmd.extend(["-net", "bridge,br=virbr0"])
# Add Debian-specific optimizations
cmd.extend([
"-cpu", "host",
"-machine", "type=q35,accel=kvm",
"-device", "virtio-net-pci,netdev=net0",
"-netdev", "user,id=net0"
])
logger.info(f"Starting QEMU VM with command: {' '.join(cmd)}")
try:
self.vm_process = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
)
self.vm_pid = self.vm_process.pid
logger.info(f"VM started with PID: {self.vm_pid}")
return True
except Exception as e:
logger.error(f"Failed to start QEMU VM: {e}")
return False
def stop(self) -> bool:
"""Stop the virtual machine."""
try:
if self.vm_process:
self.vm_process.terminate()
self.vm_process.wait(timeout=30)
logger.info("VM stopped gracefully")
return True
else:
logger.warning("No VM process to stop")
return False
except subprocess.TimeoutExpired:
logger.warning("VM did not stop gracefully, forcing termination")
if self.vm_process:
self.vm_process.kill()
return True
except Exception as e:
logger.error(f"Failed to stop VM: {e}")
return False
def is_running(self) -> bool:
"""Check if VM is running."""
if self.vm_process:
return self.vm_process.poll() is None
return False
def wait_for_boot(self, timeout: Optional[int] = None) -> bool:
"""Wait for VM to boot up."""
if timeout is None:
timeout = self.boot_timeout
logger.info(f"Waiting for VM to boot (timeout: {timeout}s)")
start_time = time.time()
while time.time() - start_time < timeout:
if self.is_running():
# Check if VM is responsive (basic boot check)
if self._check_boot_status():
logger.info("VM booted successfully")
return True
time.sleep(5)
logger.error("VM boot timeout exceeded")
return False
def _check_boot_status(self) -> bool:
"""Check if VM has booted successfully."""
# This is a simplified check - in practice, you'd want to:
# 1. Check for specific boot messages
# 2. Verify network connectivity
# 3. Check for system services
# 4. Verify Debian-specific indicators
try:
# For now, just check if the process is still running
return self.is_running()
except Exception as e:
logger.debug(f"Boot status check failed: {e}")
return False
def execute_command(self, command: str, timeout: int = 30) -> Dict[str, Any]:
"""Execute a command in the VM (requires guest agent or SSH)."""
# This is a placeholder - in practice, you'd use:
# 1. QEMU guest agent
# 2. SSH connection
# 3. Serial console interaction
logger.info(f"Executing command in VM: {command}")
# For now, return a mock result
return {
"success": True,
"stdout": f"Mock output for: {command}",
"stderr": "",
"returncode": 0
}
def get_system_info(self) -> Dict[str, Any]:
"""Get system information from the VM."""
# This would typically use guest agent or SSH
return {
"os": "Debian GNU/Linux",
"release": self.debian_release,
"architecture": self.architecture,
"kernel": "Linux",
"uptime": "0:00:00",
"memory": "0 MB",
"disk": "0 MB"
}
def check_debian_specific_features(self) -> Dict[str, bool]:
"""Check Debian-specific features in the VM."""
features = {
"ostree_integration": False,
"grub_efi": False,
"initramfs_tools": False,
"systemd": False,
"apt_package_manager": False
}
# This would check actual VM state
# For now, return mock results
features["ostree_integration"] = True
features["grub_efi"] = True
features["initramfs_tools"] = True
features["systemd"] = True
features["apt_package_manager"] = True
return features
class VMTester:
"""Test runner for virtual machines."""
def __init__(self, test_config: Dict[str, Any]):
"""Initialize VM tester."""
self.test_config = test_config
self.vms: List[VirtualMachine] = []
self.test_results: List[Dict[str, Any]] = []
def create_test_vm(self, image_path: str, vm_config: Dict[str, Any]) -> VirtualMachine:
"""Create a test virtual machine."""
vm_name = vm_config.get("name", f"test-vm-{len(self.vms)}")
vm = VirtualMachine(vm_name, image_path)
# Apply configuration
if "network" in vm_config:
vm.configure_network(**vm_config["network"])
if "resources" in vm_config:
vm.set_resources(**vm_config["resources"])
if "debian" in vm_config:
debian_config = vm_config["debian"]
if "release" in debian_config:
vm.debian_release = debian_config["release"]
if "architecture" in debian_config:
vm.architecture = debian_config["architecture"]
if "boot_timeout" in debian_config:
vm.boot_timeout = debian_config["boot_timeout"]
self.vms.append(vm)
return vm
def run_basic_boot_test(self, vm: VirtualMachine) -> Dict[str, Any]:
"""Run basic boot test on VM."""
test_result = {
"test_name": "basic_boot_test",
"vm_name": vm.name,
"start_time": time.time(),
"success": False,
"error": None,
"boot_time": None
}
try:
logger.info(f"Starting basic boot test for VM: {vm.name}")
# Start VM
if not vm.start():
test_result["error"] = "Failed to start VM"
return test_result
# Wait for boot
start_time = time.time()
if vm.wait_for_boot():
boot_time = time.time() - start_time
test_result["boot_time"] = boot_time
test_result["success"] = True
logger.info(f"Boot test passed for VM: {vm.name} (boot time: {boot_time:.2f}s)")
else:
test_result["error"] = "VM failed to boot within timeout"
except Exception as e:
test_result["error"] = str(e)
logger.error(f"Boot test failed for VM {vm.name}: {e}")
finally:
test_result["end_time"] = time.time()
if test_result["success"]:
vm.stop()
return test_result
def run_debian_feature_test(self, vm: VirtualMachine) -> Dict[str, Any]:
"""Run Debian-specific feature test on VM."""
test_result = {
"test_name": "debian_feature_test",
"vm_name": vm.name,
"start_time": time.time(),
"success": False,
"error": None,
"features": {}
}
try:
logger.info(f"Starting Debian feature test for VM: {vm.name}")
# Start VM
if not vm.start():
test_result["error"] = "Failed to start VM"
return test_result
# Wait for boot
if not vm.wait_for_boot():
test_result["error"] = "VM failed to boot"
return test_result
# Check Debian features
features = vm.check_debian_specific_features()
test_result["features"] = features
# Validate required features
required_features = ["ostree_integration", "grub_efi", "systemd"]
missing_features = [f for f in required_features if not features.get(f, False)]
if missing_features:
test_result["error"] = f"Missing required features: {missing_features}"
else:
test_result["success"] = True
logger.info(f"Debian feature test passed for VM: {vm.name}")
except Exception as e:
test_result["error"] = str(e)
logger.error(f"Debian feature test failed for VM {vm.name}: {e}")
finally:
test_result["end_time"] = time.time()
if vm.is_running():
vm.stop()
return test_result
def run_all_tests(self) -> List[Dict[str, Any]]:
"""Run all configured tests."""
logger.info("Starting VM test suite")
for vm in self.vms:
# Run basic boot test
boot_result = self.run_basic_boot_test(vm)
self.test_results.append(boot_result)
# Run Debian feature test
feature_result = self.run_debian_feature_test(vm)
self.test_results.append(feature_result)
# Generate summary
total_tests = len(self.test_results)
passed_tests = len([r for r in self.test_results if r["success"]])
failed_tests = total_tests - passed_tests
logger.info(f"Test suite completed: {passed_tests}/{total_tests} tests passed")
if failed_tests > 0:
logger.warning(f"{failed_tests} tests failed")
for result in self.test_results:
if not result["success"]:
logger.error(f"Test {result['test_name']} failed: {result.get('error', 'Unknown error')}")
return self.test_results
def cleanup(self):
"""Clean up all VMs and resources."""
logger.info("Cleaning up VM resources")
for vm in self.vms:
if vm.is_running():
vm.stop()
self.vms.clear()
self.test_results.clear()
def create_test_vm_config(image_path: str,
debian_release: str = "trixie",
architecture: str = "amd64") -> Dict[str, Any]:
"""Create a test VM configuration."""
return {
"name": f"debian-{debian_release}-{architecture}-test",
"image_path": image_path,
"network": {
"type": "user",
"port_forward": {"2222": 22} # SSH port forwarding
},
"resources": {
"memory": "2G",
"cpus": 2,
"disk_size": "10G"
},
"debian": {
"release": debian_release,
"architecture": architecture,
"boot_timeout": 300
}
}
def run_vm_test_suite(image_path: str,
debian_release: str = "trixie",
architecture: str = "amd64") -> List[Dict[str, Any]]:
"""Run a complete VM test suite."""
# Create test configuration
vm_config = create_test_vm_config(image_path, debian_release, architecture)
# Create tester
tester = VMTester({})
try:
# Create test VM
vm = tester.create_test_vm(image_path, vm_config)
# Run tests
results = tester.run_all_tests()
return results
finally:
# Cleanup
tester.cleanup()
if __name__ == "__main__":
# Example usage
test_image = "/tmp/test-image.qcow2"
if os.path.exists(test_image):
print("Running VM test suite...")
results = run_vm_test_suite(test_image)
for result in results:
status = "PASS" if result["success"] else "FAIL"
print(f"{status}: {result['test_name']} - {result['vm_name']}")
if not result["success"]:
print(f" Error: {result.get('error', 'Unknown error')}")
else:
print(f"Test image not found: {test_image}")
print("Create a test image first or update the path")