server-monitor-manager/src/ServerMonitorManager.Provisioning.Helper/ProvisioningHelperServer.cs
2026-07-19 23:59:51 +07:00

186 lines
7.1 KiB
C#

using System.Net.Sockets;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Text;
using System.Text.Json;
using ServerMonitorManager.Core;
namespace ServerMonitorManager.Provisioning.Helper;
public sealed class ProvisioningHelperServer(string socketPath)
{
private const int MaximumRequestBytes = 16 * 1024;
[SupportedOSPlatform("linux")]
public async Task RunAsync(CancellationToken cancellationToken)
{
Directory.CreateDirectory(Path.GetDirectoryName(socketPath)!);
File.Delete(socketPath);
using var listener = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified);
listener.Bind(new UnixDomainSocketEndPoint(socketPath));
File.SetUnixFileMode(socketPath,
UnixFileMode.UserRead | UnixFileMode.UserWrite
| UnixFileMode.GroupRead | UnixFileMode.GroupWrite);
listener.Listen(8);
try
{
while (!cancellationToken.IsCancellationRequested)
{
var connection = await listener.AcceptAsync(cancellationToken);
await HandleAsync(connection, cancellationToken);
}
}
finally
{
File.Delete(socketPath);
}
}
public static ProvisioningHelperResponse Execute(ProvisioningHelperRequest request)
{
if (request.ProtocolVersion != "1")
{
return Failure("protocol.unsupported", "Unsupported helper protocol version.");
}
if (request.JobId.Length != 32 || !request.JobId.All(Uri.IsHexDigit))
{
return Failure("request.invalid-job", "Invalid provisioning job identifier.");
}
if (request.SchemaVersion != 1 || request.Parameters.ValueKind != JsonValueKind.Object)
{
return Failure("action.denied", "The requested action is not allowed.");
}
return request.ActionType switch
{
"preflight" => ExecutePreflight(request),
"system.base-install" => CreateBaseInstallPlan(request),
_ => Failure("action.denied", "The requested action is not allowed.")
};
}
private static ProvisioningHelperResponse ExecutePreflight(ProvisioningHelperRequest request)
{
if (request.ModuleHash != ProvisioningActionCatalog.PreflightModuleHash
|| request.Parameters.EnumerateObject().Any())
{
return Failure("action.denied", "The requested action is not allowed.");
}
var release = ReadOperatingSystemRelease();
var result = new ProvisioningPreflightResult(
release.GetValueOrDefault("ID", "linux"),
release.GetValueOrDefault("VERSION_ID", "unknown"),
RuntimeInformation.OSArchitecture.ToString().ToLowerInvariant(),
Directory.Exists("/run/systemd/system"),
Exists("/usr/sbin/sshd", "/usr/bin/sshd", "/sbin/sshd"),
Exists("/usr/sbin/nft", "/usr/bin/nft", "/sbin/nft"),
Exists("/usr/bin/wg", "/usr/sbin/wg", "/bin/wg"),
Exists("/usr/bin/apt-get", "/bin/apt-get"));
return new ProvisioningHelperResponse(
true, "preflight.completed", "Preflight completed.", result, null);
}
private static ProvisioningHelperResponse CreateBaseInstallPlan(ProvisioningHelperRequest request)
{
if (request.ModuleHash != ProvisioningActionCatalog.SystemBaseInstallModuleHash
|| !SystemBaseInstallSchema.TryParse(request.Parameters, out var parameters))
{
return Failure("action.denied", "The requested action is not allowed.");
}
var warnings = new List<string>();
if (!Exists("/usr/bin/apt-get", "/bin/apt-get"))
{
warnings.Add("apt.missing");
}
if (!File.Exists(Path.Combine("/usr/share/zoneinfo", parameters!.Timezone)))
{
warnings.Add("timezone.missing");
}
var plan = new SystemBaseInstallPlan(
parameters.Timezone,
parameters.Locale,
parameters.AptUpdate,
parameters.AptUpgrade,
SystemBaseInstallCatalogDefinition.ExpandGroups(parameters.PackageGroupIds),
parameters.SwapMode,
parameters.SwapSizeMiB,
parameters.VmSwappiness,
parameters.EnableUnattendedUpgrades,
parameters.RebootPolicy,
[.. warnings]);
return new ProvisioningHelperResponse(
true, "system.base-install.plan-ready", "Base install plan is ready.", null, plan);
}
private static async Task HandleAsync(Socket socket, CancellationToken cancellationToken)
{
using (socket)
await using (var stream = new NetworkStream(socket, ownsSocket: false))
{
ProvisioningHelperResponse response;
try
{
var payload = await ReadRequestAsync(stream, cancellationToken);
var request = JsonSerializer.Deserialize(payload, SmmJsonContext.Default.ProvisioningHelperRequest)
?? throw new JsonException("Empty request.");
response = Execute(request);
}
catch (Exception exception) when (exception is JsonException or InvalidDataException)
{
response = Failure("request.invalid", "Invalid helper request.");
}
var json = JsonSerializer.Serialize(response, SmmJsonContext.Default.ProvisioningHelperResponse) + "\n";
await stream.WriteAsync(Encoding.UTF8.GetBytes(json), cancellationToken);
}
}
private static async Task<byte[]> ReadRequestAsync(Stream stream, CancellationToken cancellationToken)
{
using var buffer = new MemoryStream();
var singleByte = new byte[1];
while (buffer.Length <= MaximumRequestBytes)
{
var count = await stream.ReadAsync(singleByte, cancellationToken);
if (count == 0 || singleByte[0] == (byte)'\n')
{
break;
}
buffer.WriteByte(singleByte[0]);
}
if (buffer.Length == 0 || buffer.Length > MaximumRequestBytes)
{
throw new InvalidDataException("Request size is invalid.");
}
return buffer.ToArray();
}
private static Dictionary<string, string> ReadOperatingSystemRelease()
{
var result = new Dictionary<string, string>(StringComparer.Ordinal);
if (!File.Exists("/etc/os-release"))
{
return result;
}
foreach (var line in File.ReadLines("/etc/os-release"))
{
var separator = line.IndexOf('=');
if (separator <= 0)
{
continue;
}
var key = line[..separator];
if (key is "ID" or "VERSION_ID")
{
result[key] = line[(separator + 1)..].Trim().Trim('"');
}
}
return result;
}
private static bool Exists(params string[] paths) => paths.Any(File.Exists);
private static ProvisioningHelperResponse Failure(string code, string message)
=> new(false, code, message, null, null);
}