Initial commit
This commit is contained in:
380
.gitignore
vendored
Normal file
380
.gitignore
vendored
Normal file
@@ -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
|
95
QUICKSTART.md
Normal file
95
QUICKSTART.md
Normal file
@@ -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 <repository-url>
|
||||
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 <name>` - Start a VM
|
||||
- `stop <name>` - Stop a VM
|
||||
- `pause <name>` - Pause a VM
|
||||
- `resume <name>` - Resume a VM
|
||||
- `delete <name>` - 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
|
904
QemuVmManager.Console/Program.cs
Normal file
904
QemuVmManager.Console/Program.cs
Normal file
@@ -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 <name> - Create a new VM (interactive)");
|
||||
System.Console.WriteLine(" start <name> - Start a VM");
|
||||
System.Console.WriteLine(" stop <name> [--force] - Stop a VM");
|
||||
System.Console.WriteLine(" pause <name> - Pause a VM");
|
||||
System.Console.WriteLine(" resume <name> - Resume a VM");
|
||||
System.Console.WriteLine(" delete <name> - Delete a VM");
|
||||
System.Console.WriteLine(" clone <source> <target> - Clone a VM");
|
||||
System.Console.WriteLine(" export <name> <path> - Export VM configuration");
|
||||
System.Console.WriteLine(" import <path> [name] - Import VM configuration");
|
||||
System.Console.WriteLine(" status [name] - Show VM status");
|
||||
System.Console.WriteLine(" config <name> - Show VM configuration");
|
||||
System.Console.WriteLine(" disk <name> [info|resize|convert] - Manage disk images");
|
||||
System.Console.WriteLine(" validate <name> - Validate VM disk images");
|
||||
System.Console.WriteLine(" diagnose - Diagnose system and QEMU installation");
|
||||
System.Console.WriteLine(" monitor <name> [start|stop|status] - Performance monitoring");
|
||||
System.Console.WriteLine(" metrics <name> [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<DiskConfiguration>
|
||||
{
|
||||
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<NetworkInterfaceConfiguration>
|
||||
{
|
||||
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 <vm-name>");
|
||||
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 <vm-name> [--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 <vm-name>");
|
||||
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 <vm-name>");
|
||||
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 <vm-name>");
|
||||
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 <source-vm> <target-vm>");
|
||||
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 <vm-name> <export-path>");
|
||||
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 <import-path> [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 <vm-name>");
|
||||
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 <vm-name> [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 <vm-name> resize <disk-index> <new-size-gb>");
|
||||
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 <vm-name> convert <disk-index> <new-format>");
|
||||
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 <vm-name>");
|
||||
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 <vm-name> 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 <vm-name> [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 <vm-name> [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();
|
||||
}
|
||||
}
|
15
QemuVmManager.Console/QemuVmManager.Console.csproj
Normal file
15
QemuVmManager.Console/QemuVmManager.Console.csproj
Normal file
@@ -0,0 +1,15 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\QemuVmManager.Services\QemuVmManager.Services.csproj" />
|
||||
<ProjectReference Include="..\QemuVmManager.Models\QemuVmManager.Models.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
312
QemuVmManager.Core/DiskManager.cs
Normal file
312
QemuVmManager.Core/DiskManager.cs
Normal file
@@ -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<bool> 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<bool> 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<DiskInfo> 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<bool> 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<bool> 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; }
|
||||
}
|
356
QemuVmManager.Core/QemuCommandBuilder.cs
Normal file
356
QemuVmManager.Core/QemuCommandBuilder.cs
Normal file
@@ -0,0 +1,356 @@
|
||||
using QemuVmManager.Models;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace QemuVmManager.Core;
|
||||
|
||||
public class QemuCommandBuilder
|
||||
{
|
||||
private readonly VmConfiguration _config;
|
||||
private readonly List<string> _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
|
||||
}
|
||||
}
|
927
QemuVmManager.Core/QemuProcessManager.cs
Normal file
927
QemuVmManager.Core/QemuProcessManager.cs
Normal file
@@ -0,0 +1,927 @@
|
||||
using System.Diagnostics;
|
||||
using System.Threading;
|
||||
using QemuVmManager.Models;
|
||||
|
||||
namespace QemuVmManager.Core;
|
||||
|
||||
public class QemuProcessManager
|
||||
{
|
||||
private readonly Dictionary<string, Process> _runningVms = new();
|
||||
private readonly Dictionary<string, VmStatus> _vmStatuses = new();
|
||||
private readonly Dictionary<string, PerformanceMonitor> _performanceMonitors = new();
|
||||
|
||||
public event EventHandler<VmStatusChangedEventArgs>? 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<bool> 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<string>();
|
||||
var errorLines = new List<string>();
|
||||
|
||||
// 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<bool> 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<bool> 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<bool> 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<VmStatus> 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<VmResourceUsage> 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<string> outputLines, List<string> 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<VmPerformanceMetrics> 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<List<VmPerformanceMetrics>> GetPerformanceHistoryAsync(string vmName, int maxSamples = 100)
|
||||
{
|
||||
if (_performanceMonitors.TryGetValue(vmName, out var monitor))
|
||||
{
|
||||
return await monitor.GetPerformanceHistoryAsync(maxSamples);
|
||||
}
|
||||
return new List<VmPerformanceMetrics>();
|
||||
}
|
||||
}
|
||||
|
||||
public class PerformanceMonitor
|
||||
{
|
||||
private readonly Process _process;
|
||||
private readonly List<VmPerformanceMetrics> _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<VmPerformanceMetrics> 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<List<VmPerformanceMetrics>> 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;
|
||||
}
|
||||
}
|
17
QemuVmManager.Core/QemuVmManager.Core.csproj
Normal file
17
QemuVmManager.Core/QemuVmManager.Core.csproj
Normal file
@@ -0,0 +1,17 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\QemuVmManager.Models\QemuVmManager.Models.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="System.Diagnostics.PerformanceCounter" Version="8.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
13
QemuVmManager.Models/QemuVmManager.Models.csproj
Normal file
13
QemuVmManager.Models/QemuVmManager.Models.csproj
Normal file
@@ -0,0 +1,13 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="System.Text.Json" Version="8.0.5" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
29
QemuVmManager.Models/VirtualizationType.cs
Normal file
29
QemuVmManager.Models/VirtualizationType.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
namespace QemuVmManager.Models;
|
||||
|
||||
public enum VirtualizationType
|
||||
{
|
||||
/// <summary>
|
||||
/// Kernel-based Virtual Machine (Linux)
|
||||
/// </summary>
|
||||
KVM,
|
||||
|
||||
/// <summary>
|
||||
/// Hyper-V (Windows)
|
||||
/// </summary>
|
||||
HyperV,
|
||||
|
||||
/// <summary>
|
||||
/// Intel Hardware Accelerated Execution Manager (Windows/macOS)
|
||||
/// </summary>
|
||||
HAXM,
|
||||
|
||||
/// <summary>
|
||||
/// Hypervisor.framework (macOS)
|
||||
/// </summary>
|
||||
HVF,
|
||||
|
||||
/// <summary>
|
||||
/// Tiny Code Generator (Software emulation)
|
||||
/// </summary>
|
||||
TCG
|
||||
}
|
189
QemuVmManager.Models/VmConfiguration.cs
Normal file
189
QemuVmManager.Models/VmConfiguration.cs
Normal file
@@ -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<DiskConfiguration> 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<NetworkInterfaceConfiguration> 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<string> 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<SharedFolderConfiguration> SharedFolders { get; set; } = new();
|
||||
|
||||
[JsonPropertyName("extraArgs")]
|
||||
public List<string> 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;
|
||||
}
|
44
QemuVmManager.Models/VmStatus.cs
Normal file
44
QemuVmManager.Models/VmStatus.cs
Normal file
@@ -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; }
|
||||
}
|
18
QemuVmManager.Services/QemuVmManager.Services.csproj
Normal file
18
QemuVmManager.Services/QemuVmManager.Services.csproj
Normal file
@@ -0,0 +1,18 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\QemuVmManager.Core\QemuVmManager.Core.csproj" />
|
||||
<ProjectReference Include="..\QemuVmManager.Models\QemuVmManager.Models.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="System.Text.Json" Version="8.0.5" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
464
QemuVmManager.Services/VmManagementService.cs
Normal file
464
QemuVmManager.Services/VmManagementService.cs
Normal file
@@ -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<string, VmConfiguration> _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<VmConfiguration> 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<bool> 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<bool> StopVmAsync(string vmName, bool force = false)
|
||||
{
|
||||
return await _processManager.StopVmAsync(vmName, force);
|
||||
}
|
||||
|
||||
public async Task<bool> PauseVmAsync(string vmName)
|
||||
{
|
||||
return await _processManager.PauseVmAsync(vmName);
|
||||
}
|
||||
|
||||
public async Task<bool> ResumeVmAsync(string vmName)
|
||||
{
|
||||
return await _processManager.ResumeVmAsync(vmName);
|
||||
}
|
||||
|
||||
public async Task<VmConfiguration> 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<VmConfiguration> GetAllVmConfigurations()
|
||||
{
|
||||
return _vmConfigurations.Values;
|
||||
}
|
||||
|
||||
public VmStatus? GetVmStatus(string vmName)
|
||||
{
|
||||
return _processManager.GetVmStatus(vmName);
|
||||
}
|
||||
|
||||
public IEnumerable<VmStatus> GetAllVmStatuses()
|
||||
{
|
||||
return _processManager.GetAllVmStatuses();
|
||||
}
|
||||
|
||||
public bool IsVmRunning(string vmName)
|
||||
{
|
||||
return _processManager.IsVmRunning(vmName);
|
||||
}
|
||||
|
||||
public async Task<VmConfiguration> 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<string> 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<VmConfiguration> 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<VmConfiguration>(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<DiskInfo> 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<bool> 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<bool> 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<VmConfiguration>(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<VmConfiguration>(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<VmPerformanceMetrics> GetVmPerformanceMetricsAsync(string vmName)
|
||||
{
|
||||
return await _processManager.GetVmPerformanceMetricsAsync(vmName);
|
||||
}
|
||||
|
||||
public async Task<List<VmPerformanceMetrics>> GetPerformanceHistoryAsync(string vmName, int maxSamples = 100)
|
||||
{
|
||||
return await _processManager.GetPerformanceHistoryAsync(vmName, maxSamples);
|
||||
}
|
||||
}
|
39
QemuVmManager.sln
Normal file
39
QemuVmManager.sln
Normal file
@@ -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
|
308
README.md
Normal file
308
README.md
Normal file
@@ -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 <repository-url>
|
||||
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 <name>`
|
||||
Start a VM by name.
|
||||
|
||||
Example:
|
||||
```
|
||||
qemu-vm> start my-vm
|
||||
```
|
||||
|
||||
#### `stop <name> [--force]`
|
||||
Stop a VM gracefully. Use `--force` for immediate termination.
|
||||
|
||||
Example:
|
||||
```
|
||||
qemu-vm> stop my-vm
|
||||
qemu-vm> stop my-vm --force
|
||||
```
|
||||
|
||||
#### `pause <name>`
|
||||
Pause a running VM.
|
||||
|
||||
Example:
|
||||
```
|
||||
qemu-vm> pause my-vm
|
||||
```
|
||||
|
||||
#### `resume <name>`
|
||||
Resume a paused VM.
|
||||
|
||||
Example:
|
||||
```
|
||||
qemu-vm> resume my-vm
|
||||
```
|
||||
|
||||
#### `delete <name>`
|
||||
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 <source> <target>`
|
||||
Clone an existing VM configuration.
|
||||
|
||||
Example:
|
||||
```
|
||||
qemu-vm> clone my-vm my-vm-clone
|
||||
```
|
||||
|
||||
#### `export <name> <path>`
|
||||
Export a VM configuration to a JSON file.
|
||||
|
||||
Example:
|
||||
```
|
||||
qemu-vm> export my-vm /tmp/my-vm-config.json
|
||||
```
|
||||
|
||||
#### `import <path> [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 <name>`
|
||||
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 `<vm-name>.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
|
33
build.bat
Normal file
33
build.bat
Normal file
@@ -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
|
39
build.ps1
Normal file
39
build.ps1
Normal file
@@ -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
|
||||
}
|
81
examples/sample-vm-config.json
Normal file
81
examples/sample-vm-config.json
Normal file
@@ -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"
|
||||
}
|
Reference in New Issue
Block a user