feat(core): add systemd unit collector

This commit is contained in:
dualfroz committed 2026-08-23 23:10:39 +02:00
1 parent d43173fe42
commit 8fda3d8211
5 files changed
+106

No files matched your search

@@ -0,0 +1,8 @@
using Vantage.Core.Models;
namespace Vantage.Core.Collectors.Systemd;
public interface ISystemdProvider
{
Task<IReadOnlyList<SystemdUnit>> GetServicesAsync(HostConfig host, CancellationToken cancellationToken);
}
@@ -0,0 +1,22 @@
using Vantage.Core.Models;
namespace Vantage.Core.Collectors.Systemd;
public sealed class MockSystemdProvider : ISystemdProvider
{
private static readonly SystemdUnit[] Units =
{
new("docker.service", "loaded", "active", "running", "Docker Application Container Engine"),
new("nginx.service", "loaded", "active", "running", "A high performance web server"),
new("postgresql.service", "loaded", "active", "running", "PostgreSQL RDBMS"),
new("ssh.service", "loaded", "active", "running", "OpenBSD Secure Shell server"),
new("prometheus.service", "loaded", "active", "running", "Prometheus monitoring"),
new("fail2ban.service", "loaded", "active", "running", "Fail2Ban Service"),
new("systemd-journald.service", "loaded", "active", "running", "Journal Service"),
new("backup.service", "loaded", "failed", "failed", "Nightly backup job"),
new("certbot-renew.service", "loaded", "inactive", "dead", "Certbot renewal")
};
public Task<IReadOnlyList<SystemdUnit>> GetServicesAsync(HostConfig host, CancellationToken cancellationToken) =>
Task.FromResult<IReadOnlyList<SystemdUnit>>(Units);
}
@@ -0,0 +1,23 @@
using Vantage.Core.Models;
using Vantage.Core.Ssh;
namespace Vantage.Core.Collectors.Systemd;
public sealed class SshSystemdProvider : ISystemdProvider
{
public const string Command =
"systemctl list-units --type=service --all --no-legend --no-pager --plain";
private readonly ISshCommandRunner runner;
public SshSystemdProvider(ISshCommandRunner runner)
{
this.runner = runner;
}
public async Task<IReadOnlyList<SystemdUnit>> GetServicesAsync(HostConfig host, CancellationToken cancellationToken)
{
var output = await runner.RunAsync(host, Command, cancellationToken).ConfigureAwait(false);
return SystemdUnitParser.Parse(output);
}
}
@@ -0,0 +1,12 @@
namespace Vantage.Core.Collectors.Systemd;
public readonly record struct SystemdUnit(
string Unit,
string Load,
string Active,
string Sub,
string Description)
{
public bool IsRunning => Active == "active" && Sub == "running";
public bool IsFailed => Active == "failed" || Sub == "failed";
}
@@ -0,0 +1,41 @@
namespace Vantage.Core.Collectors.Systemd;
public static class SystemdUnitParser
{
public static IReadOnlyList<SystemdUnit> Parse(string listUnitsOutput)
{
var units = new List<SystemdUnit>();
foreach (var rawLine in listUnitsOutput.Replace("\r\n", "\n").Split('\n'))
{
var line = StripLeadingMarker(rawLine).TrimEnd();
if (line.Length == 0)
continue;
if (!line.Contains(".service", StringComparison.Ordinal))
continue;
var fields = line.Split(' ', 5, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
if (fields.Length < 4)
continue;
units.Add(new SystemdUnit(
Unit: fields[0],
Load: fields[1],
Active: fields[2],
Sub: fields[3],
Description: fields.Length >= 5 ? fields[4] : ""));
}
return units;
}
private static string StripLeadingMarker(string line)
{
var index = 0;
while (index < line.Length && !IsUnitStart(line[index]))
index++;
return line[index..];
}
private static bool IsUnitStart(char c) => char.IsLetterOrDigit(c) && c < 128;
}