namespace RackPeek.Domain.Discovery;
///
/// Turns a into .
/// Pure: no IO, no platform checks, so it runs and is tested identically everywhere.
///
public static class SystemFactsParser {
/// Strings that appear in DMI when the host is a guest rather than real hardware.
private static readonly string[] _virtualMachineMarkers =
[
"qemu", "kvm", "vmware", "virtualbox", "innotek", "xen", "bochs",
"bhyve", "parallels", "hyper-v", "virtual machine", "openstack"
];
/// Kernel-managed devices that are not disks anyone wants in an inventory.
private static readonly string[] _ignoredBlockDevicePrefixes =
["loop", "ram", "zram", "sr", "dm-", "fd", "md"];
public static SystemFacts Parse(RawSystemSnapshot raw) {
var type = ParseType(raw);
return new SystemFacts {
Hostname = raw.Hostname,
MachineId = ParseMachineId(raw),
Os = ParseOs(raw),
Cores = raw.Cores > 0 ? raw.Cores : 1,
RamGb = ParseRamGb(raw),
Type = type,
Ip = SelectPrimaryIp(raw.Nics),
Macs = SelectMacs(raw.Nics),
// A container sees the host's block devices through /sys/block. They belong
// to the machine underneath it, so reporting them here would attribute
// someone else's disks to this resource.
Drives = type == "container" ? [] : ParseDrives(raw.BlockDevices)
};
}
internal static string? ParseMachineId(RawSystemSnapshot raw) {
var id = Clean(raw.PlatformUuid) ?? Clean(raw.MachineIdFile);
return string.IsNullOrEmpty(id) ? null : id;
}
internal static string ParseOs(RawSystemSnapshot raw) {
var name = Clean(raw.OsName);
if (!string.IsNullOrEmpty(name))
return name;
var pretty = ReadKeyValue(raw.OsReleaseFile, "PRETTY_NAME");
if (!string.IsNullOrEmpty(pretty))
return pretty;
var id = ReadKeyValue(raw.OsReleaseFile, "NAME");
var version = ReadKeyValue(raw.OsReleaseFile, "VERSION");
if (!string.IsNullOrEmpty(id))
return string.IsNullOrEmpty(version) ? id : $"{id} {version}";
return "Unknown";
}
internal static double ParseRamGb(RawSystemSnapshot raw) {
if (raw.MemoryBytes is > 0)
return DiscoveryUnits.BytesToWholeGb(raw.MemoryBytes.Value);
// MemTotal is in kB, and is a little under the physical total because the
// kernel reserves some. Reported as-is rather than rounded up to a DIMM size.
var memTotal = ReadKeyValue(raw.MemInfoFile, "MemTotal", ':');
if (!string.IsNullOrEmpty(memTotal)) {
var digits = new string(memTotal.TakeWhile(char.IsAsciiDigit).ToArray());
if (long.TryParse(digits, out var kb) && kb > 0)
return DiscoveryUnits.BytesToWholeGb(kb * 1024L);
}
return DiscoveryUnits.BytesToWholeGb(raw.FallbackMemoryBytes);
}
internal static string ParseType(RawSystemSnapshot raw) {
if (raw.DockerEnvPresent || ContainsContainerMarker(raw.CgroupFile))
return "container";
if (raw.HypervisorPresent)
return "vm";
var dmi = $"{raw.DmiVendor} {raw.DmiProduct}".ToLowerInvariant();
if (_virtualMachineMarkers.Any(marker => dmi.Contains(marker, StringComparison.Ordinal)))
return "vm";
return "baremetal";
}
internal static string? SelectPrimaryIp(IReadOnlyList nics) {
var usable = nics
.Where(n => n is { IsUp: true, IsLoopback: false } && !string.IsNullOrWhiteSpace(n.Ipv4))
.ToList();
// An interface holding the default route is the address other machines reach
// this host on. Otherwise prefer anything that is not an obvious virtual bridge.
return usable.FirstOrDefault(n => n.HasGateway)?.Ipv4
?? usable.FirstOrDefault(n => !IsVirtual(n.Name))?.Ipv4
?? usable.FirstOrDefault()?.Ipv4;
}
///
/// The MACs a network scan could see this machine by: real interfaces only — a
/// docker bridge's MAC never crosses the wire, so recording it could only cause
/// a false unification. Normalised by the same code that reads ARP tables, so
/// both sides of the bridge always agree on the spelling.
///
internal static List SelectMacs(IReadOnlyList nics) {
return nics
.Where(n => n is { IsUp: true, IsLoopback: false } && !IsVirtual(n.Name))
.Select(n => ArpTableParser.NormaliseMac(n.Mac))
.Where(mac => mac != null)
.Select(mac => mac!)
.Distinct()
.ToList();
}
internal static bool IsVirtual(string name) {
string[] prefixes = ["docker", "br-", "veth", "virbr", "tailscale", "utun", "tun", "tap", "cni", "flannel"];
return prefixes.Any(p => name.StartsWith(p, StringComparison.OrdinalIgnoreCase));
}
internal static List ParseDrives(IReadOnlyList devices) {
return devices
.Where(d => !_ignoredBlockDevicePrefixes.Any(p =>
d.Name.StartsWith(p, StringComparison.OrdinalIgnoreCase)))
.Where(d => d.SizeBytes > 0)
.Select(d => new DriveFact(DriveType(d), (int)DiscoveryUnits.BytesToWholeGb(d.SizeBytes)))
.ToList();
}
private static string DriveType(BlockDeviceFact device) {
if (device.Name.StartsWith("nvme", StringComparison.OrdinalIgnoreCase))
return "nvme";
if (device.Name.StartsWith("mmcblk", StringComparison.OrdinalIgnoreCase))
return "sdcard";
return device.Rotational ? "hdd" : "ssd";
}
private static bool ContainsContainerMarker(string? cgroup) {
if (string.IsNullOrWhiteSpace(cgroup))
return false;
string[] markers = ["docker", "lxc", "kubepods", "containerd", "podman"];
return markers.Any(m => cgroup.Contains(m, StringComparison.OrdinalIgnoreCase));
}
/// Reads one entry out of a key=value file such as /etc/os-release.
private static string? ReadKeyValue(string? contents, string key, char separator = '=') {
if (string.IsNullOrWhiteSpace(contents))
return null;
foreach (var rawLine in contents.Split('\n')) {
var line = rawLine.Trim();
var index = line.IndexOf(separator);
if (index <= 0)
continue;
if (!line[..index].Trim().Equals(key, StringComparison.OrdinalIgnoreCase))
continue;
return line[(index + 1)..].Trim().Trim('"').Trim();
}
return null;
}
private static string? Clean(string? value) =>
string.IsNullOrWhiteSpace(value) ? null : value.Trim();
}