Wie man aus dem Graph der Netzwerktopologie programmatisch sinnvolle Konfigurationen für Nagios-basierte Systeme ableitet.
Schon Nagios 3 konnte Abhängigkeiten (“parents”) zwischen Hosts spezifizieren, damit nur der dichteste Host beim Ausfall eine Benachrichtigung generiert. Und als Vater aller Monitoring-Lösungen hat Nagios dieses Feature an fast alle heutigen Systeme vererbt.
Wer also ein vollständiges Asset Management inklusive Netzwerkverbindungen hat, möchte diese natürlich auch als Abhängigkeiten im Monitoring wiederfinden. Leider lässt sich das bei redundanten Topologien nicht 1:1 übersetzen, da die Abhängigkeiten von Nagios azyklisch sein müssen.
Ein Beispiel: Wir haben die Hosts A bis E (z.B. Router oder Switche), die im Kreis verbunden sind. An Host A hängt unser Monitoring.
Monitoring
|
|
E ---- A ---- B
| |
| |
D ----------- C
Der Host D ist also sowohl über C als auch über E erreichbar. So weit ließe sich das auch noch darstellen:
define host {
host_name D
parents C,E
}
So würden Benachrichtigungen für D unterdrückt werden, wenn sowohl C als auch E schon nicht erreichbar sind.
Nun ist ja aber auch C sowohl über B als auch über D erreichbar. Analog würde man also konfigurieren:
define host {
host_name C
parents B,D
}
Und nun ist C gleichzeitig “über” als auch “unter” D in der Hierarchie. Das funktioniert natürlich nicht.
Das ist auch inhaltlich klar: Würde Nagios solche “zyklischen” Abhängigkeiten erlauben, könnten wir schlicht A bis E jeweils von beiden Nachbarn abhängig machen (was in dieser Topologie zwar falsch wäre, aber möglich zu konfigurieren). Wenn dann alle 5 Hosts ausfallen wird für keinen eine Benachrichtigung verschickt, weil ja für jeden einzelnen Host auch seine beiden Parents/Nachbarn ausgefallen sind.
Wir können aber doch wenigstens etwas über die Abhängigkeiten in dieser Topologie konfigurieren, nämlich dass die Hosts B bis E vom Host A abhängig sind!
Nun haben wir bei PLUTEX ein gut gepflegtes Asset Management inklusive physischen (und virtuellen) Netzwerkverbindungen. Abstrakt sind wir also ganz klassisch im Bereich der Graphentheorie, mit Hosts als Knoten und Verbindungen als Kanten. Um aus diesem Graphen die zu konfigurierenden Abhängigkeiten zu ermitteln, müssen wir das obige Konstrukt in die Sprache der Graphentheorie übersetzen. Und praktischerweise gibt es für Knoten wie A einen Namen: Ein Gelenkpunkt (engl. cut point) teilt den Graphen in mehrere Blöcke, die durch entfernen des Gelenkpunkts zu eigenständigen Zusammenhangskomponenten werden.
Um also alle konfigurierbaren Abhängigkeiten in einem Graphen G zu finden dient uns daher dieser Python-Code, der den Graphen bereits als networkx-Objekt erhält:
# We will store the dependencies in a new, directed graph
dependencies = nx.DiGraph()
dependencies.add_nodes_from(G.nodes)
# We really only operate on the connected component of the root node.
# For all other connected components, we have no idea which way a
# dependency should point.
G = G.subgraph(nx.node_connected_component(G, root_node))
# We want to add dependencies from host A to host B if A is only
# reachable via B. Graph theoretically, B is then a cut vertex or
# articulation point (i.e. increases the number of connected components
# if removed), and A is not in the same connected component as the root
# node after the removal of B.
#
# We build this list of dependencies by iterating the cut vertices and
# adding dependencies on them to all nodes in the non-root components of
# the modified graph.
for cut_vertex in nx.articulation_points(G):
if cut_vertex == root_node:
# Even though the root node probably is well connected in the
# topology, it may be a cut vertex in some cases, in which case
# it doesn't yield any useful dependencies anyway
continue
tmpG = G.copy()
tmpG.remove_node(cut_vertex)
# We iterate the relevant connected components of the reduced graph
# by iterating the neighbors of the cut vertex in the original graph
for neighbor in G.neighbors(cut_vertex):
component = nx.node_connected_component(tmpG, neighbor)
if root_node in component:
# The connected component that still contains the root node
# isn't dependant on the cut vertex
continue
for host in component:
dependencies.add_edge(host, cut_vertex)
# We cannot have cyclic dependencies
assert nx.is_directed_acyclic_graph(dependencies)
Übrigens: Natürlich verwenden wir die Topologieinformationen unseres Asset Managements nicht nur zur Generierung von Konfigurationen für unser Monitoring, sondern auch zur Visualisierung unseres Netzes.