Add fact-first Link reconciliation, duplicate and orphan cleanup, generation-aware scheduling, retention, Desktop drift visibility, and strict helper contracts. Preserve exact B-3R batching and lock-safe finalization with Linux/native trimmed evidence. Co-authored-by: Ochenstarik <ochenstarik@inbox.ru>
167 lines
6.7 KiB
C#
167 lines
6.7 KiB
C#
using System.Diagnostics;
|
|
using Microsoft.Extensions.Options;
|
|
using ServerMonitorManager.Core;
|
|
|
|
namespace ServerMonitorManager.Control;
|
|
|
|
public interface ILinkPolicyApplier
|
|
{
|
|
Task<IReadOnlyList<LinkRule>> ListRulesAsync(CancellationToken cancellationToken);
|
|
Task ApplyConnectAsync(LinkPolicy link, CancellationToken cancellationToken);
|
|
Task ApplyDisconnectAsync(LinkPolicy link, CancellationToken cancellationToken);
|
|
Task ApplyDisconnectAsync(LinkRule rule, CancellationToken cancellationToken);
|
|
Task<bool> IsConnectedAsync(LinkPolicy link, CancellationToken cancellationToken);
|
|
Task<string?> GetReconciliationRequestAsync(CancellationToken cancellationToken)
|
|
=> Task.FromResult<string?>(null);
|
|
Task CompleteReconciliationAsync(string generation, CancellationToken cancellationToken)
|
|
=> Task.CompletedTask;
|
|
}
|
|
|
|
public sealed record LinkRule(string SourceNodeId, string TargetNodeId, string Protocol, int Port);
|
|
|
|
public sealed class MeshFirewallUnavailableException : InvalidOperationException
|
|
{
|
|
public MeshFirewallUnavailableException(string message) : base(message)
|
|
{
|
|
}
|
|
}
|
|
|
|
public sealed class MeshNodeNotActivatedException : InvalidOperationException
|
|
{
|
|
public MeshNodeNotActivatedException(string message) : base(message)
|
|
{
|
|
}
|
|
}
|
|
|
|
public sealed class LinkPolicyApplier(IOptions<ControlOptions> options) : ILinkPolicyApplier
|
|
{
|
|
public async Task<IReadOnlyList<LinkRule>> ListRulesAsync(CancellationToken cancellationToken)
|
|
{
|
|
var output = await RunAsync(["link-list"], cancellationToken);
|
|
if (output.Length == 0)
|
|
{
|
|
return [];
|
|
}
|
|
var rules = new List<LinkRule>();
|
|
foreach (var line in output.Split('\n', StringSplitOptions.RemoveEmptyEntries))
|
|
{
|
|
var fields = line.Trim().Split('\t');
|
|
if (fields.Length != 4
|
|
|| !int.TryParse(fields[3], System.Globalization.NumberStyles.None,
|
|
System.Globalization.CultureInfo.InvariantCulture, out var port)
|
|
|| port is < 1 or > 65535)
|
|
{
|
|
throw new InvalidOperationException("Hub policy helper returned an invalid link-list record.");
|
|
}
|
|
rules.Add(new LinkRule(fields[0], fields[1], fields[2], port));
|
|
}
|
|
return rules;
|
|
}
|
|
|
|
public Task ApplyConnectAsync(LinkPolicy link, CancellationToken cancellationToken)
|
|
=> RunAsync(
|
|
[
|
|
"link-connect",
|
|
link.SourceNodeId,
|
|
link.TargetNodeId,
|
|
link.Protocol,
|
|
link.Port.ToString(System.Globalization.CultureInfo.InvariantCulture),
|
|
link.TtlMinutes.ToString(System.Globalization.CultureInfo.InvariantCulture)
|
|
],
|
|
cancellationToken);
|
|
|
|
public Task ApplyDisconnectAsync(LinkPolicy link, CancellationToken cancellationToken)
|
|
=> ApplyDisconnectAsync(
|
|
new LinkRule(link.SourceNodeId, link.TargetNodeId, link.Protocol, link.Port),
|
|
cancellationToken);
|
|
|
|
public Task ApplyDisconnectAsync(LinkRule rule, CancellationToken cancellationToken)
|
|
=> RunAsync(
|
|
[
|
|
"link-disconnect",
|
|
rule.SourceNodeId,
|
|
rule.TargetNodeId,
|
|
rule.Protocol,
|
|
rule.Port.ToString(System.Globalization.CultureInfo.InvariantCulture)
|
|
],
|
|
cancellationToken);
|
|
|
|
public async Task<bool> IsConnectedAsync(LinkPolicy link, CancellationToken cancellationToken)
|
|
{
|
|
var output = await RunAsync(
|
|
[
|
|
"link-status",
|
|
link.SourceNodeId,
|
|
link.TargetNodeId,
|
|
link.Protocol,
|
|
link.Port.ToString(System.Globalization.CultureInfo.InvariantCulture)
|
|
],
|
|
cancellationToken);
|
|
return output switch
|
|
{
|
|
"active" => true,
|
|
"disabled" => false,
|
|
_ => throw new InvalidOperationException("Hub policy helper returned an invalid link status.")
|
|
};
|
|
}
|
|
|
|
public async Task<string?> GetReconciliationRequestAsync(CancellationToken cancellationToken)
|
|
=> await RunAsync(["reconcile-status"], cancellationToken) switch
|
|
{
|
|
"complete" => null,
|
|
var status when status.StartsWith("requested:", StringComparison.Ordinal)
|
|
&& Guid.TryParseExact(status[10..], "D", out _) => status[10..],
|
|
_ => throw new InvalidOperationException("Hub policy helper returned an invalid reconciliation marker status.")
|
|
};
|
|
|
|
public async Task CompleteReconciliationAsync(string generation, CancellationToken cancellationToken)
|
|
{
|
|
if (!Guid.TryParseExact(generation, "D", out _))
|
|
{
|
|
throw new ArgumentException("Invalid reconciliation generation.", nameof(generation));
|
|
}
|
|
_ = await RunAsync(["reconcile-complete", generation], cancellationToken);
|
|
}
|
|
|
|
private async Task<string> RunAsync(IReadOnlyList<string> arguments, CancellationToken cancellationToken)
|
|
{
|
|
var startInfo = new ProcessStartInfo
|
|
{
|
|
FileName = options.Value.PrivilegeEscalationPath,
|
|
RedirectStandardOutput = true,
|
|
RedirectStandardError = true,
|
|
UseShellExecute = false,
|
|
CreateNoWindow = true
|
|
};
|
|
startInfo.ArgumentList.Add("-n");
|
|
startInfo.ArgumentList.Add(options.Value.HubHelperPath);
|
|
foreach (var argument in arguments)
|
|
{
|
|
startInfo.ArgumentList.Add(argument);
|
|
}
|
|
|
|
using var process = Process.Start(startInfo)
|
|
?? throw new InvalidOperationException("Could not start the Hub policy helper.");
|
|
var output = process.StandardOutput.ReadToEndAsync(cancellationToken);
|
|
var error = process.StandardError.ReadToEndAsync(cancellationToken);
|
|
await process.WaitForExitAsync(cancellationToken);
|
|
if (process.ExitCode != 0)
|
|
{
|
|
var message = (await error).Trim();
|
|
if (process.ExitCode == 79
|
|
&& string.Equals(message, LinkService.FirewallUnavailableCode, StringComparison.Ordinal))
|
|
{
|
|
throw new MeshFirewallUnavailableException(LinkService.FirewallUnavailableCode);
|
|
}
|
|
if (process.ExitCode == 80
|
|
&& string.Equals(message, LinkService.NodeNotActivatedCode, StringComparison.Ordinal))
|
|
{
|
|
throw new MeshNodeNotActivatedException(LinkService.NodeNotActivatedCode);
|
|
}
|
|
throw new InvalidOperationException(string.IsNullOrWhiteSpace(message)
|
|
? $"Hub policy helper exited with code {process.ExitCode}."
|
|
: message);
|
|
}
|
|
return (await output).Trim();
|
|
}
|
|
}
|