feat: sync hardware modules, interfaces, and MAC addresses
docs: add complete copy-and-paste deployment guide
This commit is contained in:
@@ -19,11 +19,15 @@ internal static class HardwareCollector
|
||||
.Where(x => x["MACAddress"] is not null)
|
||||
.Select(x => new NetworkInfo(S(x, "Description"), S(x, "MACAddress"),
|
||||
(x["IPAddress"] as string[] ?? []).Where(ip => !ip.StartsWith("169.254.")).ToList())).ToList();
|
||||
var memoryModules = Query("Win32_PhysicalMemory", "BankLabel", "DeviceLocator", "Manufacturer", "PartNumber", "SerialNumber", "Capacity")
|
||||
.Select(x => new MemoryInfo(
|
||||
string.IsNullOrWhiteSpace(S(x, "BankLabel")) ? S(x, "DeviceLocator") : S(x, "BankLabel"),
|
||||
S(x, "Manufacturer"), S(x, "PartNumber"), S(x, "SerialNumber"), U(x, "Capacity"))).ToList();
|
||||
|
||||
return new HardwareInfo(Environment.MachineName, S(computer, "Manufacturer"), S(computer, "Model"),
|
||||
S(bios, "SerialNumber"), S(product, "UUID"), S(os, "Caption"), S(os, "Version"),
|
||||
S(os, "OSArchitecture"), S(cpu, "Name"), U(computer, "TotalPhysicalMemory"), disks, networks,
|
||||
collectSoftware ? CollectSoftware(maxSoftwareEntries) : []);
|
||||
memoryModules, collectSoftware ? CollectSoftware(maxSoftwareEntries) : []);
|
||||
}
|
||||
|
||||
private static IReadOnlyList<SoftwareInfo> CollectSoftware(int maximum)
|
||||
|
||||
+4
-1
@@ -15,6 +15,8 @@ internal sealed class AgentConfig
|
||||
[JsonPropertyName("timeout_seconds")] public int TimeoutSeconds { get; set; } = 30;
|
||||
[JsonPropertyName("collect_software")] public bool CollectSoftware { get; set; } = true;
|
||||
[JsonPropertyName("max_software_entries")] public int MaxSoftwareEntries { get; set; } = 250;
|
||||
[JsonPropertyName("create_hardware_modules")] public bool CreateHardwareModules { get; set; } = true;
|
||||
[JsonPropertyName("create_interfaces")] public bool CreateInterfaces { get; set; } = true;
|
||||
[JsonPropertyName("tags")] public List<string> Tags { get; set; } = [];
|
||||
[JsonPropertyName("custom_fields")] public Dictionary<string, string> CustomFields { get; set; } = new();
|
||||
}
|
||||
@@ -23,9 +25,10 @@ internal sealed record HardwareInfo(
|
||||
string Hostname, string Manufacturer, string Model, string Serial, string Uuid,
|
||||
string WindowsCaption, string WindowsVersion, string Architecture, string Processor,
|
||||
ulong MemoryBytes, IReadOnlyList<DiskInfo> Disks, IReadOnlyList<NetworkInfo> Networks,
|
||||
IReadOnlyList<SoftwareInfo> Software);
|
||||
IReadOnlyList<MemoryInfo> MemoryModules, IReadOnlyList<SoftwareInfo> Software);
|
||||
internal sealed record DiskInfo(string DeviceId, string Model, string Serial, ulong SizeBytes);
|
||||
internal sealed record NetworkInfo(string Name, string MacAddress, IReadOnlyList<string> IpAddresses);
|
||||
internal sealed record MemoryInfo(string Bank, string Manufacturer, string PartNumber, string Serial, ulong CapacityBytes);
|
||||
internal sealed record SoftwareInfo(string Name, string Version, string Publisher);
|
||||
|
||||
internal sealed class Page<T>
|
||||
|
||||
@@ -26,6 +26,9 @@ internal sealed class SyncService(NetBoxClient client, AgentConfig config)
|
||||
device = await client.Create("dcim/devices", devicePayload);
|
||||
} else if (!dryRun) device = await client.Patch("dcim/devices", device.Id, devicePayload);
|
||||
|
||||
if (!dryRun && config.CreateInterfaces) await SyncInterfaces(device.Id, hw.Networks);
|
||||
if (!dryRun && config.CreateHardwareModules) await SyncHardwareModules(device.Id, hw);
|
||||
|
||||
var asset = await client.FindOne("plugins/inventory/assets", ("serial", hw.Serial));
|
||||
var assetPayload = new Dictionary<string, object?> {
|
||||
["name"] = hw.Hostname, ["serial"] = hw.Serial, ["status"] = "used",
|
||||
@@ -39,6 +42,48 @@ internal sealed class SyncService(NetBoxClient client, AgentConfig config)
|
||||
return (device.Id, asset.Id);
|
||||
}
|
||||
|
||||
private async Task SyncInterfaces(int deviceId, IReadOnlyList<NetworkInfo> networks)
|
||||
{
|
||||
var usedNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var network in networks.Where(n => !string.IsNullOrWhiteSpace(n.MacAddress)))
|
||||
{
|
||||
var baseName = network.Name.Length > 60 ? network.Name[..60] : network.Name;
|
||||
var name = baseName;
|
||||
for (var suffix = 2; !usedNames.Add(name); suffix++) name = $"{baseName[..Math.Min(baseName.Length, 56)]} {suffix}";
|
||||
var iface = await client.FindOne("dcim/interfaces", ("device_id", deviceId.ToString()), ("name", name));
|
||||
var payload = new { device = deviceId, name, type = "other", enabled = true,
|
||||
description = network.IpAddresses.Count == 0 ? "Windows Agent" : $"Windows Agent; IP: {string.Join(", ", network.IpAddresses)}" };
|
||||
iface = iface is null ? await client.Create("dcim/interfaces", payload) : await client.Patch("dcim/interfaces", iface.Id, payload);
|
||||
|
||||
var mac = await client.FindOne("dcim/mac-addresses", ("mac_address", network.MacAddress));
|
||||
var macPayload = new { mac_address = network.MacAddress, assigned_object_type = "dcim.interface", assigned_object_id = iface.Id };
|
||||
mac = mac is null ? await client.Create("dcim/mac-addresses", macPayload) : await client.Patch("dcim/mac-addresses", mac.Id, macPayload);
|
||||
await client.Patch("dcim/interfaces", iface.Id, new { primary_mac_address = mac.Id });
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SyncHardwareModules(int deviceId, HardwareInfo hw)
|
||||
{
|
||||
var genericManufacturer = await EnsureManufacturer("Agent-discovered", false);
|
||||
var components = new List<(string Bay, string Model, string Serial)> { ("CPU 1", hw.Processor, "") };
|
||||
components.AddRange(hw.MemoryModules.Select((m, i) =>
|
||||
($"RAM {CleanName(string.IsNullOrWhiteSpace(m.Bank) ? (i + 1).ToString() : m.Bank)}",
|
||||
$"RAM {m.CapacityBytes / 1073741824d:F0} GiB {m.PartNumber}".Trim(), m.Serial)));
|
||||
components.AddRange(hw.Disks.Select((d, i) => ($"Disk {i + 1}", $"{d.Model} {d.SizeBytes / 1000000000d:F0} GB", d.Serial)));
|
||||
|
||||
foreach (var component in components.Where(c => !string.IsNullOrWhiteSpace(c.Model)))
|
||||
{
|
||||
var typeSlug = Slug(component.Model);
|
||||
var moduleType = await client.FindOne("dcim/module-types", ("manufacturer_id", genericManufacturer.Id.ToString()), ("model", component.Model));
|
||||
moduleType ??= await client.Create("dcim/module-types", new { manufacturer = genericManufacturer.Id, model = component.Model, slug = typeSlug });
|
||||
var bay = await client.FindOne("dcim/module-bays", ("device_id", deviceId.ToString()), ("name", component.Bay));
|
||||
bay ??= await client.Create("dcim/module-bays", new { device = deviceId, name = component.Bay, enabled = true });
|
||||
var module = await client.FindOne("dcim/modules", ("device_id", deviceId.ToString()), ("module_bay_id", bay.Id.ToString()));
|
||||
var modulePayload = new { device = deviceId, module_bay = bay.Id, module_type = moduleType.Id, status = "active", serial = component.Serial };
|
||||
if (module is null) await client.Create("dcim/modules", modulePayload); else await client.Patch("dcim/modules", module.Id, modulePayload);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<NetBoxObject?> ResolveSite()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(config.Site)) return null;
|
||||
@@ -96,5 +141,6 @@ internal sealed class SyncService(NetBoxClient client, AgentConfig config)
|
||||
return string.Join('\n', lines);
|
||||
}
|
||||
private static string Slug(string value) => string.Concat(value.Trim().ToLowerInvariant().Select(c => char.IsLetterOrDigit(c) ? c : '-')).Trim('-');
|
||||
private static string CleanName(string value) => new(value.Where(c => char.IsLetterOrDigit(c) || c is '-' or '_').ToArray());
|
||||
private static void RemoveNulls(Dictionary<string, object?> data) { foreach (var key in data.Where(x => x.Value is null).Select(x => x.Key).ToList()) data.Remove(key); }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user