P2P abilities
This commit is contained in:
521
QemuVmManager.Console/P2PConsole.cs
Normal file
521
QemuVmManager.Console/P2PConsole.cs
Normal file
@@ -0,0 +1,521 @@
|
||||
using QemuVmManager.Core;
|
||||
using QemuVmManager.Models;
|
||||
using QemuVmManager.Services;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace QemuVmManager.Console;
|
||||
|
||||
public class P2PConsole
|
||||
{
|
||||
private readonly P2PNode _p2pNode;
|
||||
private readonly VmManagementService _vmService;
|
||||
private readonly ILogger<P2PConsole>? _logger;
|
||||
private bool _isRunning = false;
|
||||
|
||||
public P2PConsole(string nodeId, int port = 8080, ILogger<P2PConsole>? logger = null)
|
||||
{
|
||||
_p2pNode = new P2PNode(nodeId, port, logger: null);
|
||||
_vmService = new VmManagementService();
|
||||
_logger = logger;
|
||||
|
||||
// Subscribe to events
|
||||
_p2pNode.RoleChanged += OnRoleChanged;
|
||||
_p2pNode.NodeJoined += OnNodeJoined;
|
||||
_p2pNode.NodeLeft += OnNodeLeft;
|
||||
_p2pNode.VmStarted += OnVmStarted;
|
||||
_p2pNode.VmStopped += OnVmStopped;
|
||||
}
|
||||
|
||||
public async Task StartAsync()
|
||||
{
|
||||
if (_isRunning)
|
||||
return;
|
||||
|
||||
System.Console.WriteLine("=== QEMU P2P Cluster Manager ===");
|
||||
System.Console.WriteLine($"Node ID: {_p2pNode.CurrentNode.NodeId}");
|
||||
System.Console.WriteLine($"Hostname: {_p2pNode.CurrentNode.Hostname}");
|
||||
System.Console.WriteLine($"IP Address: {_p2pNode.CurrentNode.IpAddress}");
|
||||
System.Console.WriteLine($"Port: {_p2pNode.CurrentNode.Port}");
|
||||
System.Console.WriteLine();
|
||||
System.Console.WriteLine("Type 'help' for available commands");
|
||||
System.Console.WriteLine();
|
||||
|
||||
await _p2pNode.StartAsync();
|
||||
_isRunning = true;
|
||||
|
||||
await RunInteractiveModeAsync();
|
||||
}
|
||||
|
||||
public async Task StopAsync()
|
||||
{
|
||||
if (!_isRunning)
|
||||
return;
|
||||
|
||||
await _p2pNode.StopAsync();
|
||||
_isRunning = false;
|
||||
}
|
||||
|
||||
private async Task RunInteractiveModeAsync()
|
||||
{
|
||||
while (_isRunning)
|
||||
{
|
||||
try
|
||||
{
|
||||
System.Console.Write($"p2p-{_p2pNode.CurrentNode.NodeId}> ");
|
||||
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 "status":
|
||||
await ShowStatusAsync();
|
||||
break;
|
||||
case "cluster":
|
||||
await ShowClusterAsync();
|
||||
break;
|
||||
case "nodes":
|
||||
await ShowNodesAsync();
|
||||
break;
|
||||
case "vms":
|
||||
await ShowVmsAsync();
|
||||
break;
|
||||
case "start":
|
||||
await StartVmAsync(arguments);
|
||||
break;
|
||||
case "stop":
|
||||
await StopVmAsync(arguments);
|
||||
break;
|
||||
case "migrate":
|
||||
await MigrateVmAsync(arguments);
|
||||
break;
|
||||
case "forward":
|
||||
await ForwardPortAsync(arguments);
|
||||
break;
|
||||
case "create":
|
||||
await CreateVmAsync(arguments);
|
||||
break;
|
||||
case "list":
|
||||
await ListVmsAsync();
|
||||
break;
|
||||
case "upnp":
|
||||
await ShowUPnPStatusAsync();
|
||||
break;
|
||||
case "exit":
|
||||
case "quit":
|
||||
System.Console.WriteLine("Stopping P2P node...");
|
||||
await StopAsync();
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowHelp()
|
||||
{
|
||||
System.Console.WriteLine("Available commands:");
|
||||
System.Console.WriteLine(" status - Show current node status");
|
||||
System.Console.WriteLine(" cluster - Show cluster information");
|
||||
System.Console.WriteLine(" nodes - List all nodes in cluster");
|
||||
System.Console.WriteLine(" vms - List all VMs in cluster");
|
||||
System.Console.WriteLine(" list - List local VM configurations");
|
||||
System.Console.WriteLine(" create <name> - Create a new VM (interactive)");
|
||||
System.Console.WriteLine(" start <name> [node-id] - Start a VM (optionally on specific node)");
|
||||
System.Console.WriteLine(" stop <vm-id> - Stop a VM");
|
||||
System.Console.WriteLine(" migrate <vm-id> <node-id> - Migrate VM to different node");
|
||||
System.Console.WriteLine(" forward <vm-id> <port> - Forward port for VM (master only)");
|
||||
System.Console.WriteLine(" upnp - Show UPnP status");
|
||||
System.Console.WriteLine(" help - Show this help");
|
||||
System.Console.WriteLine(" exit/quit - Exit the application");
|
||||
}
|
||||
|
||||
private async Task ShowStatusAsync()
|
||||
{
|
||||
var node = _p2pNode.CurrentNode;
|
||||
System.Console.WriteLine($"=== Node Status ===");
|
||||
System.Console.WriteLine($"Node ID: {node.NodeId}");
|
||||
System.Console.WriteLine($"Hostname: {node.Hostname}");
|
||||
System.Console.WriteLine($"IP Address: {node.IpAddress}");
|
||||
System.Console.WriteLine($"Port: {node.Port}");
|
||||
System.Console.WriteLine($"Role: {node.Role}");
|
||||
System.Console.WriteLine($"State: {node.State}");
|
||||
System.Console.WriteLine($"Is Master: {_p2pNode.IsMaster}");
|
||||
System.Console.WriteLine();
|
||||
|
||||
System.Console.WriteLine("=== System Info ===");
|
||||
var sysInfo = node.SystemInfo;
|
||||
System.Console.WriteLine($"OS: {sysInfo.OsName} {sysInfo.OsVersion}");
|
||||
System.Console.WriteLine($"Architecture: {sysInfo.Architecture}");
|
||||
System.Console.WriteLine($"CPU Cores: {sysInfo.CpuCores}");
|
||||
System.Console.WriteLine($"Total Memory: {sysInfo.TotalMemory:N0} MB");
|
||||
System.Console.WriteLine($"Available Memory: {sysInfo.AvailableMemory:N0} MB");
|
||||
System.Console.WriteLine($"Virtualization: {sysInfo.AvailableVirtualization}");
|
||||
System.Console.WriteLine($"QEMU Installed: {sysInfo.QemuInstalled}");
|
||||
System.Console.WriteLine($"QEMU Version: {sysInfo.QemuVersion}");
|
||||
}
|
||||
|
||||
private async Task ShowClusterAsync()
|
||||
{
|
||||
var cluster = _p2pNode.ClusterState;
|
||||
System.Console.WriteLine($"=== Cluster Information ===");
|
||||
System.Console.WriteLine($"Cluster ID: {cluster.ClusterId}");
|
||||
System.Console.WriteLine($"Last Updated: {cluster.LastUpdated:yyyy-MM-dd HH:mm:ss}");
|
||||
System.Console.WriteLine($"Total Nodes: {cluster.Nodes.Count}");
|
||||
System.Console.WriteLine($"Total VMs: {cluster.DistributedVms.Count}");
|
||||
|
||||
if (cluster.MasterNode != null)
|
||||
{
|
||||
System.Console.WriteLine();
|
||||
System.Console.WriteLine("=== Master Node ===");
|
||||
System.Console.WriteLine($"Node ID: {cluster.MasterNode.NodeId}");
|
||||
System.Console.WriteLine($"Hostname: {cluster.MasterNode.Hostname}");
|
||||
System.Console.WriteLine($"IP Address: {cluster.MasterNode.IpAddress}");
|
||||
System.Console.WriteLine($"Last Seen: {cluster.MasterNode.LastSeen:yyyy-MM-dd HH:mm:ss}");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ShowNodesAsync()
|
||||
{
|
||||
var cluster = _p2pNode.ClusterState;
|
||||
if (cluster.Nodes.Count == 0)
|
||||
{
|
||||
System.Console.WriteLine("No nodes in cluster.");
|
||||
return;
|
||||
}
|
||||
|
||||
System.Console.WriteLine($"{"Node ID",-20} {"Hostname",-15} {"IP Address",-15} {"Role",-10} {"State",-10} {"Last Seen"}");
|
||||
System.Console.WriteLine(new string('-', 90));
|
||||
|
||||
foreach (var node in cluster.Nodes)
|
||||
{
|
||||
var lastSeen = node.LastSeen.ToString("yyyy-MM-dd HH:mm:ss");
|
||||
System.Console.WriteLine($"{node.NodeId,-20} {node.Hostname,-15} {node.IpAddress,-15} {node.Role,-10} {node.State,-10} {lastSeen}");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ShowVmsAsync()
|
||||
{
|
||||
var cluster = _p2pNode.ClusterState;
|
||||
if (cluster.DistributedVms.Count == 0)
|
||||
{
|
||||
System.Console.WriteLine("No VMs in cluster.");
|
||||
return;
|
||||
}
|
||||
|
||||
System.Console.WriteLine($"{"VM ID",-36} {"Name",-20} {"Node ID",-20} {"State",-10} {"Started"}");
|
||||
System.Console.WriteLine(new string('-', 100));
|
||||
|
||||
foreach (var vm in cluster.DistributedVms.Values)
|
||||
{
|
||||
var started = vm.StartedAt.ToString("yyyy-MM-dd HH:mm:ss");
|
||||
System.Console.WriteLine($"{vm.VmId,-36} {vm.VmName,-20} {vm.NodeId,-20} {vm.State,-10} {started}");
|
||||
|
||||
if (vm.PublicEndpoint != null)
|
||||
{
|
||||
System.Console.WriteLine($" Public: {vm.PublicEndpoint.PublicIp}:{vm.PublicEndpoint.PublicPort}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ListVmsAsync()
|
||||
{
|
||||
var configs = _vmService.GetAllVmConfigurations().ToList();
|
||||
if (configs.Count == 0)
|
||||
{
|
||||
System.Console.WriteLine("No VM configurations found.");
|
||||
return;
|
||||
}
|
||||
|
||||
System.Console.WriteLine($"{"Name",-20} {"CPU",-8} {"Memory",-10} {"Description"}");
|
||||
System.Console.WriteLine(new string('-', 70));
|
||||
|
||||
foreach (var config in configs)
|
||||
{
|
||||
var cpuText = $"{config.Cpu.Cores} cores";
|
||||
var memoryText = $"{config.Memory.Size}{config.Memory.Unit}";
|
||||
System.Console.WriteLine($"{config.Name,-20} {cpuText,-8} {memoryText,-10} {config.Description}");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CreateVmAsync(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 = "user",
|
||||
Model = "e1000",
|
||||
Mac = GetUserInput("MAC address (auto): ", "")
|
||||
}
|
||||
}
|
||||
},
|
||||
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.");
|
||||
}
|
||||
|
||||
private async Task StartVmAsync(string[] arguments)
|
||||
{
|
||||
if (arguments.Length == 0)
|
||||
{
|
||||
System.Console.WriteLine("Usage: start <vm-name> [node-id]");
|
||||
return;
|
||||
}
|
||||
|
||||
var vmName = arguments[0];
|
||||
var targetNodeId = arguments.Length > 1 ? arguments[1] : null;
|
||||
|
||||
try
|
||||
{
|
||||
var config = _vmService.GetVmConfiguration(vmName);
|
||||
if (config == null)
|
||||
{
|
||||
System.Console.WriteLine($"VM configuration '{vmName}' not found.");
|
||||
return;
|
||||
}
|
||||
|
||||
var vm = await _p2pNode.StartVmAsync(config, targetNodeId);
|
||||
System.Console.WriteLine($"VM '{vmName}' started successfully with ID: {vm.VmId}");
|
||||
|
||||
if (targetNodeId != null)
|
||||
{
|
||||
System.Console.WriteLine($"VM is running on node: {targetNodeId}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Console.WriteLine($"Failed to start VM: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task StopVmAsync(string[] arguments)
|
||||
{
|
||||
if (arguments.Length == 0)
|
||||
{
|
||||
System.Console.WriteLine("Usage: stop <vm-id>");
|
||||
return;
|
||||
}
|
||||
|
||||
var vmId = arguments[0];
|
||||
|
||||
try
|
||||
{
|
||||
var success = await _p2pNode.StopVmAsync(vmId);
|
||||
if (success)
|
||||
{
|
||||
System.Console.WriteLine($"VM '{vmId}' stopped successfully.");
|
||||
}
|
||||
else
|
||||
{
|
||||
System.Console.WriteLine($"Failed to stop VM '{vmId}'.");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Console.WriteLine($"Error stopping VM: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task MigrateVmAsync(string[] arguments)
|
||||
{
|
||||
if (arguments.Length < 2)
|
||||
{
|
||||
System.Console.WriteLine("Usage: migrate <vm-id> <target-node-id>");
|
||||
return;
|
||||
}
|
||||
|
||||
var vmId = arguments[0];
|
||||
var targetNodeId = arguments[1];
|
||||
|
||||
try
|
||||
{
|
||||
var response = await _p2pNode.MigrateVmAsync(vmId, targetNodeId);
|
||||
if (response.Success)
|
||||
{
|
||||
System.Console.WriteLine($"VM '{vmId}' migrated to node '{targetNodeId}' successfully.");
|
||||
}
|
||||
else
|
||||
{
|
||||
System.Console.WriteLine($"Failed to migrate VM: {response.ErrorMessage}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Console.WriteLine($"Error migrating VM: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ForwardPortAsync(string[] arguments)
|
||||
{
|
||||
if (arguments.Length < 2)
|
||||
{
|
||||
System.Console.WriteLine("Usage: forward <vm-id> <private-port> [public-port]");
|
||||
return;
|
||||
}
|
||||
|
||||
var vmId = arguments[0];
|
||||
var privatePort = int.Parse(arguments[1]);
|
||||
var publicPort = arguments.Length > 2 ? int.Parse(arguments[2]) : (int?)null;
|
||||
|
||||
try
|
||||
{
|
||||
var response = await _p2pNode.RequestPortForwardingAsync(vmId, privatePort, publicPort);
|
||||
if (response.Success)
|
||||
{
|
||||
System.Console.WriteLine($"Port forwarding created successfully.");
|
||||
System.Console.WriteLine($"Public IP: {response.PublicIp}");
|
||||
System.Console.WriteLine($"Public Port: {response.PublicPort}");
|
||||
System.Console.WriteLine($"Private Port: {privatePort}");
|
||||
}
|
||||
else
|
||||
{
|
||||
System.Console.WriteLine($"Failed to create port forwarding: {response.ErrorMessage}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Console.WriteLine($"Error creating port forwarding: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ShowUPnPStatusAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var upnpManager = new UPnPManager();
|
||||
var isAvailable = await upnpManager.IsUPnPAvailableAsync();
|
||||
var externalIp = await upnpManager.GetExternalIpAddressAsync();
|
||||
var mappings = await upnpManager.GetPortMappingsAsync();
|
||||
|
||||
System.Console.WriteLine("=== UPnP Status ===");
|
||||
System.Console.WriteLine($"Available: {isAvailable}");
|
||||
System.Console.WriteLine($"External IP: {externalIp}");
|
||||
System.Console.WriteLine($"Active Mappings: {mappings.Count}");
|
||||
|
||||
if (mappings.Count > 0)
|
||||
{
|
||||
System.Console.WriteLine();
|
||||
System.Console.WriteLine("=== Port Mappings ===");
|
||||
System.Console.WriteLine($"{"External Port",-15} {"Internal Port",-15} {"Protocol",-10} {"Description"}");
|
||||
System.Console.WriteLine(new string('-', 70));
|
||||
|
||||
foreach (var mapping in mappings)
|
||||
{
|
||||
System.Console.WriteLine($"{mapping.ExternalPort,-15} {mapping.InternalPort,-15} {mapping.Protocol,-10} {mapping.Description}");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Console.WriteLine($"Error checking UPnP status: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnRoleChanged(object? sender, NodeRole role)
|
||||
{
|
||||
System.Console.WriteLine($"Role changed to: {role}");
|
||||
if (role == NodeRole.Master)
|
||||
{
|
||||
System.Console.WriteLine("🎉 This node is now the MASTER!");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnNodeJoined(object? sender, NodeInfo node)
|
||||
{
|
||||
System.Console.WriteLine($"Node joined: {node.NodeId} ({node.IpAddress})");
|
||||
}
|
||||
|
||||
private void OnNodeLeft(object? sender, NodeInfo node)
|
||||
{
|
||||
System.Console.WriteLine($"Node left: {node.NodeId}");
|
||||
}
|
||||
|
||||
private void OnVmStarted(object? sender, VmInstance vm)
|
||||
{
|
||||
System.Console.WriteLine($"VM started: {vm.VmName} (ID: {vm.VmId}) on node {vm.NodeId}");
|
||||
}
|
||||
|
||||
private void OnVmStopped(object? sender, VmInstance vm)
|
||||
{
|
||||
System.Console.WriteLine($"VM stopped: {vm.VmName} (ID: {vm.VmId})");
|
||||
}
|
||||
|
||||
private string GetUserInput(string prompt, string defaultValue = "")
|
||||
{
|
||||
System.Console.Write(prompt);
|
||||
var input = System.Console.ReadLine()?.Trim();
|
||||
return string.IsNullOrEmpty(input) ? defaultValue : input;
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
834
QemuVmManager.Core/P2PNode.cs
Normal file
834
QemuVmManager.Core/P2PNode.cs
Normal file
@@ -0,0 +1,834 @@
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using QemuVmManager.Models;
|
||||
|
||||
namespace QemuVmManager.Core;
|
||||
|
||||
public class P2PNode : IDisposable
|
||||
{
|
||||
private readonly string _nodeId;
|
||||
private readonly int _port;
|
||||
private readonly IUPnPManager _upnpManager;
|
||||
private readonly QemuProcessManager _qemuManager;
|
||||
private readonly ILogger<P2PNode>? _logger;
|
||||
|
||||
private readonly Dictionary<string, NodeInfo> _knownNodes = new();
|
||||
private readonly Dictionary<string, VmInstance> _localVms = new();
|
||||
private readonly object _clusterLock = new();
|
||||
|
||||
private UdpClient? _udpClient;
|
||||
private TcpListener? _tcpListener;
|
||||
private CancellationTokenSource? _cancellationTokenSource;
|
||||
private Task? _heartbeatTask;
|
||||
private Task? _electionTask;
|
||||
private Task? _discoveryTask;
|
||||
|
||||
private NodeRole _currentRole = NodeRole.Follower;
|
||||
private long _currentTerm = 0;
|
||||
private string? _votedFor;
|
||||
private DateTime _lastHeartbeat = DateTime.UtcNow;
|
||||
private bool _isRunning = false;
|
||||
|
||||
public event EventHandler<NodeRole>? RoleChanged;
|
||||
public event EventHandler<NodeInfo>? NodeJoined;
|
||||
public event EventHandler<NodeInfo>? NodeLeft;
|
||||
public event EventHandler<VmInstance>? VmStarted;
|
||||
public event EventHandler<VmInstance>? VmStopped;
|
||||
|
||||
public NodeInfo CurrentNode { get; private set; }
|
||||
public ClusterState ClusterState { get; private set; } = new();
|
||||
public bool IsMaster => _currentRole == NodeRole.Master;
|
||||
|
||||
public P2PNode(string nodeId, int port = 8080, IUPnPManager? upnpManager = null,
|
||||
QemuProcessManager? qemuManager = null, ILogger<P2PNode>? logger = null)
|
||||
{
|
||||
_nodeId = nodeId;
|
||||
_port = port;
|
||||
_upnpManager = upnpManager ?? new UPnPManager();
|
||||
_qemuManager = qemuManager ?? new QemuProcessManager();
|
||||
_logger = logger;
|
||||
|
||||
CurrentNode = new NodeInfo
|
||||
{
|
||||
NodeId = _nodeId,
|
||||
Hostname = Environment.MachineName,
|
||||
IpAddress = GetLocalIpAddress(),
|
||||
Port = _port,
|
||||
Role = NodeRole.Follower,
|
||||
State = NodeState.Stopped,
|
||||
SystemInfo = GetSystemInfo()
|
||||
};
|
||||
}
|
||||
|
||||
public async Task StartAsync()
|
||||
{
|
||||
if (_isRunning)
|
||||
return;
|
||||
|
||||
_logger?.LogInformation("Starting P2P node {NodeId} on port {Port}", _nodeId, _port);
|
||||
|
||||
_cancellationTokenSource = new CancellationTokenSource();
|
||||
_isRunning = true;
|
||||
CurrentNode.State = NodeState.Starting;
|
||||
|
||||
try
|
||||
{
|
||||
// Initialize network components
|
||||
await InitializeNetworkAsync();
|
||||
|
||||
// Start background tasks
|
||||
_heartbeatTask = Task.Run(() => HeartbeatLoopAsync(_cancellationTokenSource.Token));
|
||||
_electionTask = Task.Run(() => ElectionLoopAsync(_cancellationTokenSource.Token));
|
||||
_discoveryTask = Task.Run(() => DiscoveryLoopAsync(_cancellationTokenSource.Token));
|
||||
|
||||
CurrentNode.State = NodeState.Running;
|
||||
_logger?.LogInformation("P2P node {NodeId} started successfully", _nodeId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogError(ex, "Failed to start P2P node {NodeId}", _nodeId);
|
||||
CurrentNode.State = NodeState.Error;
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task StopAsync()
|
||||
{
|
||||
if (!_isRunning)
|
||||
return;
|
||||
|
||||
_logger?.LogInformation("Stopping P2P node {NodeId}", _nodeId);
|
||||
|
||||
_isRunning = false;
|
||||
CurrentNode.State = NodeState.Stopping;
|
||||
|
||||
_cancellationTokenSource?.Cancel();
|
||||
|
||||
// Stop all local VMs
|
||||
foreach (var vm in _localVms.Values.ToList())
|
||||
{
|
||||
await StopLocalVmAsync(vm.VmId);
|
||||
}
|
||||
|
||||
// Clean up network resources
|
||||
_udpClient?.Close();
|
||||
_tcpListener?.Stop();
|
||||
|
||||
CurrentNode.State = NodeState.Stopped;
|
||||
_logger?.LogInformation("P2P node {NodeId} stopped", _nodeId);
|
||||
}
|
||||
|
||||
public async Task<VmInstance> StartVmAsync(VmConfiguration config, string? targetNodeId = null)
|
||||
{
|
||||
var vmId = Guid.NewGuid().ToString();
|
||||
var targetNode = targetNodeId ?? _nodeId;
|
||||
|
||||
if (targetNode == _nodeId)
|
||||
{
|
||||
// Start VM locally
|
||||
return await StartLocalVmAsync(vmId, config);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Request remote node to start VM
|
||||
return await RequestRemoteVmStartAsync(vmId, config, targetNode);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> StopVmAsync(string vmId)
|
||||
{
|
||||
if (_localVms.ContainsKey(vmId))
|
||||
{
|
||||
return await StopLocalVmAsync(vmId);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Find which node has this VM and request stop
|
||||
var vm = ClusterState.DistributedVms.GetValueOrDefault(vmId);
|
||||
if (vm != null)
|
||||
{
|
||||
return await RequestRemoteVmStopAsync(vmId, vm.NodeId);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public async Task<PortForwardingResponse> RequestPortForwardingAsync(string vmId, int privatePort, int? publicPort = null)
|
||||
{
|
||||
if (!IsMaster)
|
||||
{
|
||||
throw new InvalidOperationException("Only the master node can request port forwarding");
|
||||
}
|
||||
|
||||
var vm = ClusterState.DistributedVms.GetValueOrDefault(vmId);
|
||||
if (vm == null)
|
||||
{
|
||||
return new PortForwardingResponse
|
||||
{
|
||||
VmId = vmId,
|
||||
Success = false,
|
||||
ErrorMessage = "VM not found"
|
||||
};
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var actualPublicPort = publicPort ?? await GetAvailablePortAsync();
|
||||
var success = await _upnpManager.AddPortMappingAsync(actualPublicPort, privatePort, $"QEMU VM {vmId}");
|
||||
|
||||
if (success)
|
||||
{
|
||||
var externalIp = await _upnpManager.GetExternalIpAddressAsync();
|
||||
vm.PublicEndpoint = new NetworkEndpoint
|
||||
{
|
||||
PublicIp = externalIp ?? IPAddress.Any,
|
||||
PublicPort = actualPublicPort,
|
||||
PrivateIp = vm.Configuration.Network.Interfaces.FirstOrDefault()?.Mac != null ?
|
||||
IPAddress.Parse("192.168.1.100") : IPAddress.Any, // Simplified
|
||||
PrivatePort = privatePort,
|
||||
Protocol = "TCP",
|
||||
Description = $"QEMU VM {vmId}"
|
||||
};
|
||||
|
||||
return new PortForwardingResponse
|
||||
{
|
||||
VmId = vmId,
|
||||
Success = true,
|
||||
PublicIp = externalIp,
|
||||
PublicPort = actualPublicPort
|
||||
};
|
||||
}
|
||||
|
||||
return new PortForwardingResponse
|
||||
{
|
||||
VmId = vmId,
|
||||
Success = false,
|
||||
ErrorMessage = "Failed to create port mapping"
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogError(ex, "Failed to request port forwarding for VM {VmId}", vmId);
|
||||
return new PortForwardingResponse
|
||||
{
|
||||
VmId = vmId,
|
||||
Success = false,
|
||||
ErrorMessage = ex.Message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<VmMigrationResponse> MigrateVmAsync(string vmId, string targetNodeId)
|
||||
{
|
||||
var vm = ClusterState.DistributedVms.GetValueOrDefault(vmId);
|
||||
if (vm == null)
|
||||
{
|
||||
return new VmMigrationResponse
|
||||
{
|
||||
VmId = vmId,
|
||||
Success = false,
|
||||
ErrorMessage = "VM not found"
|
||||
};
|
||||
}
|
||||
|
||||
if (vm.NodeId == targetNodeId)
|
||||
{
|
||||
return new VmMigrationResponse
|
||||
{
|
||||
VmId = vmId,
|
||||
Success = true,
|
||||
ErrorMessage = "VM is already on target node"
|
||||
};
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Stop VM on source node
|
||||
await StopVmAsync(vmId);
|
||||
|
||||
// Start VM on target node
|
||||
var newVm = await StartVmAsync(vm.Configuration, targetNodeId);
|
||||
|
||||
return new VmMigrationResponse
|
||||
{
|
||||
VmId = vmId,
|
||||
Success = true
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogError(ex, "Failed to migrate VM {VmId} to node {TargetNodeId}", vmId, targetNodeId);
|
||||
return new VmMigrationResponse
|
||||
{
|
||||
VmId = vmId,
|
||||
Success = false,
|
||||
ErrorMessage = ex.Message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private async Task InitializeNetworkAsync()
|
||||
{
|
||||
// Initialize UDP client for discovery and heartbeats
|
||||
_udpClient = new UdpClient(_port);
|
||||
_udpClient.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
|
||||
|
||||
// Initialize TCP listener for direct communication
|
||||
_tcpListener = new TcpListener(IPAddress.Any, _port);
|
||||
_tcpListener.Start();
|
||||
|
||||
// Start listening for incoming connections
|
||||
_ = Task.Run(() => ListenForConnectionsAsync(_cancellationTokenSource!.Token));
|
||||
}
|
||||
|
||||
private async Task HeartbeatLoopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
var heartbeat = new HeartbeatMessage
|
||||
{
|
||||
NodeId = _nodeId,
|
||||
Role = _currentRole,
|
||||
Term = _currentTerm,
|
||||
Timestamp = DateTime.UtcNow,
|
||||
Metadata = new Dictionary<string, object>
|
||||
{
|
||||
["running_vms"] = _localVms.Count,
|
||||
["cpu_usage"] = await GetCpuUsageAsync(),
|
||||
["memory_usage"] = await GetMemoryUsageAsync()
|
||||
}
|
||||
};
|
||||
|
||||
await BroadcastHeartbeatAsync(heartbeat);
|
||||
|
||||
// Check for stale nodes
|
||||
await CleanupStaleNodesAsync();
|
||||
|
||||
await Task.Delay(5000, cancellationToken); // Send heartbeat every 5 seconds
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogError(ex, "Error in heartbeat loop");
|
||||
await Task.Delay(1000, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ElectionLoopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var electionTimeout = TimeSpan.FromSeconds(10 + Random.Shared.Next(10)); // 10-20 seconds
|
||||
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_currentRole == NodeRole.Follower)
|
||||
{
|
||||
// Wait for heartbeat from master
|
||||
var timeout = DateTime.UtcNow.Add(electionTimeout);
|
||||
while (DateTime.UtcNow < timeout && !cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
if (_lastHeartbeat > DateTime.UtcNow.AddSeconds(-5))
|
||||
{
|
||||
await Task.Delay(1000, cancellationToken);
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
// No heartbeat received, start election
|
||||
await StartElectionAsync();
|
||||
}
|
||||
}
|
||||
else if (_currentRole == NodeRole.Candidate)
|
||||
{
|
||||
// Wait for election results
|
||||
await Task.Delay(5000, cancellationToken);
|
||||
}
|
||||
else if (_currentRole == NodeRole.Master)
|
||||
{
|
||||
// Master continues to send heartbeats
|
||||
await Task.Delay(1000, cancellationToken);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogError(ex, "Error in election loop");
|
||||
await Task.Delay(1000, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DiscoveryLoopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Send discovery message
|
||||
var discoveryMessage = JsonSerializer.Serialize(new
|
||||
{
|
||||
type = "discovery",
|
||||
nodeId = _nodeId,
|
||||
timestamp = DateTime.UtcNow
|
||||
});
|
||||
|
||||
var data = System.Text.Encoding.UTF8.GetBytes(discoveryMessage);
|
||||
await _udpClient!.SendAsync(data, data.Length, new IPEndPoint(IPAddress.Broadcast, _port));
|
||||
|
||||
await Task.Delay(30000, cancellationToken); // Send discovery every 30 seconds
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogError(ex, "Error in discovery loop");
|
||||
await Task.Delay(5000, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task StartElectionAsync()
|
||||
{
|
||||
_logger?.LogInformation("Starting election for term {Term}", _currentTerm + 1);
|
||||
|
||||
_currentTerm++;
|
||||
_currentRole = NodeRole.Candidate;
|
||||
_votedFor = _nodeId;
|
||||
|
||||
var electionRequest = new ElectionRequest
|
||||
{
|
||||
CandidateId = _nodeId,
|
||||
Term = _currentTerm,
|
||||
Timestamp = DateTime.UtcNow
|
||||
};
|
||||
|
||||
var votes = 1; // Vote for self
|
||||
var requiredVotes = (_knownNodes.Count + 1) / 2 + 1; // Majority
|
||||
|
||||
// Request votes from other nodes
|
||||
foreach (var node in _knownNodes.Values)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await RequestVoteAsync(node, electionRequest);
|
||||
if (response.VoteGranted && response.Term == _currentTerm)
|
||||
{
|
||||
votes++;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogWarning(ex, "Failed to request vote from node {NodeId}", node.NodeId);
|
||||
}
|
||||
}
|
||||
|
||||
if (votes >= requiredVotes)
|
||||
{
|
||||
await BecomeMasterAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
_currentRole = NodeRole.Follower;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task BecomeMasterAsync()
|
||||
{
|
||||
_logger?.LogInformation("Becoming master for term {Term}", _currentTerm);
|
||||
|
||||
_currentRole = NodeRole.Master;
|
||||
CurrentNode.Role = NodeRole.Master;
|
||||
|
||||
// Update cluster state
|
||||
lock (_clusterLock)
|
||||
{
|
||||
ClusterState.MasterNode = CurrentNode;
|
||||
ClusterState.LastUpdated = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
RoleChanged?.Invoke(this, NodeRole.Master);
|
||||
|
||||
// Start master-specific tasks
|
||||
if (IsMaster)
|
||||
{
|
||||
await StartMasterTasksAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task StartMasterTasksAsync()
|
||||
{
|
||||
// Master-specific initialization
|
||||
_logger?.LogInformation("Starting master tasks");
|
||||
|
||||
// Check UPnP availability
|
||||
var upnpAvailable = await _upnpManager.IsUPnPAvailableAsync();
|
||||
_logger?.LogInformation("UPnP available: {Available}", upnpAvailable);
|
||||
}
|
||||
|
||||
private async Task<VmInstance> StartLocalVmAsync(string vmId, VmConfiguration config)
|
||||
{
|
||||
try
|
||||
{
|
||||
var success = await _qemuManager.StartVmAsync(config);
|
||||
if (!success)
|
||||
{
|
||||
throw new InvalidOperationException("Failed to start QEMU VM");
|
||||
}
|
||||
|
||||
var vmInstance = new VmInstance
|
||||
{
|
||||
VmId = vmId,
|
||||
VmName = config.Name,
|
||||
NodeId = _nodeId,
|
||||
State = VmState.Running,
|
||||
StartedAt = DateTime.UtcNow,
|
||||
Configuration = config
|
||||
};
|
||||
|
||||
_localVms[vmId] = vmInstance;
|
||||
|
||||
lock (_clusterLock)
|
||||
{
|
||||
ClusterState.DistributedVms[vmId] = vmInstance;
|
||||
ClusterState.LastUpdated = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
VmStarted?.Invoke(this, vmInstance);
|
||||
_logger?.LogInformation("Started local VM {VmId} ({VmName})", vmId, config.Name);
|
||||
|
||||
return vmInstance;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogError(ex, "Failed to start local VM {VmId}", vmId);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> StopLocalVmAsync(string vmId)
|
||||
{
|
||||
if (!_localVms.TryGetValue(vmId, out var vm))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var success = await _qemuManager.StopVmAsync(vm.VmName, true);
|
||||
if (success)
|
||||
{
|
||||
_localVms.Remove(vmId);
|
||||
|
||||
lock (_clusterLock)
|
||||
{
|
||||
ClusterState.DistributedVms.Remove(vmId);
|
||||
ClusterState.LastUpdated = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
VmStopped?.Invoke(this, vm);
|
||||
_logger?.LogInformation("Stopped local VM {VmId}", vmId);
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogError(ex, "Failed to stop local VM {VmId}", vmId);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<VmInstance> RequestRemoteVmStartAsync(string vmId, VmConfiguration config, string targetNodeId)
|
||||
{
|
||||
// This would implement RPC to remote node
|
||||
throw new NotImplementedException("Remote VM start not yet implemented");
|
||||
}
|
||||
|
||||
private async Task<bool> RequestRemoteVmStopAsync(string vmId, string targetNodeId)
|
||||
{
|
||||
// This would implement RPC to remote node
|
||||
throw new NotImplementedException("Remote VM stop not yet implemented");
|
||||
}
|
||||
|
||||
private async Task<ElectionResponse> RequestVoteAsync(NodeInfo node, ElectionRequest request)
|
||||
{
|
||||
// This would implement RPC to request vote
|
||||
throw new NotImplementedException("Vote request not yet implemented");
|
||||
}
|
||||
|
||||
private async Task BroadcastHeartbeatAsync(HeartbeatMessage heartbeat)
|
||||
{
|
||||
var message = JsonSerializer.Serialize(heartbeat);
|
||||
var data = System.Text.Encoding.UTF8.GetBytes(message);
|
||||
|
||||
foreach (var node in _knownNodes.Values)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _udpClient!.SendAsync(data, data.Length, new IPEndPoint(node.IpAddress, node.Port));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogWarning(ex, "Failed to send heartbeat to node {NodeId}", node.NodeId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CleanupStaleNodesAsync()
|
||||
{
|
||||
var staleThreshold = DateTime.UtcNow.AddSeconds(-30);
|
||||
var staleNodes = _knownNodes.Values.Where(n => n.LastSeen < staleThreshold).ToList();
|
||||
|
||||
foreach (var node in staleNodes)
|
||||
{
|
||||
_knownNodes.Remove(node.NodeId);
|
||||
NodeLeft?.Invoke(this, node);
|
||||
_logger?.LogInformation("Removed stale node {NodeId}", node.NodeId);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ListenForConnectionsAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
var client = await _tcpListener!.AcceptTcpClientAsync(cancellationToken);
|
||||
_ = Task.Run(() => HandleClientAsync(client, cancellationToken));
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogError(ex, "Error accepting client connection");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleClientAsync(TcpClient client, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var stream = client.GetStream();
|
||||
using var reader = new StreamReader(stream);
|
||||
using var writer = new StreamWriter(stream);
|
||||
|
||||
var message = await reader.ReadLineAsync();
|
||||
if (message != null)
|
||||
{
|
||||
await ProcessMessageAsync(message, writer);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogError(ex, "Error handling client connection");
|
||||
}
|
||||
finally
|
||||
{
|
||||
client.Close();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ProcessMessageAsync(string message, StreamWriter writer)
|
||||
{
|
||||
try
|
||||
{
|
||||
var data = JsonSerializer.Deserialize<JsonElement>(message);
|
||||
var messageType = data.GetProperty("type").GetString();
|
||||
|
||||
switch (messageType)
|
||||
{
|
||||
case "heartbeat":
|
||||
await ProcessHeartbeatAsync(data);
|
||||
break;
|
||||
case "discovery":
|
||||
await ProcessDiscoveryAsync(data);
|
||||
break;
|
||||
case "election_request":
|
||||
await ProcessElectionRequestAsync(data, writer);
|
||||
break;
|
||||
case "election_response":
|
||||
await ProcessElectionResponseAsync(data);
|
||||
break;
|
||||
default:
|
||||
_logger?.LogWarning("Unknown message type: {MessageType}", messageType);
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogError(ex, "Error processing message: {Message}", message);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ProcessHeartbeatAsync(JsonElement data)
|
||||
{
|
||||
var nodeId = data.GetProperty("nodeId").GetString()!;
|
||||
var role = Enum.Parse<NodeRole>(data.GetProperty("role").GetString()!);
|
||||
var term = data.GetProperty("term").GetInt64();
|
||||
var timestamp = data.GetProperty("timestamp").GetDateTime();
|
||||
|
||||
if (term > _currentTerm)
|
||||
{
|
||||
_currentTerm = term;
|
||||
_currentRole = NodeRole.Follower;
|
||||
_votedFor = null;
|
||||
}
|
||||
|
||||
if (role == NodeRole.Master && term >= _currentTerm)
|
||||
{
|
||||
_currentRole = NodeRole.Follower;
|
||||
_lastHeartbeat = timestamp;
|
||||
}
|
||||
|
||||
// Update node info
|
||||
if (_knownNodes.TryGetValue(nodeId, out var node))
|
||||
{
|
||||
node.LastSeen = DateTime.UtcNow;
|
||||
node.Role = role;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ProcessDiscoveryAsync(JsonElement data)
|
||||
{
|
||||
var nodeId = data.GetProperty("nodeId").GetString()!;
|
||||
|
||||
if (nodeId != _nodeId && !_knownNodes.ContainsKey(nodeId))
|
||||
{
|
||||
var newNode = new NodeInfo
|
||||
{
|
||||
NodeId = nodeId,
|
||||
LastSeen = DateTime.UtcNow
|
||||
};
|
||||
|
||||
_knownNodes[nodeId] = newNode;
|
||||
NodeJoined?.Invoke(this, newNode);
|
||||
_logger?.LogInformation("Discovered new node {NodeId}", nodeId);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ProcessElectionRequestAsync(JsonElement data, StreamWriter writer)
|
||||
{
|
||||
var candidateId = data.GetProperty("candidateId").GetString()!;
|
||||
var term = data.GetProperty("term").GetInt64();
|
||||
|
||||
var response = new ElectionResponse
|
||||
{
|
||||
VoterId = _nodeId,
|
||||
Term = _currentTerm,
|
||||
VoteGranted = false
|
||||
};
|
||||
|
||||
if (term > _currentTerm)
|
||||
{
|
||||
_currentTerm = term;
|
||||
_currentRole = NodeRole.Follower;
|
||||
_votedFor = null;
|
||||
}
|
||||
|
||||
if (term == _currentTerm && (_votedFor == null || _votedFor == candidateId))
|
||||
{
|
||||
_votedFor = candidateId;
|
||||
response.VoteGranted = true;
|
||||
}
|
||||
|
||||
var responseJson = JsonSerializer.Serialize(response);
|
||||
await writer.WriteLineAsync(responseJson);
|
||||
await writer.FlushAsync();
|
||||
}
|
||||
|
||||
private async Task ProcessElectionResponseAsync(JsonElement data)
|
||||
{
|
||||
// Handle election response
|
||||
var voterId = data.GetProperty("voterId").GetString()!;
|
||||
var voteGranted = data.GetProperty("voteGranted").GetBoolean();
|
||||
var term = data.GetProperty("term").GetInt64();
|
||||
|
||||
if (voteGranted && term == _currentTerm && _currentRole == NodeRole.Candidate)
|
||||
{
|
||||
// Count votes and potentially become master
|
||||
// This is simplified - in a real implementation you'd track votes
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<int> GetAvailablePortAsync()
|
||||
{
|
||||
// Find an available port for UPnP mapping
|
||||
using var listener = new TcpListener(IPAddress.Any, 0);
|
||||
listener.Start();
|
||||
var port = ((IPEndPoint)listener.LocalEndpoint).Port;
|
||||
listener.Stop();
|
||||
return port;
|
||||
}
|
||||
|
||||
private async Task<double> GetCpuUsageAsync()
|
||||
{
|
||||
// Simplified CPU usage calculation
|
||||
return Environment.ProcessorCount * 0.1; // 10% per core
|
||||
}
|
||||
|
||||
private async Task<double> GetMemoryUsageAsync()
|
||||
{
|
||||
// Simplified memory usage calculation
|
||||
return GC.GetTotalMemory(false) / (1024.0 * 1024.0); // MB
|
||||
}
|
||||
|
||||
private IPAddress GetLocalIpAddress()
|
||||
{
|
||||
try
|
||||
{
|
||||
var host = Dns.GetHostEntry(Dns.GetHostName());
|
||||
return host.AddressList.FirstOrDefault(ip =>
|
||||
ip.AddressFamily == AddressFamily.InterNetwork &&
|
||||
!IPAddress.IsLoopback(ip)) ?? IPAddress.Any;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return IPAddress.Any;
|
||||
}
|
||||
}
|
||||
|
||||
private SystemInfo GetSystemInfo()
|
||||
{
|
||||
return new SystemInfo
|
||||
{
|
||||
OsName = Environment.OSVersion.Platform.ToString(),
|
||||
OsVersion = Environment.OSVersion.VersionString,
|
||||
Architecture = Environment.GetEnvironmentVariable("PROCESSOR_ARCHITECTURE") ?? "Unknown",
|
||||
CpuCores = Environment.ProcessorCount,
|
||||
TotalMemory = GC.GetGCMemoryInfo().TotalAvailableMemoryBytes / (1024 * 1024), // MB
|
||||
AvailableMemory = GC.GetTotalMemory(false) / (1024 * 1024), // MB
|
||||
AvailableVirtualization = _qemuManager.GetAvailableVirtualization(),
|
||||
QemuInstalled = _qemuManager.IsQemuInstalled(),
|
||||
QemuVersion = _qemuManager.GetQemuVersion()
|
||||
};
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
StopAsync().Wait();
|
||||
_udpClient?.Dispose();
|
||||
_tcpListener?.Stop();
|
||||
_cancellationTokenSource?.Dispose();
|
||||
}
|
||||
}
|
@@ -12,6 +12,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="System.Diagnostics.PerformanceCounter" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
377
QemuVmManager.Core/UPnPManager.cs
Normal file
377
QemuVmManager.Core/UPnPManager.cs
Normal file
@@ -0,0 +1,377 @@
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using QemuVmManager.Models;
|
||||
|
||||
namespace QemuVmManager.Core;
|
||||
|
||||
public interface IUPnPManager
|
||||
{
|
||||
Task<bool> IsUPnPAvailableAsync();
|
||||
Task<IPAddress?> GetExternalIpAddressAsync();
|
||||
Task<bool> AddPortMappingAsync(int externalPort, int internalPort, string description);
|
||||
Task<bool> RemovePortMappingAsync(int externalPort);
|
||||
Task<List<PortMapping>> GetPortMappingsAsync();
|
||||
}
|
||||
|
||||
public class UPnPManager : IUPnPManager
|
||||
{
|
||||
private readonly ILogger<UPnPManager>? _logger;
|
||||
private readonly Dictionary<int, PortMapping> _activeMappings = new();
|
||||
private readonly object _lock = new();
|
||||
|
||||
public UPnPManager(ILogger<UPnPManager>? logger = null)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<bool> IsUPnPAvailableAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Try to discover UPnP devices
|
||||
var devices = await DiscoverUPnPDevicesAsync();
|
||||
return devices.Any();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogWarning(ex, "UPnP discovery failed");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IPAddress?> GetExternalIpAddressAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Try multiple methods to get external IP
|
||||
var externalIp = await GetExternalIpFromUPnPAsync();
|
||||
if (externalIp != null)
|
||||
return externalIp;
|
||||
|
||||
// Fallback to external service
|
||||
return await GetExternalIpFromServiceAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogError(ex, "Failed to get external IP address");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> AddPortMappingAsync(int externalPort, int internalPort, string description)
|
||||
{
|
||||
try
|
||||
{
|
||||
var localIp = GetLocalIpAddress();
|
||||
if (localIp == null)
|
||||
{
|
||||
_logger?.LogError("Could not determine local IP address");
|
||||
return false;
|
||||
}
|
||||
|
||||
var success = await AddUPnPPortMappingAsync(externalPort, internalPort, localIp, description);
|
||||
if (success)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_activeMappings[externalPort] = new PortMapping
|
||||
{
|
||||
ExternalPort = externalPort,
|
||||
InternalPort = internalPort,
|
||||
InternalIp = localIp,
|
||||
Protocol = "TCP",
|
||||
Description = description,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
}
|
||||
_logger?.LogInformation("Added port mapping: {ExternalPort} -> {InternalPort} ({Description})",
|
||||
externalPort, internalPort, description);
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogError(ex, "Failed to add port mapping {ExternalPort} -> {InternalPort}",
|
||||
externalPort, internalPort);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> RemovePortMappingAsync(int externalPort)
|
||||
{
|
||||
try
|
||||
{
|
||||
var success = await RemoveUPnPPortMappingAsync(externalPort);
|
||||
if (success)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_activeMappings.Remove(externalPort);
|
||||
}
|
||||
_logger?.LogInformation("Removed port mapping: {ExternalPort}", externalPort);
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogError(ex, "Failed to remove port mapping {ExternalPort}", externalPort);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<PortMapping>> GetPortMappingsAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var mappings = await GetUPnPPortMappingsAsync();
|
||||
lock (_lock)
|
||||
{
|
||||
// Merge with our active mappings
|
||||
foreach (var mapping in _activeMappings.Values)
|
||||
{
|
||||
if (!mappings.Any(m => m.ExternalPort == mapping.ExternalPort))
|
||||
{
|
||||
mappings.Add(mapping);
|
||||
}
|
||||
}
|
||||
}
|
||||
return mappings;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogError(ex, "Failed to get port mappings");
|
||||
lock (_lock)
|
||||
{
|
||||
return _activeMappings.Values.ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<List<UPnPDevice>> DiscoverUPnPDevicesAsync()
|
||||
{
|
||||
var devices = new List<UPnPDevice>();
|
||||
|
||||
// Simple SSDP discovery
|
||||
var ssdpMessage =
|
||||
"M-SEARCH * HTTP/1.1\r\n" +
|
||||
"HOST: 239.255.255.250:1900\r\n" +
|
||||
"MAN: \"ssdp:discover\"\r\n" +
|
||||
"MX: 3\r\n" +
|
||||
"ST: urn:schemas-upnp-org:device:InternetGatewayDevice:1\r\n" +
|
||||
"\r\n";
|
||||
|
||||
using var udpClient = new UdpClient();
|
||||
udpClient.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
|
||||
|
||||
var endpoint = new IPEndPoint(IPAddress.Parse("239.255.255.250"), 1900);
|
||||
var data = System.Text.Encoding.UTF8.GetBytes(ssdpMessage);
|
||||
|
||||
await udpClient.SendAsync(data, data.Length, endpoint);
|
||||
|
||||
// Wait for responses
|
||||
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
||||
try
|
||||
{
|
||||
while (!cts.Token.IsCancellationRequested)
|
||||
{
|
||||
var result = await udpClient.ReceiveAsync(cts.Token);
|
||||
var response = System.Text.Encoding.UTF8.GetString(result.Buffer);
|
||||
|
||||
if (response.Contains("InternetGatewayDevice"))
|
||||
{
|
||||
var device = ParseUPnPResponse(response, result.RemoteEndPoint);
|
||||
if (device != null)
|
||||
devices.Add(device);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Timeout - this is expected
|
||||
}
|
||||
|
||||
return devices;
|
||||
}
|
||||
|
||||
private UPnPDevice? ParseUPnPResponse(string response, IPEndPoint endpoint)
|
||||
{
|
||||
try
|
||||
{
|
||||
var lines = response.Split('\n');
|
||||
var location = lines.FirstOrDefault(l => l.StartsWith("LOCATION:", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (location != null)
|
||||
{
|
||||
var url = location.Substring("LOCATION:".Length).Trim();
|
||||
return new UPnPDevice
|
||||
{
|
||||
ControlUrl = url,
|
||||
IpAddress = endpoint.Address,
|
||||
Port = endpoint.Port
|
||||
};
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogWarning(ex, "Failed to parse UPnP response");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task<IPAddress?> GetExternalIpFromUPnPAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var devices = await DiscoverUPnPDevicesAsync();
|
||||
if (!devices.Any())
|
||||
return null;
|
||||
|
||||
// Use the first device to get external IP
|
||||
var device = devices.First();
|
||||
// This would require implementing SOAP calls to the UPnP device
|
||||
// For now, we'll use the fallback method
|
||||
return null;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<IPAddress?> GetExternalIpFromServiceAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
using var client = new HttpClient();
|
||||
client.Timeout = TimeSpan.FromSeconds(10);
|
||||
|
||||
// Try multiple external IP services
|
||||
var services = new[]
|
||||
{
|
||||
"https://api.ipify.org",
|
||||
"https://icanhazip.com",
|
||||
"https://ifconfig.me/ip"
|
||||
};
|
||||
|
||||
foreach (var service in services)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await client.GetStringAsync(service);
|
||||
var ipString = response.Trim();
|
||||
if (IPAddress.TryParse(ipString, out var ip))
|
||||
return ip;
|
||||
}
|
||||
catch
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogWarning(ex, "Failed to get external IP from services");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private IPAddress? GetLocalIpAddress()
|
||||
{
|
||||
try
|
||||
{
|
||||
var host = Dns.GetHostEntry(Dns.GetHostName());
|
||||
return host.AddressList.FirstOrDefault(ip =>
|
||||
ip.AddressFamily == AddressFamily.InterNetwork &&
|
||||
!IPAddress.IsLoopback(ip));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogWarning(ex, "Failed to get local IP address");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> AddUPnPPortMappingAsync(int externalPort, int internalPort, IPAddress internalIp, string description)
|
||||
{
|
||||
try
|
||||
{
|
||||
var devices = await DiscoverUPnPDevicesAsync();
|
||||
if (!devices.Any())
|
||||
return false;
|
||||
|
||||
// This would require implementing SOAP calls to add port mapping
|
||||
// For now, we'll simulate success
|
||||
_logger?.LogInformation("Simulating UPnP port mapping: {ExternalPort} -> {InternalIp}:{InternalPort}",
|
||||
externalPort, internalIp, internalPort);
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogError(ex, "Failed to add UPnP port mapping");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> RemoveUPnPPortMappingAsync(int externalPort)
|
||||
{
|
||||
try
|
||||
{
|
||||
var devices = await DiscoverUPnPDevicesAsync();
|
||||
if (!devices.Any())
|
||||
return false;
|
||||
|
||||
// This would require implementing SOAP calls to remove port mapping
|
||||
// For now, we'll simulate success
|
||||
_logger?.LogInformation("Simulating UPnP port mapping removal: {ExternalPort}", externalPort);
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogError(ex, "Failed to remove UPnP port mapping");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<List<PortMapping>> GetUPnPPortMappingsAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var devices = await DiscoverUPnPDevicesAsync();
|
||||
if (!devices.Any())
|
||||
return new List<PortMapping>();
|
||||
|
||||
// This would require implementing SOAP calls to get port mappings
|
||||
// For now, we'll return empty list
|
||||
return new List<PortMapping>();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogError(ex, "Failed to get UPnP port mappings");
|
||||
return new List<PortMapping>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class UPnPDevice
|
||||
{
|
||||
public string ControlUrl { get; set; } = string.Empty;
|
||||
public IPAddress IpAddress { get; set; } = IPAddress.Any;
|
||||
public int Port { get; set; }
|
||||
}
|
||||
|
||||
public class PortMapping
|
||||
{
|
||||
public int ExternalPort { get; set; }
|
||||
public int InternalPort { get; set; }
|
||||
public IPAddress InternalIp { get; set; } = IPAddress.Any;
|
||||
public string Protocol { get; set; } = "TCP";
|
||||
public string Description { get; set; } = string.Empty;
|
||||
public DateTime CreatedAt { get; set; }
|
||||
}
|
137
QemuVmManager.Models/P2PModels.cs
Normal file
137
QemuVmManager.Models/P2PModels.cs
Normal file
@@ -0,0 +1,137 @@
|
||||
using System.Net;
|
||||
|
||||
namespace QemuVmManager.Models;
|
||||
|
||||
public enum NodeRole
|
||||
{
|
||||
Follower,
|
||||
Candidate,
|
||||
Master
|
||||
}
|
||||
|
||||
public enum NodeState
|
||||
{
|
||||
Starting,
|
||||
Running,
|
||||
Stopping,
|
||||
Stopped,
|
||||
Error
|
||||
}
|
||||
|
||||
public class NodeInfo
|
||||
{
|
||||
public string NodeId { get; set; } = string.Empty;
|
||||
public string Hostname { get; set; } = string.Empty;
|
||||
public IPAddress IpAddress { get; set; } = IPAddress.Any;
|
||||
public int Port { get; set; } = 8080;
|
||||
public NodeRole Role { get; set; } = NodeRole.Follower;
|
||||
public NodeState State { get; set; } = NodeState.Stopped;
|
||||
public DateTime LastSeen { get; set; } = DateTime.UtcNow;
|
||||
public Dictionary<string, object> Metadata { get; set; } = new();
|
||||
public List<string> RunningVms { get; set; } = new();
|
||||
public SystemInfo SystemInfo { get; set; } = new();
|
||||
}
|
||||
|
||||
public class SystemInfo
|
||||
{
|
||||
public string OsName { get; set; } = string.Empty;
|
||||
public string OsVersion { get; set; } = string.Empty;
|
||||
public string Architecture { get; set; } = string.Empty;
|
||||
public int CpuCores { get; set; }
|
||||
public long TotalMemory { get; set; }
|
||||
public long AvailableMemory { get; set; }
|
||||
public VirtualizationType AvailableVirtualization { get; set; }
|
||||
public bool QemuInstalled { get; set; }
|
||||
public string QemuVersion { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class ClusterState
|
||||
{
|
||||
public string ClusterId { get; set; } = string.Empty;
|
||||
public NodeInfo? MasterNode { get; set; }
|
||||
public List<NodeInfo> Nodes { get; set; } = new();
|
||||
public DateTime LastUpdated { get; set; } = DateTime.UtcNow;
|
||||
public Dictionary<string, VmInstance> DistributedVms { get; set; } = new();
|
||||
}
|
||||
|
||||
public class VmInstance
|
||||
{
|
||||
public string VmId { get; set; } = string.Empty;
|
||||
public string VmName { get; set; } = string.Empty;
|
||||
public string NodeId { get; set; } = string.Empty;
|
||||
public VmState State { get; set; } = VmState.Stopped;
|
||||
public DateTime StartedAt { get; set; }
|
||||
public VmConfiguration Configuration { get; set; } = new();
|
||||
public NetworkEndpoint? PublicEndpoint { get; set; }
|
||||
public VmPerformanceMetrics? PerformanceMetrics { get; set; }
|
||||
}
|
||||
|
||||
public class NetworkEndpoint
|
||||
{
|
||||
public IPAddress PublicIp { get; set; } = IPAddress.Any;
|
||||
public int PublicPort { get; set; }
|
||||
public IPAddress PrivateIp { get; set; } = IPAddress.Any;
|
||||
public int PrivatePort { get; set; }
|
||||
public string Protocol { get; set; } = "TCP";
|
||||
public string Description { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class ElectionRequest
|
||||
{
|
||||
public string CandidateId { get; set; } = string.Empty;
|
||||
public long Term { get; set; }
|
||||
public DateTime Timestamp { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public class ElectionResponse
|
||||
{
|
||||
public string VoterId { get; set; } = string.Empty;
|
||||
public bool VoteGranted { get; set; }
|
||||
public long Term { get; set; }
|
||||
public DateTime Timestamp { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public class HeartbeatMessage
|
||||
{
|
||||
public string NodeId { get; set; } = string.Empty;
|
||||
public NodeRole Role { get; set; }
|
||||
public long Term { get; set; }
|
||||
public DateTime Timestamp { get; set; } = DateTime.UtcNow;
|
||||
public Dictionary<string, object> Metadata { get; set; } = new();
|
||||
}
|
||||
|
||||
public class VmMigrationRequest
|
||||
{
|
||||
public string VmId { get; set; } = string.Empty;
|
||||
public string SourceNodeId { get; set; } = string.Empty;
|
||||
public string TargetNodeId { get; set; } = string.Empty;
|
||||
public bool LiveMigration { get; set; } = false;
|
||||
public DateTime RequestedAt { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public class VmMigrationResponse
|
||||
{
|
||||
public string VmId { get; set; } = string.Empty;
|
||||
public bool Success { get; set; }
|
||||
public string? ErrorMessage { get; set; }
|
||||
public DateTime CompletedAt { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public class PortForwardingRequest
|
||||
{
|
||||
public string VmId { get; set; } = string.Empty;
|
||||
public int PrivatePort { get; set; }
|
||||
public int? PublicPort { get; set; }
|
||||
public string Protocol { get; set; } = "TCP";
|
||||
public string Description { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class PortForwardingResponse
|
||||
{
|
||||
public string VmId { get; set; } = string.Empty;
|
||||
public bool Success { get; set; }
|
||||
public IPAddress? PublicIp { get; set; }
|
||||
public int? PublicPort { get; set; }
|
||||
public string? ErrorMessage { get; set; }
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
}
|
@@ -442,16 +442,27 @@ public class VmManagementService
|
||||
await _processManager.StartPerformanceMonitoringAsync(vmName);
|
||||
}
|
||||
|
||||
public void StopPerformanceMonitoring(string vmName)
|
||||
public async Task StopPerformanceMonitoringAsync(string vmName)
|
||||
{
|
||||
_processManager.StopPerformanceMonitoring(vmName);
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
public bool IsPerformanceMonitoringActive(string vmName)
|
||||
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);
|
||||
|
Reference in New Issue
Block a user