Aktionen

Zabbix PowerShell JSON und LLD per system.run

Aus znilwiki

Changelog:

  • 12.10.2023 erste Version

Vorwort

Ich wollte bei einem Kunden eine Überwachung eines Failoverclusters einrichten - und wollte weder extra dafür ein Skript schreiben noch den Agenten per UserParameter um Funktionen erweitern.
Ich musste etwas probieren, aber dann war es doch ganz leicht (wenn man es erst einmal verstanden hat).


Konfiguration des ZabbixAgenten

Ich setze hier den ZabbixAgent2 in der Version 6.0 ein, der normale Agent (also nicht 2) nutzt die gleichen Einstellungen.
In der Konfigurationsdatei für den Agenten müssen/sollten folgende Optionen gesetzt sein:

DenyKey=system.run[rmdir]  
AllowKey=system.run[*]

Das erlaubt jeden Befehl per system.run außer dem Befehl rmdir.
Alternativ könnte ihr auch nur PowerShell erlauben: AllowKey=system.run[powershell]

Nach der Änderung müsstet Ihr den Dienst des Agenten einmal neu starten.


Konfiguration des RAW-Items

Hier die einzelnen Schritte bis zum fertigen RAW-Item.
Als RAW-Item bezeichne ich das Item welches später das JSON holt. Dieses JSON nutzen wird dann sowohl für das Discovery / LLD als auch um die anderen Werte in Items zu schreiben. Etwas was ich an Zabbix wirklich genial finde, wir stellen nur eine Frage an das Zielsystem und füllen duzende Items mit der Antwort.


PowerShell-Befehl austüfteln

Habt Geduld - ich erkläre den Aufbau des PowerShell Befehls nicht ohne Grund so genau!
Zunächst habe ich auf dem Zielsystem natürlich den PowerShell-Befehl ausgeklingelt. In diesem Fall will ich die vorhandenen Clusterknoten ermitteln und den Status dann per Items abfragen.
Gestartet habe ich zunächst mit dem Befehl

Get-ClusterNode -Cluster FILECLUSTER

Ausgabe:

Name          State Type
----          ----- ----
FILESERVER-A  Up    Node
FILESERVER-B  Up    Node

Das ist noch etwas Dünn, wir wollen alle Informationen, deshalb

Get-ClusterNode -Cluster FILECLUSTER| Select-Object *

Ausgabe:

BuildNumber           : 20348
Cluster               : FILECLUSTER
CSDVersion            :
Description           :
DrainStatus           : NotInitiated
DrainTarget           : 4294967295
DrainErrorCode        : 0
DynamicWeight         : 0
Id                    : 1
MajorVersion          : 10
MinorVersion          : 0
Name                  : FILESERVER-A
NeedsPreventQuorum    : 0
NodeHighestVersion    : 720900
NodeInstanceID        : 00000000-0000-0000-0000-000000000001
NodeLowestVersion     : 720900
NodeName              : FILESERVER-A
NodeWeight            : 1
FaultDomain           : {Site:Site 192.168.0.0/24, Rack:, Chassis:}
Model                 : VMware7,1
Manufacturer          : VMware, Inc.
SerialNumber          : VMware-42 19 37 83 8b 83 e6 18-ad b3 73 bf da eb cb e1
State                 : Up
StatusInformation     : Normal
Type                  : Node
UniqueID              : ad2d4545-37b6-4c2c-b78f-9ecf72cd459c
DetectedCloudPlatform : None

BuildNumber           : 20348
Cluster               : FILECLUSTER
CSDVersion            :
Description           :
DrainStatus           : NotInitiated
DrainTarget           : 4294967295
DrainErrorCode        : 0
DynamicWeight         : 1
Id                    : 2
MajorVersion          : 10
MinorVersion          : 0
Name                  : FILESERVER-B
NeedsPreventQuorum    : 0
NodeHighestVersion    : 720900
NodeInstanceID        : 00000000-0000-0000-0000-000000000002
NodeLowestVersion     : 720900
NodeName              : FILESERVER-B
NodeWeight            : 1
FaultDomain           : {Site:Site 192.168.0.0/24, Rack:, Chassis:}
Model                 : VMware7,1
Manufacturer          : VMware, Inc.
SerialNumber          : VMware-42 19 2f 2a ed fa 97 ca-39 76 ff 79 0f fc 0e 78
State                 : Up
StatusInformation     : Normal
Type                  : Node
UniqueID              : 5f4e5ff5-1452-47fb-b9f8-b919d7815a39
DetectedCloudPlatform : None

Super, eine Frage, alle Antworten, z.B. der State
Damit wir das in Zabbix besser weiter verarbeiten können brauchen wir ein JSON, also Umwandeln:

Get-ClusterNode -Cluster FILECLUSTER| Select-Object * | ConvertTo-Json

Die Ausgabe ist sehr umfangreich, auch die Unterobjekte wie Cluster und FaultDomain werden noch mal extra aufgeschlüsselt:

       "NodeWeight":  1,
       "FaultDomain":  [
                           "Site:Site 192.168.0.0/24",
                           "Rack:",
                           "Chassis:"
                       ],
       "Model":  "VMware7,1",
       "Manufacturer":  "VMware, Inc.",

Das brauche ich in diesem Fall gar nicht. Die Tiefe beim Aufschlüssel lässt sich begrenzen, also

Get-ClusterNode -Cluster FILECLUSTER | Select-Object * | ConvertTo-Json -Depth 1

und schon ist die Ausgabe wieder kürzer:

[
    {
        "BuildNumber":  20348,
        "Cluster":  "FILECLUSTER",
        "CSDVersion":  "",
        "Description":  "",
        "DrainStatus":  0,
        "DrainTarget":  4294967295,
        "DrainErrorCode":  0,
        "DynamicWeight":  0,
        "Id":  "1",
        "MajorVersion":  10,
        "MinorVersion":  0,
        "Name":  "FILESERVER-A",
        "NeedsPreventQuorum":  0,
        "NodeHighestVersion":  720900,
        "NodeInstanceID":  "00000000-0000-0000-0000-000000000001",
        "NodeLowestVersion":  720900,
        "NodeName":  "FILESERVER-A",
        "NodeWeight":  1,
        "FaultDomain":  "Site:Site 192.168.0.0/24 Rack: Chassis:",
        "Model":  "VMware7,1",
        "Manufacturer":  "VMware, Inc.",
        "SerialNumber":  "VMware-42 19 37 83 8b 83 e6 18-ad b3 73 bf da eb cb e1",
        "State":  0,
        "StatusInformation":  0,
        "Type":  0,
        "UniqueID":  "ad2d4545-37b6-4c2c-b78f-9ecf72cd459c",
        "DetectedCloudPlatform":  0
    },
    {
        "BuildNumber":  20348,
        "Cluster":  "FILECLUSTER",
        "CSDVersion":  "",
        "Description":  "",
        "DrainStatus":  0,
        "DrainTarget":  4294967295,
        "DrainErrorCode":  0,
        "DynamicWeight":  1,
        "Id":  "2",
        "MajorVersion":  10,
        "MinorVersion":  0,
        "Name":  "FILESERVER-B",
        "NeedsPreventQuorum":  0,
        "NodeHighestVersion":  720900,
        "NodeInstanceID":  "00000000-0000-0000-0000-000000000002",
        "NodeLowestVersion":  720900,
        "NodeName":  "FILESERVER-B",
        "NodeWeight":  1,
        "FaultDomain":  "Site:Site 192.168.0.0/24 Rack: Chassis:",
        "Model":  "VMware7,1",
        "Manufacturer":  "VMware, Inc.",
        "SerialNumber":  "VMware-42 19 2f 2a ed fa 97 ca-39 76 ff 79 0f fc 0e 78",
        "State":  0,
        "StatusInformation":  0,
        "Type":  0,
        "UniqueID":  "5f4e5ff5-1452-47fb-b9f8-b919d7815a39",
        "DetectedCloudPlatform":  0
    }
]

Das könnte man nun schon so in Zabbix übernehmen. Aber mich störte noch eines:
Aus

State                 : Up

wird im JSON ein

"State":  0,

0 heißt in diesem Fall wohl Up - aber ich weis gar nicht welche anderen Werte es gäbe. Und hätte deshalb an dieser Steller lieber den Text.
Dieses Umwandeln macht er aber nur wenn man nach JSON wandelt, würde man nach CSV wandeln passiert das nicht. Das machen wir uns zu nutze:

Get-ClusterNode -Cluster FILECLUSTER| Select-Object * | ConvertTo-Csv | ConvertFrom-Csv | ConvertTo-Json -Depth 1

Wir wandeln das Ergebnis also zurerst ins CSV-Format, dann wieder zurück und zuletzt doch ins JSON Format.
Und schon haben wir den Text statt der Zahlen:

[
    {
        "BuildNumber":  "20348",
        "Cluster":  "FILECLUSTER",
        "CSDVersion":  "",
        "Description":  "",
        "DrainStatus":  "NotInitiated",
        "DrainTarget":  "4294967295",
        "DrainErrorCode":  "0",
        "DynamicWeight":  "0",
        "Id":  "1",
        "MajorVersion":  "10",
        "MinorVersion":  "0",
        "Name":  "FILESERVER-A",
        "NeedsPreventQuorum":  "0",
        "NodeHighestVersion":  "720900",
        "NodeInstanceID":  "00000000-0000-0000-0000-000000000001",
        "NodeLowestVersion":  "720900",
        "NodeName":  "FILESERVER-A",
        "NodeWeight":  "1",
        "FaultDomain":  "System.String[]",
        "Model":  "VMware7,1",
        "Manufacturer":  "VMware, Inc.",
        "SerialNumber":  "VMware-42 19 37 83 8b 83 e6 18-ad b3 73 bf da eb cb e1",
        "State":  "Up",
        "StatusInformation":  "Normal",
        "Type":  "Node",
        "UniqueID":  "ad2d4545-37b6-4c2c-b78f-9ecf72cd459c",
        "DetectedCloudPlatform":  "None"
    },
    {
        "BuildNumber":  "20348",
        "Cluster":  "FILECLUSTER",
        "CSDVersion":  "",
        "Description":  "",
        "DrainStatus":  "NotInitiated",
        "DrainTarget":  "4294967295",
        "DrainErrorCode":  "0",
        "DynamicWeight":  "1",
        "Id":  "2",
        "MajorVersion":  "10",
        "MinorVersion":  "0",
        "Name":  "FILESERVER-B",
        "NeedsPreventQuorum":  "0",
        "NodeHighestVersion":  "720900",
        "NodeInstanceID":  "00000000-0000-0000-0000-000000000002",
        "NodeLowestVersion":  "720900",
        "NodeName":  "FILESERVER-B",
        "NodeWeight":  "1",
        "FaultDomain":  "System.String[]",
        "Model":  "VMware7,1",
        "Manufacturer":  "VMware, Inc.",
        "SerialNumber":  "VMware-42 19 2f 2a ed fa 97 ca-39 76 ff 79 0f fc 0e 78",
        "State":  "Up",
        "StatusInformation":  "Normal",
        "Type":  "Node",
        "UniqueID":  "5f4e5ff5-1452-47fb-b9f8-b919d7815a39",
        "DetectedCloudPlatform":  "None"
    }
]

Damit wäre der Befehl an dieser Stelle schon fertig. Diesen Befehl können wir problemlos in unser Item einfügen.


PowerShell-Befehl hat Kommas oder andere Sonderzeichen

Important.png
Hinweis:Das nachfolgende könnt Ihr auch Überspringen, aber vielleicht könnt Ihr das ja mal brauchen


Man könnte nun auch das Select-Object anpassen das nur die nötigsten Informationen gesammelt werden:

Get-ClusterNode -Cluster FILECLUSTER | Select-Object NodeName,State | ConvertTo-Csv | ConvertFrom-Csv | ConvertTo-Json -Depth 1

Sammelt nur die Daten von NodeName,State, die Ausgabe wäre:

[
    {
        "NodeName":  "FILESERVER-A",
        "State":  "Up"
    },
    {
        "NodeName":  "FILESERVER-B",
        "State":  "Up"
    }
]

Aber es gibt dann doch ein Problem. Wir wollen den PowerShell-Befehl ja in unser Item unter system.run[ ... ] einfügen. Und dort wird das Komma für die Unterscheidung der Parameter in Zabbix genutzt. Das habe ich eine Weile probiert - das escapen geht an dieser Stelle nicht und auch mit " ... " hatte ich Probleme. Dies Lösung in diesem Fall wäre folgende:

$myContent = "Get-ClusterNode -Cluster FILECLUSTER | Select-Object NodeName,State | ConvertTo-Csv | ConvertFrom-Csv | ConvertTo-Json -Depth 1"
$base64 = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($myContent))
write-host $base64

Da kommt dann "Kauderwelsch":

RwBlAHQALQBDAGwAdQBzAHQAZQByAE4AbwBkAGUAIAAtAEMAbAB1AHMAdABlAHIAIABCAEUAUgBMAEkATgBDAEwAVQBTAFQARQBSACAAfAAgAFMAZQBsAGUAYwB0AC0ATwBiAGoAZQBjAHQAIABOAG8AZABlAE4AYQBtAGUALABTAHQAYQB0AGUAIAB8ACAAQwBvAG4AdgBlAHIAdABUAG8ALQBDAHMAdgAgAHwAIABDAG8AbgB2AGUAcgB0AEYAcgBvAG0ALQBDAHMAdgAgAHwAIABDAG8AbgB2AGUAcgB0AFQAbwAtAEoAcwBvAG4AIAAtAEQAZQBwAHQAaAAgADEA

Der aber als gültiger PowerShell-Befehl genutzt werden kann (z.B. in einer Eingabeaufforderung):

powershell.exe -noprofile -nologo -encodedCommand "RwBlAHQALQBDAGwAdQBzAHQAZQByAE4AbwBkAGUAIAAtAEMAbAB1AHMAdABlAHIAIABCAEUAUgBMAEkATgBDAEwAVQBTAFQARQBSACAAfAAgAFMAZQBsAGUAYwB0AC0ATwBiAGoAZQBjAHQAIABOAG8AZABlAE4AYQBtAGUALABTAHQAYQB0AGUAIAB8ACAAQwBvAG4AdgBlAHIAdABUAG8ALQBDAHMAdgAgAHwAIABDAG8AbgB2AGUAcgB0AEYAcgBvAG0ALQBDAHMAdgAgAHwAIABDAG8AbgB2AGUAcgB0AFQAbwAtAEoAcwBvAG4AIAAtAEQAZQBwAHQAaAAgADEA"

Ergibt die gleiche Ausgabe wie zuvor. Gefunden habe ich die Idee dazu hier: https://serverthings.com/zabbix-running-powershell-scripts-on-hosts-with-system-run/


JSON verkleinern

ConvertTo-Json

liefert ja ein schön lesbares JSON. Es geht aber auch kompakter per

ConvertTo-Json -Compress

dann wird alles hintereinander geschrieben ohne neue Zeilen, also alles in einer Zeile:

Get-ClusterNode -Cluster FILECLUSTER | Select-Object NodeName,State | ConvertTo-Csv | ConvertFrom-Csv | ConvertTo-Json -Depth 1 -Compress

Ausgabe:

[{"NodeName":"FILESERVER-A","State":"Up"},{"NodeName":"FILESERVER-B","State":"Up"}]

Das eigentlich Item

ImageRAWITEM202310121003.png
Der Typ ist Zabbix agent oder Zabbix agent (actice), zum Testen ist der Agent ohne active einfacher weil wir den Wert dann vom Server aus anfordern können.
Der Key ist:

system.run[powershell.exe -noprofile -nologo "Get-ClusterNode -Cluster {$CLUSTERNAME}| Select-Object * | ConvertTo-Csv | ConvertFrom-Csv | ConvertTo-Json -Compress"]

Ich habe hier den Clusternamen als Makro gesetzt, das geht also ohne Probleme und wird von Zabbix ersetzt.
Der Typ ist Text, wir brauchen ansonsten keine Preprocessing oder anderes.

Nach der eingestellten Zeit oder per "Execute now" am Host sollte unser JSON eintreffen:
ImageRAWITEM202310121013.png


Das Discovery-Item

Auf Basis dieses JSON, welches in meinem Fall alle 10 Minuten aktualisiert wird, bauen wir uns ein Discovery:
ImageDiscovery202310121015a.png

Es ist also ein Dependent item welches auf unser RAW-Item zeigt.
Den Key können wir uns selbst beliebig ausdenken. Wichtig ist nun das Preprocessing:
ImageDiscovery202310121018.png
Für eine LLD brauchen wir dessen speziellen Makronamen {#......}, diese enthält unser JSON aber nicht.
Das JSON sieht so aus (Auschnitt):

[
    {
        "BuildNumber":  "20348",
        "Cluster":  "FILECLUSTER",
        "CSDVersion":  "",
        "Description":  "",
        "DrainStatus":  "NotInitiated",
        "DrainTarget":  "4294967295",
        "DrainErrorCode":  "0",
        "DynamicWeight":  "0",
        "Id":  "1",
        "MajorVersion":  "10",
        "MinorVersion":  "0",
        "Name":  "FILESERVER-A",
        "NeedsPreventQuorum":  "0",
        "NodeHighestVersion":  "720900",
        "NodeInstanceID":  "00000000-0000-0000-0000-000000000001",
        "NodeLowestVersion":  "720900",
        "NodeName":  "FILESERVER-A",
        "NodeWeight":  "1",
        "FaultDomain":  "System.String[]",
        "Model":  "VMware7,1",
        "Manufacturer":  "VMware, Inc.",
        "SerialNumber":  "VMware-42 19 37 83 8b 83 e6 18-ad b3 73 bf da eb cb e1",
        "State":  "Up",
        "StatusInformation":  "Normal",
        "Type":  "Node",
        "UniqueID":  "ad2d4545-37b6-4c2c-b78f-9ecf72cd459c",
        "DetectedCloudPlatform":  "None"
    }
]


Für die Items brauche ich die Werte von Id, NodeName, State, StatusInformationen, UniqueID und Cluster.
Diese ziehen wir mit einem JavaScript aus dem JSON und wandeln diese in die LLD-Makros um:

//2023-10-11 Bernhard.Linz@datagroup.de / Bernhard@znil.de
output = JSON.parse(value).map(function(mycluster){
    return {
        "{#CLUSTERNODEID}": mycluster.Id,
        "{#CLUSTERNODENAME}": mycluster.NodeName,
        "{#CLUSTERNODESTATE}": mycluster.State,
        "{#CLUSTERNODESTATUSINFORMATION}": mycluster.StatusInformation,
        "{#CLUSTERNODEUNIQUEID}" : mycluster.UniqueID,
        "{#CLUSTERNODECLUSTER}": mycluster.Cluster
    }})
return JSON.stringify({"data": output})

Als Ausgabe kommt da also ein LLD-JSON zurück:
ImageDiscovery202310121034.png
Das ganze habe ich mir nicht selbst ausgedacht sondern aus den aktuellen Templates von Zabbix 6.0 abgeschaut, konkret beim aktuellen Template für Windows, die Erkennung der Netzwerkkarten per WMI.
Nun fehlen nur noch die Item prototypes


Item prototypes

Der Type ist wiederum Dependent item mit unserem RAW-Item als Quelle.
In unserem JSON gibt es ja pro Node einen Aufzählung, die Werte wiederholen sich also pro Node.
Alle Werte des ersten Elements, der ersten Gruppe könnte man per $[0].xxx ansprechen, die der zweiten Gruppe per $[1].xxx und so weiter. Nun wollte ich mich aber nicht auf die Reihenfolge der Gruppen verlassen, die könnte sich ja auch mal ändern. Oder es fällt eines weg und ein neues käme hinzu. In diesem Falle würde sich alles verschieben. Also gehen wir nicht stur nach der Reihenfolge sondern suchen uns für jede Gruppe etwas woran man diese eindeutig Identifizieren kann.
Das muss also einer der Werte sein der dann jedes mal anders ist. Hier hätten wir gleich mehrere Möglichkeiten, die Id, den NodeName oder die UniqueID.
Ich habe mich hier für die UniqueID entschieden, die wir im Key dann als erstes einsetzen damit jedes Item einen eigene Key bekommt:

failovercluster.clusternode.state[{#CLUSTERNODEUNIQUEID}]

Der Key-Name ist komplett ausgedacht, wichtig ist das der Variable Teil in [ ... ] und am Ende steht.
Also Variablen Teil habe ich das LLD Makro der UniqueID eingetragen.
Da als Wert hier ein "Up" oder "Down" (oder etwas anderes) zurück kommt ist der Type of information Text (Character hätte wohl auch gereicht)

Um an den Wert selbst zu kommen brauchen wir das Preprocessing:
ImagePrototype202310121059.png
Die härteste Nuss für mich war der korrekte JSON-Path. Damit kenne ich mich - eigentlich - aus, ich musste tüfteln bis ich den Index ( $.[x].. ) richtig übersprungen hatte.
Geholfen hatte mir dabei tatsächlich mal ChatGPT, das erste mal das mir das Ding auf anhieb eine richtige Lösung präsentierte:

$.[?(@.UniqueID == '{#CLUSTERNODEUNIQUEID}')].State

gibt Wert von State aus der Gruppe zurück in der die UniqueID unserem aktuellen Item entspricht:
ImagePrototype202310121105.png Wie ihr im Screenshot seht gibt er aber

["Up"]

zurück. Das ist der Nachteil wenn man anhand eines Objekt nach einem Wert sucht, das ist dann nun mal leider so.
Aber kein Problem für Zabbix, in den nächsten beiden Schritten schneiden rechts die Zeichen "]" und links die Zeichen [" ab:
ImagePrototype202310121109.png

Die weiteren Item prototyes habe ich sinngemäß genauso erstellt.


Ergebnis

Das Discovery läuft jedes mal wenn der Wert des RAW-Items aktualisiert wird, beim nächsten Durchgang werden unsere Items also erstellt (haben dann aber noch keine Werte):
Image202310121113.png

Wenn dann das nächste mal wieder das RAW Item abgerufen wird so füllen sich unsere Items auch mit Werten:
Image202310121116.png


Optimierungen

Man könnte das ganze nun noch verbessern, zum Beispiel im Preprocessing ein Throttling einführen das nur geänderte Werte gespeichert würden.
Das würde ich aber erst nach der Testphase machen. Nutzt Ihr das beim RAW Item so werdet Ihr wahnsinnig weil die Items dann nicht angelegt werden (wenn sich nichts im JSON ändert) und es keine Werte gibt.


Kommentare

Loading comments...