feat: sync hardware modules, interfaces, and MAC addresses
docs: add complete copy-and-paste deployment guide
This commit is contained in:
@@ -1,36 +1,69 @@
|
|||||||
# NetBox Windows Client Agent
|
# NetBox Client Agent für Windows
|
||||||
|
|
||||||
Ein schlanker Windows-Agent, der Hardwaredaten per WMI erfasst und einen NetBox-Device-Datensatz sowie das zugehörige Asset im Plugin [ArnesSI/netbox-inventory](https://github.com/ArnesSI/netbox-inventory) anlegt oder aktualisiert. Das Release ist eine einzelne, selbstenthaltende `agent.exe`; auf dem Zielsystem muss kein .NET installiert sein.
|
Der Agent inventarisiert einen Windows-PC automatisch in NetBox und im Plugin [ArnesSI/netbox-inventory](https://github.com/ArnesSI/netbox-inventory). Das Ergebnis ist eine einzelne, selbstenthaltende `agent.exe`; auf dem Client muss kein .NET installiert sein.
|
||||||
|
|
||||||
## Was synchronisiert wird
|
## Ergebnis in NetBox
|
||||||
|
|
||||||
- Rechnername, Hersteller, Modell, BIOS-Seriennummer und Geräte-UUID
|
Nach dem ersten Lauf entstehen beziehungsweise aktualisieren sich:
|
||||||
- Windows-Version und Architektur, CPU, RAM, Datenträger inklusive Seriennummern, MAC- und IP-Adressen
|
|
||||||
- Maschinenweit installierte Software aus der 32- und 64-Bit-Windows-Registry
|
|
||||||
- NetBox Site und Location aus der Konfiguration
|
|
||||||
- `dcim.Device` sowie das verknüpfte `netbox_inventory.Asset`
|
|
||||||
- Hersteller und Device Type werden bei Bedarf angelegt
|
|
||||||
- Wiederholbare Updates anhand der BIOS-Seriennummer statt doppelter Datensätze
|
|
||||||
|
|
||||||
Der Agent löscht keine NetBox-Objekte. Site, Location und Device Role müssen bereits existieren. Die Region dient zur eindeutigen Auswahl der Site.
|
- ein `DCIM > Device` für den Windows-PC,
|
||||||
|
- ein damit verknüpftes Inventory-Asset,
|
||||||
|
- Interfaces für erkannte Netzwerkadapter,
|
||||||
|
- je Interface eine zugewiesene primäre MAC-Adresse,
|
||||||
|
- Module und Module Bays für CPU, RAM-Riegel und Datenträger,
|
||||||
|
- ein Hardware- und Softwarebericht in den Kommentaren von Device und Asset.
|
||||||
|
|
||||||
## Voraussetzungen in NetBox
|
Der Agent erkennt den PC anhand seiner BIOS-Seriennummer wieder. Wiederholte Ausführungen aktualisieren deshalb die vorhandenen Objekte. Es werden keine NetBox-Objekte gelöscht.
|
||||||
|
|
||||||
1. Das Plugin `netbox-inventory` ist installiert.
|
> NetBox 4.2 oder neuer wird empfohlen, da MAC-Adressen seit 4.2 eigenständige Objekte sind. Module sind in NetBox eigentlich für austauschbare Hardwarekomponenten vorgesehen; für PC-Inventarisierung werden CPU, RAM und Datenträger bewusst auf dieses Modell abgebildet.
|
||||||
2. Eine Device Role mit dem konfigurierten Slug (Standard: `windows-client`) existiert.
|
|
||||||
3. Site/Location existieren, falls sie in `config.json` gesetzt sind.
|
|
||||||
4. Der API-Token darf Devices, Manufacturers, Device Types und Inventory Assets lesen, anlegen und ändern.
|
|
||||||
|
|
||||||
Optionale Custom Fields müssen für `dcim.Device` existieren. Standardmäßig werden keine Custom Fields gesendet. Die linke Seite in `custom_fields` ist der Agent-Schlüssel, die rechte Seite der NetBox-Custom-Field-Name. Unterstützte Agent-Schlüssel sind `agent_version`, `windows_version`, `device_uuid` und `last_sync`.
|
## 1. NetBox vorbereiten
|
||||||
|
|
||||||
## Konfiguration
|
Erforderlich:
|
||||||
|
|
||||||
`config.example.json` als `config.json` neben die EXE kopieren. Die URL enthält nur die NetBox-Basis-URL, nicht `/api`.
|
1. `netbox-inventory` ist installiert.
|
||||||
|
2. Eine Device Role mit dem Slug `windows-client` existiert.
|
||||||
|
3. Die in der Konfiguration genannten Site und Location existieren, sofern sie verwendet werden.
|
||||||
|
4. Der API-Token besitzt Lese-, Erstell- und Änderungsrechte für:
|
||||||
|
- DCIM Devices, Manufacturers und Device Types
|
||||||
|
- DCIM Interfaces und MAC Addresses
|
||||||
|
- DCIM Module Types, Module Bays und Modules
|
||||||
|
- Inventory Assets des Plugins
|
||||||
|
|
||||||
|
Site, Location und Device Role legt der Agent absichtlich nicht automatisch an. Hersteller, Device Types und erkannte Hardwaretypen dagegen schon.
|
||||||
|
|
||||||
|
## 2. Repository herunterladen und EXE bauen
|
||||||
|
|
||||||
|
PowerShell öffnen und ausführen:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
git clone https://git.mrblake.cc/MrBlake/Netbox-Client-Agent-for-Windows.git
|
||||||
|
Set-Location .\Netbox-Client-Agent-for-Windows
|
||||||
|
|
||||||
|
powershell.exe -NoProfile -ExecutionPolicy Bypass `
|
||||||
|
-File .\scripts\build.ps1 `
|
||||||
|
-Runtime win-x64
|
||||||
|
```
|
||||||
|
|
||||||
|
Zum Bauen wird das [.NET 8 SDK](https://dotnet.microsoft.com/download/dotnet/8.0) oder neuer benötigt. Die fertige Datei liegt anschließend hier:
|
||||||
|
|
||||||
|
```text
|
||||||
|
dist\win-x64\agent.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Konfiguration erstellen
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Copy-Item .\config.example.json .\dist\win-x64\config.json
|
||||||
|
notepad .\dist\win-x64\config.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Minimalbeispiel zum Kopieren:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"netbox_url": "https://netbox.example.com",
|
"netbox_url": "https://netbox.example.local",
|
||||||
"api_token": "0123456789abcdef",
|
"api_token": "HIER_DEN_NETBOX_TOKEN_EINTRAGEN",
|
||||||
"region": "emea",
|
"region": "emea",
|
||||||
"site": "berlin",
|
"site": "berlin",
|
||||||
"location": "office-1",
|
"location": "office-1",
|
||||||
@@ -40,61 +73,163 @@ Optionale Custom Fields müssen für `dcim.Device` existieren. Standardmäßig w
|
|||||||
"timeout_seconds": 30,
|
"timeout_seconds": 30,
|
||||||
"collect_software": true,
|
"collect_software": true,
|
||||||
"max_software_entries": 250,
|
"max_software_entries": 250,
|
||||||
"tags": [],
|
"create_hardware_modules": true,
|
||||||
|
"create_interfaces": true,
|
||||||
"custom_fields": {}
|
"custom_fields": {}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
`site` und `location` sind optional. Für NetBox-Installationen mit selbstsigniertem Zertifikat kann `allow_insecure_tls` auf `true` gesetzt werden. Damit werden sowohl Fehler der Zertifikatskette als auch abweichende Zertifikatsnamen ignoriert; dies reduziert die Sicherheit und sollte nur in vertrauenswürdigen internen Netzen verwendet werden. `verify_tls: false` bleibt aus Kompatibilitätsgründen ebenfalls unterstützt.
|
`site` und `location` dürfen vollständig entfernt oder auf `null` gesetzt werden. `region` grenzt die Suche nach der Site ein.
|
||||||
|
|
||||||
`collect_software` aktiviert die Softwareinventarisierung. `max_software_entries` begrenzt die Anzahl der alphabetisch sortierten Einträge zwischen 0 und 1000. Erfasst wird maschinenweit installierte klassische Windows-Software; benutzerspezifische Store-/AppX-Pakete sind im SYSTEM-Kontext nicht zuverlässig verfügbar.
|
### Selbstsigniertes Zertifikat
|
||||||
|
|
||||||
## Ausführen und bauen
|
Wenn NetBox ein selbstsigniertes Zertifikat verwendet und Windows der ausstellenden CA nicht vertraut:
|
||||||
|
|
||||||
```powershell
|
```json
|
||||||
.\agent.exe --dry-run
|
"allow_insecure_tls": true
|
||||||
.\agent.exe
|
|
||||||
.\agent.exe --config C:\ProgramData\NetBoxAgent\config.json
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Der Dry-Run liest NetBox und prüft die Zuordnungen, schreibt aber nichts. Zum lokalen Build wird das .NET 8 SDK benötigt:
|
Dies ignoriert Zertifikatsketten- und Namensfehler. In produktiven Umgebungen ist es sicherer, die interne CA auf dem Client zu installieren und `allow_insecure_tls` auf `false` zu lassen.
|
||||||
|
|
||||||
|
### Optionale Funktionen abschalten
|
||||||
|
|
||||||
|
```json
|
||||||
|
"collect_software": false,
|
||||||
|
"create_hardware_modules": false,
|
||||||
|
"create_interfaces": false
|
||||||
|
```
|
||||||
|
|
||||||
|
`max_software_entries` darf zwischen 0 und 1000 liegen. Erfasst wird maschinenweit installierte klassische Windows-Software aus der 32- und 64-Bit-Registry. Benutzerspezifische Store-/AppX-Pakete sind beim Lauf als SYSTEM nicht zuverlässig verfügbar.
|
||||||
|
|
||||||
|
Custom Fields werden standardmäßig nicht benötigt. Sollen sie verwendet werden, müssen sie vorher für `DCIM > Device` in NetBox existieren. Unterstützte Zuordnungen sind:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"custom_fields": {
|
||||||
|
"agent_version": "netbox_agent_version",
|
||||||
|
"windows_version": "windows_version",
|
||||||
|
"device_uuid": "device_uuid",
|
||||||
|
"last_sync": "agent_last_sync"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. Verbindung testen
|
||||||
|
|
||||||
|
Der Dry-Run liest Hardware und NetBox, schreibt aber absichtlich nichts:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
|
.\dist\win-x64\agent.exe `
|
||||||
|
--config .\dist\win-x64\config.json `
|
||||||
|
--dry-run
|
||||||
|
```
|
||||||
|
|
||||||
|
Erwartete Ausgabe:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Erfasst: CLIENT01, LENOVO 21MW, S/N ABC123
|
||||||
|
Dry-Run erfolgreich; keine Änderungen geschrieben.
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. Erste echte Synchronisation
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\dist\win-x64\agent.exe `
|
||||||
|
--config .\dist\win-x64\config.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Erwartete Ausgabe:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Synchronisiert: Device #123, Asset #456
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. Dauerhaft installieren
|
||||||
|
|
||||||
|
PowerShell **als Administrator** öffnen:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Set-Location C:\Pfad\zum\Netbox-Client-Agent-for-Windows
|
||||||
|
|
||||||
|
powershell.exe -NoProfile -ExecutionPolicy Bypass `
|
||||||
|
-File .\scripts\install.ps1 `
|
||||||
|
-SourceDirectory .\dist\win-x64
|
||||||
|
```
|
||||||
|
|
||||||
|
Das Skript:
|
||||||
|
|
||||||
|
- kopiert EXE und Konfiguration nach `C:\Program Files\NetBox Windows Agent`,
|
||||||
|
- erlaubt nur SYSTEM und lokalen Administratoren den Zugriff auf den API-Token,
|
||||||
|
- erstellt die tägliche geplante Aufgabe `NetBox Windows Agent`,
|
||||||
|
- startet sofort die erste Synchronisation.
|
||||||
|
|
||||||
|
Deinstallation, ebenfalls als Administrator:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
powershell.exe -NoProfile -ExecutionPolicy Bypass `
|
||||||
|
-File .\scripts\uninstall.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
## Intune-Verteilung
|
||||||
|
|
||||||
|
Diese vier Dateien gemeinsam als Win32-App (`.intunewin`) paketieren:
|
||||||
|
|
||||||
|
```text
|
||||||
|
agent.exe
|
||||||
|
config.json
|
||||||
|
install.ps1
|
||||||
|
uninstall.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
Copy-&-Paste-Werte für Intune:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Installationsbefehl:
|
||||||
|
powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\install.ps1 -SourceDirectory .
|
||||||
|
|
||||||
|
Deinstallationsbefehl:
|
||||||
|
powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\uninstall.ps1
|
||||||
|
|
||||||
|
Installationsverhalten:
|
||||||
|
System
|
||||||
|
|
||||||
|
Erkennungsregel:
|
||||||
|
Datei C:\Program Files\NetBox Windows Agent\agent.exe vorhanden
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fehlerbehebung
|
||||||
|
|
||||||
|
### `agent.exe fehlt im Quellverzeichnis`
|
||||||
|
|
||||||
|
Als `SourceDirectory` muss der Ordner angegeben werden, in dem sowohl `agent.exe` als auch `config.json` liegen:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
-SourceDirectory .\dist\win-x64
|
||||||
|
```
|
||||||
|
|
||||||
|
### `RemoteCertificateNameMismatch` oder `RemoteCertificateChainErrors`
|
||||||
|
|
||||||
|
Zum Testen `allow_insecure_tls` auf `true` setzen. Dauerhaft besser die korrekte interne CA installieren und einen zur URL passenden Zertifikatsnamen verwenden.
|
||||||
|
|
||||||
|
### Custom Field ist nicht vorhanden
|
||||||
|
|
||||||
|
Wenn keine eigenen NetBox Custom Fields angelegt wurden:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"custom_fields": {}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `403 Forbidden`
|
||||||
|
|
||||||
|
Dem Benutzer des API-Tokens fehlen Rechte für den im Fehler genannten Objekttyp.
|
||||||
|
|
||||||
|
### `400 Bad Request`
|
||||||
|
|
||||||
|
Die vollständige Antwort hinter `FEHLER:` enthält das von NetBox beanstandete Feld. Häufig fehlen die konfigurierte Device Role, Site oder Location.
|
||||||
|
|
||||||
|
## Entwicklung
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
dotnet build
|
||||||
.\scripts\build.ps1 -Runtime win-x64
|
.\scripts\build.ps1 -Runtime win-x64
|
||||||
```
|
```
|
||||||
|
|
||||||
Das Ergebnis liegt in `dist\win-x64\agent.exe`. GitHub Actions erzeugt zusätzlich ARM64-Artefakte.
|
GitHub Actions baut automatisch x64- und ARM64-Artefakte. `config.json`, `dist`, `bin`, `obj` und lokale Build-Werkzeuge werden nicht committed.
|
||||||
|
|
||||||
## Manuelle Installation und Intune
|
|
||||||
|
|
||||||
Für eine manuelle Installation `agent.exe`, `config.json`, `install.ps1` und `uninstall.ps1` in dasselbe Paketverzeichnis legen und PowerShell als Administrator starten:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
.\install.ps1 -SourceDirectory $PWD
|
|
||||||
```
|
|
||||||
|
|
||||||
Die Installation kopiert die Dateien nach `%ProgramFiles%\NetBox Windows Agent`, schränkt die ACL auf Administratoren/SYSTEM ein, erstellt die tägliche geplante Aufgabe `NetBox Windows Agent` und führt die erste Synchronisation aus.
|
|
||||||
|
|
||||||
Für Intune die vier Dateien mit dem Microsoft Win32 Content Prep Tool als `.intunewin` paketieren:
|
|
||||||
|
|
||||||
- Installationsbefehl: `powershell.exe -ExecutionPolicy Bypass -File .\install.ps1 -SourceDirectory .`
|
|
||||||
- Deinstallationsbefehl: `powershell.exe -ExecutionPolicy Bypass -File .\uninstall.ps1`
|
|
||||||
- Installationskontext: System
|
|
||||||
- Erkennungsregel: Datei `%ProgramFiles%\NetBox Windows Agent\agent.exe` vorhanden
|
|
||||||
- Rückgabecode `0` bedeutet erfolgreiche erste Synchronisation
|
|
||||||
|
|
||||||
## Sicherheit und Betrieb
|
|
||||||
|
|
||||||
- Einen eigenen NetBox-Token mit minimal nötigen Rechten verwenden.
|
|
||||||
- `verify_tls` in Produktion immer aktiviert lassen.
|
|
||||||
- Für größere Umgebungen den Token künftig besser über Windows Credential Manager oder ein Intune-verwaltetes Maschinenzertifikat bereitstellen.
|
|
||||||
- Logs können in Intune über die Ausgabe der geplanten Aufgabe ergänzt werden; eine Event-Log-Integration ist ein sinnvoller nächster Ausbau.
|
|
||||||
|
|
||||||
## Vorgeschlagene Commits
|
|
||||||
|
|
||||||
```text
|
|
||||||
feat: add Windows hardware inventory collector and NetBox sync
|
|
||||||
feat: add self-contained Windows build and Intune deployment scripts
|
|
||||||
docs: document configuration, permissions, and deployment
|
|
||||||
ci: build x64 and arm64 agent artifacts
|
|
||||||
```
|
|
||||||
|
|||||||
+2
-1
@@ -10,6 +10,7 @@
|
|||||||
"timeout_seconds": 30,
|
"timeout_seconds": 30,
|
||||||
"collect_software": true,
|
"collect_software": true,
|
||||||
"max_software_entries": 250,
|
"max_software_entries": 250,
|
||||||
"tags": ["windows-agent"],
|
"create_hardware_modules": true,
|
||||||
|
"create_interfaces": true,
|
||||||
"custom_fields": {}
|
"custom_fields": {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,11 +19,15 @@ internal static class HardwareCollector
|
|||||||
.Where(x => x["MACAddress"] is not null)
|
.Where(x => x["MACAddress"] is not null)
|
||||||
.Select(x => new NetworkInfo(S(x, "Description"), S(x, "MACAddress"),
|
.Select(x => new NetworkInfo(S(x, "Description"), S(x, "MACAddress"),
|
||||||
(x["IPAddress"] as string[] ?? []).Where(ip => !ip.StartsWith("169.254.")).ToList())).ToList();
|
(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"),
|
return new HardwareInfo(Environment.MachineName, S(computer, "Manufacturer"), S(computer, "Model"),
|
||||||
S(bios, "SerialNumber"), S(product, "UUID"), S(os, "Caption"), S(os, "Version"),
|
S(bios, "SerialNumber"), S(product, "UUID"), S(os, "Caption"), S(os, "Version"),
|
||||||
S(os, "OSArchitecture"), S(cpu, "Name"), U(computer, "TotalPhysicalMemory"), disks, networks,
|
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)
|
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("timeout_seconds")] public int TimeoutSeconds { get; set; } = 30;
|
||||||
[JsonPropertyName("collect_software")] public bool CollectSoftware { get; set; } = true;
|
[JsonPropertyName("collect_software")] public bool CollectSoftware { get; set; } = true;
|
||||||
[JsonPropertyName("max_software_entries")] public int MaxSoftwareEntries { get; set; } = 250;
|
[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("tags")] public List<string> Tags { get; set; } = [];
|
||||||
[JsonPropertyName("custom_fields")] public Dictionary<string, string> CustomFields { get; set; } = new();
|
[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 Hostname, string Manufacturer, string Model, string Serial, string Uuid,
|
||||||
string WindowsCaption, string WindowsVersion, string Architecture, string Processor,
|
string WindowsCaption, string WindowsVersion, string Architecture, string Processor,
|
||||||
ulong MemoryBytes, IReadOnlyList<DiskInfo> Disks, IReadOnlyList<NetworkInfo> Networks,
|
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 DiskInfo(string DeviceId, string Model, string Serial, ulong SizeBytes);
|
||||||
internal sealed record NetworkInfo(string Name, string MacAddress, IReadOnlyList<string> IpAddresses);
|
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 record SoftwareInfo(string Name, string Version, string Publisher);
|
||||||
|
|
||||||
internal sealed class Page<T>
|
internal sealed class Page<T>
|
||||||
|
|||||||
@@ -26,6 +26,9 @@ internal sealed class SyncService(NetBoxClient client, AgentConfig config)
|
|||||||
device = await client.Create("dcim/devices", devicePayload);
|
device = await client.Create("dcim/devices", devicePayload);
|
||||||
} else if (!dryRun) device = await client.Patch("dcim/devices", device.Id, 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 asset = await client.FindOne("plugins/inventory/assets", ("serial", hw.Serial));
|
||||||
var assetPayload = new Dictionary<string, object?> {
|
var assetPayload = new Dictionary<string, object?> {
|
||||||
["name"] = hw.Hostname, ["serial"] = hw.Serial, ["status"] = "used",
|
["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);
|
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()
|
private async Task<NetBoxObject?> ResolveSite()
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(config.Site)) return null;
|
if (string.IsNullOrWhiteSpace(config.Site)) return null;
|
||||||
@@ -96,5 +141,6 @@ internal sealed class SyncService(NetBoxClient client, AgentConfig config)
|
|||||||
return string.Join('\n', lines);
|
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 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); }
|
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