commit eb00a5472f31a5969f1ce2f152b1ae7925c74c49 Author: Mahesh Kommareddi Date: Sat Aug 30 18:55:20 2025 -0400 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2980855 --- /dev/null +++ b/.gitignore @@ -0,0 +1,380 @@ +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these files may be visible to others. +*.azurePubxml + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment the next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +CConversionReportFiles/ + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio 6 auto-generated project file (contains which files were open etc.) +*.vbp + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files +*.ncb +*.aps + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +*.sln.iml + +# VM Configuration files (user data) +vm-configs/ + +# QEMU disk images (user data) +*.qcow2 +*.raw +*.vmdk +*.vdi +*.vhd +*.vhdx + +# Temporary files +*.tmp +*.temp diff --git a/QUICKSTART.md b/QUICKSTART.md new file mode 100644 index 0000000..44dffd3 --- /dev/null +++ b/QUICKSTART.md @@ -0,0 +1,95 @@ +# Quick Start Guide + +## Prerequisites + +1. **Install .NET 8.0 SDK** + - Download from: https://dotnet.microsoft.com/download + - Verify installation: `dotnet --version` + +2. **Install QEMU** + - **Windows**: Download from https://qemu.weilnetz.de/ + - **Linux**: `sudo apt install qemu-system-x86` (Ubuntu/Debian) + - **macOS**: `brew install qemu` + +3. **For KVM acceleration (Linux only)**: + ```bash + sudo apt install qemu-kvm libvirt-daemon-system libvirt-clients bridge-utils + sudo usermod -aG kvm $USER + sudo usermod -aG libvirt $USER + ``` + +## Quick Start + +1. **Clone and build**: + ```bash + git clone + cd skystack + dotnet build + ``` + +2. **Run the application**: + ```bash + dotnet run --project QemuVmManager.Console + ``` + +3. **Create your first VM**: + ``` + qemu-vm> create my-first-vm + Enter VM name: my-first-vm + Description (optional): My first VM + CPU cores (2): 2 + CPU model (qemu64): qemu64 + Memory size in MB (2048): 2048 + Disk path: /path/to/your/disk.qcow2 + Disk size in GB (10): 10 + Disk format (qcow2): qcow2 + Disk interface (virtio): virtio + Network bridge (virbr0): virbr0 + Display type (gtk): gtk + VGA type (virtio): virtio + ``` + +4. **Start the VM**: + ``` + qemu-vm> start my-first-vm + ``` + +5. **Check status**: + ``` + qemu-vm> status my-first-vm + ``` + +## Common Commands + +- `list` - Show all VMs +- `start ` - Start a VM +- `stop ` - Stop a VM +- `pause ` - Pause a VM +- `resume ` - Resume a VM +- `delete ` - Delete a VM +- `help` - Show all commands + +## Troubleshooting + +### QEMU not found +- Ensure QEMU is installed and in your PATH +- Windows: Add QEMU installation directory to PATH + +### Permission denied (Linux) +- Add your user to kvm and libvirt groups +- Restart your session after adding groups + +### Network bridge not found +- Create the bridge: `sudo virsh net-start default` +- Or use user networking: change bridge to "user" in VM config + +### Build issues +- Ensure .NET 8.0 SDK is installed +- Run `dotnet --version` to verify +- Try `dotnet restore` before building + +## Next Steps + +1. Read the full [README.md](README.md) for detailed documentation +2. Check the [examples/](examples/) directory for sample configurations +3. Explore advanced features like SPICE remote desktop and shared folders diff --git a/QemuVmManager.Console/Program.cs b/QemuVmManager.Console/Program.cs new file mode 100644 index 0000000..d118955 --- /dev/null +++ b/QemuVmManager.Console/Program.cs @@ -0,0 +1,904 @@ +using QemuVmManager.Services; +using QemuVmManager.Models; + +namespace QemuVmManager.Console; + +class Program +{ + private static VmManagementService _vmService = null!; + + static async Task Main(string[] args) + { + try + { + _vmService = new VmManagementService(); + + System.Console.WriteLine("=== QEMU VM Manager ==="); + System.Console.WriteLine("Type 'help' for available commands"); + System.Console.WriteLine(); + + await RunInteractiveMode(); + } + catch (Exception ex) + { + System.Console.WriteLine($"Error: {ex.Message}"); + System.Console.WriteLine("Press any key to exit..."); + System.Console.ReadKey(); + } + } + + static async Task RunInteractiveMode() + { + while (true) + { + try + { + System.Console.Write("qemu-vm> "); + var input = System.Console.ReadLine()?.Trim(); + + if (string.IsNullOrEmpty(input)) + continue; + + var parts = input.Split(' ', StringSplitOptions.RemoveEmptyEntries); + var command = parts[0].ToLower(); + var arguments = parts.Skip(1).ToArray(); + + switch (command) + { + case "help": + ShowHelp(); + break; + case "list": + await ListVms(); + break; + case "create": + await CreateVm(arguments); + break; + case "start": + await StartVm(arguments); + break; + case "stop": + await StopVm(arguments); + break; + case "pause": + await PauseVm(arguments); + break; + case "resume": + await ResumeVm(arguments); + break; + case "delete": + await DeleteVm(arguments); + break; + case "clone": + await CloneVm(arguments); + break; + case "export": + await ExportVm(arguments); + break; + case "import": + await ImportVm(arguments); + break; + case "status": + await ShowVmStatus(arguments); + break; + case "config": + await ShowVmConfig(arguments); + break; + case "disk": + await ManageDisk(arguments); + break; + case "validate": + await ValidateVm(arguments); + break; + case "diagnose": + await DiagnoseSystem(); + break; + case "monitor": + await MonitorPerformance(arguments); + break; + case "metrics": + await ShowMetrics(arguments); + break; + case "exit": + case "quit": + System.Console.WriteLine("Goodbye!"); + return; + default: + System.Console.WriteLine($"Unknown command: {command}"); + System.Console.WriteLine("Type 'help' for available commands"); + break; + } + } + catch (Exception ex) + { + System.Console.WriteLine($"Error: {ex.Message}"); + } + + System.Console.WriteLine(); + } + } + + static void ShowHelp() + { + System.Console.WriteLine("Available commands:"); + System.Console.WriteLine(" list - List all VMs"); + System.Console.WriteLine(" create - Create a new VM (interactive)"); + System.Console.WriteLine(" start - Start a VM"); + System.Console.WriteLine(" stop [--force] - Stop a VM"); + System.Console.WriteLine(" pause - Pause a VM"); + System.Console.WriteLine(" resume - Resume a VM"); + System.Console.WriteLine(" delete - Delete a VM"); + System.Console.WriteLine(" clone - Clone a VM"); + System.Console.WriteLine(" export - Export VM configuration"); + System.Console.WriteLine(" import [name] - Import VM configuration"); + System.Console.WriteLine(" status [name] - Show VM status"); + System.Console.WriteLine(" config - Show VM configuration"); + System.Console.WriteLine(" disk [info|resize|convert] - Manage disk images"); + System.Console.WriteLine(" validate - Validate VM disk images"); + System.Console.WriteLine(" diagnose - Diagnose system and QEMU installation"); + System.Console.WriteLine(" monitor [start|stop|status] - Performance monitoring"); + System.Console.WriteLine(" metrics [current|history] - Show performance metrics"); + System.Console.WriteLine(" help - Show this help"); + System.Console.WriteLine(" exit/quit - Exit the application"); + } + + static Task ListVms() + { + var configs = _vmService.GetAllVmConfigurations().ToList(); + var statuses = _vmService.GetAllVmStatuses().ToDictionary(s => s.Name); + + if (configs.Count == 0) + { + System.Console.WriteLine("No VMs configured."); + return Task.CompletedTask; + } + + System.Console.WriteLine($"{"Name",-20} {"Status",-10} {"CPU",-8} {"Memory",-10} {"Description"}"); + System.Console.WriteLine(new string('-', 80)); + + foreach (var config in configs) + { + var status = statuses.GetValueOrDefault(config.Name); + var statusText = status?.State.ToString() ?? "Unknown"; + var cpuText = $"{config.Cpu.Cores} cores"; + var memoryText = $"{config.Memory.Size}{config.Memory.Unit}"; + + System.Console.WriteLine($"{config.Name,-20} {statusText,-10} {cpuText,-8} {memoryText,-10} {config.Description}"); + } + + return Task.CompletedTask; + } + + static async Task CreateVm(string[] arguments) + { + string vmName; + if (arguments.Length > 0) + { + vmName = arguments[0]; + } + else + { + System.Console.Write("Enter VM name: "); + vmName = System.Console.ReadLine()?.Trim() ?? ""; + } + + if (string.IsNullOrEmpty(vmName)) + { + System.Console.WriteLine("VM name cannot be empty."); + return; + } + + var config = new VmConfiguration + { + Name = vmName, + Description = GetUserInput("Description (optional): "), + Cpu = new CpuConfiguration + { + Cores = int.Parse(GetUserInput("CPU cores (2): ", "2")), + Model = GetUserInput("CPU model (qemu64): ", "qemu64") + }, + Memory = new MemoryConfiguration + { + Size = long.Parse(GetUserInput("Memory size in MB (2048): ", "2048")), + Unit = "M" + }, + Storage = new StorageConfiguration + { + Disks = new List + { + new DiskConfiguration + { + Path = GetUserInput($"Disk path (vm-disks/{vmName}.qcow2): ", $"vm-disks/{vmName}.qcow2"), + Size = long.Parse(GetUserInput("Disk size in GB (10): ", "10")), + Format = GetUserInput("Disk format (qcow2): ", "qcow2"), + Interface = GetUserInput("Disk interface (virtio): ", "virtio"), + IsBoot = true + } + } + }, + Network = new NetworkConfiguration + { + Interfaces = new List + { + new NetworkInterfaceConfiguration + { + Type = "bridge", + Model = "virtio-net-pci", + Bridge = GetUserInput("Network bridge (virbr0): ", "virbr0") + } + } + }, + Display = new DisplayConfiguration + { + Type = GetUserInput("Display type (gtk): ", "gtk"), + Vga = GetUserInput("VGA type (virtio): ", "virtio") + } + }; + + await _vmService.CreateVmAsync(config); + System.Console.WriteLine($"VM '{vmName}' created successfully."); + } + + static async Task StartVm(string[] arguments) + { + if (arguments.Length == 0) + { + System.Console.WriteLine("Usage: start "); + return; + } + + var vmName = arguments[0]; + var success = await _vmService.StartVmAsync(vmName); + + if (success) + System.Console.WriteLine($"VM '{vmName}' started successfully."); + else + System.Console.WriteLine($"Failed to start VM '{vmName}'."); + } + + static async Task StopVm(string[] arguments) + { + if (arguments.Length == 0) + { + System.Console.WriteLine("Usage: stop [--force]"); + return; + } + + var vmName = arguments[0]; + var force = arguments.Contains("--force"); + + var success = await _vmService.StopVmAsync(vmName, force); + + if (success) + System.Console.WriteLine($"VM '{vmName}' stopped successfully."); + else + System.Console.WriteLine($"Failed to stop VM '{vmName}'."); + } + + static async Task PauseVm(string[] arguments) + { + if (arguments.Length == 0) + { + System.Console.WriteLine("Usage: pause "); + return; + } + + var vmName = arguments[0]; + var success = await _vmService.PauseVmAsync(vmName); + + if (success) + System.Console.WriteLine($"VM '{vmName}' paused successfully."); + else + System.Console.WriteLine($"Failed to pause VM '{vmName}'."); + } + + static async Task ResumeVm(string[] arguments) + { + if (arguments.Length == 0) + { + System.Console.WriteLine("Usage: resume "); + return; + } + + var vmName = arguments[0]; + var success = await _vmService.ResumeVmAsync(vmName); + + if (success) + System.Console.WriteLine($"VM '{vmName}' resumed successfully."); + else + System.Console.WriteLine($"Failed to resume VM '{vmName}'."); + } + + static async Task DeleteVm(string[] arguments) + { + if (arguments.Length == 0) + { + System.Console.WriteLine("Usage: delete "); + return; + } + + var vmName = arguments[0]; + + System.Console.Write($"Are you sure you want to delete VM '{vmName}'? (y/N): "); + var confirm = System.Console.ReadLine()?.Trim().ToLower(); + + if (confirm == "y" || confirm == "yes") + { + await _vmService.DeleteVmAsync(vmName); + System.Console.WriteLine($"VM '{vmName}' deleted successfully."); + } + else + { + System.Console.WriteLine("Deletion cancelled."); + } + } + + static async Task CloneVm(string[] arguments) + { + if (arguments.Length < 2) + { + System.Console.WriteLine("Usage: clone "); + return; + } + + var sourceVm = arguments[0]; + var targetVm = arguments[1]; + + var clonedConfig = await _vmService.CloneVmAsync(sourceVm, targetVm); + System.Console.WriteLine($"VM '{sourceVm}' cloned to '{targetVm}' successfully."); + } + + static async Task ExportVm(string[] arguments) + { + if (arguments.Length < 2) + { + System.Console.WriteLine("Usage: export "); + return; + } + + var vmName = arguments[0]; + var exportPath = arguments[1]; + + var exportedPath = await _vmService.ExportVmConfigurationAsync(vmName, exportPath); + System.Console.WriteLine($"VM '{vmName}' exported to '{exportedPath}' successfully."); + } + + static async Task ImportVm(string[] arguments) + { + if (arguments.Length == 0) + { + System.Console.WriteLine("Usage: import [new-name]"); + return; + } + + var importPath = arguments[0]; + var newName = arguments.Length > 1 ? arguments[1] : null; + + var importedConfig = await _vmService.ImportVmConfigurationAsync(importPath, newName); + System.Console.WriteLine($"VM '{importedConfig.Name}' imported successfully."); + } + + static Task ShowVmStatus(string[] arguments) + { + if (arguments.Length == 0) + { + // Show all VM statuses + var statuses = _vmService.GetAllVmStatuses(); + + if (!statuses.Any()) + { + System.Console.WriteLine("No VMs found."); + return Task.CompletedTask; + } + + System.Console.WriteLine($"{"Name",-20} {"Status",-10} {"PID",-8} {"Started",-20} {"Error"}"); + System.Console.WriteLine(new string('-', 80)); + + foreach (var status in statuses) + { + var pidText = status.ProcessId > 0 ? status.ProcessId.ToString() : "-"; + var startedText = status.StartedAt?.ToString("yyyy-MM-dd HH:mm:ss") ?? "-"; + var errorText = status.ErrorMessage ?? ""; + + System.Console.WriteLine($"{status.Name,-20} {status.State,-10} {pidText,-8} {startedText,-20} {errorText}"); + } + } + else + { + // Show specific VM status + var vmName = arguments[0]; + var status = _vmService.GetVmStatus(vmName); + + if (status == null) + { + System.Console.WriteLine($"VM '{vmName}' not found."); + return Task.CompletedTask; + } + + System.Console.WriteLine($"VM: {status.Name}"); + System.Console.WriteLine($"Status: {status.State}"); + System.Console.WriteLine($"Process ID: {status.ProcessId}"); + System.Console.WriteLine($"Started: {status.StartedAt}"); + System.Console.WriteLine($"Stopped: {status.StoppedAt}"); + + if (status.ResourceUsage != null) + { + System.Console.WriteLine($"CPU Usage: {status.ResourceUsage.CpuUsage:F1}%"); + System.Console.WriteLine($"Memory Usage: {status.ResourceUsage.MemoryUsage} MB"); + } + + if (!string.IsNullOrEmpty(status.ErrorMessage)) + { + System.Console.WriteLine($"Error: {status.ErrorMessage}"); + } + } + + return Task.CompletedTask; + } + + static Task ShowVmConfig(string[] arguments) + { + if (arguments.Length == 0) + { + System.Console.WriteLine("Usage: config "); + return Task.CompletedTask; + } + + var vmName = arguments[0]; + var config = _vmService.GetVmConfiguration(vmName); + + if (config == null) + { + System.Console.WriteLine($"VM '{vmName}' not found."); + return Task.CompletedTask; + } + + System.Console.WriteLine($"VM Configuration: {config.Name}"); + System.Console.WriteLine($"Description: {config.Description}"); + System.Console.WriteLine($"Created: {config.Created}"); + System.Console.WriteLine($"Modified: {config.LastModified}"); + System.Console.WriteLine(); + + System.Console.WriteLine("CPU Configuration:"); + System.Console.WriteLine($" Cores: {config.Cpu.Cores}"); + System.Console.WriteLine($" Model: {config.Cpu.Model}"); + System.Console.WriteLine($" KVM: {config.Cpu.EnableKvm}"); + System.Console.WriteLine(); + + System.Console.WriteLine("Memory Configuration:"); + System.Console.WriteLine($" Size: {config.Memory.Size}{config.Memory.Unit}"); + System.Console.WriteLine(); + + System.Console.WriteLine("Storage Configuration:"); + foreach (var disk in config.Storage.Disks) + { + System.Console.WriteLine($" Disk: {disk.Path}"); + System.Console.WriteLine($" Size: {disk.Size} GB"); + System.Console.WriteLine($" Format: {disk.Format}"); + System.Console.WriteLine($" Interface: {disk.Interface}"); + System.Console.WriteLine($" Boot: {disk.IsBoot}"); + } + + if (!string.IsNullOrEmpty(config.Storage.Cdrom)) + { + System.Console.WriteLine($" CD-ROM: {config.Storage.Cdrom}"); + } + System.Console.WriteLine(); + + System.Console.WriteLine("Network Configuration:"); + foreach (var nic in config.Network.Interfaces) + { + System.Console.WriteLine($" Interface: {nic.Model}"); + System.Console.WriteLine($" Type: {nic.Type}"); + System.Console.WriteLine($" Bridge: {nic.Bridge}"); + if (!string.IsNullOrEmpty(nic.Mac)) + { + System.Console.WriteLine($" MAC: {nic.Mac}"); + } + } + System.Console.WriteLine(); + + System.Console.WriteLine("Display Configuration:"); + System.Console.WriteLine($" Type: {config.Display.Type}"); + System.Console.WriteLine($" VGA: {config.Display.Vga}"); + System.Console.WriteLine($" SPICE: {config.Display.EnableSpice}"); + if (config.Display.EnableSpice) + { + System.Console.WriteLine($" SPICE Port: {config.Display.SpicePort}"); + } + + return Task.CompletedTask; + } + + static async Task ManageDisk(string[] arguments) + { + if (arguments.Length < 1) + { + System.Console.WriteLine("Usage: disk [info|resize|convert]"); + return; + } + + var vmName = arguments[0]; + var action = arguments.Length > 1 ? arguments[1].ToLower() : "info"; + + switch (action) + { + case "info": + await ShowDiskInfo(vmName); + break; + case "resize": + await ResizeDisk(vmName, arguments.Skip(2).ToArray()); + break; + case "convert": + await ConvertDisk(vmName, arguments.Skip(2).ToArray()); + break; + default: + System.Console.WriteLine($"Unknown disk action: {action}"); + System.Console.WriteLine("Available actions: info, resize, convert"); + break; + } + } + + static async Task ShowDiskInfo(string vmName) + { + try + { + var config = _vmService.GetVmConfiguration(vmName); + if (config == null) + { + System.Console.WriteLine($"VM '{vmName}' not found."); + return; + } + + System.Console.WriteLine($"Disk Information for VM: {vmName}"); + System.Console.WriteLine(new string('-', 50)); + + for (int i = 0; i < config.Storage.Disks.Count; i++) + { + var disk = config.Storage.Disks[i]; + System.Console.WriteLine($"Disk {i}:"); + System.Console.WriteLine($" Path: {disk.Path}"); + System.Console.WriteLine($" Format: {disk.Format}"); + System.Console.WriteLine($" Size: {disk.Size} GB"); + System.Console.WriteLine($" Interface: {disk.Interface}"); + System.Console.WriteLine($" Boot: {disk.IsBoot}"); + + var diskInfo = await _vmService.GetDiskInfoAsync(vmName, i); + if (diskInfo.Exists) + { + System.Console.WriteLine($" Virtual Size: {diskInfo.VirtualSize}"); + System.Console.WriteLine($" Disk Size: {diskInfo.DiskSize}"); + System.Console.WriteLine($" Format: {diskInfo.Format}"); + } + else + { + System.Console.WriteLine(" Status: Not found"); + } + System.Console.WriteLine(); + } + } + catch (Exception ex) + { + System.Console.WriteLine($"Error: {ex.Message}"); + } + } + + static async Task ResizeDisk(string vmName, string[] arguments) + { + if (arguments.Length < 2) + { + System.Console.WriteLine("Usage: disk resize "); + return; + } + + try + { + var diskIndex = int.Parse(arguments[0]); + var newSizeGB = long.Parse(arguments[1]); + + var success = await _vmService.ResizeDiskAsync(vmName, diskIndex, newSizeGB); + if (success) + { + System.Console.WriteLine($"Disk {diskIndex} resized to {newSizeGB} GB successfully."); + } + else + { + System.Console.WriteLine("Failed to resize disk."); + } + } + catch (Exception ex) + { + System.Console.WriteLine($"Error: {ex.Message}"); + } + } + + static async Task ConvertDisk(string vmName, string[] arguments) + { + if (arguments.Length < 2) + { + System.Console.WriteLine("Usage: disk convert "); + System.Console.WriteLine("Available formats: qcow2, raw, vmdk, vdi, vhd"); + return; + } + + try + { + var diskIndex = int.Parse(arguments[0]); + var newFormat = arguments[1]; + + var success = await _vmService.ConvertDiskAsync(vmName, diskIndex, newFormat); + if (success) + { + System.Console.WriteLine($"Disk {diskIndex} converted to {newFormat} format successfully."); + } + else + { + System.Console.WriteLine("Failed to convert disk."); + } + } + catch (Exception ex) + { + System.Console.WriteLine($"Error: {ex.Message}"); + } + } + + static async Task ValidateVm(string[] arguments) + { + if (arguments.Length == 0) + { + System.Console.WriteLine("Usage: validate "); + return; + } + + var vmName = arguments[0]; + + try + { + var isValid = _vmService.ValidateDiskImages(vmName); + if (isValid) + { + System.Console.WriteLine($"VM '{vmName}' disk images are valid."); + } + else + { + System.Console.WriteLine($"VM '{vmName}' has invalid disk images."); + System.Console.WriteLine("Use 'disk info' to check disk status."); + } + } + catch (Exception ex) + { + System.Console.WriteLine($"Error: {ex.Message}"); + } + } + + static async Task DiagnoseSystem() + { + System.Console.WriteLine("=== System Diagnosis ==="); + System.Console.WriteLine(); + + // Check .NET version + System.Console.WriteLine("1. .NET Runtime:"); + System.Console.WriteLine($" Version: {Environment.Version}"); + System.Console.WriteLine($" OS: {Environment.OSVersion}"); + System.Console.WriteLine($" Architecture: {Environment.GetEnvironmentVariable("PROCESSOR_ARCHITECTURE")}"); + System.Console.WriteLine(); + + // Check QEMU installation and virtualization + System.Console.WriteLine("2. QEMU Installation:"); + try + { + var processManager = new QemuVmManager.Core.QemuProcessManager(); + var isInstalled = processManager.IsQemuInstalled(); + var version = processManager.GetQemuVersion(); + var accelerators = processManager.GetQemuAccelerators(); + var virtualizationEnabled = processManager.IsVirtualizationEnabled(); + var availableVirtualization = processManager.GetAvailableVirtualization(); + + System.Console.WriteLine($" Installed: {(isInstalled ? "Yes" : "No")}"); + System.Console.WriteLine($" Version: {version}"); + System.Console.WriteLine($" Available Accelerators:"); + foreach (var line in accelerators.Split('\n', StringSplitOptions.RemoveEmptyEntries)) + { + System.Console.WriteLine($" {line.Trim()}"); + } + System.Console.WriteLine($" Virtualization Enabled in BIOS: {(virtualizationEnabled ? "Yes" : "No")}"); + System.Console.WriteLine($" Available Virtualization: {availableVirtualization}"); + + if (!isInstalled) + { + System.Console.WriteLine(" ❌ QEMU is not installed or not found in PATH"); + System.Console.WriteLine(" Please install QEMU and ensure it's available in your system PATH"); + System.Console.WriteLine(" Windows: Download from https://qemu.weilnetz.de/"); + System.Console.WriteLine(" Linux: sudo apt-get install qemu-system-x86_64"); + System.Console.WriteLine(" macOS: brew install qemu"); + } + else if (!virtualizationEnabled) + { + System.Console.WriteLine(" ⚠️ Virtualization is not enabled in BIOS/UEFI"); + System.Console.WriteLine(" Please enable VT-x (Intel) or AMD-V (AMD) in your BIOS settings"); + System.Console.WriteLine(" This will significantly improve VM performance"); + } + else if (availableVirtualization == VirtualizationType.TCG) + { + System.Console.WriteLine(" ⚠️ Only software emulation (TCG) is available"); + System.Console.WriteLine(" Hardware virtualization is not available or not properly configured"); + System.Console.WriteLine(" This may be due to:"); + System.Console.WriteLine(" - QEMU build not supporting hardware acceleration"); + System.Console.WriteLine(" - Missing virtualization drivers"); + System.Console.WriteLine(" - Hyper-V or other virtualization software conflicts"); + } + else + { + System.Console.WriteLine($" ✅ Hardware virtualization ({availableVirtualization}) is available"); + } + } + catch (Exception ex) + { + System.Console.WriteLine($" ❌ Error checking QEMU: {ex.Message}"); + } + System.Console.WriteLine(); + + // Check disk manager + System.Console.WriteLine("3. Disk Manager:"); + try + { + var diskManager = new QemuVmManager.Core.DiskManager(); + System.Console.WriteLine(" ✅ Disk manager initialized successfully"); + } + catch (Exception ex) + { + System.Console.WriteLine($" ❌ Error initializing disk manager: {ex.Message}"); + } + System.Console.WriteLine(); + + // Check VM configurations + System.Console.WriteLine("4. VM Configurations:"); + try + { + var configs = _vmService.GetAllVmConfigurations().ToList(); + System.Console.WriteLine($" Found {configs.Count} VM configuration(s)"); + + foreach (var config in configs) + { + System.Console.WriteLine($" - {config.Name}: {config.Description}"); + + // Check disk images + foreach (var disk in config.Storage.Disks) + { + var exists = File.Exists(disk.Path); + System.Console.WriteLine($" Disk: {disk.Path} - {(exists ? "✅ Exists" : "❌ Missing")}"); + } + } + } + catch (Exception ex) + { + System.Console.WriteLine($" ❌ Error checking VM configurations: {ex.Message}"); + } + System.Console.WriteLine(); + + // Check running VMs + System.Console.WriteLine("5. Running VMs:"); + try + { + var statuses = _vmService.GetAllVmStatuses().ToList(); + var runningVms = statuses.Where(s => s.State == QemuVmManager.Models.VmState.Running).ToList(); + + System.Console.WriteLine($" Running: {runningVms.Count}"); + foreach (var vm in runningVms) + { + System.Console.WriteLine($" - {vm.Name} (PID: {vm.ProcessId})"); + } + } + catch (Exception ex) + { + System.Console.WriteLine($" ❌ Error checking running VMs: {ex.Message}"); + } + System.Console.WriteLine(); + + System.Console.WriteLine("=== Diagnosis Complete ==="); + } + + static string GetUserInput(string prompt, string defaultValue = "") + { + System.Console.Write(prompt); + var input = System.Console.ReadLine()?.Trim(); + return string.IsNullOrEmpty(input) ? defaultValue : input; + } + + static async Task MonitorPerformance(string[] arguments) + { + if (arguments.Length == 0) + { + System.Console.WriteLine("Usage: monitor [start|stop|status]"); + return; + } + + var vmName = arguments[0]; + var action = arguments.Length > 1 ? arguments[1].ToLower() : "status"; + + try + { + switch (action) + { + case "start": + await _vmService.StartPerformanceMonitoringAsync(vmName); + System.Console.WriteLine($"Performance monitoring started for VM '{vmName}'"); + break; + case "stop": + _vmService.StopPerformanceMonitoring(vmName); + System.Console.WriteLine($"Performance monitoring stopped for VM '{vmName}'"); + break; + case "status": + default: + var isMonitoring = _vmService.IsPerformanceMonitoringActive(vmName); + System.Console.WriteLine($"Performance monitoring for VM '{vmName}': {(isMonitoring ? "Active" : "Inactive")}"); + break; + } + } + catch (Exception ex) + { + System.Console.WriteLine($"Error: {ex.Message}"); + } + } + + static async Task ShowMetrics(string[] arguments) + { + if (arguments.Length == 0) + { + System.Console.WriteLine("Usage: metrics [current|history]"); + return; + } + + var vmName = arguments[0]; + var type = arguments.Length > 1 ? arguments[1].ToLower() : "current"; + + try + { + switch (type) + { + case "current": + var currentMetrics = await _vmService.GetVmPerformanceMetricsAsync(vmName); + DisplayPerformanceMetrics(currentMetrics, "Current"); + break; + case "history": + var history = await _vmService.GetPerformanceHistoryAsync(vmName, 20); + System.Console.WriteLine($"Performance History for VM '{vmName}' (Last {history.Count} samples):"); + System.Console.WriteLine(); + + foreach (var metrics in history.TakeLast(10)) + { + DisplayPerformanceMetrics(metrics, metrics.Timestamp.ToString("HH:mm:ss")); + } + break; + default: + System.Console.WriteLine("Invalid metrics type. Use 'current' or 'history'"); + break; + } + } + catch (Exception ex) + { + System.Console.WriteLine($"Error: {ex.Message}"); + } + } + + static void DisplayPerformanceMetrics(VmPerformanceMetrics metrics, string label) + { + System.Console.WriteLine($"=== {label} Performance Metrics ==="); + System.Console.WriteLine($"Timestamp: {metrics.Timestamp:yyyy-MM-dd HH:mm:ss}"); + System.Console.WriteLine($"Process ID: {metrics.ProcessId}"); + System.Console.WriteLine(); + System.Console.WriteLine("CPU Usage:"); + System.Console.WriteLine($" VM CPU: {metrics.CpuUsagePercent:F2}%"); + System.Console.WriteLine($" System CPU: {metrics.SystemCpuUsagePercent:F2}%"); + System.Console.WriteLine(); + System.Console.WriteLine("Memory Usage:"); + System.Console.WriteLine($" Working Set: {metrics.MemoryUsageMB:N0} MB"); + System.Console.WriteLine($" Private Memory: {metrics.PrivateMemoryMB:N0} MB"); + System.Console.WriteLine($" Virtual Memory: {metrics.VirtualMemoryMB:N0} MB"); + System.Console.WriteLine(); + System.Console.WriteLine("Process Info:"); + System.Console.WriteLine($" Threads: {metrics.ThreadCount}"); + System.Console.WriteLine($" Handles: {metrics.HandleCount}"); + System.Console.WriteLine(); + } + } diff --git a/QemuVmManager.Console/QemuVmManager.Console.csproj b/QemuVmManager.Console/QemuVmManager.Console.csproj new file mode 100644 index 0000000..8379cbe --- /dev/null +++ b/QemuVmManager.Console/QemuVmManager.Console.csproj @@ -0,0 +1,15 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + diff --git a/QemuVmManager.Core/DiskManager.cs b/QemuVmManager.Core/DiskManager.cs new file mode 100644 index 0000000..1ff74eb --- /dev/null +++ b/QemuVmManager.Core/DiskManager.cs @@ -0,0 +1,312 @@ +using System.Diagnostics; +using QemuVmManager.Models; + +namespace QemuVmManager.Core; + +public class DiskManager +{ + private readonly string _diskDirectory; + + public DiskManager(string diskDirectory = "vm-disks") + { + _diskDirectory = diskDirectory; + Directory.CreateDirectory(_diskDirectory); + } + + public async Task CreateDiskImageAsync(DiskConfiguration diskConfig) + { + try + { + // Ensure the directory exists + var directory = Path.GetDirectoryName(diskConfig.Path); + if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) + { + Directory.CreateDirectory(directory); + } + + // Check if disk already exists + if (File.Exists(diskConfig.Path)) + { + return true; // Disk already exists + } + + // Create disk image using qemu-img + var sizeInBytes = diskConfig.Size * 1024 * 1024 * 1024; // Convert GB to bytes + var sizeInMB = diskConfig.Size * 1024; // Convert GB to MB for qemu-img + + var process = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = "qemu-img", + Arguments = $"create -f {diskConfig.Format} \"{diskConfig.Path}\" {sizeInMB}M", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + } + }; + + var started = process.Start(); + if (!started) + { + throw new InvalidOperationException("Failed to start qemu-img process"); + } + + await process.WaitForExitAsync(); + + if (process.ExitCode != 0) + { + var error = await process.StandardError.ReadToEndAsync(); + throw new InvalidOperationException($"Failed to create disk image: {error}"); + } + + return true; + } + catch (Exception ex) + { + throw new InvalidOperationException($"Failed to create disk image '{diskConfig.Path}': {ex.Message}", ex); + } + } + + public async Task CreateDiskImagesForVmAsync(VmConfiguration vmConfig) + { + try + { + foreach (var disk in vmConfig.Storage.Disks) + { + await CreateDiskImageAsync(disk); + } + return true; + } + catch (Exception ex) + { + throw new InvalidOperationException($"Failed to create disk images for VM '{vmConfig.Name}': {ex.Message}", ex); + } + } + + public bool ValidateDiskImage(string diskPath, string format) + { + try + { + if (!File.Exists(diskPath)) + { + return false; + } + + // Use qemu-img info to validate the disk + var process = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = "qemu-img", + Arguments = $"info -f {format} \"{diskPath}\"", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + } + }; + + var started = process.Start(); + if (!started) + { + return false; + } + + process.WaitForExit(); + return process.ExitCode == 0; + } + catch + { + return false; + } + } + + public async Task GetDiskInfoAsync(string diskPath, string format) + { + try + { + if (!File.Exists(diskPath)) + { + return new DiskInfo { Exists = false }; + } + + var process = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = "qemu-img", + Arguments = $"info -f {format} \"{diskPath}\"", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + } + }; + + var started = process.Start(); + if (!started) + { + return new DiskInfo { Exists = false }; + } + + await process.WaitForExitAsync(); + + if (process.ExitCode != 0) + { + return new DiskInfo { Exists = false }; + } + + var output = await process.StandardOutput.ReadToEndAsync(); + return ParseDiskInfo(output, diskPath); + } + catch + { + return new DiskInfo { Exists = false }; + } + } + + private DiskInfo ParseDiskInfo(string qemuImgOutput, string diskPath) + { + var info = new DiskInfo + { + Path = diskPath, + Exists = true + }; + + var lines = qemuImgOutput.Split('\n', StringSplitOptions.RemoveEmptyEntries); + foreach (var line in lines) + { + var trimmedLine = line.Trim(); + if (trimmedLine.StartsWith("virtual size:")) + { + var sizePart = trimmedLine.Substring("virtual size:".Length).Trim(); + info.VirtualSize = sizePart; + } + else if (trimmedLine.StartsWith("disk size:")) + { + var sizePart = trimmedLine.Substring("disk size:".Length).Trim(); + info.DiskSize = sizePart; + } + else if (trimmedLine.StartsWith("format:")) + { + var formatPart = trimmedLine.Substring("format:".Length).Trim(); + info.Format = formatPart; + } + } + + return info; + } + + public async Task ResizeDiskAsync(string diskPath, string format, long newSizeGB) + { + try + { + if (!File.Exists(diskPath)) + { + throw new FileNotFoundException($"Disk image not found: {diskPath}"); + } + + var newSizeMB = newSizeGB * 1024; // Convert GB to MB + + var process = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = "qemu-img", + Arguments = $"resize -f {format} \"{diskPath}\" {newSizeMB}M", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + } + }; + + var started = process.Start(); + if (!started) + { + throw new InvalidOperationException("Failed to start qemu-img process"); + } + + await process.WaitForExitAsync(); + + if (process.ExitCode != 0) + { + var error = await process.StandardError.ReadToEndAsync(); + throw new InvalidOperationException($"Failed to resize disk image: {error}"); + } + + return true; + } + catch (Exception ex) + { + throw new InvalidOperationException($"Failed to resize disk image '{diskPath}': {ex.Message}", ex); + } + } + + public async Task ConvertDiskAsync(string sourcePath, string sourceFormat, string targetPath, string targetFormat) + { + try + { + var process = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = "qemu-img", + Arguments = $"convert -f {sourceFormat} -O {targetFormat} \"{sourcePath}\" \"{targetPath}\"", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + } + }; + + var started = process.Start(); + if (!started) + { + throw new InvalidOperationException("Failed to start qemu-img process"); + } + + await process.WaitForExitAsync(); + + if (process.ExitCode != 0) + { + var error = await process.StandardError.ReadToEndAsync(); + throw new InvalidOperationException($"Failed to convert disk image: {error}"); + } + + return true; + } + catch (Exception ex) + { + throw new InvalidOperationException($"Failed to convert disk image: {ex.Message}", ex); + } + } + + public bool DeleteDiskImage(string diskPath) + { + try + { + if (File.Exists(diskPath)) + { + File.Delete(diskPath); + return true; + } + return false; + } + catch + { + return false; + } + } +} + +public class DiskInfo +{ + public string Path { get; set; } = string.Empty; + public bool Exists { get; set; } + public string? VirtualSize { get; set; } + public string? DiskSize { get; set; } + public string? Format { get; set; } +} diff --git a/QemuVmManager.Core/QemuCommandBuilder.cs b/QemuVmManager.Core/QemuCommandBuilder.cs new file mode 100644 index 0000000..a82ca62 --- /dev/null +++ b/QemuVmManager.Core/QemuCommandBuilder.cs @@ -0,0 +1,356 @@ +using QemuVmManager.Models; +using System.Diagnostics; + +namespace QemuVmManager.Core; + +public class QemuCommandBuilder +{ + private readonly VmConfiguration _config; + private readonly List _arguments = new(); + private readonly VirtualizationType _virtualizationType; + + public QemuCommandBuilder(VmConfiguration config, VirtualizationType virtualizationType = VirtualizationType.TCG) + { + _config = config; + _virtualizationType = virtualizationType; + } + + public string BuildCommand() + { + _arguments.Clear(); + + // Basic QEMU command + _arguments.Add("qemu-system-x86_64"); + + // Machine and CPU configuration + AddMachineConfiguration(); + AddCpuConfiguration(); + AddMemoryConfiguration(); + + // Storage configuration + AddStorageConfiguration(); + + // Network configuration + AddNetworkConfiguration(); + + // Display configuration + AddDisplayConfiguration(); + + // Boot configuration + AddBootConfiguration(); + + // Advanced features + AddAdvancedConfiguration(); + + // Extra arguments + _arguments.AddRange(_config.Advanced.ExtraArgs); + + // Handle CPU model based on virtualization type + if (_virtualizationType == VirtualizationType.TCG || _virtualizationType == VirtualizationType.HyperV) + { + // Replace 'host' CPU model with 'qemu64' for TCG compatibility + for (int i = 0; i < _arguments.Count; i++) + { + if (_arguments[i] == "-cpu" && i + 1 < _arguments.Count && _arguments[i + 1] == "host") + { + _arguments[i + 1] = "qemu64"; + } + } + } + + // Remove -enable-kvm if not using KVM + if (_virtualizationType != VirtualizationType.KVM) + { + _arguments.RemoveAll(arg => arg == "-enable-kvm"); + } + + // Add WHPX-specific configurations + if (_virtualizationType == VirtualizationType.HyperV) + { + AddWHPXSpecificConfiguration(); + } + + return string.Join(" ", _arguments); + } + + private void AddMachineConfiguration() + { + _arguments.Add("-machine"); + + switch (_virtualizationType) + { + case VirtualizationType.KVM: + _arguments.Add("type=q35,accel=kvm:tcg"); + break; + case VirtualizationType.HyperV: + // Use WHPX hardware acceleration + _arguments.Add("type=pc-i440fx-10.1,accel=whpx:tcg,kernel-irqchip=off"); + break; + case VirtualizationType.HAXM: + _arguments.Add("type=q35,accel=hax:tcg"); + break; + case VirtualizationType.HVF: + _arguments.Add("type=q35,accel=hvf:tcg"); + break; + case VirtualizationType.TCG: + default: + _arguments.Add("type=q35,accel=tcg"); + break; + } + } + + private void AddCpuConfiguration() + { + var cpu = _config.Cpu; + + _arguments.Add("-cpu"); + _arguments.Add(cpu.Model); + + // For WHPX, use simpler CPU configuration to avoid exit code 4 + if (_virtualizationType == VirtualizationType.HyperV) + { + _arguments.Add("-smp"); + _arguments.Add($"{Math.Min(cpu.Cores, 2)},cores={Math.Min(cpu.Cores, 2)},sockets=1,threads=1"); + } + else + { + _arguments.Add("-smp"); + _arguments.Add($"{cpu.Cores},cores={cpu.Cores},sockets={cpu.Sockets},threads={cpu.Threads}"); + } + } + + private void AddMemoryConfiguration() + { + var memory = _config.Memory; + + // For WHPX, use smaller memory allocation to avoid exit code 4 + if (_virtualizationType == VirtualizationType.HyperV) + { + var whpxMemorySize = Math.Min(memory.Size, 4096); // Limit to 4GB for WHPX + _arguments.Add("-m"); + _arguments.Add($"{whpxMemorySize}{memory.Unit}"); + } + else + { + _arguments.Add("-m"); + _arguments.Add($"{memory.Size}{memory.Unit}"); + } + } + + private void AddStorageConfiguration() + { + var storage = _config.Storage; + + // Add disks + for (int i = 0; i < storage.Disks.Count; i++) + { + var disk = storage.Disks[i]; + var driveLetter = (char)('a' + i); + + _arguments.Add("-drive"); + var driveArgs = $"file={disk.Path.Replace('\\', '/')},format={disk.Format},cache={disk.Cache},id=drive{i},if=none"; + _arguments.Add(driveArgs); + + // For WHPX, only use the first disk on IDE to avoid conflicts + if (_virtualizationType == VirtualizationType.HyperV && i > 0) + { + Console.WriteLine($"Warning: Additional disk {i} disabled for WHPX compatibility to avoid IDE conflicts. Disk: {disk.Path}"); + continue; + } + + // Add device + _arguments.Add("-device"); + if (disk.Interface == "virtio" && _virtualizationType != VirtualizationType.HyperV) + { + // Use virtio for better performance, but avoid with WHPX due to MSI issues + _arguments.Add($"virtio-blk-pci,drive=drive{i}"); + } + else + { + // Use IDE for WHPX compatibility + _arguments.Add($"ide-hd,drive=drive{i}"); + } + } + + // Add CD-ROM if specified + if (!string.IsNullOrEmpty(storage.Cdrom)) + { + if (_virtualizationType == VirtualizationType.HyperV) + { + // For WHPX, use the second IDE controller for CD-ROM + _arguments.Add("-drive"); + _arguments.Add($"file={storage.Cdrom.Replace('\\', '/')},media=cdrom,if=ide,index=1"); + } + else + { + // Use standard CD-ROM for other virtualization types + _arguments.Add("-cdrom"); + _arguments.Add(storage.Cdrom.Replace('\\', '/')); + } + } + } + + private void AddNetworkConfiguration() + { + var network = _config.Network; + + for (int i = 0; i < network.Interfaces.Count; i++) + { + var nic = network.Interfaces[i]; + + // Use user network as fallback since bridge might not be available + _arguments.Add("-netdev"); + var netdevArgs = $"user,id=net{i}"; + _arguments.Add(netdevArgs); + + _arguments.Add("-device"); + var deviceArgs = ""; + + // Use different network models based on virtualization type + if (nic.Model == "virtio-net-pci" && _virtualizationType == VirtualizationType.HyperV) + { + // Use e1000 for WHPX compatibility to avoid MSI issues + deviceArgs = $"e1000,netdev=net{i}"; + } + else + { + deviceArgs = $"{nic.Model},netdev=net{i}"; + } + + if (!string.IsNullOrEmpty(nic.Mac)) + { + deviceArgs += $",mac={nic.Mac}"; + } + _arguments.Add(deviceArgs); + } + } + + private void AddDisplayConfiguration() + { + var display = _config.Display; + + _arguments.Add("-display"); + _arguments.Add(display.Type); + + _arguments.Add("-vga"); + _arguments.Add(display.Vga); + + if (display.EnableSpice) + { + _arguments.Add("-spice"); + _arguments.Add($"port={display.SpicePort},addr=127.0.0.1,disable-ticketing=on"); + } + } + + private void AddBootConfiguration() + { + var boot = _config.Boot; + + if (boot.Order.Count > 0) + { + _arguments.Add("-boot"); + _arguments.Add($"order={string.Join("", boot.Order)}"); + } + + if (!string.IsNullOrEmpty(boot.Kernel)) + { + _arguments.Add("-kernel"); + _arguments.Add(boot.Kernel); + } + + if (!string.IsNullOrEmpty(boot.Initrd)) + { + _arguments.Add("-initrd"); + _arguments.Add(boot.Initrd); + } + + if (!string.IsNullOrEmpty(boot.Cmdline)) + { + _arguments.Add("-append"); + _arguments.Add(boot.Cmdline); + } + } + + private void AddAdvancedConfiguration() + { + var advanced = _config.Advanced; + + if (advanced.EnableAudio) + { + _arguments.Add("-device"); + _arguments.Add("intel-hda"); + _arguments.Add("-device"); + _arguments.Add("hda-duplex"); + } + + if (advanced.EnableUsb) + { + _arguments.Add("-usb"); + _arguments.Add("-device"); + _arguments.Add("usb-tablet"); + } + + // Disable virtio devices for WHPX to avoid MSI issues + if (advanced.EnableBalloon && _virtualizationType != VirtualizationType.HyperV) + { + _arguments.Add("-device"); + _arguments.Add("virtio-balloon-pci"); + } + + if (advanced.EnableVirtioRng && _virtualizationType != VirtualizationType.HyperV) + { + _arguments.Add("-device"); + _arguments.Add("virtio-rng-pci"); + } + + if (advanced.EnableVirtioFs && _virtualizationType != VirtualizationType.HyperV) + { + _arguments.Add("-device"); + _arguments.Add("virtio-fs-pci"); + } + + // Add shared folders (disabled for compatibility) + // Note: 9p filesystem support is not available in all QEMU builds + // For now, shared folders are disabled to ensure compatibility + if (advanced.SharedFolders.Any()) + { + // Log that shared folders are disabled + Console.WriteLine($"Warning: Shared folders are disabled for compatibility. {advanced.SharedFolders.Count} folder(s) configured but not used."); + } + } + + private void AddWHPXSpecificConfiguration() + { + // Add WHPX-specific configurations to avoid MSI issues + + // Disable MSI for better WHPX compatibility + _arguments.Add("-global"); + _arguments.Add("pcie-root-port.msi=off"); + + // Use legacy interrupt mode for better compatibility + _arguments.Add("-global"); + _arguments.Add("pcie-root-port.msix=off"); + + // Add additional WHPX optimizations + _arguments.Add("-global"); + _arguments.Add("pcie-root-port.ari=off"); + + // Add WHPX-specific optimizations + _arguments.Add("-rtc"); + _arguments.Add("base=localtime"); + + // Memory allocation optimizations for WHPX + // Note: -mem-path is not needed for WHPX and can cause issues + + // Memory preallocation is not available in this QEMU build + + // Add additional WHPX optimizations to avoid exit code 4 + _arguments.Add("-no-reboot"); + _arguments.Add("-no-shutdown"); + + // Use simpler interrupt handling (removed KVM-specific option) + + // Disable some features that might cause issues with WHPX + // _arguments.Add("-no-acpi"); // Commented out as it might cause issues + } +} diff --git a/QemuVmManager.Core/QemuProcessManager.cs b/QemuVmManager.Core/QemuProcessManager.cs new file mode 100644 index 0000000..8586cc9 --- /dev/null +++ b/QemuVmManager.Core/QemuProcessManager.cs @@ -0,0 +1,927 @@ +using System.Diagnostics; +using System.Threading; +using QemuVmManager.Models; + +namespace QemuVmManager.Core; + +public class QemuProcessManager +{ + private readonly Dictionary _runningVms = new(); + private readonly Dictionary _vmStatuses = new(); + private readonly Dictionary _performanceMonitors = new(); + + public event EventHandler? VmStatusChanged; + + public bool IsQemuInstalled() + { + try + { + var process = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = "qemu-system-x86_64", + Arguments = "--version", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + } + }; + + var started = process.Start(); + if (!started) + { + return false; + } + + process.WaitForExit(5000); + return process.ExitCode == 0; + } + catch + { + return false; + } + } + + public string GetQemuVersion() + { + try + { + var process = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = "qemu-system-x86_64", + Arguments = "--version", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + } + }; + + var started = process.Start(); + if (!started) + { + return "Unknown"; + } + + var output = process.StandardOutput.ReadToEnd(); + process.WaitForExit(5000); + + if (process.ExitCode == 0) + { + var lines = output.Split('\n', StringSplitOptions.RemoveEmptyEntries); + return lines.FirstOrDefault() ?? "Unknown"; + } + + return "Unknown"; + } + catch + { + return "Unknown"; + } + } + + public string GetQemuAccelerators() + { + try + { + var process = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = "qemu-system-x86_64", + Arguments = "-accel help", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + } + }; + + var started = process.Start(); + if (!started) + { + return "Unknown"; + } + + var output = process.StandardOutput.ReadToEnd(); + process.WaitForExit(5000); + + if (process.ExitCode == 0) + { + return output.Trim(); + } + + return "Unknown"; + } + catch + { + return "Unknown"; + } + } + + public VirtualizationType GetAvailableVirtualization() + { + try + { + // First check if virtualization is enabled in BIOS + if (!IsVirtualizationEnabled()) + { + return VirtualizationType.TCG; + } + + if (OperatingSystem.IsLinux()) + { + // Check for KVM support on Linux + if (File.Exists("/dev/kvm")) + { + // Test if KVM is accessible + var process = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = "qemu-system-x86_64", + Arguments = "-accel help", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + } + }; + + if (process.Start()) + { + process.WaitForExit(3000); + var output = process.StandardOutput.ReadToEnd(); + if (output.Contains("kvm") && process.ExitCode == 0) + { + return VirtualizationType.KVM; + } + } + } + return VirtualizationType.TCG; + } + else if (OperatingSystem.IsWindows()) + { + // Check QEMU's available accelerators first + var process = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = "qemu-system-x86_64", + Arguments = "-accel help", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + } + }; + + if (process.Start()) + { + process.WaitForExit(3000); + var output = process.StandardOutput.ReadToEnd(); + + // Check for WHPX (Windows Hypervisor Platform) support + if (output.Contains("whpx") && process.ExitCode == 0) + { + return VirtualizationType.HyperV; + } + + // Check for Hyper-V support (hvf) + if (output.Contains("hvf") && process.ExitCode == 0) + { + return VirtualizationType.HVF; + } + + // Check for HAXM support + if (output.Contains("hax") && process.ExitCode == 0) + { + return VirtualizationType.HAXM; + } + } + + // Fallback: Check for Hyper-V support using WMI + try + { + var wmiProcess = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = "powershell", + Arguments = "-Command \"Get-WmiObject -Class Msvm_VirtualSystemSettingData -Namespace root\\virtualization\\v2 -ErrorAction SilentlyContinue | Select-Object -First 1\"", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + } + }; + + if (wmiProcess.Start()) + { + wmiProcess.WaitForExit(3000); + if (wmiProcess.ExitCode == 0 && !string.IsNullOrEmpty(wmiProcess.StandardOutput.ReadToEnd())) + { + return VirtualizationType.HyperV; + } + } + } + catch + { + // Hyper-V check failed + } + + return VirtualizationType.TCG; + } + else if (OperatingSystem.IsMacOS()) + { + // Check for HVF (Hypervisor.framework) on macOS + var process = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = "qemu-system-x86_64", + Arguments = "-accel help", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + } + }; + + if (process.Start()) + { + process.WaitForExit(3000); + var output = process.StandardOutput.ReadToEnd(); + if (output.Contains("hvf") && process.ExitCode == 0) + { + return VirtualizationType.HVF; + } + } + + return VirtualizationType.TCG; + } + + return VirtualizationType.TCG; + } + catch + { + return VirtualizationType.TCG; + } + } + + public bool IsVirtualizationEnabled() + { + try + { + if (OperatingSystem.IsLinux()) + { + // Check if virtualization is enabled in BIOS + var process = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = "lscpu", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + } + }; + + if (process.Start()) + { + process.WaitForExit(3000); + var output = process.StandardOutput.ReadToEnd(); + return output.Contains("Virtualization:") && + (output.Contains("VT-x") || output.Contains("AMD-V") || output.Contains("SVM")); + } + } + else if (OperatingSystem.IsWindows()) + { + // Multiple methods to check virtualization on Windows + + // Method 1: Check using systeminfo + try + { + var process = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = "systeminfo", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + } + }; + + if (process.Start()) + { + process.WaitForExit(3000); + var output = process.StandardOutput.ReadToEnd(); + + // Check multiple possible virtualization indicators + if (output.Contains("Virtualization Enabled In Firmware: Yes") || + output.Contains("Virtualization: Enabled") || + output.Contains("Hyper-V Requirements:") && output.Contains("Yes")) + { + return true; + } + } + } + catch + { + // Continue to next method + } + + // Method 2: Check using PowerShell Get-ComputerInfo + try + { + var process = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = "powershell", + Arguments = "-Command \"Get-ComputerInfo | Select-Object HyperVRequirementVirtualizationFirmwareEnabled\"", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + } + }; + + if (process.Start()) + { + process.WaitForExit(3000); + var output = process.StandardOutput.ReadToEnd(); + return output.Contains("True"); + } + } + catch + { + // Continue to next method + } + + // Method 3: Check using wmic + try + { + var process = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = "wmic", + Arguments = "cpu get VirtualizationFirmwareEnabled", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + } + }; + + if (process.Start()) + { + process.WaitForExit(3000); + var output = process.StandardOutput.ReadToEnd(); + return output.Contains("TRUE"); + } + } + catch + { + // Continue to next method + } + + // Method 4: Check using PowerShell Get-WmiObject + try + { + var process = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = "powershell", + Arguments = "-Command \"Get-WmiObject -Class Msvm_VirtualSystemSettingData -Namespace root\\virtualization\\v2 | Select-Object VirtualSystemIdentifiers\"", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + } + }; + + if (process.Start()) + { + process.WaitForExit(3000); + var output = process.StandardOutput.ReadToEnd(); + // If we can query Hyper-V WMI, virtualization is likely enabled + return !output.Contains("Get-WmiObject") && output.Length > 0; + } + } + catch + { + // All methods failed + } + } + + return false; + } + catch + { + return false; + } + } + + public async Task StartVmAsync(VmConfiguration config) + { + try + { + if (_runningVms.ContainsKey(config.Name)) + { + throw new InvalidOperationException($"VM '{config.Name}' is already running"); + } + + var virtualizationType = GetAvailableVirtualization(); + var commandBuilder = new QemuCommandBuilder(config, virtualizationType); + var command = commandBuilder.BuildCommand(); + + Console.WriteLine($"[{config.Name}] Starting QEMU with command:"); + Console.WriteLine($"[{config.Name}] {command}"); + + var process = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = "qemu-system-x86_64", + Arguments = command.Replace("qemu-system-x86_64 ", ""), + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = false + }, + EnableRaisingEvents = true + }; + + var outputLines = new List(); + var errorLines = new List(); + + // Set up event handlers for output capture + process.OutputDataReceived += (sender, e) => + { + if (!string.IsNullOrEmpty(e.Data)) + { + outputLines.Add(e.Data); + Console.WriteLine($"[{config.Name}] {e.Data}"); + } + }; + + process.ErrorDataReceived += (sender, e) => + { + if (!string.IsNullOrEmpty(e.Data)) + { + errorLines.Add(e.Data); + Console.WriteLine($"[{config.Name}] ERROR: {e.Data}"); + } + }; + + process.Exited += (sender, e) => OnVmExitedWithDetails(config.Name, process, outputLines, errorLines); + + var started = process.Start(); + if (!started) + { + throw new InvalidOperationException("Failed to start QEMU process"); + } + + // Start reading output + process.BeginOutputReadLine(); + process.BeginErrorReadLine(); + + // Wait a moment to see if it starts successfully + await Task.Delay(3000); + + if (process.HasExited) + { + var errorMessage = $"QEMU process exited immediately with code {process.ExitCode}"; + if (errorLines.Any()) + { + errorMessage += $". Errors: {string.Join("; ", errorLines)}"; + } + if (outputLines.Any()) + { + errorMessage += $". Output: {string.Join("; ", outputLines)}"; + } + + Console.WriteLine($"[{config.Name}] {errorMessage}"); + UpdateVmStatus(config.Name, VmState.Error, -1, errorMessage); + throw new InvalidOperationException(errorMessage); + } + + _runningVms[config.Name] = process; + UpdateVmStatus(config.Name, VmState.Running, process.Id); + + // Start monitoring in background + _ = Task.Run(() => MonitorVmAsync(config.Name, process)); + + return true; + } + catch (Exception ex) + { + UpdateVmStatus(config.Name, VmState.Error, -1, ex.Message); + throw; + } + } + + public async Task StopVmAsync(string vmName, bool force = false) + { + if (!_runningVms.TryGetValue(vmName, out var process)) + { + return false; + } + + try + { + UpdateVmStatus(vmName, VmState.Stopping, process.Id); + + if (force) + { + process.Kill(); + } + else + { + // Try graceful shutdown first + process.CloseMainWindow(); + + // Wait for graceful shutdown + try + { + var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30)); + await process.WaitForExitAsync(cts.Token); + } + catch (OperationCanceledException) + { + // Timeout occurred, kill the process + process.Kill(); + } + } + + return true; + } + catch (Exception ex) + { + UpdateVmStatus(vmName, VmState.Error, process.Id, ex.Message); + throw; + } + } + + public async Task PauseVmAsync(string vmName) + { + if (!_runningVms.TryGetValue(vmName, out var process)) + { + return false; + } + + try + { + // Use QEMU monitor to pause VM + await SendQemuCommandAsync(vmName, "stop"); + UpdateVmStatus(vmName, VmState.Paused, process.Id); + return true; + } + catch (Exception ex) + { + UpdateVmStatus(vmName, VmState.Error, process.Id, ex.Message); + throw; + } + } + + public async Task ResumeVmAsync(string vmName) + { + if (!_runningVms.TryGetValue(vmName, out var process)) + { + return false; + } + + try + { + // Use QEMU monitor to resume VM + await SendQemuCommandAsync(vmName, "cont"); + UpdateVmStatus(vmName, VmState.Running, process.Id); + return true; + } + catch (Exception ex) + { + UpdateVmStatus(vmName, VmState.Error, process.Id, ex.Message); + throw; + } + } + + public VmStatus? GetVmStatus(string vmName) + { + return _vmStatuses.TryGetValue(vmName, out var status) ? status : null; + } + + public IEnumerable GetAllVmStatuses() + { + return _vmStatuses.Values; + } + + public bool IsVmRunning(string vmName) + { + return _runningVms.ContainsKey(vmName) && !_runningVms[vmName].HasExited; + } + + private async Task MonitorVmAsync(string vmName, Process process) + { + try + { + while (!process.HasExited) + { + await Task.Delay(5000); // Check every 5 seconds + + // Update resource usage if VM is running + if (process.HasExited == false) + { + var resourceUsage = await GetVmResourceUsageAsync(vmName); + UpdateVmResourceUsage(vmName, resourceUsage); + } + } + } + catch (Exception ex) + { + UpdateVmStatus(vmName, VmState.Error, process.Id, ex.Message); + } + } + + private Task GetVmResourceUsageAsync(string vmName) + { + try + { + // This is a simplified implementation + // In a real scenario, you would use QEMU monitor commands or libvirt + var usage = new VmResourceUsage(); + + // Get CPU usage from process + if (_runningVms.TryGetValue(vmName, out var process)) + { + var startTime = process.StartTime; + var totalProcessorTime = process.TotalProcessorTime; + var realTime = DateTime.Now - startTime; + + usage.CpuUsage = (totalProcessorTime.TotalMilliseconds / (Environment.ProcessorCount * realTime.TotalMilliseconds)) * 100; + + // Get memory usage + usage.MemoryUsage = process.WorkingSet64 / (1024 * 1024); // Convert to MB + } + + return Task.FromResult(usage); + } + catch + { + return Task.FromResult(new VmResourceUsage()); + } + } + + private async Task SendQemuCommandAsync(string vmName, string command) + { + // This would require QEMU monitor socket or similar mechanism + // For now, this is a placeholder + await Task.Delay(100); + } + + private void OnVmExited(string vmName) + { + if (_runningVms.TryGetValue(vmName, out var process)) + { + _runningVms.Remove(vmName); + UpdateVmStatus(vmName, VmState.Stopped, -1); + } + } + + private void OnVmExitedWithDetails(string vmName, Process process, List outputLines, List errorLines) + { + if (_runningVms.TryGetValue(vmName, out var runningProcess)) + { + _runningVms.Remove(vmName); + } + + var errorMessage = $"Process exited with code {process.ExitCode}"; + if (errorLines.Any()) + { + errorMessage += $". Errors: {string.Join("; ", errorLines)}"; + } + if (outputLines.Any()) + { + errorMessage += $". Output: {string.Join("; ", outputLines)}"; + } + + Console.WriteLine($"[{vmName}] {errorMessage}"); + UpdateVmStatus(vmName, VmState.Stopped, -1, errorMessage); + } + + private void UpdateVmStatus(string vmName, VmState state, int processId, string? errorMessage = null) + { + if (!_vmStatuses.TryGetValue(vmName, out var status)) + { + status = new VmStatus { Name = vmName }; + _vmStatuses[vmName] = status; + } + + var oldState = status.State; + + status.State = state; + status.ProcessId = processId; + status.ErrorMessage = errorMessage; + + switch (state) + { + case VmState.Running: + status.StartedAt = DateTime.UtcNow; + status.StoppedAt = null; + break; + case VmState.Stopped: + case VmState.Error: + status.StoppedAt = DateTime.UtcNow; + break; + } + + VmStatusChanged?.Invoke(this, new VmStatusChangedEventArgs(vmName, oldState, state)); + } + + private void UpdateVmResourceUsage(string vmName, VmResourceUsage usage) + { + if (_vmStatuses.TryGetValue(vmName, out var status)) + { + status.ResourceUsage = usage; + } + } + + // Enhanced performance monitoring methods + public async Task GetVmPerformanceMetricsAsync(string vmName) + { + if (!_performanceMonitors.TryGetValue(vmName, out var monitor)) + { + return new VmPerformanceMetrics(); + } + + return await monitor.GetCurrentMetricsAsync(); + } + + public Task StartPerformanceMonitoringAsync(string vmName) + { + if (_runningVms.TryGetValue(vmName, out var process)) + { + var monitor = new PerformanceMonitor(process); + _performanceMonitors[vmName] = monitor; + return monitor.StartMonitoringAsync(); + } + return Task.CompletedTask; + } + + public void StopPerformanceMonitoring(string vmName) + { + if (_performanceMonitors.TryGetValue(vmName, out var monitor)) + { + monitor.StopMonitoring(); + _performanceMonitors.Remove(vmName); + } + } + + public async Task> GetPerformanceHistoryAsync(string vmName, int maxSamples = 100) + { + if (_performanceMonitors.TryGetValue(vmName, out var monitor)) + { + return await monitor.GetPerformanceHistoryAsync(maxSamples); + } + return new List(); + } +} + +public class PerformanceMonitor +{ + private readonly Process _process; + private readonly List _history = new(); + private readonly object _lock = new(); + private bool _isMonitoring = false; + private CancellationTokenSource? _cancellationTokenSource; + + public PerformanceMonitor(Process process) + { + _process = process; + } + + public async Task StartMonitoringAsync() + { + if (_isMonitoring) return; + + _isMonitoring = true; + _cancellationTokenSource = new CancellationTokenSource(); + + _ = Task.Run(async () => + { + while (_isMonitoring && !_process.HasExited) + { + try + { + var metrics = await GetCurrentMetricsAsync(); + + lock (_lock) + { + _history.Add(metrics); + // Keep only last 1000 samples + if (_history.Count > 1000) + { + _history.RemoveAt(0); + } + } + + await Task.Delay(2000, _cancellationTokenSource.Token); // Sample every 2 seconds + } + catch (OperationCanceledException) + { + break; + } + catch + { + // Continue monitoring even if there's an error + } + } + }, _cancellationTokenSource.Token); + } + + public void StopMonitoring() + { + _isMonitoring = false; + _cancellationTokenSource?.Cancel(); + } + + public Task GetCurrentMetricsAsync() + { + try + { + var metrics = new VmPerformanceMetrics + { + Timestamp = DateTime.UtcNow, + ProcessId = _process.Id + }; + + if (!_process.HasExited) + { + _process.Refresh(); + + // CPU metrics + metrics.CpuUsagePercent = _process.TotalProcessorTime.TotalMilliseconds / + (Environment.ProcessorCount * (DateTime.Now - _process.StartTime).TotalMilliseconds) * 100; + + // Memory metrics + metrics.MemoryUsageMB = _process.WorkingSet64 / (1024 * 1024); + metrics.PrivateMemoryMB = _process.PrivateMemorySize64 / (1024 * 1024); + metrics.VirtualMemoryMB = _process.VirtualMemorySize64 / (1024 * 1024); + + // Process metrics + metrics.ThreadCount = _process.Threads.Count; + metrics.HandleCount = _process.HandleCount; + + // Performance counters (if available) + try + { + using var cpuCounter = new PerformanceCounter("Processor", "% Processor Time", "_Total"); + metrics.SystemCpuUsagePercent = cpuCounter.NextValue(); + } + catch + { + metrics.SystemCpuUsagePercent = 0; + } + } + + return Task.FromResult(metrics); + } + catch + { + return Task.FromResult(new VmPerformanceMetrics { Timestamp = DateTime.UtcNow }); + } + } + + public Task> GetPerformanceHistoryAsync(int maxSamples = 100) + { + lock (_lock) + { + return Task.FromResult(_history.TakeLast(maxSamples).ToList()); + } + } +} + + + +public class VmStatusChangedEventArgs : EventArgs +{ + public string VmName { get; } + public VmState OldState { get; } + public VmState NewState { get; } + + public VmStatusChangedEventArgs(string vmName, VmState oldState, VmState newState) + { + VmName = vmName; + OldState = oldState; + NewState = newState; + } +} diff --git a/QemuVmManager.Core/QemuVmManager.Core.csproj b/QemuVmManager.Core/QemuVmManager.Core.csproj new file mode 100644 index 0000000..cc9f32f --- /dev/null +++ b/QemuVmManager.Core/QemuVmManager.Core.csproj @@ -0,0 +1,17 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + diff --git a/QemuVmManager.Models/QemuVmManager.Models.csproj b/QemuVmManager.Models/QemuVmManager.Models.csproj new file mode 100644 index 0000000..ecb16a9 --- /dev/null +++ b/QemuVmManager.Models/QemuVmManager.Models.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + enable + enable + + + + + + + diff --git a/QemuVmManager.Models/VirtualizationType.cs b/QemuVmManager.Models/VirtualizationType.cs new file mode 100644 index 0000000..9462373 --- /dev/null +++ b/QemuVmManager.Models/VirtualizationType.cs @@ -0,0 +1,29 @@ +namespace QemuVmManager.Models; + +public enum VirtualizationType +{ + /// + /// Kernel-based Virtual Machine (Linux) + /// + KVM, + + /// + /// Hyper-V (Windows) + /// + HyperV, + + /// + /// Intel Hardware Accelerated Execution Manager (Windows/macOS) + /// + HAXM, + + /// + /// Hypervisor.framework (macOS) + /// + HVF, + + /// + /// Tiny Code Generator (Software emulation) + /// + TCG +} diff --git a/QemuVmManager.Models/VmConfiguration.cs b/QemuVmManager.Models/VmConfiguration.cs new file mode 100644 index 0000000..9d068e3 --- /dev/null +++ b/QemuVmManager.Models/VmConfiguration.cs @@ -0,0 +1,189 @@ +using System.Text.Json.Serialization; + +namespace QemuVmManager.Models; + +public class VmConfiguration +{ + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + [JsonPropertyName("description")] + public string Description { get; set; } = string.Empty; + + [JsonPropertyName("cpu")] + public CpuConfiguration Cpu { get; set; } = new(); + + [JsonPropertyName("memory")] + public MemoryConfiguration Memory { get; set; } = new(); + + [JsonPropertyName("storage")] + public StorageConfiguration Storage { get; set; } = new(); + + [JsonPropertyName("network")] + public NetworkConfiguration Network { get; set; } = new(); + + [JsonPropertyName("display")] + public DisplayConfiguration Display { get; set; } = new(); + + [JsonPropertyName("boot")] + public BootConfiguration Boot { get; set; } = new(); + + [JsonPropertyName("advanced")] + public AdvancedConfiguration Advanced { get; set; } = new(); + + [JsonPropertyName("created")] + public DateTime Created { get; set; } = DateTime.UtcNow; + + [JsonPropertyName("lastModified")] + public DateTime LastModified { get; set; } = DateTime.UtcNow; +} + +public class CpuConfiguration +{ + [JsonPropertyName("cores")] + public int Cores { get; set; } = 2; + + [JsonPropertyName("sockets")] + public int Sockets { get; set; } = 1; + + [JsonPropertyName("threads")] + public int Threads { get; set; } = 1; + + [JsonPropertyName("model")] + public string Model { get; set; } = "qemu64"; + + [JsonPropertyName("enableKvm")] + public bool EnableKvm { get; set; } = true; +} + +public class MemoryConfiguration +{ + [JsonPropertyName("size")] + public long Size { get; set; } = 2048; // MB + + [JsonPropertyName("unit")] + public string Unit { get; set; } = "M"; +} + +public class StorageConfiguration +{ + [JsonPropertyName("disks")] + public List Disks { get; set; } = new(); + + [JsonPropertyName("cdrom")] + public string? Cdrom { get; set; } +} + +public class DiskConfiguration +{ + [JsonPropertyName("path")] + public string Path { get; set; } = string.Empty; + + [JsonPropertyName("size")] + public long Size { get; set; } = 10; // GB + + [JsonPropertyName("format")] + public string Format { get; set; } = "qcow2"; + + [JsonPropertyName("interface")] + public string Interface { get; set; } = "virtio"; + + [JsonPropertyName("cache")] + public string Cache { get; set; } = "writeback"; + + [JsonPropertyName("isBoot")] + public bool IsBoot { get; set; } = false; +} + +public class NetworkConfiguration +{ + [JsonPropertyName("interfaces")] + public List Interfaces { get; set; } = new(); + + [JsonPropertyName("bridge")] + public string Bridge { get; set; } = "virbr0"; +} + +public class NetworkInterfaceConfiguration +{ + [JsonPropertyName("type")] + public string Type { get; set; } = "bridge"; + + [JsonPropertyName("model")] + public string Model { get; set; } = "virtio-net-pci"; + + [JsonPropertyName("mac")] + public string? Mac { get; set; } + + [JsonPropertyName("bridge")] + public string Bridge { get; set; } = "virbr0"; +} + +public class DisplayConfiguration +{ + [JsonPropertyName("type")] + public string Type { get; set; } = "gtk"; + + [JsonPropertyName("vga")] + public string Vga { get; set; } = "virtio"; + + [JsonPropertyName("resolution")] + public string Resolution { get; set; } = "1024x768"; + + [JsonPropertyName("enableSpice")] + public bool EnableSpice { get; set; } = false; + + [JsonPropertyName("spicePort")] + public int SpicePort { get; set; } = 5930; +} + +public class BootConfiguration +{ + [JsonPropertyName("order")] + public List Order { get; set; } = new() { "c", "d", "n" }; + + [JsonPropertyName("kernel")] + public string? Kernel { get; set; } + + [JsonPropertyName("initrd")] + public string? Initrd { get; set; } + + [JsonPropertyName("cmdline")] + public string? Cmdline { get; set; } +} + +public class AdvancedConfiguration +{ + [JsonPropertyName("enableAudio")] + public bool EnableAudio { get; set; } = false; + + [JsonPropertyName("enableUsb")] + public bool EnableUsb { get; set; } = false; + + [JsonPropertyName("enableBalloon")] + public bool EnableBalloon { get; set; } = true; + + [JsonPropertyName("enableVirtioRng")] + public bool EnableVirtioRng { get; set; } = true; + + [JsonPropertyName("enableVirtioFs")] + public bool EnableVirtioFs { get; set; } = false; + + [JsonPropertyName("sharedFolders")] + public List SharedFolders { get; set; } = new(); + + [JsonPropertyName("extraArgs")] + public List ExtraArgs { get; set; } = new(); +} + +public class SharedFolderConfiguration +{ + [JsonPropertyName("hostPath")] + public string HostPath { get; set; } = string.Empty; + + [JsonPropertyName("guestPath")] + public string GuestPath { get; set; } = string.Empty; + + [JsonPropertyName("readOnly")] + public bool ReadOnly { get; set; } = false; +} diff --git a/QemuVmManager.Models/VmStatus.cs b/QemuVmManager.Models/VmStatus.cs new file mode 100644 index 0000000..39dcdc5 --- /dev/null +++ b/QemuVmManager.Models/VmStatus.cs @@ -0,0 +1,44 @@ +namespace QemuVmManager.Models; + +public enum VmState +{ + Stopped, + Starting, + Running, + Paused, + Stopping, + Error +} + +public class VmStatus +{ + public string Name { get; set; } = string.Empty; + public VmState State { get; set; } = VmState.Stopped; + public int ProcessId { get; set; } = -1; + public DateTime? StartedAt { get; set; } + public DateTime? StoppedAt { get; set; } + public string? ErrorMessage { get; set; } + public VmResourceUsage? ResourceUsage { get; set; } +} + +public class VmResourceUsage +{ + public double CpuUsage { get; set; } = 0.0; // Percentage + public long MemoryUsage { get; set; } = 0; // MB + public long DiskUsage { get; set; } = 0; // MB + public long NetworkRx { get; set; } = 0; // Bytes + public long NetworkTx { get; set; } = 0; // Bytes +} + +public class VmPerformanceMetrics +{ + public DateTime Timestamp { get; set; } + public int ProcessId { get; set; } + public double CpuUsagePercent { get; set; } + public double SystemCpuUsagePercent { get; set; } + public long MemoryUsageMB { get; set; } + public long PrivateMemoryMB { get; set; } + public long VirtualMemoryMB { get; set; } + public int ThreadCount { get; set; } + public int HandleCount { get; set; } +} diff --git a/QemuVmManager.Services/QemuVmManager.Services.csproj b/QemuVmManager.Services/QemuVmManager.Services.csproj new file mode 100644 index 0000000..f4849d2 --- /dev/null +++ b/QemuVmManager.Services/QemuVmManager.Services.csproj @@ -0,0 +1,18 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + diff --git a/QemuVmManager.Services/VmManagementService.cs b/QemuVmManager.Services/VmManagementService.cs new file mode 100644 index 0000000..5d0c137 --- /dev/null +++ b/QemuVmManager.Services/VmManagementService.cs @@ -0,0 +1,464 @@ +using System.Text.Json; +using QemuVmManager.Core; +using QemuVmManager.Models; + +namespace QemuVmManager.Services; + +public class VmManagementService +{ + private readonly QemuProcessManager _processManager; + private readonly DiskManager _diskManager; + private readonly string _configDirectory; + private readonly Dictionary _vmConfigurations = new(); + + public VmManagementService(string configDirectory = "vm-configs") + { + _processManager = new QemuProcessManager(); + _diskManager = new DiskManager(); + _configDirectory = configDirectory; + + // Ensure config directory exists + Directory.CreateDirectory(_configDirectory); + + // Subscribe to VM status changes + _processManager.VmStatusChanged += OnVmStatusChanged; + + // Load existing configurations + LoadVmConfigurations(); + } + + public async Task CreateVmAsync(VmConfiguration config) + { + // Validate configuration + ValidateConfiguration(config); + + // Create disk images if they don't exist + await _diskManager.CreateDiskImagesForVmAsync(config); + + // Save configuration + await SaveVmConfigurationAsync(config); + + // Add to in-memory cache + _vmConfigurations[config.Name] = config; + + return config; + } + + public async Task StartVmAsync(string vmName) + { + if (!_vmConfigurations.TryGetValue(vmName, out var config)) + { + throw new ArgumentException($"VM configuration '{vmName}' not found"); + } + + // Check if QEMU is installed + if (!_processManager.IsQemuInstalled()) + { + throw new InvalidOperationException("QEMU is not installed or not found in PATH. Please install QEMU and ensure it's available in your system PATH."); + } + + // Ensure disk images exist before starting + await _diskManager.CreateDiskImagesForVmAsync(config); + + return await _processManager.StartVmAsync(config); + } + + public async Task StopVmAsync(string vmName, bool force = false) + { + return await _processManager.StopVmAsync(vmName, force); + } + + public async Task PauseVmAsync(string vmName) + { + return await _processManager.PauseVmAsync(vmName); + } + + public async Task ResumeVmAsync(string vmName) + { + return await _processManager.ResumeVmAsync(vmName); + } + + public async Task UpdateVmAsync(string vmName, VmConfiguration updatedConfig) + { + if (!_vmConfigurations.ContainsKey(vmName)) + { + throw new ArgumentException($"VM configuration '{vmName}' not found"); + } + + // Ensure name consistency + updatedConfig.Name = vmName; + updatedConfig.LastModified = DateTime.UtcNow; + + // Validate configuration + ValidateConfiguration(updatedConfig); + + // Save updated configuration + await SaveVmConfigurationAsync(updatedConfig); + + // Update in-memory cache + _vmConfigurations[vmName] = updatedConfig; + + return updatedConfig; + } + + public async Task DeleteVmAsync(string vmName) + { + // Stop VM if running + if (_processManager.IsVmRunning(vmName)) + { + await _processManager.StopVmAsync(vmName, true); + } + + // Get configuration before removing from cache + var config = _vmConfigurations.GetValueOrDefault(vmName); + + // Remove from in-memory cache + _vmConfigurations.Remove(vmName); + + // Delete configuration file + var configPath = Path.Combine(_configDirectory, $"{vmName}.json"); + if (File.Exists(configPath)) + { + File.Delete(configPath); + } + + // Delete disk images if they exist + if (config != null) + { + foreach (var disk in config.Storage.Disks) + { + _diskManager.DeleteDiskImage(disk.Path); + } + } + } + + public VmConfiguration? GetVmConfiguration(string vmName) + { + return _vmConfigurations.TryGetValue(vmName, out var config) ? config : null; + } + + public IEnumerable GetAllVmConfigurations() + { + return _vmConfigurations.Values; + } + + public VmStatus? GetVmStatus(string vmName) + { + return _processManager.GetVmStatus(vmName); + } + + public IEnumerable GetAllVmStatuses() + { + return _processManager.GetAllVmStatuses(); + } + + public bool IsVmRunning(string vmName) + { + return _processManager.IsVmRunning(vmName); + } + + public async Task CloneVmAsync(string sourceVmName, string newVmName, string? newDescription = null) + { + if (!_vmConfigurations.TryGetValue(sourceVmName, out var sourceConfig)) + { + throw new ArgumentException($"Source VM configuration '{sourceVmName}' not found"); + } + + if (_vmConfigurations.ContainsKey(newVmName)) + { + throw new ArgumentException($"VM configuration '{newVmName}' already exists"); + } + + // Create clone + var clonedConfig = CloneConfiguration(sourceConfig, newVmName, newDescription); + + // Save cloned configuration + await SaveVmConfigurationAsync(clonedConfig); + + // Add to in-memory cache + _vmConfigurations[newVmName] = clonedConfig; + + return clonedConfig; + } + + public async Task ExportVmConfigurationAsync(string vmName, string exportPath) + { + if (!_vmConfigurations.TryGetValue(vmName, out var config)) + { + throw new ArgumentException($"VM configuration '{vmName}' not found"); + } + + var json = JsonSerializer.Serialize(config, new JsonSerializerOptions + { + WriteIndented = true + }); + + await File.WriteAllTextAsync(exportPath, json); + + return exportPath; + } + + public async Task ImportVmConfigurationAsync(string importPath, string? newName = null) + { + if (!File.Exists(importPath)) + { + throw new FileNotFoundException($"Configuration file '{importPath}' not found"); + } + + var json = await File.ReadAllTextAsync(importPath); + var config = JsonSerializer.Deserialize(json); + + if (config == null) + { + throw new InvalidOperationException("Failed to deserialize VM configuration"); + } + + // Use new name if provided + if (!string.IsNullOrEmpty(newName)) + { + config.Name = newName; + } + + // Validate and save + ValidateConfiguration(config); + + // Create disk images if they don't exist + await _diskManager.CreateDiskImagesForVmAsync(config); + + await SaveVmConfigurationAsync(config); + + // Add to in-memory cache + _vmConfigurations[config.Name] = config; + + return config; + } + + // Disk management methods + public async Task GetDiskInfoAsync(string vmName, int diskIndex = 0) + { + if (!_vmConfigurations.TryGetValue(vmName, out var config)) + { + throw new ArgumentException($"VM configuration '{vmName}' not found"); + } + + if (diskIndex >= config.Storage.Disks.Count) + { + throw new ArgumentException($"Disk index {diskIndex} is out of range"); + } + + var disk = config.Storage.Disks[diskIndex]; + return await _diskManager.GetDiskInfoAsync(disk.Path, disk.Format); + } + + public async Task ResizeDiskAsync(string vmName, int diskIndex, long newSizeGB) + { + if (!_vmConfigurations.TryGetValue(vmName, out var config)) + { + throw new ArgumentException($"VM configuration '{vmName}' not found"); + } + + if (diskIndex >= config.Storage.Disks.Count) + { + throw new ArgumentException($"Disk index {diskIndex} is out of range"); + } + + var disk = config.Storage.Disks[diskIndex]; + var success = await _diskManager.ResizeDiskAsync(disk.Path, disk.Format, newSizeGB); + + if (success) + { + // Update the configuration + disk.Size = newSizeGB; + config.LastModified = DateTime.UtcNow; + await SaveVmConfigurationAsync(config); + } + + return success; + } + + public async Task ConvertDiskAsync(string vmName, int diskIndex, string newFormat) + { + if (!_vmConfigurations.TryGetValue(vmName, out var config)) + { + throw new ArgumentException($"VM configuration '{vmName}' not found"); + } + + if (diskIndex >= config.Storage.Disks.Count) + { + throw new ArgumentException($"Disk index {diskIndex} is out of range"); + } + + var disk = config.Storage.Disks[diskIndex]; + var newPath = Path.ChangeExtension(disk.Path, newFormat); + + var success = await _diskManager.ConvertDiskAsync(disk.Path, disk.Format, newPath, newFormat); + + if (success) + { + // Update the configuration + disk.Path = newPath; + disk.Format = newFormat; + config.LastModified = DateTime.UtcNow; + await SaveVmConfigurationAsync(config); + } + + return success; + } + + public bool ValidateDiskImages(string vmName) + { + if (!_vmConfigurations.TryGetValue(vmName, out var config)) + { + throw new ArgumentException($"VM configuration '{vmName}' not found"); + } + + foreach (var disk in config.Storage.Disks) + { + if (!_diskManager.ValidateDiskImage(disk.Path, disk.Format)) + { + return false; + } + } + + return true; + } + + private void ValidateConfiguration(VmConfiguration config) + { + if (string.IsNullOrWhiteSpace(config.Name)) + { + throw new ArgumentException("VM name cannot be empty"); + } + + if (config.Cpu.Cores <= 0) + { + throw new ArgumentException("CPU cores must be greater than 0"); + } + + if (config.Memory.Size <= 0) + { + throw new ArgumentException("Memory size must be greater than 0"); + } + + if (config.Storage.Disks.Count == 0) + { + throw new ArgumentException("At least one disk must be configured"); + } + + // Validate disk paths + foreach (var disk in config.Storage.Disks) + { + if (string.IsNullOrWhiteSpace(disk.Path)) + { + throw new ArgumentException("Disk path cannot be empty"); + } + } + } + + private async Task SaveVmConfigurationAsync(VmConfiguration config) + { + var configPath = Path.Combine(_configDirectory, $"{config.Name}.json"); + var json = JsonSerializer.Serialize(config, new JsonSerializerOptions + { + WriteIndented = true + }); + + await File.WriteAllTextAsync(configPath, json); + } + + private void LoadVmConfigurations() + { + if (!Directory.Exists(_configDirectory)) + { + return; + } + + var configFiles = Directory.GetFiles(_configDirectory, "*.json"); + + foreach (var configFile in configFiles) + { + try + { + var json = File.ReadAllText(configFile); + var config = JsonSerializer.Deserialize(json); + + if (config != null && !string.IsNullOrWhiteSpace(config.Name)) + { + _vmConfigurations[config.Name] = config; + } + } + catch (Exception ex) + { + // Log error but continue loading other configurations + Console.WriteLine($"Failed to load configuration from {configFile}: {ex.Message}"); + } + } + } + + private VmConfiguration CloneConfiguration(VmConfiguration source, string newName, string? newDescription) + { + var json = JsonSerializer.Serialize(source); + var cloned = JsonSerializer.Deserialize(json); + + if (cloned == null) + { + throw new InvalidOperationException("Failed to clone configuration"); + } + + cloned.Name = newName; + cloned.Description = newDescription ?? $"Clone of {source.Name}"; + cloned.Created = DateTime.UtcNow; + cloned.LastModified = DateTime.UtcNow; + + // Update disk paths to avoid conflicts + for (int i = 0; i < cloned.Storage.Disks.Count; i++) + { + var disk = cloned.Storage.Disks[i]; + var directory = Path.GetDirectoryName(disk.Path); + var fileName = Path.GetFileName(disk.Path); + var extension = Path.GetExtension(fileName); + var nameWithoutExtension = Path.GetFileNameWithoutExtension(fileName); + + disk.Path = Path.Combine(directory ?? "", $"{nameWithoutExtension}_{newName}{extension}"); + } + + return cloned; + } + + private void OnVmStatusChanged(object? sender, VmStatusChangedEventArgs e) + { + // Log status changes + Console.WriteLine($"VM '{e.VmName}' status changed from {e.OldState} to {e.NewState}"); + + // You could add additional logic here, such as: + // - Sending notifications + // - Updating a database + // - Triggering automated actions + } + + // Performance monitoring methods + public async Task StartPerformanceMonitoringAsync(string vmName) + { + await _processManager.StartPerformanceMonitoringAsync(vmName); + } + + public void StopPerformanceMonitoring(string vmName) + { + _processManager.StopPerformanceMonitoring(vmName); + } + + public bool IsPerformanceMonitoringActive(string vmName) + { + return _processManager.GetAllVmStatuses().Any(s => s.Name == vmName && s.State == VmState.Running); + } + + public async Task GetVmPerformanceMetricsAsync(string vmName) + { + return await _processManager.GetVmPerformanceMetricsAsync(vmName); + } + + public async Task> GetPerformanceHistoryAsync(string vmName, int maxSamples = 100) + { + return await _processManager.GetPerformanceHistoryAsync(vmName, maxSamples); + } +} diff --git a/QemuVmManager.sln b/QemuVmManager.sln new file mode 100644 index 0000000..34c4c50 --- /dev/null +++ b/QemuVmManager.sln @@ -0,0 +1,39 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QemuVmManager.Console", "QemuVmManager.Console\QemuVmManager.Console.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QemuVmManager.Core", "QemuVmManager.Core\QemuVmManager.Core.csproj", "{B2C3D4E5-F6G7-8901-BCDE-F23456789012}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QemuVmManager.Models", "QemuVmManager.Models\QemuVmManager.Models.csproj", "{C3D4E5F6-G7H8-9012-CDEF-345678901234}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QemuVmManager.Services", "QemuVmManager.Services\QemuVmManager.Services.csproj", "{D4E5F6G7-H8I9-0123-DEF0-456789012345}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.Build.0 = Release|Any CPU + {B2C3D4E5-F6G7-8901-BCDE-F23456789012}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B2C3D4E5-F6G7-8901-BCDE-F23456789012}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B2C3D4E5-F6G7-8901-BCDE-F23456789012}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B2C3D4E5-F6G7-8901-BCDE-F23456789012}.Release|Any CPU.Build.0 = Release|Any CPU + {C3D4E5F6-G7H8-9012-CDEF-345678901234}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C3D4E5F6-G7H8-9012-CDEF-345678901234}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C3D4E5F6-G7H8-9012-CDEF-345678901234}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C3D4E5F6-G7H8-9012-CDEF-345678901234}.Release|Any CPU.Build.0 = Release|Any CPU + {D4E5F6G7-H8I9-0123-DEF0-456789012345}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D4E5F6G7-H8I9-0123-DEF0-456789012345}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D4E5F6G7-H8I9-0123-DEF0-456789012345}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D4E5F6G7-H8I9-0123-DEF0-456789012345}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/README.md b/README.md new file mode 100644 index 0000000..8a920d9 --- /dev/null +++ b/README.md @@ -0,0 +1,308 @@ +# QEMU VM Manager + +A comprehensive .NET application for managing QEMU virtual machines with an intuitive console interface. + +## Features + +- **VM Lifecycle Management**: Create, start, stop, pause, resume, and delete VMs +- **Configuration Management**: Store and manage VM configurations in JSON format +- **VM Cloning**: Clone existing VMs with automatic disk path management +- **Import/Export**: Export and import VM configurations +- **Status Monitoring**: Real-time VM status and resource usage tracking +- **Interactive Console**: User-friendly command-line interface +- **Cross-Platform**: Works on Windows, Linux, and macOS + +## Prerequisites + +- .NET 8.0 SDK or Runtime +- QEMU installed and available in PATH +- For KVM acceleration: KVM support (Linux) or Hyper-V (Windows) + +## Installation + +1. **Clone the repository**: + ```bash + git clone + cd skystack + ``` + +2. **Build the solution**: + ```bash + dotnet build + ``` + +3. **Run the application**: + ```bash + dotnet run --project QemuVmManager.Console + ``` + +## Usage + +### Starting the Application + +```bash +dotnet run --project QemuVmManager.Console +``` + +You'll see the interactive prompt: +``` +=== QEMU VM Manager === +Type 'help' for available commands + +qemu-vm> +``` + +### Available Commands + +#### `help` +Display all available commands and their usage. + +#### `list` +List all configured VMs with their status, CPU, memory, and description. + +#### `create [name]` +Create a new VM configuration interactively. If no name is provided, you'll be prompted for one. + +Example: +``` +qemu-vm> create my-vm +Enter VM name: my-vm +Description (optional): My test VM +CPU cores (2): 4 +CPU model (qemu64): qemu64 +Memory size in MB (2048): 4096 +Disk path: /path/to/disk.qcow2 +Disk size in GB (10): 20 +Disk format (qcow2): qcow2 +Disk interface (virtio): virtio +Network bridge (virbr0): virbr0 +Display type (gtk): gtk +VGA type (virtio): virtio +``` + +#### `start ` +Start a VM by name. + +Example: +``` +qemu-vm> start my-vm +``` + +#### `stop [--force]` +Stop a VM gracefully. Use `--force` for immediate termination. + +Example: +``` +qemu-vm> stop my-vm +qemu-vm> stop my-vm --force +``` + +#### `pause ` +Pause a running VM. + +Example: +``` +qemu-vm> pause my-vm +``` + +#### `resume ` +Resume a paused VM. + +Example: +``` +qemu-vm> resume my-vm +``` + +#### `delete ` +Delete a VM configuration and stop it if running. + +Example: +``` +qemu-vm> delete my-vm +Are you sure you want to delete VM 'my-vm'? (y/N): y +``` + +#### `clone ` +Clone an existing VM configuration. + +Example: +``` +qemu-vm> clone my-vm my-vm-clone +``` + +#### `export ` +Export a VM configuration to a JSON file. + +Example: +``` +qemu-vm> export my-vm /tmp/my-vm-config.json +``` + +#### `import [name]` +Import a VM configuration from a JSON file. + +Example: +``` +qemu-vm> import /tmp/my-vm-config.json my-imported-vm +``` + +#### `status [name]` +Show VM status. Without a name, shows all VMs. + +Example: +``` +qemu-vm> status +qemu-vm> status my-vm +``` + +#### `config ` +Display detailed VM configuration. + +Example: +``` +qemu-vm> config my-vm +``` + +#### `exit` or `quit` +Exit the application. + +## Configuration + +VM configurations are stored in JSON format in the `vm-configs` directory. Each VM has its own configuration file named `.json`. + +### Configuration Structure + +```json +{ + "name": "my-vm", + "description": "My test VM", + "cpu": { + "cores": 4, + "sockets": 1, + "threads": 1, + "model": "qemu64", + "enableKvm": true + }, + "memory": { + "size": 4096, + "unit": "M" + }, + "storage": { + "disks": [ + { + "path": "/path/to/disk.qcow2", + "size": 20, + "format": "qcow2", + "interface": "virtio", + "cache": "writeback", + "isBoot": true + } + ], + "cdrom": null + }, + "network": { + "interfaces": [ + { + "type": "bridge", + "model": "virtio-net-pci", + "mac": null, + "bridge": "virbr0" + } + ], + "bridge": "virbr0" + }, + "display": { + "type": "gtk", + "vga": "virtio", + "resolution": "1024x768", + "enableSpice": false, + "spicePort": 5930 + }, + "boot": { + "order": ["c", "d", "n"], + "kernel": null, + "initrd": null, + "cmdline": null + }, + "advanced": { + "enableAudio": false, + "enableUsb": false, + "enableBalloon": true, + "enableVirtioRng": true, + "enableVirtioFs": false, + "sharedFolders": [], + "extraArgs": [] + }, + "created": "2024-01-01T00:00:00Z", + "lastModified": "2024-01-01T00:00:00Z" +} +``` + +## Project Structure + +``` +skystack/ +├── QemuVmManager.sln # Solution file +├── QemuVmManager.Models/ # Data models and DTOs +│ ├── VmConfiguration.cs # Main VM configuration model +│ └── VmStatus.cs # VM status and resource usage +├── QemuVmManager.Core/ # Core QEMU operations +│ ├── QemuCommandBuilder.cs # QEMU command generation +│ └── QemuProcessManager.cs # VM process management +├── QemuVmManager.Services/ # High-level services +│ └── VmManagementService.cs # Main VM management service +├── QemuVmManager.Console/ # Console application +│ └── Program.cs # Main program and UI +└── README.md # This file +``` + +## Architecture + +The application follows a layered architecture: + +1. **Models Layer** (`QemuVmManager.Models`): Contains data structures for VM configuration and status +2. **Core Layer** (`QemuVmManager.Core`): Handles QEMU command generation and process management +3. **Services Layer** (`QemuVmManager.Services`): Provides high-level VM management operations +4. **Console Layer** (`QemuVmManager.Console`): User interface and command processing + +## QEMU Integration + +The application generates QEMU command-line arguments based on VM configurations. Key features: + +- **KVM Acceleration**: Automatically enables KVM when available +- **VirtIO Support**: Uses VirtIO devices for better performance +- **Network Bridging**: Supports bridge networking +- **SPICE Support**: Optional SPICE remote desktop +- **Shared Folders**: 9P filesystem sharing +- **Advanced Features**: Audio, USB, balloon driver, RNG + +## Troubleshooting + +### Common Issues + +1. **QEMU not found**: Ensure QEMU is installed and in your PATH +2. **Permission denied**: Run with appropriate permissions for KVM/bridge access +3. **Network bridge not found**: Create the bridge interface (e.g., `virbr0`) +4. **Disk file not found**: Ensure disk paths are correct and accessible + +### Debug Mode + +To see the generated QEMU commands, you can modify the `QemuProcessManager.cs` to log the command before execution. + +## Contributing + +1. Fork the repository +2. Create a feature branch +3. Make your changes +4. Add tests if applicable +5. Submit a pull request + +## License + +This project is licensed under the MIT License - see the LICENSE file for details. + +## Acknowledgments + +- QEMU team for the excellent virtualization platform +- .NET community for the robust framework +- Contributors and users of this project diff --git a/build.bat b/build.bat new file mode 100644 index 0000000..478272a --- /dev/null +++ b/build.bat @@ -0,0 +1,33 @@ +@echo off +echo === QEMU VM Manager Build Script === + +REM Check if .NET is available +dotnet --version >nul 2>&1 +if %errorlevel% neq 0 ( + echo Error: .NET is not available or not properly installed. + echo Please install .NET 8.0 SDK from: https://dotnet.microsoft.com/download + pause + exit /b 1 +) + +echo Cleaning previous builds... +dotnet clean + +echo Restoring packages... +dotnet restore + +echo Building solution... +dotnet build --configuration Release + +if %errorlevel% equ 0 ( + echo Build completed successfully! + echo. + echo To run the application: + echo dotnet run --project QemuVmManager.Console +) else ( + echo Build failed! + pause + exit /b 1 +) + +pause diff --git a/build.ps1 b/build.ps1 new file mode 100644 index 0000000..59345ae --- /dev/null +++ b/build.ps1 @@ -0,0 +1,39 @@ +#!/usr/bin/env pwsh + +Write-Host "=== QEMU VM Manager Build Script ===" -ForegroundColor Green + +# Check if .NET is available +try { + $dotnetVersion = dotnet --version + Write-Host "Using .NET version: $dotnetVersion" -ForegroundColor Yellow +} catch { + Write-Host "Error: .NET is not available or not properly installed." -ForegroundColor Red + Write-Host "Please install .NET 8.0 SDK from: https://dotnet.microsoft.com/download" -ForegroundColor Yellow + exit 1 +} + +# Clean previous builds +Write-Host "Cleaning previous builds..." -ForegroundColor Blue +dotnet clean + +# Restore packages +Write-Host "Restoring packages..." -ForegroundColor Blue +dotnet restore + +# Build the solution +Write-Host "Building solution..." -ForegroundColor Blue +dotnet build --configuration Release + +if ($LASTEXITCODE -eq 0) { + Write-Host "Build completed successfully!" -ForegroundColor Green + + # Show build output + Write-Host "`nBuild output:" -ForegroundColor Yellow + dotnet build --configuration Release --verbosity quiet + + Write-Host "`nTo run the application:" -ForegroundColor Cyan + Write-Host "dotnet run --project QemuVmManager.Console" -ForegroundColor White +} else { + Write-Host "Build failed!" -ForegroundColor Red + exit 1 +} diff --git a/examples/sample-vm-config.json b/examples/sample-vm-config.json new file mode 100644 index 0000000..94ea07c --- /dev/null +++ b/examples/sample-vm-config.json @@ -0,0 +1,81 @@ +{ + "name": "ubuntu-desktop", + "description": "Ubuntu Desktop VM for development", + "cpu": { + "cores": 4, + "sockets": 1, + "threads": 1, + "model": "qemu64", + "enableKvm": true + }, + "memory": { + "size": 8192, + "unit": "M" + }, + "storage": { + "disks": [ + { + "path": "C:\\Users\\mahes\\Disks\\ubuntu-desktop.qcow2", + "size": 50, + "format": "qcow2", + "interface": "virtio", + "cache": "writeback", + "isBoot": true + }, + { + "path": "C:\\Users\\mahes\\Disks\\ubuntu-data.qcow2", + "size": 100, + "format": "qcow2", + "interface": "virtio", + "cache": "writeback", + "isBoot": false + } + ], + "cdrom": "C:\\Users\\mahes\\Downloads\\ubuntu-24.04.3-desktop-amd64.iso" + }, + "network": { + "interfaces": [ + { + "type": "bridge", + "model": "virtio-net-pci", + "mac": "52:54:00:12:34:56", + "bridge": "virbr0" + } + ], + "bridge": "virbr0" + }, + "display": { + "type": "gtk", + "vga": "virtio", + "resolution": "1920x1080", + "enableSpice": true, + "spicePort": 5930 + }, + "boot": { + "order": ["c", "d", "n"], + "kernel": null, + "initrd": null, + "cmdline": null + }, + "advanced": { + "enableAudio": true, + "enableUsb": true, + "enableBalloon": true, + "enableVirtioRng": true, + "enableVirtioFs": false, + "sharedFolders": [ + { + "hostPath": "C:\\Users\\mahes\\Shared", + "guestPath": "/mnt/shared", + "readOnly": false + } + ], + "extraArgs": [ + "-enable-kvm", + "-cpu", + "host" + ] + }, + "created": "2024-01-01T10:00:00Z", + "lastModified": "2024-01-01T10:00:00Z" +}