476 lines
15 KiB
C#
476 lines
15 KiB
C#
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 async Task StopPerformanceMonitoringAsync(string vmName)
|
|
{
|
|
_processManager.StopPerformanceMonitoring(vmName);
|
|
await Task.CompletedTask;
|
|
}
|
|
|
|
public async Task<bool> IsPerformanceMonitoringActiveAsync(string vmName)
|
|
{
|
|
return _processManager.GetAllVmStatuses().Any(s => s.Name == vmName && s.State == VmState.Running);
|
|
}
|
|
|
|
public async Task<VmPerformanceMetrics?> GetCurrentPerformanceMetricsAsync(string vmName)
|
|
{
|
|
return await _processManager.GetVmPerformanceMetricsAsync(vmName);
|
|
}
|
|
|
|
public async Task<List<VmPerformanceMetrics>> GetPerformanceHistoryAsync(string vmName)
|
|
{
|
|
return await _processManager.GetPerformanceHistoryAsync(vmName);
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|