diff --git a/src/Vantage.Core/Collectors/Systemd/ISystemdProvider.cs b/src/Vantage.Core/Collectors/Systemd/ISystemdProvider.cs new file mode 100644 index 0000000..c64913b --- /dev/null +++ b/src/Vantage.Core/Collectors/Systemd/ISystemdProvider.cs @@ -0,0 +1,8 @@ +using Vantage.Core.Models; + +namespace Vantage.Core.Collectors.Systemd; + +public interface ISystemdProvider +{ + Task> GetServicesAsync(HostConfig host, CancellationToken cancellationToken); +} diff --git a/src/Vantage.Core/Collectors/Systemd/MockSystemdProvider.cs b/src/Vantage.Core/Collectors/Systemd/MockSystemdProvider.cs new file mode 100644 index 0000000..cb38bc5 --- /dev/null +++ b/src/Vantage.Core/Collectors/Systemd/MockSystemdProvider.cs @@ -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> GetServicesAsync(HostConfig host, CancellationToken cancellationToken) => + Task.FromResult>(Units); +} diff --git a/src/Vantage.Core/Collectors/Systemd/SshSystemdProvider.cs b/src/Vantage.Core/Collectors/Systemd/SshSystemdProvider.cs new file mode 100644 index 0000000..c9f8d5b --- /dev/null +++ b/src/Vantage.Core/Collectors/Systemd/SshSystemdProvider.cs @@ -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> GetServicesAsync(HostConfig host, CancellationToken cancellationToken) + { + var output = await runner.RunAsync(host, Command, cancellationToken).ConfigureAwait(false); + return SystemdUnitParser.Parse(output); + } +} diff --git a/src/Vantage.Core/Collectors/Systemd/SystemdUnit.cs b/src/Vantage.Core/Collectors/Systemd/SystemdUnit.cs new file mode 100644 index 0000000..f4fe152 --- /dev/null +++ b/src/Vantage.Core/Collectors/Systemd/SystemdUnit.cs @@ -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"; +} diff --git a/src/Vantage.Core/Collectors/Systemd/SystemdUnitParser.cs b/src/Vantage.Core/Collectors/Systemd/SystemdUnitParser.cs new file mode 100644 index 0000000..b921a17 --- /dev/null +++ b/src/Vantage.Core/Collectors/Systemd/SystemdUnitParser.cs @@ -0,0 +1,41 @@ +namespace Vantage.Core.Collectors.Systemd; + +public static class SystemdUnitParser +{ + public static IReadOnlyList Parse(string listUnitsOutput) + { + var units = new List(); + + 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; +}