Kapitel 4. Junos PyEZ

Diese Arbeit wurde mithilfe von KI übersetzt. Wir freuen uns über dein Feedback und deine Kommentare: translation-feedback@oreilly.com

Dieses Kapitel befasst sich mit Junos PyEZ, einem weiteren Automatisierungstool, mit dem Remote Procedure Calls auf Junos-Geräten aufgerufen werden können. PyEZ ist eine Python-Bibliothek, die die Verwaltung und Automatisierung von Junos-Geräten ermöglicht. Es handelt sich um ein Open-Source-Projekt, das von Juniper Networks mit Beiträgen aus der Nutzergemeinschaft gepflegt und unterstützt wird. Das Junos PyEZ-Projekt wird auf GitHub unter https://github.com/Juniper/py-junos-eznc gehostet .

Die PyEZ-APIs bieten ein "Mini-Framework", mit dem sich sowohl einfache als auch komplexe Automatisierungsaufgaben lösen lassen. PyEZ kann über die interaktive Python-Shell verwendet werden, um schnell einfache Aufgaben auf einem oder mehreren Junos-Geräten auszuführen, oder in umfassende Python-Skripte unterschiedlicher Komplexität integriert werden, um die Verwaltung und Administration eines ganzen Netzwerks von Junos-Geräten zu automatisieren. In den ersten Abschnitten dieses Kapitels wird die Eingabe von Befehlen in der interaktiven Python-Shell erläutert, die durch die Eingabeaufforderung >>> angezeigt wird. Im Abschnitt "Ein PyEZ-Beispiel" wird ein vollständiges Python-Skript gezeigt, das die PyEZ-Bibliothek verwendet.

PyEZ bietet eine Abstraktionsschicht, die auf dem in Kapitel 2 behandelten NETCONF-Protokoll aufbaut. Es erfordert keine direkte NETCONF-Interaktion, sondern nutzt die herstellerunabhängige ncclient1 Bibliothek für ihren NETCONF-Transport. Da die PyEZ-Bibliothek NETCONF für ihre Remote Procedure Calls nutzt, kann sie mit allen derzeit unterstützten Junos-Softwareversionen und Junos-Plattformen verwendet werden.

Wie die Junos RESTful API, die in Kapitel 3 behandelt wird, unterstützt die PyEZ-Bibliothek den Aufruf einzelner Junos RPCs und das Abrufen der daraus resultierenden Antworten. Im Gegensatz zum Junos RESTful API Service bietet PyEZ jedoch auch optionale Funktionen, die gängige Automatisierungsaufgaben weiter vereinfachen. Ein Beispiel für diese Funktionen ist die automatische Verbindung mit einem Junos-Gerät über die PyEZ-Bibliothek. Standardmäßig sammelt die PyEZ-Bibliothek grundlegende Informationen über das Gerät und speichert diese Informationen in einem Python-Wörterbuch. Auf dieses Wörterbuch kann mit über das Attribut facts zugegriffen werden, das in "Fakten sammeln" beschrieben wird .

Andere Abstraktionsmerkmale der PyEZ-Bibliothek betreffen den Umgang mit RPC-Antworten. Anstatt RPC-Antworten als XML- oder JSON-Strings zurückzugeben, nutzt PyEZ die lxml Bibliothek, um XML-spezifische Python-Datenstrukturen direkt zurückzugeben. Sie kann auch mit der jxmlease-Bibliothek kombiniert werden, die in "Strukturierte Daten in Python verwenden" vorgestellt wurde , um das Parsen dieser XML-spezifischen Datenstrukturen in eine native Python-Datenstruktur zu vereinfachen. Tabellen und Ansichten sind ein weiteres Werkzeug, um RPC-Antworten in native Python-Datenstrukturen umzuwandeln. PyEZ bietet mehrere vordefinierte Tabellen und Ansichten für gängige RPCs und ermöglicht es den Nutzern auch, eigene zu definieren, um Informationen aus jedem Junos-RPC zu extrahieren.

Für die Konfiguration unterstützt PyEZ Änderungen in Text-, XML- oder Set-Formaten. Außerdem enthält es eine Engine, die vom Benutzer eingegebene Werte mit Templates kombiniert, um dynamisch geräte-, kunden- oder funktionsspezifische Konfigurationsänderungen zu erzeugen.

Installation

Die Ausführung eines Python Skripts, das die Junos PyEZ-Bibliothek nutzt, erfordert, dass Junos PyEZ auf dem Automatisierungshost installiert ist, der das Skript ausführt. Junos PyEZ ist abhängig von Python und mehreren Systembibliotheken, deren Installation betriebssystemspezifisch ist. Diese Abhängigkeiten können sich auch mit neuen PyEZ-Versionen ändern. Daher wird in diesem Buch nicht versucht, das Verfahren zur Installation der vorausgesetzten Systemsoftware zu beschreiben. Stattdessen findest du im Abschnitt "Installieren von PyEZ" des versionsspezifischen "Junos PyEZ Developer Guide" auf der Junos PyEZ Landing Page von Juniper Informationen zur Installation der erforderlichen Systemsoftware auf gängigen Betriebssystemen.

Hinweis

Zum Zeitpunkt der Erstellung dieses Dokuments unterstützt PyEZ nicht Python 3.x. Stelle sicher, dass Python 2.7 (oder eine neuere 2.x-Version) auf deinem System installiert ist und dass das System so konfiguriert ist, dass es Python 2.x für alle Skripte verwendet, die die Junos PyEZ-Bibliothek nutzen.

Sobald die entsprechenden Systembibliotheken auf dem Hostsystem installiert sind, kann PyPI, der Python Package Index, verwendet werden, um Junos PyEZ und seine abhängigen Python-Bibliotheken zu installieren. Führen Sie einfach den Befehl pip install junos-eznc in einer Root-Shell Eingabeaufforderung aus, um die neueste stabile Version von Junos PyEZ zu installieren. Sie können auch den Befehl pip install git+https://github.com/Juniper/py-junos-eznc.git2 verwenden, um die neueste Entwicklungsversion von Junos PyEZ direkt aus dem GitHub-Repository zu installieren.

Warnung

PyPI installiert automatisch die erforderlichen Python-Module, wenn du Junos PyEZ mit dem Befehl pip install installierst. Eines dieser vorausgesetzten Python-Module ist lxml. Als Teil seiner Installation besteht das lxml-Modul darauf, die libxml2 C-Bibliothek aus dem Quellcode herunterzuladen und zu kompilieren, auch wenn auf deinem System bereits eine funktionierende libxml2 Bibliothek installiert ist. Diese Anforderung bedeutet, dass auf deinem Rechner alle notwendigen Tools installiert sein müssen, um libxml2 zu kompilieren. Dazu gehören ein C-Compiler, ein make-Programm und eine zlib Komprimierungsbibliothek mit Header-Dateien (normalerweise in einem zlib-devPaket). Fehlt eines dieser Werkzeuge auf dem Rechner, kann die Installation von Junos PyEZ fehlschlagen.

Gerätekonnektivität

Wie in der Einleitung des Kapitels erläutert, verwendet PyEZ NETCONF, um mit einem entfernten Junos-Gerät zu kommunizieren. PyEZ setzt daher voraus, dass NETCONF auf dem Zielgerät aktiviert ist. Alle derzeit unterstützten Versionen der Junos-Software unterstützen das NETCONF-Protokoll über einen SSH-Transport, aber dieser NETCONF-over-SSH-Dienst ist standardmäßig nicht aktiviert. Die minimale Konfiguration, die erforderlich ist, um den NETCONF-over-SSH-Dienst zu aktivieren, ist:

set system services netconf ssh

Der NETCONF-over-SSH-Dienst lauscht standardmäßig am TCP-Port 830 und kann sowohl über IPv4 als auch über IPv6 betrieben werden. Die folgende Junos-CLI-Ausgabe zeigt, wie du den NETCONF-over-SSH-Dienst konfigurierst und überprüfst, ob der Dienst tatsächlich am TCP-Port 830 lauscht:

user@r0> configure 
Entering configuration mode

[edit]
user@r0# set system services netconf ssh 

[edit]
user@r0# commit and-quit 
commit complete
Exiting configuration mode

user@r0> show system connections inet | match 830 
tcp4       0      0  *.830                    *.*                       LISTEN

user@r0> show system connections inet6 | match 830   
tcp6       0      0  *.830                    *.*                       LISTEN
Hinweis

Wenn der SSH-Dienst mit der Konfiguration set system services ssh aktiviert ist, ist es auch möglich, den NETCONF-over-SSH-Dienst auf TCP-Port 22 zu erreichen. Die Konfiguration set system services netconf ssh ist jedoch vorzuziehen, da die PyEZ-Bibliothek standardmäßig versucht, sich mit dem NETCONF-over-SSH-Port (TCP-Port 830) zu verbinden.

Sobald der NETCONF-over-SSH-Dienst konfiguriert wurde, ist das Gerät bereit für die Verwendung mit Junos PyEZ.

Erstellen einer Geräteinstanz

Die PyEZ-Bibliothek stellt eine Klasse jnpr.junos.Device zur Verfügung, die ein Junos-Gerät darstellt, auf das die PyEZ-Bibliothek zugreift ( ). Der erste Schritt bei der Verwendung der Bibliothek besteht darin, eine Instanz dieser Klasse mit den spezifischen Parametern für das Junos-Gerät zu instanziieren:

user@h0$ python 1
Python 2.7.9 (default, Mar  1 2015, 12:57:24) 
[GCC 4.9.2] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> from jnpr.junos import Device 2
>>> r0 = Device(host='r0',user='user',password='user123') 3
1

Rufe zunächst die interaktive Python-Shell mit dem Befehl python auf.

Hinweis

Der genaue Befehl zum Aufrufen der interaktiven Python-Shell hängt vom Betriebssystem und der Python-Installation auf dem Automatisierungsrechner ab. Verwende den Befehl, der für deine spezielle Umgebung geeignet ist.

2

Bevor die Klasse jnpr.junos.Device verwendet werden kann, muss sie zunächst importiert werden. Diese Zeile importiert das Python-Paket jnpr.junos und kopiert den Namen Device in den lokalen Namensraum, so dass du einfach auf Device() verweisen kannst. Eine alternative Syntax ist import jnpr.junos. Auch hier wird das Python-Paket jnpr.junos importiert, aber der Name Device wird nicht in den lokalen Namensraum kopiert. Bei dieser Syntax musst du die Klasse als Attribut von jnpr.junos mit der Syntax jnpr.junos.Device() referenzieren.

3

Durch den Aufruf des Klassenobjekts Device mit der Syntax Device() wird ein neues Instanzobjekt erstellt. Diese Instanz repräsentiert eine bestimmte NETCONF-over-SSH-Sitzung zu einem bestimmten Junos-Gerät. In diesem Fall wird das Instanzobjekt der Variablen mit dem Namen r0 zugewiesen. Der Name r0 ist nichts Besonderes und kann durch jeden anderen gültigen Python-Variablennamen ersetzt werden. Die Parameter des Aufrufs Device() legen die Anfangswerte der Attribute der Instanz fest. In diesem Beispiel wurden die Parameter host, user und password auf die entsprechenden Werte für das Junos-Gerät mit dem Hostnamen r0 gesetzt.

Während es üblich ist, die Parameter host, user und password anzugeben, ist das einzige obligatorische Argument für den Aufruf Device()der Parameter host. Die Hostinformationen können auch als erstes (unbenanntes) Argument für den Aufruf Device() angegeben werden. In Tabelle 4-1 sind die Device() Parameter und ihre Standardwerte aufgeführt.

Tabelle 4-1. Parameter für die Klasse jnpr.junos.Device
ParameterBeschreibungStandardwert
hostEin Hostname, Domainname oder eine IPv4- oder IPv6-Adresse, auf der das Junos-Gerät den NETCONF-over-SSH-Dienst ausführt. Wenn ein Hostname oder Domänenname verwendet wird, muss er in eine IPv4- oder IPv6-Adresse aufgelöst werden. Dieser Parameter kann alternativ auch als erstes unbenanntes Argument beim Aufruf von Device() angegeben werden.None (Muss vom Anrufer angegeben werden.)
portDer TCP Port, auf dem der NETCONF-over-SSH-Dienst erreichbar ist. Wenn set system services ssh auf dem Junos-Gerät konfiguriert ist, kannst du den NETCONF-Dienst erreichen, indem du das Argument port = 22 beim Aufruf von Device() angibst.830
userDer Benutzername , mit dem du dich am Junos-Gerät anmeldest. Wie unter "Authentifizierung und Autorisierung" beschrieben, wird die RPC-Ausführung durch die Autorisierungskonfiguration dieses Benutzerkontos auf dem Junos-Gerät gesteuert.Der Wert von die Umgebungsvariable $USERfür das Konto, das das Python-Skript auf dem Automatisierungshost ausführt. Normalerweise wird dieser Wert auf den Benutzernamen des Benutzers gesetzt, der das Python-Skript ausführt. Das Standardverhalten kann sinnvoll sein, wenn der Benutzername auf dem Automatisierungshost und dem Junos-Gerät identisch ist. In der interaktiven Python-Shell kannst du den Wert der Umgebungsvariablen $USER mit bestätigen:
>>> import os
>>> print(os.environ['USER'])
user
passwordDas Passwort, das zur Authentifizierung des Benutzers auf dem Junos-Gerät verwendet wird. Wenn SSH-Schlüssel verwendet werden, wird dieser Wert als Passphrase verwendet, um den privaten SSH-Schlüssel zu entsperren. Ansonsten wird dieser Wert für die Passwortauthentifizierung verwendet.None (Für einen SSH-Schlüssel mit einer leeren Passphrase wird kein Passwort benötigt).
gather_factsEin boolescher Wert der angibt, ob bei der ersten Verbindung mit der Instanzmethode open() grundlegende Informationen vom Gerät erfasst werden oder nicht. Siehe " Fakten sammeln" für weitere Informationen.True (Fakten werden gesammelt.)
auto_probeMit dieser Einstellung wird geprüft, ob der durch port angegebene TCP-Port erreichbar ist, indem zunächst eine einfache TCP-Verbindung zu diesem Port versucht wird. Erst wenn diese Testverbindung erfolgreich ist, wird die echte NETCONF-over-SSH-Verbindung versucht. Der Wert auto_probeist eine ganze Zahl, die die Anzahl der Sekunden angibt, in denen versucht wird, diese Test-TCP-Verbindung zu port herzustellen, bevor die Zeit abgelaufen ist. Wenn der Wert 0 lautet, ist die automatische Überprüfung deaktiviert und es wird keine Test-TCP-Verbindung versucht. (In diesem Fall wird die echte NETCONF-over-SSH-Verbindung sofort aufgebaut).0 (Autoprobing ist deaktiviert. Dieser Wert wird bei der Instanziierung vom Attribut auto_probe der Klasse Device geerbt. Der Wert auto_probekann für alle Geräteinstanzen geändert werden, indem er Device.auto_probe = value vor der Instanziierung der Geräteinstanzen gesetzt wird.)
ssh_configDer Pfad, auf dem Automatisierungshost, zu einer SSH-Client-Konfigurationsdatei, die für die SSH-Verbindung verwendet wird. Mit der SSH-Client-Konfigurationsdatei lassen sich viele Aspekte der SSH-Verbindung steuern. Bei Unix-ähnlichen Automatisierungshosts findest du unter man ssh_config weitere Informationen zu den verfügbaren Einstellungen.~/.ssh/config (Die ~ wird auf das Heimatverzeichnis des Benutzers erweitert. Wenn keine SSH-Konfigurationsdatei gefunden wird, werden die systemweiten Standardwerte verwendet).
ssh_private_key_fileDer Pfad, auf dem Automatisierungshost, zu einer SSH-Schlüsseldatei, die für die SSH-Schlüsselauthentifizierung verwendet wird.Keine (Wenn angegeben, werden die in der Datei ssh_config konfigurierten SSH-Schlüsseldateien verwendet).
normalizeEin boolescher Wert , der angibt, ob die XML-Antworten dieses Geräts mit Leerzeichen normalisiert werden sollen oder nicht. Weitere Informationen findest du unter "Antwortnormalisierung".False (Whitespace ist nicht normalisiert.)

Die Verbindung herstellen

Das Erstellen einer Instanz der DeviceKlasse initiiert keine NETCONF-Verbindung zu der Instanz. Die Instanz wurde zwar mit allen notwendigen Informationen initialisiert, um eine NETCONF-Verbindung herzustellen, aber die eigentliche Verbindung wird erst hergestellt, wenn die open() Instanzmethode aufgerufen wird. Eine NETCONF-Verbindung zu der im vorherigen Beispiel erstellten Geräteinstanz r0 wird mit initiiert:

>>> r0.open()
Device(r0)

Die Methode open() gibt die Geräteinstanz zurück, wie die Ausgabe Device(r0) zeigt3 in unserem Beispiel zeigt. Dieser Rückgabewert wird in diesem Beispiel nicht benötigt, da er auf das gleiche Objekt wie die Variable r0 zeigt. Die Rückgabe der Geräteinstanz hat jedoch einen Sinn: Sie ermöglicht eine alternative Syntax, die die Aufrufe von Device() und open() in einer einzigen Zeile zusammenfasst. Hier ist die alternative Syntax:

>>> r0 = Device(host='r0',user='user',password='user123').open()
>>>

Unabhängig davon, ob der Aufruf open() auf eine bestehende Instanzvariable angewendet oder mit der Instanziierung Device()verkettet wird, wird die NETCONF-Verbindung mit den Informationen geöffnet, die in den Attributen der Geräteinstanz gespeichert sind. Zu diesen Attributen gehören die auto_probe, host, port, user und password Parameter sowie die SSH Konfigurationsinformationen. Alle diese Attribute werden bei der Instanzerstellung durch die Übergabe von Argumenten an den Aufruf Device() oder durch die Verwendung der in Tabelle 4-1 aufgeführten Standardwerte gesetzt. Es ist jedoch möglich, die auto_probe, gather_facts und normalize Einstellungen zu überschreiben, indem du dem Aufruf open() Parameter mitgibst. Wenn einer dieser Parameter als Argument für den Aufruf von open() angegeben wird, überschreibt er die Attribute der Geräteinstanz.

Authentifizierung und Autorisierung

Im Gegensatz zum RESTful API-Dienst muss bei PyEZ nicht jeder API-Aufruf authentifiziert werden. Stattdessen erfolgt die Authentifizierung nur, wenn die NETCONF-over-SSH-Sitzung durch den Aufruf von open() initiiert wird. Die Authentifizierung des NETCONF-over-SSH-Dienstes kann entweder mit dem öffentlichen SSH-Schlüssel oder mit einem Passwort erfolgen.

Die verwendeten SSH-Authentifizierungsmethoden und die Reihenfolge, in der sie ausprobiert werden, hängen von der SSH-Konfiguration des Clients ab. Bei einer typischen Client-SSH-Konfiguration wird zuerst die Authentifizierung mit dem öffentlichen Schlüssel und dann die Passwort-Authentifizierung versucht. In beiden Fällen verwendet das Junos-Gerät das Standard-Authentifizierungssystem von Junos, das auf der Hierarchieebene [edit system] der Junos-Konfiguration angegeben ist, um die Authentifizierung zuzulassen oder zu verweigern. Mit anderen Worten: NETCONF-over-SSH-Sitzungen werden genauso authentifiziert wie normale SSH-Verbindungen zur CLI.

Wenn die Authentifizierungsmethode mit öffentlichem Schlüssel verwendet wird, muss der öffentliche SSH-Schlüssel auf dem Junos-Gerät unter [edit system login user konfiguriert werden. username authentication] Ebene der Konfigurationshierarchie konfiguriert werden. Je nach Typ des öffentlichen SSH-Schlüssels wird der Schlüssel mit der Konfigurationsanweisung ssh-dsa, ssh-ecdsa, ssh-ed25519 oder ssh-rsa angegeben. Der öffentliche Schlüssel muss nicht nur auf dem Junos-Gerät konfiguriert werden, sondern auch der entsprechende private SSH-Schlüssel muss auf dem Automatisierungshost vorhanden sein. Um von PyEZ verwendet werden zu können, muss sich dieser private SSH-Schlüssel am Standardspeicherort oder in dem Pfad befinden, der durch das Attribut ssh_private_key_file device instance festgelegt wurde. Wenn der private SSH-Schlüssel eine Passphrase benötigt, versucht die Methode open(), das Attribut password der Instanz als Passphrase zu verwenden. Wenn die Passphrase des privaten SSH-Schlüssels leer ist, muss das Attribut password der Instanz nicht gesetzt werden.

Hier ist ein Beispiel für eine Junos-Konfiguration mit einem öffentlichen Schlüssel für den Benutzer user:

user@r0> show configuration system login user user 
uid 2001;
class super-user;
authentication {
    ssh-dsa "ssh-dss AAAAB3Nza ...output trimmed... SJCS9boQ== user@h0";
}

Der zugehörige private SSH-Schlüssel befindet sich am Standardspeicherort ~/.ssh/id_dsa auf dem Automatisierungshost:

user@h0$ cat ~/.ssh/id_dsa
-----BEGIN DSA PRIVATE KEY-----
MIIBugIBAAKBgQCqBuyGycDhwXmEDb3hXcEfSpD5gaomT91ojlcsSPVtoj773KqZ
...ouput trimmed...
PO+bL6L74rIKIi3cfFk=
-----END DSA PRIVATE KEY-----

Dieser private Schlüssel hat eine leere Passphrase,4 ermöglicht eine NETCONF-Verbindung zu r0 ohne Angabe eines Passworts:

user@h0$ python
Python 2.7.9 (default, Mar  1 2015, 12:57:24) 
[GCC 4.9.2] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> from jnpr.junos import Device
>>> r0 = Device('r0')
>>> r0.open()
Device(r0)
>>>

In diesem Beispiel wurde der Parameter userauch beim Aufruf von Device() weggelassen. Das funktioniert, weil die Umgebungsvariable $USER user ist, die mit dem entfernten Benutzernamen auf r0 übereinstimmt.

Bei der Passwortauthentifizierung kann das Junos-Gerät RADIUS, TACACS+ oder eine lokale Passwortdatenbank verwenden, um das Passwort zu verifizieren. Die genaue Authentifizierungsreihenfolge wird durch die [edit system authentication-order]Konfigurationshierarchie bestimmt. Dies ist genau dasselbe wie die Authentifizierung einer SSH-Verbindung zum CLI. Die konfigurierte authentication-order kann mehrere Passwortdatenbanken ausprobieren, bevor sie den Authentifizierungsversuch schließlich zulässt oder ablehnt.

Die Authentifizierung erfolgt beim Aufbau der NETCONF-over-SSH-Sitzung, aber die Autorisierung erfolgt bei jedem Remote Procedure Call. Die NETCONF-Autorisierung verwendet genau denselben Mechanismus und dieselbe Konfiguration wie die CLI-Autorisierung. Die Berechtigung zur Ausführung eines bestimmten RPCs wird durch die Zuordnung eines Benutzers (oder möglicherweise eines Template-Benutzers im Falle der RADIUS/TACACS+-Authentifizierung) zu einer Login-Klasse festgelegt. Die Loginklasse wiederum legt eine Reihe von Berechtigungen fest, die bestimmen, ob ein RPC-Aufruf erlaubt oder verweigert wird. Bei Bedarf kannst du unter "Authentifizierung und Autorisierung" nachlesen, wie die Beziehung zwischen der Junos-Konfiguration und der RPC-Autorisierung aussieht.

Verbindungsausnahmen

Die Junos PyEZ-Bibliothek definiert mehrere Ausnahmen, die ausgelöst werden können, wenn der Aufruf von open() fehlschlägt. Diese Ausnahmen sind Unterklassen der allgemeineren jnpr.junos.exception.ConnectError Klasse, obwohl eine generische jnpr.junos.exception.ConnectError Diese Ausnahmen sind Unterklassen der allgemeineren Klasse, obwohl eine allgemeine Ausnahme auch bei anderen unerkannten Verbindungsfehlern ausgelöst werden kann. Tabelle 4-2 enthält eine Liste und Beschreibungen dieser Ausnahmen und der Situation, in der jede Ausnahme ausgelöst wird.

Tabelle 4-2. Mögliche Ausnahmen, die von der Methode open() ausgelöst werden
ExceptionBeschreibung
jnpr.junos.exception.ProbeErrorWird ausgelöst, wenn auto_probeungleich ist und die Probe-Aktion fehlschlägt. Dies bedeutet in der Regel, dass das Gerät host auf dem TCP-Port port nicht erreichbar ist. Dies könnte auf ein Problem bei der Namensauflösung, ein Problem bei der IP-Erreichbarkeit oder eine Fehlkonfiguration des Geräts hinweisen. Auch wenn diese Ausnahme keinen genauen Hinweis auf das Problem liefert, ist sie ein Zeichen dafür, dass die NETCONF-Verbindung nicht erfolgreich sein kann.
jnpr.junos.exception.ConnectUnknownHostErrorWird ausgelöst, wenn der im Attribut host angegebene Hostname nicht in eine IP-Adresse aufgelöst werden kann. Dies deutet in der Regel auf ein Problem mit dem Auflösungsprozess des Domain Name Systems (DNS) hin.
jnpr.junos.exception.ConnectTimeoutErrorWird ausgelöst, wenn es ein IP-Erreichbarkeitsproblem bei der Verbindung zu port auf host gibt. Dies bedeutet, dass keine Antwort vom Junos-Gerät empfangen wurde. Mögliche Ursachen sind eine falsche IP-Adresse, Firewall-Filterung oder allgemeine Routing-Probleme zwischen dem Automatisierungshost und dem Junos-Gerät.
jnpr.junos.exception.ConnectRefusedErrorWird ausgelöst, wenn das Junos -Gerät die TCP-Verbindung zu port ablehnt. Dies kann bedeuten, dass der NETCONF-over-SSH-Dienst auf port nicht konfiguriert ist oder dass die maximale Anzahl gleichzeitiger Verbindungen erreicht wurde.
jnpr.junos.exception.ConnectAuthErrorWird ausgelöst , wenn die user oder password falsch ist und das Junos-Gerät den Authentifizierungsversuch ablehnt.
jnpr.junos.exception.ConnectNotMasterErrorWird ausgelöst, wenn die Verbindung zu einer Nicht-Master-Routing-Engine auf einem Multi-RE-Gerät hergestellt wird.
jnpr.junos.exception.ConnectErrorEine allgemeine Ausnahme, die ausgelöst wird, wenn die Bibliothek ncclient während des Verbindungsvorgangs eine unerkannte Ausnahme auslöst.

Um diese Ausnahmen abzufangen und angemessen zu behandeln, solltest du die Methode open() in einen try/exceptBlock einschließen. Hier ist ein einfaches Beispiel, das eine Meldung ausgibt, wenn eine dieser Ausnahmen auftritt. In der Ausgabe wird eine jnpr.junos.exception.​ConnectAuthError absichtlich ausgelöst, indem ein falsches Passwort angegeben wurde:

>>> from jnpr.junos import Device
>>> import jnpr.junos.exception
>>> r0 = Device(host='r0',user='user',password='badpass')
>>> try:
...     r0.open()
... except jnpr.junos.exception.ConnectError as err:
...     print('Error: ' + repr(err))
... 
Error: ConnectAuthError(r0)
>>>

Das Modul jnpr.junos.exception muss von der Anweisung import importiert werden, bevor die spezifische Ausnahme jnpr.junos.exception.ConnectError in der Anweisung exceptreferenziert wird.

Hinweis

Da alle diese Ausnahmen Unterklassen der Klasse jnpr.junos.exception.ConnectError sind, fängt die Angabe einer einzigen Ausnahme alle möglichen Ausnahmen ab, die von open() ausgelöst werden.

Sammeln von Fakten

Standardmäßig sammelt die PyEZ-Bibliothek beim Aufruf von open() grundlegende Informationen über das Junos-Gerät. PyEZ bezeichnet diese grundlegenden Informationen als Fakten, die über das factsDictionary-Attribut der Instanz des Geräts zugänglich sind. In diesem Beispiel wird das pprint Modul verwendet, um das facts Wörterbuch, das während des r0.open()Aufrufs gesammelt wurde, "hübsch auszudrucken":

>>> r0.open()
Device(r0)
>>> from pprint import pprint
>>> pprint(r0.facts)
{'2RE': False,
 'HOME': '/var/home/user',
 'RE0': {'last_reboot_reason': 'Router rebooted after a normal shutdown.',
         'mastership_state': 'master',
         'model': 'RE-VMX',
         'status': 'OK',
         'up_time': '6 days, 7 hours, 36 minutes, 44 seconds'},
 'domain': 'example.com',
 'fqdn': 'r0.example.com',
 'hostname': 'r0',
 'ifd_style': 'CLASSIC',
 'master': 'RE0',
 'model': 'MX960',
 'personality': 'MX',
 'serialnumber': 'VMX5868',
 'switch_style': 'BRIDGE_DOMAIN',
 'vc_capable': False,
 'version': '15.1R1.9',
 'version_RE0': '15.1R1.9',
 'version_info': junos.version_info(major=(15, 1), type=R, minor=1, build=9)}
>>>

Diese Fakten können leicht in einem Skript getestet werden, um eine auf den Fakten basierende Logik zu implementieren. Dieser Code zeigt ein einfaches Beispiel, das auf dem Schlüssel model im Wörterbuch facts basiert. Der Schlüssel model gibt das Modell des Junos-Geräts an:

if r0.facts['model'] == 'MX480':
    # Handle the MX480 case
    ... MX480 code ...
else:
    # Handle the case of other models
    ... Other models code ...

Das Beispiel folgt einem Codepfad, wenn es sich bei dem Junos-Gerät um ein MX480 handelt, und einem anderen Codepfad für alle anderen Modelle von Junos-Geräten.

Die Faktensammlung ist zwar standardmäßig aktiviert, kann aber deaktiviert werden, indem der optionale Parameter gather_facts in den Aufrufen Device() oder open() auf False gesetzt wird. Du kannst die Datenerfassung deaktivieren, um die erste Verbindung zu beschleunigen oder wenn es ein unerwartetes Problem bei der Datenerfassung auf einem Gerät gibt.

Wenn das Sammeln von Fakten während der ersten NETCONF-Verbindung deaktiviert ist, kann es später immer noch durch den Aufruf der facts_refresh() Instanzmethode initiiert werden. Wie der Name schon sagt, kann die Methode facts_refresh() auch verwendet werden, um ein bestehendes facts Wörterbuch mit den neuesten Informationen vom Junos-Gerät zu aktualisieren.

Schließen der Verbindung

Die Methode close() beendet die NETCONF-Sitzung , die durch die erfolgreiche Ausführung der Methode open() eingeleitet wurde. Die Methode close() sollte aufgerufen werden, wenn du die RPC-Aufrufe an die Geräteinstanz beendet hast:

>>> r0.close()

Wenn du die Methode close() aufrufst, wird die Geräteinstanz nicht zerstört, sondern nur ihre NETCONF-Sitzung geschlossen. Wenn du einen RPC aufrufst, nachdem eine Instanz geschlossen wurde, wird eine jnpr.junos.exception.ConnectClosedErrorAusnahme ausgelöst, wie in diesem Beispiel gezeigt:

>>> r0.close()
>>> version_info = r0.rpc.get_software_information()
Traceback (most recent call last):
...ouput trimmed...
jnpr.junos.exception.ConnectClosedError: ConnectClosedError(r0)
>>> r0.open()
Device(r0)
>>> version_info = r0.rpc.get_software_information()
>>> r0.close()

Das Beispiel zeigt auch, wie eine Instanz, die zuvor geschlossen wurde, wieder geöffnet werden kann, indem die Methode open() instance erneut aufgerufen wird. Der RPC wird ausgeführt, und die Instanz wird erneut geschlossen.

RPC-Ausführung

Dieser Abschnitt beginnt mit einer der Low-Level-Funktionen von PyEZ. PyEZ ermöglicht es dem Benutzer, Junos XML RPCs mit einfachen Python-Anweisungen aufzurufen. PyEZ bietet zwar Abstraktionen auf höherer Ebene, wie z. B. Tabellen und Ansichten (siehe "Operative Tabellen und Ansichten"), vereinfacht aber dennoch den Aufruf von Junos XML RPCs und das Parsen der
der entsprechenden XML-Antworten. PyEZ erfordert keine Formatierung der RPC
als XML zu formatieren, und es ist keine direkte Interaktion mit NETCONF erforderlich.

RPC auf Abruf

Sobald eine Geräteinstanz erstellt und die Methode open() aufgerufen wurde, um die NETCONF-Sitzung zu initiieren, kann die Eigenschaft rpc der Geräteinstanz verwendet werden, um einen RPC auszuführen. Jeder Junos XML RPC kann als Methode der Eigenschaft rpcaufgerufen werden. Das allgemeine Format für diese Methodenaufrufe ist:

device_instance_variable.rpc.rpc_method_name()

Zum Beispiel: wird die RPC "get-route-summary-information" auf der bereits geöffneten r0 Geräteinstanz mit aufgerufen:

>>> route_summary_info = r0.rpc.get_route_summary_information()
Hinweis

Der XML-RPC-Name lautet get-route-summary-information, während der Methodenname get_route_summary_information lautet. Der Methodenname wird aus dem XML-RPC-Namen abgeleitet, indem die Bindestriche einfach durch Unterstriche ersetzt werden. Diese Ersetzung ist erforderlich, weil die Python-Namensregeln keine Bindestriche in den Methodennamen zulassen.

Die Antwort von get-route-summary-information RPC wird von der Methode r0.rpc.get_route_summary_information() zurückgegeben und in der route_summary_info Variable gespeichert. Mach dir vorerst keine Gedanken über den Inhalt der Antwort. Der Inhalt der RPC-Antwort wird ausführlich in "RPC-Antworten" behandelt .

PyEZ bezeichnet dieses Konzept als "RPC on Demand", weil die PyEZ-Bibliothek nicht für jeden Junos XML RPC eine Methode enthält. Eine eigene Methode für jede Junos XML RPC würde Tausende von Methoden erfordern. Außerdem müsste PyEZ eng an die Junos-Plattform und -Version gekoppelt sein, um zu verhindern, dass diese Methoden ständig nicht mit den Fähigkeiten des Geräts übereinstimmen. Mit RPC on Demand gibt es keine enge Kopplung; neue Funktionen werden mit jeder Junos-Version zu jeder Plattform hinzugefügt und die bestehende Version von PyEZ kann sofort auf diese RPCs zugreifen.

Stattdessen wird RPC on Demand mit dem Konzept der Metaprogrammierung umgesetzt. Jede RPC-Methode wird dynamisch erzeugt und ausgeführt, wenn sie aufgerufen wird. PyEZ-Benutzer müssen nicht im Detail verstehen, wie diese Metaprogrammierung umgesetzt wird, aber es ist hilfreich, das allgemeine Konzept zu verstehen.

Da diese RPC-Methoden "on Demand" erzeugt werden, kann eine RPC-Methode für jede Junos-XML-RPC auf jeder Junos-Plattform aufgerufen werden. Wenn ein Junos-Gerät eine bestimmte XML-RPC unterstützt, kann diese XML-RPC immer von PyEZ aus mit RPC on Demand aufgerufen werden.

Der entsprechende Aspekt dieses Entwurfsparadigmas ist, dass die PyEZ-Bibliothek nicht im Voraus wissen kann, ob eine XML-RPC gültig ist. Jeder RPC-Methodenname wird zunächst in den entsprechenden XML-RPC-Namen umgewandelt, indem Bindestriche durch Unterstriche ersetzt werden, dann in die richtigen XML-Elemente verpackt und über die NETCONF-Sitzung an das Gerät gesendet. Erst wenn die NETCONF-Antwort vom Gerät empfangen wird, kann ein Fehler entdeckt und eine Ausnahme ausgelöst werden. "RPC-Ausnahmen" beschreibt diesen Fall und andere mögliche Ausnahmen.

Warnung

PyEZ bietet auch eine cli() Geräteinstanzmethode, aber diese Methode ist in erster Linie für das Debugging gedacht und sollte in PyEZ-Skripten vermieden werden. Standardmäßig gibt diese Methode einen Textstring zurück, der die normale CLI-Ausgabe enthält:

>>> response = r0.cli('show system uptime')
/usr/local/lib/python2.7/dist-packages/jnpr/junos/devi...
  warnings.warn("CLI command is for debug use only!", ...)
>>> print response

Current time: 2015-07-13 14:02:00 PDT
Time Source:  NTP CLOCK 
System booted: 2015-07-13 07:42:46 PDT (06:19:14 ago)
Protocols started: 2015-07-13 07:42:46 PDT (06:19:14 ago)
Last configured: 2015-07-13 08:07:26 PDT (05:54:34 ago) by root
 2:02PM  up 6:19, 1 users, load averages: 0.59, 0.41, 0.33

>>>

Die Methode cli() ist anfällig für alle Probleme, die bei der traditionellen "Screen Scraping"-Netzwerkautomatisierung auftreten. Weder der Befehl noch die Antwort liegen in einem strukturierten Datenformat vor. Beide sind anfällig für Fehler beim Parsen oder sogar für Änderungen zwischen verschiedenen Junos-Versionen. Aus diesem Grund solltest du diese Methode bei einer Produktionsautomatisierung vermeiden.

RPC-Parameter

Wie du beim Junos RESTful API Service unter "Hinzufügen von Parametern zu RPCs" gesehen hast , unterstützen einige Junos XML RPCs optionale Parameter, die die XML-Antwort einschränken oder verändern. Wenn du die RPC im XML-Format beschreibst, erscheinen diese Parameter als verschachtelte XML-Tags innerhalb des XML-Tags des RPC-Namens. Hier ist zum Beispiel die entsprechende XML-RPC für den CLI-Befehl show route protocol isis 10.0.15.0/24 active-path :

user@r0> show route protocol isis 10.0.15.0/24 active-path | display xml rpc
<rpc-reply xmlns:junos="http://xml.juniper.net/junos/15.1R1/junos">
    <rpc>
        <get-route-information>
                <destination>10.0.15.0/24</destination>
                <active-path/>
                <protocol>isis</protocol>
        </get-route-information>
    </rpc>
    <cli>
        <banner></banner>
    </cli>
</rpc-reply>

In der vorangegangenen Ausgabe siehst du, dass drei XML-Elemente in das <get-route-information> XML-Element der RPC verschachtelt sind. Die Tags <destination> und <protocol> haben Werte in ihrem Inhalt, während der Tag <active-path/> ein leeres XML-Element ist. Der PyEZ RPC-Mechanismus ermöglicht es dem Benutzer, diese Parameter anzugeben, ohne sie in XML formatieren zu müssen. Die Parameter werden einfach als Schlüsselwortargumente für die in "RPC on Demand" vorgestellten RPC-Methoden angegeben . Ein entsprechender PyEZ-Methodenaufruf für den CLI-Befehl show route protocol isis 10.0.15.0/24 active-path lautet:

>>> isis_route = r0.rpc.get_route_information(protocol='isis',
...                                           destination='10.0.15.0/24',
...                                           active_path=True)

Die XML-Tags werden zu Schlüsselwortargumenten und jeder XML-Inhalt wird zum Wert des Schlüsselworts. Genau wie bei den RPC-Methodennamen werden die Bindestriche in den XML-Tags durch Unterstriche ersetzt. So wird das Tag <active-path/> zum Schlüsselwort active_path. Wenn der RPC-Parameter keinen Wert benötigt, wie es bei <active-path/> der Fall ist, sollte der Wert des Schlüsselworts auf True gesetzt werden.

RPC Zeitüberschreitung

Standardmäßig wird ein RPC-Methodenaufruf zeitlich begrenzt, wenn die vollständige Antwort nicht innerhalb von 30 Sekunden vom Junos-Gerät empfangen wird. Obwohl diese Vorgabe für die meisten RPC-Aufrufe angemessen ist, gibt es Fälle, in denen du die Vorgabe außer Kraft setzen musst. Ein Beispiel für einen RPC, dessen Ausführung länger als 30 Sekunden dauern kann , ist get-flow-session-information auf einer Firewall der SRX-Serie, die viele tausend aktive Sicherheitsflüsse hat. Ein anderes Beispiel ist get-route-information auf einem Router, der die gesamte Internet-Routing-Tabelle (500.000 Routen und mehr) enthält.

Wenn du in eine dieser Situationen gerätst, sollte deine erste Frage immer lauten: "Brauche ich wirklich alle diese Informationen?" Vielleicht brauchst du nur die Routen von einem bestimmten Peer-AS oder die Sicherheitsströme mit einem TCP-Zielport von 443. In diesen Fällen kannst du die Ausgabe direkt auf dem Gerät filtern lassen, indem du der RPC die entsprechenden Parameter übergibst, wie im vorherigen Abschnitt beschrieben.

Es gibt jedoch andere Fälle, in denen die Situation unvermeidlich ist. Das beste Beispiel ist die Installation eines Junos-Softwarepakets mit dem request-package-addRPC. In diesem Fall musst du das 30-Sekunden-Timeout mit einem längeren Timeout überschreiben. In anderen Situationen möchtest du vielleicht den Standard-Timeout verkürzen, damit dein Skript ein nicht reagierendes Gerät schneller erkennt. Unabhängig davon, ob du die 30-Sekunden-Zeitüberschreitung erhöhen oder verringern musst, gibt es zwei Möglichkeiten, dieses Ziel zu erreichen.

Die erste Methode zum Ändern der RPC-Zeitüberschreitung besteht darin, die Eigenschaft timeoutder Instanz des Geräts zu setzen. Das Setzen der Eigenschaft timeoutwirkt sich auf alle RPCs aus, die mit diesem Gerätehandle aufgerufen werden. Hier ein Beispiel mit der bestehenden Instanzvariable r0:

>>> r0.timeout
30
>>> r0.timeout = 10
>>> r0.timeout
10

Das vorangehende Beispiel bestätigt die standardmäßige 30-Sekunden-Zeitüberschreitung, indem es den Wert des Attributs r0.timeout anzeigt. Die Zeitüberschreitung wird dann auf 10 Sekunden gesetzt, und die letzte Zeile bestätigt, dass der Wert nun 10 Sekunden beträgt.

Die zweite Methode zur Änderung des RPC-Timeouts ändert den Timeout-Wert nur für einen einzelnen RPC. Künftige RPCs verwenden weiterhin den Standardwert oder den Wert, der durch die Zuweisung des timeout Attributs der Geräteinstanz festgelegt wurde. Diese zweite Methode wird durch die Übergabe eines dev_timeout Schlüsselwortparameters an die RPC-Methode erreicht. Hier ist ein Beispiel für diese Methode:

>>> summary_info = r0.rpc.get_route_summary_information()
>>> bgp_routes = r0.rpc.get_route_information(dev_timeout = 180,
...                                           protocol='bgp')
>>> isis_routes = r0.rpc.get_route_information(protocol='isis')

In diesem Beispiel wird der RPC "get-route-summary-information" mit dem Standard-Timeout von 30 Sekunden ausgeführt. Dann werden BGP-Routen mit einer bestimmten Zeitspanne von 180 Sekunden gesammelt. Schließlich werden die ISIS-Routen mit der Standardzeitüberschreitung von 30 Sekunden gesammelt .

RPC-Ausnahmen

Zusätzlich zu den verbindungsbezogenen Ausnahmen, die unter "Verbindungsausnahmen" beschrieben werden , definiert die Junos PyEZ-Bibliothek mehrere Ausnahmen, die ausgelöst werden können, wenn eine RPC on Demand-Methode fehlschlägt. Tabelle 4-3 enthält eine Liste und Beschreibungen dieser Ausnahmen und der Situation, in der die jeweilige Ausnahme ausgelöst wird.

Tabelle 4-3. Mögliche Ausnahmen, die von RPC on Demand-Methoden ausgelöst werden
ExceptionBeschreibung
jnpr.junos.exception.ConnectClosedErrorWird ausgelöst, wenn die zugrunde liegende NETCONF-Sitzung unerwartet geschlossen wurde, bevor die RPC-Methode aufgerufen wurde. Dies kann aufgrund einer RE-Umschaltung auf dem Zielgerät, eines Netzwerkerreichbarkeitsproblems zwischen dem Automatisierungshost und dem Zielgerät oder eines Ausfalls des Zielgeräts selbst passieren, oder weil du vergessen hast, die open() Methode aufzurufen.
jnpr.junos.exception.RpcTimeoutErrorWird ausgelöst, wenn die zugrunde liegende NETCONF-Sitzung verbunden ist und die RPC erfolgreich an das Gerät gesendet wurde, aber innerhalb der RPC-Zeitüberschreitung keine Antwort vom Gerät empfangen wurde (wie im vorherigen Abschnitt beschrieben).
jnpr.junos.exception.PermissionErrorWird ausgelöst, wenn die Autorisierung, wie in "Authentifizierung und Autorisierung" beschrieben, die Ausführung des RPCs nicht zulässt.
jnpr.junos.exception.RpcErrorWird ausgelöst, wenn <xnm:error> oder <xnm:warning> Elemente in der RPC-Antwort vorhanden sind. Diese Ausnahme wird auch ausgelöst, wenn NETCONF <rpc-error>Elemente in der Antwort enthalten sind oder wenn die ncclient Bibliothek eine nicht erkannte Ausnahme auslöst.

Wie diese Ausnahmen genau behandelt werden sollen, kann sehr spezifisch für deine besonderen Automatisierungsanforderungen sein. Wenn zum Beispiel die Antwort eines bestimmten RPCs für die weitere Verarbeitung benötigt wird, würde eine jnpr.junos.exception.PermissionErrorAusnahme auf einen Fehler hinweisen, der die weitere Verarbeitung verhindert (zumindest für diese bestimmte Geräteinstanz). In diesem Fall kann es sinnvoll sein, den Fehler auszudrucken und das Skript zu beenden (oder mit der nächsten Geräteinstanz in der Schleife fortzufahren). Es kann jedoch auch Fälle geben, in denen der Benutzer Eingaben macht, die den auszuführenden RPC auswählen. In diesem Fall solltest du die Ausnahme elegant behandeln jnpr.junos.exception.PermissionError Ausnahme zu behandeln, indem du den Fehler ausgibst und den Benutzer aufforderst, einen anderen RPC anzugeben.

Eine häufige Ausnahmebedingung ist der Versuch, die NETCONF-Verbindung wieder zu öffnen, wenn eine jnpr.junos.exception.ConnectClosedErrorAusnahme empfangen wird. Da diese Ausnahme auf einen vorübergehenden Zustand hinweist, kann es möglich sein, sich von diesem Zustand zu erholen. Diese Ausnahme kann aber auch auf ein hartnäckigeres Problem hinweisen. Der Versuch, die NETCONF-Verbindung ohne Einschränkungen wieder zu öffnen, könnte zu einer Endlosschleife führen.

Der folgende Python-Codeausschnitt veranschaulicht einen Algorithmus, mit dem versucht wird, die NETCONF-Verbindung erneut zu öffnen und den fehlgeschlagenen RPC eine begrenzte Anzahl von Malen auszuführen. Er veranschaulicht nicht nur eine gängige Anforderung, sondern bietet auch einen Rahmen für zusätzliche, spezifischere Ausnahmebehandlungen. Durch einfaches Hinzufügen eines weiteren except Blocks könnte eine zusätzliche Fehlerbehandlung für andere Ausnahmen hinzugefügt werden:

import jnpr.junos.exception 1
from time import sleep

MAX_ATTEMPTS = 3 2
WAIT_BEFORE_RECONNECT = 10

# Assumes r0 already exists and is a connected device instance

for attempt in range(MAX_ATTEMPTS): 3
    try: 4
        routes = r0.rpc.get_route_information()
    except jnpr.junos.exception.ConnectClosedError: 5
        sleep(WAIT_BEFORE_RECONNECT) 6
        try: r0.open()
        except jnpr.junos.exception.ConnectError: pass
    else: 7
        # Success. No exception was raised.
        # break will skip the for loop's else.
        break
else: 8
    # Max attempts exceeded. All attempts have failed.
    # Re-raise most recent exception from last attempt.
    raise

# ... continue with the rest of script if RPC succeeded ...
1

Der Import des PyEZ-Exception-Moduls ist erforderlich, bevor bestimmte PyEZ-Exceptions von einer except -Anweisung abgefangen werden können. Importiere die Funktion sleep() aus dem Modul time in den lokalen Namespace.

2

MAX_ATTEMPTS wird als Konstante verwendet, um anzugeben, wie oft ein RPC maximal wiederholt werden soll. In diesem Beispiel wird ein RPC maximal dreimal wiederholt. WAIT_BEFORE_RECONNECT wird als Konstante verwendet, um die Anzahl der Sekunden anzugeben, die nach einem RPC-Fehler gewartet werden soll, bevor versucht wird, die NETCONF-Verbindung wieder zu öffnen. Dies erlaubt bis zu 30 Sekunden (WAIT_BEFORE_RECONNECT * MAX_ATTEMPTS), um einen vorübergehenden Zustand zu beheben.

3

Die for Schleife versucht, den RPC bis zu MAX_ATTEMPTS Mal auszuführen.

4

Die Anweisung try markiert einen Anweisungsblock, der so lange ausgeführt wird, bis eine Ausnahme auftritt. Die Funktion RPC on Demand führt die RPC-Anweisung get-route-information innerhalb dieses tryBlocks aus. Das Ergebnis des RPCs wird in der Variablen routes gespeichert. Diese RPC kann ohne Ausnahme erfolgreich sein oder eine der in Tabelle 4-3 aufgeführten Ausnahmen auslösen.

5

Wenn die Anweisung im try Block eine Ausnahme auslöst, wird geprüft, ob sie mit einer jnpr.junos.exception.ConnectClosedErrorAusnahme übereinstimmt. Wenn die Ausnahme passt, werden die Anweisungen in diesem except Block ausgeführt. Wenn die Ausnahme nicht zu diesem except Block passt, hält Pythons Standard-Ausnahmebehandlung die Programmausführung an und gibt den Fehler aus. Es können weitere Ausnahmeanweisungen hinzugefügt werden, um andere mögliche Ausnahmen zu behandeln. Jeder except Block wird der Reihe nach getestet, bis eine Übereinstimmung gefunden wird oder die Liste der Ausnahmen erschöpft ist.

6

Ein sofortiger Versuch, eine geschlossene NETCONF-Verbindung wiederherzustellen, wird wahrscheinlich fehlschlagen. Wenn du stattdessen eine gewisse Zeit wartest, hast du die Möglichkeit, einen vorübergehenden Zustand (RE-Umschaltung, Netzwerkkonvertierung usw.) abzuwarten. Die Anweisung sleep unterbricht die Ausführung des Skripts für WAIT_BEFORE_RECONNECTSekunden. Danach wird die Ausführung mit der nächsten Zeile fortgesetzt. Die Methode open() ist in einen try Block eingeschlossen, um alle Ausnahmen abzufangen, die auftreten, wenn der Aufruf fehlschlägt, weil die zugrunde liegende Netzwerkbedingung noch besteht. Die Anweisung except: pass fängt alle jnpr.junos.exception.ConnectErrorAusnahmen ab, die von r0.open() ausgelöst werden, und ignoriert sie.

Hinweis

Alle Ausnahmen, die von r0.open() ausgelöst werden, sind in Tabelle 4-2 aufgeführt. Diese Ausnahmen sind alle Unterklassen von jnpr.junos.exception.ConnectErrorWenn du diese eine Ausnahmeklasse abfängst, werden daher alle von r0.open() ausgelösten Ausnahmen abgefangen.

Wenn die NETCONF-Verbindung immer noch nicht geöffnet ist, wird diese Bedingung als weitere jnpr.junos.exception.ConnectClosedError Ausnahme beim nächsten Versuch, den RPC auszuführen.

7

Der else -Block einer try/except/else -Verbundanweisung wird nur ausgeführt, wenn der try -Block keine Ausnahmen ausgelöst hat. Mit anderen Worten: Der else -Block wird nur ausgeführt, wenn r0.rpc.get_route_information() erfolgreich ausgeführt wurde. Die break Anweisung verlässt die for Schleife, wenn der RPC erfolgreich war. Das Verlassen einer for Schleife mit einer break Anweisung überspringt die else Klausel der Schleife.

8

Die else Klausel einer Python for Schleife wird nur ausgeführt, wenn die Schleife normal beendet wird. In diesem Code bedeutet "normal beenden", dass alle Versuche ausgeschöpft wurden (attempt > MAX_ATTEMPTS) und die RPC-Ausführung bei jedem Versuch fehlgeschlagen ist (eine Ausnahme ausgelöst hat). Wenn alle Versuche fehlgeschlagen sind, wird die letzte Ausnahme ausgelöst.

In diesem Beispiel wird davon ausgegangen, dass der Variable r0bereits eine Geräteinstanz zugewiesen wurde, wie in " Erstellen einer Geräteinstanz" beschrieben, und dass bereits eine NETCONF-Verbindung geöffnet ist, wie in "Herstellen der Verbindung" beschrieben. Das Beispiel behandelt außerdem jede Ausnahme, die vom RPC ausgelöst wird, außer jnpr.junos.exception.ConnectClosedError, als fatalen Fehler. Wenn die Ausnahme jnpr.junos.exception.ConnectClosedErrorlänger als MAX_ATTEMPTS andauert, wird die letzte Ausnahme an den Standardfehlerbehandlungsmechanismus von Python weitergeleitet. Bei der letzten Ausnahme handelt es sich wahrscheinlich um eine der Ausnahmen aus Tabelle 4-2, die durch den letzten Versuch, die Verbindung wieder zu öffnen, ausgelöst wurde.

RPC-Antworten

Nachdem du auf gesehen hast, wie man Junos XML RPCs mit einfachen Python-Anweisungen aufruft, geht es in diesem Abschnitt darum, was man mit den resultierenden Antworten macht. PyEZ bietet mehrere Möglichkeiten, eine Antwort in Python-Datenstrukturen zu zerlegen, und bietet außerdem einen optionalen Mechanismus zum Entfernen von Leerzeichen aus der Antwort. In diesem Abschnitt werden die einzelnen Funktionen zur Steuerung von RPC-Antworten beschrieben.

lxml-Elemente

Die Standardantwort auf eine NETCONF XML RPC ist ein String, der ein XML-Dokument darstellt. Wie du gesehen hast, ist diese RPC-Antwort die gleiche wie die Ausgabe, die von der CLI angezeigt wird, wenn der | display xml Modifikator an den entsprechenden CLI-Befehl angehängt wird . Zum Beispiel:

user@r0> show system users | display xml
<rpc-reply xmlns:junos="http://xml.juniper.net/junos/15.1R1/junos">
    <system-users-information xmlns="http://xml.juniper.net/junos/15.1R1/junos">
        <uptime-information>
            <date-time junos:seconds="1436915514">4:11PM</date-time>
            <up-time junos:seconds="116940">1 day,  8:29</up-time>
            <active-user-count junos:format="4 users">4</active-user-count>
            <load-average-1>0.56</load-average-1>
            <load-average-5>0.43</load-average-5>
            <load-average-15>0.36</load-average-15>
            <user-table>
                <user-entry>
                    <user>root</user>
                    <tty>u0</tty>
                    <from>-</from>
                    <login-time junos:seconds="1436897214">11:06AM</login-time>
                    <idle-time junos:seconds="60">1</idle-time>
                    <command>cli</command>
                </user-entry>
                <user-entry>
                    <user>foo</user>
                    <tty>pts/0</tty>
                    <from>172.29.104.149</from>
                    <login-time junos:seconds="1436884614">7:36AM</login-time>
                    <idle-time junos:seconds="30900">8:35</idle-time>
                    <command>-cli (cli)</command>
                </user-entry>
                <user-entry>
                    <user>bar</user>
                    <tty>pts/1</tty>
                    <from>172.29.104.149</from>
                    <login-time junos:seconds="1436884614">7:36AM</login-time>
                    <idle-time junos:seconds="30900">8:35</idle-time>
                    <command>-cli (cli)</command>
                </user-entry>
                <user-entry>
                    <user>user</user>
                    <tty>pts/2</tty>
                    <from>172.29.104.149</from>
                    <login-time junos:seconds="1436884614">7:36AM</login-time>
                    <idle-time junos:seconds="0">-</idle-time>
                    <command>-cli (cli)</command>
                </user-entry>
            </user-table>
        </uptime-information>
    </system-users-information>
    <cli>
        <banner></banner>
    </cli>
</rpc-reply>

Anstatt diesen String des XML-Dokuments direkt an den Nutzer zurückzugeben, wie du es beim RESTful-API-Dienst in "HTTP-Antworten formatieren" gesehen hast , verwendet die PyEZ-Bibliothek die lxml-Bibliothek, um das XML-Dokument zu parsen und die bereits geparste Antwort zurückzugeben. Die Antwort ist ein lxml.etree.Element Objekt, das im ersten Kindelement des <rpc-reply> Elements verankert ist. Im Fall von , dem show system usersBefehl oder dem entsprechenden get-system-users-information RPC, ist das erste Kindelement des <rpc-reply> Elements das <system-users-information> Element. Dies wird durch die Anzeige des tagAttributs der get-system-users-information RPC Antwort veranschaulicht:

>>> response = r0.rpc.get_system_users_information(normalize=True)
>>> response.tag
'system-users-information'
Hinweis

In diesem Beispiel wurde das Argument normalize=True an die Methode r0.rpc.get_system_users_information()übergeben. Die Antwortnormalisierung wird in "Antwortnormalisierung" ausführlich behandelt . Für die Zwecke dieser Beispiele musst du lediglich sicherstellen, dass du auch das Argument normalize einfügst. Wenn du das nicht tust, werden einige der folgenden Beispiele andere Ergebnisse liefern oder ganz fehlschlagen.

Jedes lxml.etree.Element Objekt hat Links zu übergeordneten, untergeordneten und geschwisterlichen lxml.etree.Element Objekten, die einen Baum bilden, der die geparste XML-Antwort darstellt. Zu Debugging-Zwecken kann die Funktion lxml.etree.dump() verwendet werden, um den XML-Text der Antwort auszugeben (allerdings ohne die hübsche Formatierung der Junos CLI):

>>> from lxml import etree
>>> etree.dump(response)
<system-users-information>
<uptime-information>
...ouput trimmed...
</uptime-information>
</system-users-information>

>>>

lxml.etree.ElementObjekte mögen zwar komplizierter erscheinen als eine Datenstruktur, die aus nativen Python-Listen und -Dicts besteht, aber lxml.etree.Element Objekte bieten eine robuste Reihe von APIs zum Auswählen und Extrahieren von Teilen der RPC-Antwort. Viele dieser APIs verwenden einen XPath-Ausdruck, um einen oder mehrere Teilbäume aus der Antwort zu finden. Du hast XPath-Ausdrücke in " Zugriff auf XML-Daten mit XPath" kennengelernt. Die folgende Seitenleiste ergänzt die vorherige Einführung mit spezifischen Informationen über die Untergruppe der XPath-Ausdrücke, die in lxml verfügbar sind. Wenn du dich eingehender mit XPath im Allgemeinen beschäftigen möchtest, schau dir das W3Schools XPath Tutorial an.

Nachdem du nun ein grundlegendes Verständnis von XPath hast, wollen wir uns einige Beispiele ansehen, wie die lxml-APIs verwendet werden können, um Informationen aus einer RPC-Antwort auszuwählen. Diese Beispiele verwenden dieselbe RPC-Variable get-system-users-information response aus den vorherigen Beispielen. Es kann hilfreich sein, jede dieser Anweisungen und ihre Ergebnisse anhand der show system users | display xmlAusgabe am Anfang dieses Abschnitts zu analysieren.5

Der Textinhalt des ersten XML-Elements, das einem XPath-Ausdruck entspricht, kann mit der Methode findtext() abgefragt werden:

>>> response.findtext("uptime-information/up-time")
'1 day, 8:29'

Das Argument der Methode findtext()ist ein XPath, der sich auf das Element response bezieht. Da response das Element <system-users-information> darstellt, passt der XPath uptime-information/up-time zum Tag <up-time> in der Antwort.

Das Element <up-time> enthält außerdem das Attribut seconds, das die Betriebszeit des Systems in einer einfacher zu analysierenden Anzahl von Sekunden seit dem Systemstart angibt. Auf den Wert dieses Attributs kann durch Verkettung der Methode find()und des Attributs attribzugegriffen werden:

>>> response.find("uptime-information/up-time").attrib['seconds']
'116940'

Während die Methode findtext() einen String zurückgibt, liefert die Methode find() ein lxml.etree.Element Objekt. Auf die XML-Attribute dieses lxml.etree.Element Objekts kann dann über das attrib Wörterbuch zugegriffen werden. Das attrib Wörterbuch wird über den Namen des XML-Attributs verschlüsselt.

In der Variable response befindet sich ein <user-entry> XML-Element für jeden derzeit am Gerät angemeldeten Benutzer. Jedes <user-entry> Element enthält ein <user> Element mit dem Benutzernamen des Benutzers. Die Methode findall() gibt eine Liste von lxml.etree.Element Objekten zurück, die einem XPath entsprechen. In diesem Beispiel wird findall() verwendet, um das <user> Element innerhalb jedes <user-entry> Elements auszuwählen:

>>> users = response.findall("uptime-information/user-table/user-entry/user")
>>> for user in users:
...     print user.text
... 
root
foo
bar
user
>>>

Das Ergebnis dieses Beispiels ist eine Liste der Benutzernamen für alle Benutzer, die derzeit am Junos-Gerät angemeldet sind.

Das nächste Beispiel kombiniert die Methode findtext() mit einem XPath-Ausdruck, der das erste übereinstimmende XML-Element auswählt, das ein bestimmtes übereinstimmendes Kindelement hat:

>>> response.findtext("uptime-information/user-table/user-entry[tty='u0']/user")
'root'

Das Ergebnis dieses Beispiels ist der Benutzername des Benutzers, der gerade an der Konsole des Junos-Geräts angemeldet ist. (Auf diesem Gerät hat die Konsole den tty-Namen u0.) Der XPath wählt das Element <user> aus dem ersten Element <user-entry> aus, das auch ein Kindelement <tty> mit dem Wert u0 hat.

Im nächsten Beispiel wird die Anzahl der Sekunden abgefragt, die die Sitzung des Nutzers barim Leerlauf war. Dies geschieht durch die Kombination der Methode find(), eines XPath mit dem [tag='text'] Prädikat und dem attrib Attributwörterbuch:

>>> XPATH = "uptime-information/user-table/user-entry[user='bar']/idle-time"
>>> response.find(XPATH).attrib['seconds']
'30900'
Hinweis

Im vorangegangenen Beispiel wird die Variable XPATH als Konstante verwendet, um einen Zeilenumbruch im gedruckten Buch zu vermeiden.

Da im vorangegangenen Beispiel die Methode find() verwendet wurde, werden nur Informationen für die erste CLI-Sitzung des Benutzers bar zurückgegeben. Wenn der Benutzer bar mehrere CLI-Sitzungen geöffnet hat, kannst du eine Liste der Leerlaufzeiten abrufen, indem du find() durch findall() ersetzt.

Ein wesentlicher Unterschied zwischen den Objekten von lxml.etree.Element mit ihren entsprechenden Methoden und den nativen Datenstrukturen von Python besteht darin, wie mit nicht vorhandenen Daten umgegangen wird. Um dies zu demonstrieren, erstellen wir ein entsprechendes mehrstufiges Python-Wörterbuch, um die Uptime-Informationen zu speichern:

>>> from pprint import pprint
>>> example_dict = { 'uptime-information' : { 'up-time' : '1 day, 8:29' }}
>>> pprint(example_dict)
{'uptime-information': {'up-time': '1 day, 8:29'}}

Der Zugriff auf die Informationen in diesem Wörterbuch ist wohl einfacher als die Methode findtext():

>>> example_dict['uptime-information']['up-time']
'1 day, 8:29'

Aber was passiert, wenn du versuchst, auf Daten zuzugreifen, die in der Antwort nicht vorhanden sind? Das native Python-Wörterbuch löst eine KeyError Ausnahme aus:

>>> example_dict['foo']['bar']
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError: 'foo'

Wenn die Möglichkeit besteht, dass eine Information in der Antwort fehlt, muss der Zugriff auf das Wörterbuch in einen try/except Block eingeschlossen werden, der die resultierende KeyErrorAusnahme elegant behandelt. Umgekehrt, wenn die angeforderten Informationen in einem lxml.etree.Element Objekt fehlen, geben die Methoden find() und findtext() einfach None zurück, anstatt eine Ausnahme auszulösen:

>>> print response.find("foo/bar")
None
>>> print response.findtext("foo/bar")
None

Die Methode findall() zeigt ein ähnliches Verhalten. Sie gibt eine leere Liste zurück, wenn der XPath-Ausdruck mit keinem XML-Element fehlschlägt:

>>> print response.findall("foo/bar")
[]

Es ist auch wichtig, sich daran zu erinnern, dass die Methode findall() immer eine Liste von lxml.etree.Element Objekten zurückgibt. Sie gibt nicht direkt ein lxml.etree.ElementObjekt zurück. Das gilt auch dann, wenn nur ein einziges Objekt in der Liste enthalten ist:

>>> user_entries = response.findall("uptime-information/user-table/user-entry") 
>>> type(user_entries)
<type 'list'>
>>> len(user_entries)
4

Wenn mehrere Benutzer angemeldet sind, enthält diese Liste ein Objekt für jeden Benutzer. Das nächste Beispiel zeigt, wie die Antwort ausfällt, wenn nur ein Benutzer angemeldet ist:

>>> new_response = r0.rpc.get_system_users_information(normalize=True)
>>> new_users = new_response.findall("uptime-information/user-table/user-entry") 
>>> type(new_users)
<type 'list'>
>>> len(new_users)
1

In diesem Fall mit nur einem Benutzer ist die Antwort immer noch eine Liste, nur eben mit einem einzigen Eintrag. So kann das Programm die Liste der Benutzereinträge in einer Schleife durchlaufen, ohne dass es unterschiedliche Codepfade für die Fälle "kein Benutzer", "ein Benutzer" und "mehrere Benutzer" erstellen muss.

Es gibt noch eine letzte Situation, die wichtig ist: Der Versuch, auf ein nicht existierendes XML-Attribut zuzugreifen, löst immer noch ein KeyError aus. Das liegt daran, dass das lxml.etree.Element Objekts attrib ein Python-Wörterbuch ist, das auf den Namen des XML-Attributs verweist:

>>> response.find("uptime-information/up-time").attrib['foo']
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "lxml.etree.pyx", line 2366, in lxml.etree._Attrib.__getitem__ (src/lx...
KeyError: 'foo'

Eine verwandte, aber etwas andere Situation ist der Versuch, auf ein XML-Attribut eines nicht existierenden Elements zuzugreifen. In diesem Beispiel gibt die Methode find() None zurück, weil es kein XML-Element gibt, das dem XPath-Ausdruck entspricht.

Eine Möglichkeit, den Zugriff auf das attrib Wörterbuch zu behandeln, besteht darin, den Zugriff in einen try/except Block zu verpacken, der sowohl AttributeError als auch und KeyError abfängt:

>>> try: foo_attrib = response.find("uptime-information/bar").attrib['foo']
... except (AttributeError, KeyError): foo_attrib = None
... 
>>> print foo_attrib
None
>>> try: foo_attrib = response.find("uptime-information/up-time").attrib['foo']
... except (AttributeError, KeyError): foo_attrib = None
... 
>>> print foo_attrib
None

Auch wenn diese Beispiele nicht vollständig sind, hast du jetzt die Grundlagen für den Zugriff auf PyEZ RPC-Antworten im Standardformat lxml.etree.Element kennengelernt. Diese Beispiele haben einige der gängigsten Möglichkeiten gezeigt, auf die Antwortinformationen zuzugreifen, aber die lxml-Bibliothek bietet noch viele weitere Werkzeuge. Die vollständige Dokumentation der API von lxmlfindest du auf der Website lxml . Die lxml-API ist größtenteils auch mit der bekannten ElementTree API kompatibel. Die Dokumentation zu der ElementTree API ist Teil der Dokumentation der Python Standard Library unter ElementTree XML API.

Der nächste Abschnitt befasst sich mit den anderen optionalen Formaten, die PyEZ verwenden kann, um RPC Antworten zurückzugeben.

Antwort-Normalisierung

Die Normalisierung der Antwort ist eine PyEZ-Funktion, die den von einer RPC-Methode zurückgegebenen XML-Inhalt tatsächlich verändert. Es gibt einige Junos-RPCs, die XML-Daten zurückgeben, bei denen die Werte bestimmter XML-Elemente in Zeilenumbrüche oder andere Leerzeichen eingeschlossen sind. Ein Beispiel für diese zusätzlichen Leerzeichen findest du unter bei der RPC "get-system-users-information", die wir im vorherigen Abschnitt verwendet haben:

>>> response = r0.rpc.get_system_users_information()
>>> response.findtext("uptime-information/up-time")
'\n4 days, 17 mins\n'

Beachte, dass der Text für das Element <up-time> ein Zeilenumbruchzeichen vor und nach dem Wertstring hat. Die Antwortnormalisierung wurde entwickelt, um diese Situation zu lösen. Wenn die Antwortnormalisierung aktiviert ist, werden alle Leerzeichen am Anfang und Ende des Wertes jedes XML-Elements entfernt. Die Antwortnormalisierung ist standardmäßig deaktiviert (außer bei der Verwendung von Tabellen und Views, wie in "Operative Tabellen und Views" beschrieben ). Sie kann durch Hinzufügen des Arguments normalize=True zu einer RPC-Methode aktiviert werden:

>>> response = r0.rpc.get_system_users_information(normalize=True)
>>> response.findtext("uptime-information/up-time")
'4 days, 17 mins'

Beachte, dass die Zeilenumbrüche am Anfang und am Ende des Wertes entfernt wurden, aber die Leerzeichen innerhalb des Wertes bleiben erhalten.

Du hast vielleicht bemerkt, dass das Argument normalize=True zum Aufruf der Methode r0.rpc.get_system_users_information()im vorherigen Abschnitt hinzugefügt wurde. Warum wurde das Argument in diesen Beispielen verwendet? Die zusätzlichen Leerzeichen in einigen RPC-Antworten machen einige XPath-Ausdrücke schwieriger und weniger intuitiv. Ein Beispiel dafür ist der XPath-Ausdruck , mit dem der an der Konsole des Junos-Geräts angemeldete Benutzer gefunden werden soll (auf diesem Gerät hat die Konsole den tty-Namen u0). Ohne Antwortnormalisierung schlägt das vorherige XPath-Beispiel fehl und gibt kein passendes XML-Element zurück:

>>> response = r0.rpc.get_system_users_information()
>>> response.findtext("uptime-information/user-table/user-entry[tty='u0']/user")
>>>

Die leere Antwort kommt daher, dass der Wert im [tag='value'] Teil des XPath-Ausdrucks genau übereinstimmen muss. Wenn du den Wert des <tty> Tags für den gewünschten Benutzereintrag (der in diesem Beispiel zufällig an Position 7 steht) explizit anzeigst, siehst du, dass der Wert führende und nachfolgende Zeilenumbrüche enthält:

>>> response.findtext("uptime-information/user-table/user-entry[7]/tty")
'\nu0\n'
>>>

Du könntest den XPath-Ausdruck ändern, um nach diesem bestimmten Wert zu suchen, wie in diesem Beispiel gezeigt:

>>> response.findtext(
      ...    "uptime-information/user-table/user-entry[tty='\nu0\n']/user"
      ... )
'\nroot\n'
>>>

Es kann jedoch etwas unvorhersehbar sein, wenn der Wert eines bestimmten XML-Elements in einer bestimmten RPC-Antwort zusätzliche Leerzeichen enthält. Stattdessen ist es einfacher, die Antwortnormalisierung zu verwenden, um den erwarteten Wert abzugleichen, ohne sich um führende oder nachgestellte Leerzeichen kümmern zu müssen:

>>> response = r0.rpc.get_system_users_information(normalize=True)
>>> response.findtext("uptime-information/user-table/user-entry[tty='u0']/user")
'root'
>>>

Die Antwortnormalisierung entfernt führende und nachgestellte Leerzeichen aus den Werten aller XML-Elemente in der Antwort. Dadurch wird nicht nur der XPath-Ausdruck vereinfacht, sondern auch eine zusätzliche Verarbeitung zur Entfernung von Leerzeichen aus dem Wert des Benutzernamens, auf den zugegriffen wird, vermieden.

Ein letzter Hinweis zur Antwortnormalisierung: Sie kann, wie bisher gezeigt, pro RPC oder für alle RPCs einer Geräteinstanz oder einer NETCONF-Sitzung aktiviert werden, indem das Argument normalize=True bei den Aufrufen Device() bzw. open() angegeben wird. Hier ist ein Beispiel für die Aktivierung für die Geräteinstanz:

>>> r0 = Device(host='r0',user='user',password='user123',normalize=True)
>>> r0.open()
Device(r0)
>>> response = r0.rpc.get_system_users_information()
>>> response.findtext("uptime-information/up-time")
'4 days, 2:03'

Wenn die Antwortnormalisierung für die Geräteinstanz aktiviert ist, wie soeben gezeigt, ist es immer noch möglich, das Verhalten pro RPC zu überschreiben, indem du ein normalize=False Argument beim Aufrufen der RPC Methode angeben.

jxmlease

Neben dem Parsen von XML-Dokumenten in lxml.etree.Element Objekte, kannst du auch die jxmlease-Bibliothek verwenden, um RPC-Antworten in jxmlease-Objekte zu parsen. Möglicherweise wirst du jxmlease, das in "Strukturierte Daten in Python verwenden" beschrieben wird, der Verwendung von XPath und der lxml-Bibliothek vorziehen. Dieses Format bietet ein hervorragendes Gleichgewicht zwischen Funktionalität und Benutzerfreundlichkeit. Auf die Werte von XML-Elementen kann mit denselben Werkzeugen zugegriffen werden wie auf native Python-Wörterbücher und -Listen. Ein Beispiel für die Verwendung von jxmlease hast du in dem Beispielskript in Kapitel 3 gesehen. In diesem Beispiel wurde jxmlease verwendet, um den vom Junos RESTful API Service zurückgegebenen XML-String zu parsen. jxmlease kann aber auch verwendet werden, um ein lxml.etree.ElementObjekt direkt zu parsen. Dazu wird ein lxml.etree.Element Objekt an eine Instanz von der Klasse jxmlease.EtreeParser übergeben. Hier ist ein Beispiel für die Verwendung dieser Technik, um die Ausgabe des get-system-users-information RPC als ein jxmlease.XMLDictNodeObjekt zurückzugeben:

>>> import jxmlease
>>> parser = jxmlease.EtreeParser()
>>> response = parser(r0.rpc.get_system_users_information())
>>> response.prettyprint(depth=3)
{'system-users-information': {'uptime-information': {'active-user-count': u'6',
                                                     'date-time': u'12:39PM',
                                                     'load-average-1': u'0.29',
                                                     'load-average-15': u'0.41',
                                                     'load-average-5': u'0.43',
                                                     'up-time': u'3 days, 4:57',
                                                     'user-table': {...}}}}
>>> type(response)
<class 'jxmlease.XMLDictNode'>

Die Antwort ist eigentlich ein jxmlease.XMLDictNode Objekt, verhält sich aber ähnlich wie ein geordnetes Wörterbuch.

Du kannst auf jede Ebene des jxmlease.XMLDictNode Objekts zugreifen, indem du eine Kette von Wörterbuchschlüsseln angibst. Der Tag jedes XML-Elements wird als Wörterbuchschlüssel verwendet und beginnt mit dem Tag des Root-Elements der RPC-Antwort. Diese Anweisung greift zum Beispiel auf die Betriebszeit des Systems zu:

>>> print response['system-users-information']['uptime-information']['up-time']
3 days, 4:19

Beachte, dass die Schlüssel mit ['system-users-information'] beginnen und das Äquivalent der Antwortnormalisierung standardmäßig auf jxmlease.XMLDictNode Objekte angewendet wird.

Bei XML-Elementen, die Attribute haben, kannst du die Methode get_xml_attr() des Objekts verwenden, um den Wert des Attributs abzurufen:

>>> ut = response['system-users-information']['uptime-information']['up-time']
>>> ut.get_xml_attr('seconds')
'274740'

get_xml_attr() kann auch ein Standardwert zurückgegeben werden, wenn der XML-Attributname nicht existiert:

>>> ut.get_xml_attr('foo',0)
0

Das PyEZ-Beispielskript in "Ein PyEZ-Beispiel" demonstriert die Verwendung von jxmlease mit PyEZ RPC Antworten.

JSON

Junos unterstützt das JSON-Ausgabeformat seit Version 14.2 oder höher. Wenn du PyEZ verwendest, um einen RPC auf einem Junos-Gerät mit Version 14.2 oder höher aufzurufen, kann PyEZ diese JSON-Ausgabe anfordern. Du forderst die JSON-Ausgabe einer RPC an, indem du ein Argument übergibst, das ein Wörterbuchmit einem einzelnen 'format' Schlüssel mit dem Wert 'json' ist. Dieses Argument bewirkt, dass das Junos-Gerät die RPC-Antwort als JSON-String zurückgibt. Der JSON-String wird jedoch nicht direkt an den Benutzer zurückgegeben. Stattdessen ruft PyEZ json.loads() auf, um den JSON-String in eine native Python-Datenstruktur zu parsen, die aus Wörterbüchern und Listen besteht. Hier ist ein Beispiel:

>>> response = r0.rpc.get_system_users_information({'format': 'json'})
>>> type(response)
<type 'dict'>
>>> from pprint import pprint
>>> pprint(response, depth=3)
{u'system-users-information': [{u'attributes': {...},
                                u'uptime-information': [...]}]}

Dieses Verhalten des automatischen Parsens der JSON-Antwort in eine Python-Datenstruktur entspricht dem Verhalten des Standard-XML-Formats. Bei der Verwendung des Standard-XML-Formats gibt das Gerät eine Zeichenkette zurück, die ein XML-Dokument enthält, und PyEZ parst das XML-Dokument in eine Python-Datenstruktur. Auf die resultierende Antwort kann mit allen normalen Python-Werkzeugen für den Umgang mit Wörterbüchern und Listen zugegriffen werden. Unter "JSON-Daten" findest du weitere Informationen über das JSON-Format, einschließlich seiner Einschränkungen.

Operative Tabellen und Ansichten

Neben der Funktion "RPC on Demand" ( ) bietet PyEZ eine weitere Methode, um einen RPC aufzurufen und die Antwort in eine Python-Datenstruktur zu übertragen. Die Funktion "Tabellen und Ansichten" ermöglicht eine präzise Steuerung der Abbildung von Teilen der RPC-Antwort in eine Python-Datenstruktur. Außerdem kann diese Zuordnung für eine spätere Wiederverwendung gespeichert werden. PyEZ wird mit einer Reihe von Beispieltabellen und -ansichten ausgeliefert, die du verwenden oder an deine eigenen Bedürfnisse anpassen kannst.

Die Betriebsdaten eines Junos-Geräts sind die Zustandsdaten, die die aktuellen Betriebsbedingungen des Geräts darstellen. Betriebsdaten sind schreibgeschützte Informationen, die von den Konfigurationsinformationen des Geräts getrennt sind. In der Regel kannst du die Betriebsdaten über CLI-Befehle show oder über XML-RPCs einsehen. Du kannst dir diese Betriebsdaten ähnlich wie eine Datenbank vorstellen. Datenbanken sind in einer Sammlung von Tabellen organisiert, und ähnlich organisiert PyEZ die Betriebsdaten eines Junos-Geräts in einer Sammlung von Tabellen.

In PyEZ stellt eine "Tabelle" die Informationen dar, die von einem bestimmten XML-RPC zurückgegeben werden. Eine PyEZ-Tabelle ist weiter unterteilt in eine Liste von "Items". Diese Elemente sind alle XML-Knoten in der RPC-Ausgabe, die einem bestimmten XPath-Ausdruck entsprechen.

Ähnlich wie eine Datenbankansicht eine Teilmenge von Feldern aus einer Datenbanktabelle auswählt und darstellt, wählt eine PyEZ-"Ansicht" eine Gruppe von Feldern (XML-Knoten) aus jedem PyEZ-Tabellenelement aus und bildet sie in einer nativen Python-Datenstruktur ab. Jede PyEZ-Tabelle verfügt über mindestens eine Ansicht, die Standardansicht, mit der die Felder eines Elements in eine native Python-Datenstruktur abgebildet werden. Es können weitere Ansichten definiert werden, aber nur die Standardansicht ist erforderlich.

Mehrere Ansichten werden verwendet, um verschiedene Informationen aus den Tabellenelementen auszuwählen. Das Konzept der Mehrfachansichten ist vergleichbar mit den Flags terse, brief, detail und extensive für verschiedene CLI-Befehle. So wie jeder dieser CLI-Flags unterschiedliche Informationen aus demselben CLI-Befehl ausgibt, zeigen verschiedene Ansichten unterschiedliche Informationen aus derselben Tabelle.

Vorgefertigte betriebliche Tabellen und Ansichten

Beginnen wir mit der vorhandenen Tabellen und Ansichten, die in PyEZ enthalten sind. Tabellen und Ansichten werden in YAML-Dateien definiert, die die Dateinamenerweiterung .yml haben. Der Inhalt dieser YAML-Dateien wird in der nächsten Sitzung ausführlich behandelt, wenn wir erklären, wie du deine eigenen Tabellen und Ansichten erstellst.

Die vorgefertigten Tabellen und Ansichten, die in PyEZ enthalten sind, befinden sich im Unterverzeichnis op des Installationsverzeichnisses des Moduls jnpr.junos, wie die folgende Verzeichnisliste zeigt:

user@h0$ pwd
/usr/local/lib/python2.7/dist-packages/jnpr/junos
user@h0$ ls op/*.yml
op/arp.yml                     op/fpc.yml           op/lacp.yml  op/phyport.yml
op/bfd.yml                     op/idpattacks.yml    op/ldp.yml   op/routes.yml
op/ccc.yml                     op/intopticdiag.yml  op/lldp.yml  op/teddb.yml
op/ethernetswitchingtable.yml  op/isis.yml          op/nd.yml    op/vlan.yml
op/ethport.yml                 op/l2circuit.yml     op/ospf.yml  op/xcvr.yml

Das Unterverzeichnis op ist relativ zu dem Ort, an dem das Modul jnpr.junos der PyEZ-Bibliothek installiert ist. Auf dem Beispiel-Automatisierungshost ist dieses Verzeichnis /usr/local/lib/python2.7/dist-packages/jnpr/junos, aber der Ort ist installationsabhängig und kann auf deinem Rechner anders sein. Ermitteln Sie den Speicherort des Verzeichnisses auf Ihrem Rechner, indem Sie das Verzeichnis des Attributs jnpr.junos.__file__ anzeigen lassen. Verwende dieses Rezept:

>>> import jnpr.junos
>>> import os.path
>>> os.path.dirname(jnpr.junos.__file__)
'/usr/local/lib/python2.7/dist-packages/jnpr/junos'

Um die vorgefertigten Tabellen und Ansichten verwenden zu können, musst du die Namen der verfügbaren Tabellen kennen. Du kannst die Tabellennamen ermitteln, indem du in den .yml-Dateien nach der Zeichenfolge Table: suchst, wie in diesem Beispiel gezeigt:

user@h0$ pwd
/usr/local/lib/python2.7/dist-packages/jnpr/junos
user@h0$ grep Table: op/*.yml
op/arp.yml:ArpTable:
op/bfd.yml:BfdSessionTable:
op/bfd.yml:_BfdSessionClientTable:
op/ccc.yml:CCCTable:
op/ethernetswitchingtable.yml:EthernetSwitchingTable:
op/ethernetswitchingtable.yml:_MacTableEntriesTable:
op/ethernetswitchingtable.yml:_MacTableInterfacesTable:
op/ethport.yml:EthPortTable:
op/fpc.yml:FpcHwTable:
op/fpc.yml:FpcMiReHwTable:
op/fpc.yml:FpcInfoTable:
op/fpc.yml:FpcMiReInfoTable:
op/idpattacks.yml:IDPAttackTable:
op/intopticdiag.yml:PhyPortDiagTable:
op/isis.yml:IsisAdjacencyTable:
op/isis.yml:_IsisAdjacencyLogTable:
op/l2circuit.yml:L2CircuitConnectionTable:
op/lacp.yml:LacpPortTable:
op/lacp.yml:_LacpPortStateTable:
op/lacp.yml:_LacpPortProtoTable:
op/ldp.yml:LdpNeighborTable:
op/ldp.yml:_LdpNeighborHelloFlagsTable:
op/ldp.yml:_LdpNeighborTypesTable:
op/lldp.yml:LLDPNeighborTable:
op/nd.yml:NdTable:
op/ospf.yml:OspfNeighborTable:
op/phyport.yml:PhyPortTable:
op/phyport.yml:PhyPortStatsTable:
op/phyport.yml:PhyPortErrorTable:
op/routes.yml:RouteTable:
op/routes.yml:RouteSummaryTable:
op/routes.yml:_rspTable:
op/teddb.yml:TedTable:
op/teddb.yml:_linkTable:
op/teddb.yml:TedSummaryTable:
op/vlan.yml:VlanTable:
op/xcvr.yml:XcvrTable:

Der erste Schritt zur Verwendung einer dieser Tabellen ist der Import der entsprechenden Python-Klasse. (Der Name der Python-Klasse ist derselbe wie der Name der Tabelle, die in der YAML-Datei definiert ist). Nehmen wir ArpTable aus der ersten Zeile der vorangegangenen Ausgabe als Beispiel. Um die Klasse ArpTable zu importieren, gibst du die folgende Anweisung from ein:

>>> from jnpr.junos.op.arp import ArpTable

Genau wie die RPC on Demand-Methoden arbeiten auch die Tabellen und Ansichten auf einer PyEZ-Geräteinstanz mit einer offenen NETCONF-Sitzung. Wie zuvor verwenden wir die Aufrufe Device() und open(), um die Geräteinstanzvariable r0 zu erstellen und zu öffnen:

>>> from jnpr.junos import Device
>>> r0 = Device(host='r0',user='user',password='user123')
>>> r0.open()
Device(r0)
>>>

Erstelle eine leere Tabelleninstanz, indem du die Geräteinstanzvariable (r0) als Argument an den Klassenkonstruktor (ArpTable()) übergibst:

>>> arp_table = ArpTable(r0)

Die Instanzvariable arp_table kann nun mit allen Tabellenelementen gefüllt werden, indem die Instanzmethode get() aufgerufen wird:

>>> arp_table.get()
ArpTable:r0: 9 items

In der vorangehenden Ausgabe führt die Methode get() einen RPC aus und verwendet die Ergebnisse, um die Instanz arp_tablemit neun Elementen zu füllen.

Hinweis

Im Fall von ArpTable steht jedes Element für einen <arp-table-entry> Knoten aus der XML-Ausgabe des RPCs get-arp-table-information.

Eine andere Möglichkeit, die Tabelle zu erstellen und aufzufüllen, besteht darin, die Tabelle als Attribut der Geräteinstanzvariable zu binden. Die Methode get() wird dann für dieses gebundene Attribut aufgerufen:

>>> from jnpr.junos.op.arp import ArpTable
>>> r0.bind(arp_table=ArpTable)
>>> r0.arp_table.get()
ArpTable:r0: 9 items

In der vorangegangenen Ausgabe sehen wir wieder, dass die Methode get() einen RPC ausführt und das Attribut r0.arp_tablemit neun Einträgen füllt. Die Speicherung von arp_table als Attribut der Geräteinstanzvariable ist eine praktische Schreibweise, wenn du eine Tabelle für mehrere Geräte pflegst. So kannst du z. B. problemlos separate ArpTable Tabellen für die Geräte r0, r1 und r2 speichern und darauf zugreifen. Du kannst dann auf jede Tabelle als Attribut der jeweiligen Geräteinstanzvariable zugreifen.

Es ist auch möglich, bestimmte Tabellenelemente abzurufen, indem Argumente an die Methode get() übergeben werden. Wenn die Methode get() aufgerufen wird, führt sie eine entsprechende XML-RPC aus, die in der YAML-Datei der Tabelle definiert ist. Der Methode get() können die gleichen Parameter übergeben werden, wie wenn du die RPC mit RPC on Demand aufrufst. Im Fall von ArpTable wird die RPC get-arp-table-information ausgeführt. Die RPC get-arp-table-information unterstützt das Argument <interface>, um die Antwort auf ARP-Einträge auf einer bestimmten Schnittstelle zu beschränken. Hier ein Beispiel, um nur die ARP-Einträge auf der logischen Schnittstelle ge-0/0/0.0 abzurufen:

>>> arp_table.get(interface='ge-0/0/0.0')
ArpTable:r0: 1 items
>>> pprint(arp_table.items())
[('00:05:86:18:ec:02',
  [('interface_name', 'ge-0/0/0.0'),
   ('ip_address', '10.0.4.2'),
   ('mac_address', '00:05:86:18:ec:02')])]

Wenn du die Methode get() einer Tabelle aufrufst, werden die Elemente der Tabelle immer aktualisiert, indem ein XML-RPC auf dem Junos-Gerät ausgeführt und die Antwort des RPCs empfangen wird. Wenn du die Methode get() aufrufst, überschreibt die neue Antwort alle vorherigen Daten in der Tabelle. Erinnere dich daran, die Methode get() mit den entsprechenden Argumenten aufzurufen, wenn du die in der Tabelle gespeicherten Daten aktualisieren musst.

Hinweis

Die Normalisierung von Tabellen- und View-Daten ist standardmäßig aktiviert. Weitere Informationen zur Normalisierung findest du unter "Antwortnormalisierung". Wenn du die Normalisierung deaktivieren möchtest, übergib das Argument normalize=False an die Methode get():

>>> arp_table.get(normalize=False)
ArpTable:r0: 9 items

Wenn du eine Instanz einer Tabelle erstellst, gibt sie ein jnpr.junos.factory.OpTable Objekt zurück. Jedes jnpr.junos.factory.OpTable Objekt funktioniert ähnlich wie ein Python OrderedDict View-Objekt. Das folgende Beispiel aktualisiert die Instanz arp_tablemit allen ARP-Tabelleneinträgen und zeigt dann arp_table's type is jnpr.junos.factory.OpTable.ArpTableist, der eine Unterklasse von jnpr.junos.factory.OpTableWie bei einem Wörterbuch wird auf die Schlüssel und Werte von arp_table mit der Methode items() zugegriffen:

>>> arp_table.get()
ArpTable:r0: 9 items
>>> type(arp_table)
<class 'jnpr.junos.factory.OpTable.ArpTable'>
>>> from pprint import pprint
>>> pprint(arp_table.items())
[('00:05:86:48:49:00',
  [('interface_name', 'ge-0/0/4.0'),
   ('ip_address', '10.0.1.2'),
   ('mac_address', '00:05:86:48:49:00')]),
 ('00:05:86:78:2a:02',
  [('interface_name', 'ge-0/0/1.0'),
   ('ip_address', '10.0.2.2'),
   ('mac_address', '00:05:86:78:2a:02')]),
 ('00:05:86:68:0b:02',
  [('interface_name', 'ge-0/0/2.0'),
   ('ip_address', '10.0.3.2'),
   ('mac_address', '00:05:86:68:0b:02')]),
 ('00:05:86:18:ec:02',
  [('interface_name', 'ge-0/0/0.0'),
   ('ip_address', '10.0.4.2'),
   ('mac_address', '00:05:86:18:ec:02')]),
 ('00:05:86:08:cd:00',
  [('interface_name', 'ge-0/0/3.0'),
   ('ip_address', '10.0.5.2'),
   ('mac_address', '00:05:86:08:cd:00')]),
 ('10:0e:7e:b1:f4:00',
  [('interface_name', 'fxp0.0'),
   ('ip_address', '10.102.191.252'),
   ('mac_address', '10:0e:7e:b1:f4:00')]),
 ('10:0e:7e:b1:b0:80',
  [('interface_name', 'fxp0.0'),
   ('ip_address', '10.102.191.253'),
   ('mac_address', '10:0e:7e:b1:b0:80')]),
 ('00:00:5e:00:01:c9',
  [('interface_name', 'fxp0.0'),
   ('ip_address', '10.102.191.254'),
   ('mac_address', '00:00:5e:00:01:c9')]),
 ('56:68:a6:6a:47:b2',
  [('interface_name', 'em1.0'),
   ('ip_address', '128.0.0.16'),
   ('mac_address', '56:68:a6:6a:47:b2')])]
>>>
Hinweis

Im vorherigen Beispiel wurde die erste Methode zum Erstellen und Auffüllen der Tabelle verwendet, die Instanzvariable arp_table. Wenn die Tabelle mit der Instanzvariablen des Geräts verknüpft wurde und die zweite Methode verwendet wird, wird sie mit r0.arp_table und nicht mit arp_table aufgerufen:

>>> type(r0.arp_table)
<class 'jnpr.junos.factory.OpTable.ArpTable'>

Da die Tabelle ähnlich wie eine OrderedDict funktioniert, kann auf die einzelnen Elemente (die jnpr.junos.factory.View.ArpViewObjekte sind) entweder nach Position oder Schlüssel zugegriffen werden:

>>> type(arp_table[0])
<class 'jnpr.junos.factory.View.ArpView'>
>>> pprint(arp_table[0].items())
[('interface_name', 'ge-0/0/4.0'),
 ('ip_address', '10.0.1.2'),
 ('mac_address', '00:05:86:48:49:00')]
>>> pprint(arp_table['00:05:86:48:49:00'].items())
[('interface_name', 'ge-0/0/4.0'),
 ('ip_address', '10.0.1.2'),
 ('mac_address', '00:05:86:48:49:00')]
>>>

Auf einzelne Werte innerhalb eines View Items kann mit zweistufigen Referenzen zugegriffen werden, indem entweder ein Index oder ein Schlüsselwert für die äußere Referenz verwendet wird:

>>> arp_table['00:05:86:48:49:00']['ip_address']
'10.0.1.2'
>>> arp_table[0]['ip_address']
'10.0.1.2'
>>> arp_table['00:05:86:48:49:00']['interface_name']
'ge-0/0/4.0'
>>> arp_table[0]['interface_name']
'ge-0/0/4.0'

Wenn du ein View-Objekt (eine Unterklasse von jnpr.junos.factory.View) einer Variablen zuweist, kannst du auch auf die einzelnen Felder des View-Objekts als Python-Eigenschaften oder als Wörterbuch zugreifen:

>>> arp_item = arp_table[0]
>>> arp_item.ip_address
'10.0.4.2'
>>> arp_item.interface_name
'ge-0/0/0.0'
>>> arp_item.mac_address
'00:05:86:18:ec:02'
>>>
>>> arp_item.keys()
['interface_name', 'ip_address', 'mac_address']
>>> arp_item.items()
[('interface_name', 'ge-0/0/0.0'), 
 ('ip_address', '10.0.4.2'), 
 ('mac_address', '00:05:86:18:ec:02')]

Darüber hinaus hat jedes View-Objekt zwei spezielle Eigenschaften, name und T. Die Eigenschaft name enthält den eindeutigen Namen oder Schlüssel der Ansicht innerhalb der Tabelle. Die Eigenschaft Tist ein Verweis auf die zugehörige Tabelle, die die Ansicht enthält:

>>> arp_item.name
'00:05:86:18:ec:02'
>>> arp_item.T
ArpTable:r0: 9 items

In diesem Abschnitt hast du gesehen, wie du vorgefertigte Tabellen und Ansichten einrichten, auffüllen und aufrufen kannst. Jetzt schauen wir uns an, wie du eine benutzerdefinierte Tabelle und Ansicht definieren kannst, anstatt dich auf die vorgefertigten Tabellen und Ansichten zu beschränken, die mit PyEZ enthält.

Erstellen neuer operativer Tabellen und Ansichten

PyEZ-Tabellen und -Ansichten werden in .yml-Dateien im YAML-Format definiert. YAML verwendet eine einfache und intuitiv lesbare Syntax, um hierarchische Datenstrukturen zu definieren, die aus Skalaren, Listen und assoziativen Arrays bestehen (ähnlich wie Python-Wörterbücher). Weitere Informationen zu YAML findest du in der folgenden Sidebar "YAML im Überblick" und umfassende Informationen zu YAML in der YAML-Spezifikation.

PyEZ-Tabellen- und View-Definitionen verwenden eine Untermenge der vollständigen YAML-Syntax. Sie verwenden hauptsächlich assoziative Arrays mit mehreren Hierarchieebenen. Die Werte sind in der Regel String-Datentypen. Hier ein Beispiel anhand der Datei arp.yml, in der die im vorherigen Abschnitt verwendeten Klassen ArpTable und ArpView definiert sind:

---
ArpTable: 1
  rpc: get-arp-table-information 2
  item: arp-table-entry 3
  key: mac-address 4
  view: ArpView 5

ArpView: 6
  fields: 7
    mac_address: mac-address 8
    ip_address: ip-address 9
    interface_name: interface-name 10
1

Der PyEZ YAML-Lader geht davon aus, dass alles in der ersten Spalte eine Tabellen- oder View-Definition ist. View-Definitionen werden anhand ihrer Schlüssel von Tabellendefinitionen unterschieden. Mit dieser Zeile beginnt eine Tabellendefinition. Der Tabellenname wird zum Namen der entsprechenden Python-Klasse. Die einzige Einschränkung für den Tabellennamen ist, dass er ein gültiger Name für eine Python-Klasse sein muss.

2

Die Junos XML RPC(get-arp-table-information), die aufgerufen wird, um die Elementdaten der Tabelle abzurufen.

3

Ein XPath-Ausdruck, der verwendet wird, um jedes Tabellenelement aus der RPC-Antwort auszuwählen.

4

Der Name eines XML-Elements innerhalb jedes Tabellenelements. Der Wert dieses XML-Elements wird als Schlüssel für den Zugriff auf jedes Element in der nativen Python-Datenstruktur verwendet.

5

Der Name der Standard-View, die verwendet wird, um ein Tabellenelement in eine native Python-Datenstruktur abzubilden. Dieser Wert muss genau mit einem View-Namen übereinstimmen, der in demselben YAML-Dokument definiert ist. In diesem Dokument ist die einzige Ansicht ArpView, die durch den Schlüssel des assoziierten Arrays auf oberster Ebene definiert ist.

6

Der PyEZ YAML-Lader geht davon aus, dass alles in der ersten Spalte eine Tabellen- oder View-Definition ist. View-Definitionen werden anhand ihrer Schlüssel von Tabellendefinitionen unterschieden. Diese Zeile beginnt eine View-Definition.

7

Der Wert des Schlüssels fields ist ein assoziatives Array, das XPath-Ausdrücke auf Namen abbildet. Die Namen werden als Schlüssel in der nativen Python-Datenstruktur (dem View-Objekt) verwendet. Die XPath-Ausdrücke werden verwendet, um Werte aus dem XML-Objekt der einzelnen Elemente zu holen.

8

Der XPath-Ausdruck mac-address wird verwendet, um den Wert des Schlüssels mac_address im nativen Python-View-Objekt zu setzen. Da mac_address als Schlüssel in einer Python-Datenstruktur verwendet wird, muss er den Anforderungen für die Benennung von Python-Variablen entsprechen (er sollte Unterstriche und keine Bindestriche verwenden).

9

Auch hier ist ip-address ein XPath-Ausdruck und ip_addressist der Name des Schlüssels im Python-View-Objekt.

10

Der letzte Schlüssel in jedem Python-View-Objekt ist interface_name. Der Wert des Schlüssels interface_name wird durch den XPath-Ausdruck interface-name bestimmt.

Die Datei arp.yml veranschaulicht zwar die erforderliche Struktur und den Inhalt von PyEZ-Tabellen- und View-Definitionen, enthält aber nicht alle möglichen Schlüssel/Wertpaare. Tabelle 4-5 enthält eine Beschreibung aller verfügbaren Schlüssel/Wertpaare, die in einer Tabellendefinition verwendet werden können.

Tabelle 4-5. Tasten zur Definition einer PyEZ-Tabelle
Name des SchlüsselsErforderlich oder optionalBeschreibung
rpcErforderlichDer Name der Junos XML RPC, die aufgerufen wird, um die Elementdaten der Tabelle abzurufen. Dieser Wert sollte den tatsächlichen RPC-XML-Tag-Namen (mit Bindestrichen) und nicht den PyEZ-RPC-on-Demand-Methodennamen (mit Unterstrichen) verwenden.
argsOptionalEin assoziatives Array, dessen Elemente als Standardargumente an rpc übergeben werden. Der Parameter args sollte nur angegeben werden, wenn rpc immer mit argsArgumenten aufgerufen werden soll. Die Schlüssel des assoziativen Arrays sind die Argumente, die an die PyEZ RPC on Demand Methode übergeben werden (mit Unterstrichen, nicht mit Bindestrichen). Wenn ein RPC-Argument ein Flag ist, setze den Wert des assoziativen Arrays auf True.
args_keyOptional

Der Name eines optionalen, unbenannten ersten Arguments für die Methode get(). Die vorgefertigte RouteTableDefinition enthält zum Beispiel:

RouteTable:
  rpc: get-route-information
  args_key: destination
Diese Definition ermöglicht es dem Benutzer, aufzurufen:
>>> route_table.get('10.0.0.0/8')

Dadurch wird die folgende RPC an das Junos-Gerät gesendet:

<get-route-information>
    <destination>10.0.0.0/8</destination>
</get-route-information>
itemErforderlichEin XML XPath-Ausdruck, der jedes Tabellenelement (Datensatz) aus der RPC-Antwort auswählt. Jedes XML-Element in der RPC-Antwort, das mit dem XPath-Ausdruck übereinstimmt, wird zu einem Tabellenelement. Der XPath-Ausdruck ist relativ zum ersten Element in der Antwort nach dem <rpc-reply> Tag. Das ist dasselbe Verhalten, das wir in "lxml-Elemente" beobachtet haben, wenn wir lxml-Methoden mit RPC-Antworten verwenden.
keyOptional, aber empfohlen

Ein XPath, um ein XML-Element innerhalb jedes Tabellenelements auszuwählen. Der Wert des XML-Elements wird als Schlüssel für den Zugriff auf jedes Element in der nativen Python-Datenstruktur verwendet.

Wenn keynicht angegeben ist, wird der XPath name als Schlüssel verwendet. Wenn mehrere XML-Elemente zur eindeutigen Identifizierung eines Tabellenelements erforderlich sind, ist der Wert dieses Schlüssels eine YAML-Liste, die einen XPath für jedes XML-Element enthält, das den Schlüssel bildet. Es wird empfohlen, key explizit anzugeben, auch wenn sein Wert auf den Standardwert name gesetzt ist.

viewErforderlichaDer Name der Standard-View, die verwendet wird, um ein Tabellenelement in eine native Python-Datenstruktur abzubilden. Dieser Wert muss genau mit dem Namen einer View übereinstimmen, die im selben YAML-Dokument definiert ist.

a Wenn der Schlüssel view nicht vorhanden ist, wird jedes Tabellenelement als lxml.etree.Element Objekt zurückgegeben. Dadurch wird ein Großteil der Vorteile von Tabellen und Ansichten zunichte gemacht, so dass der Schlüssel view tatsächlich erforderlich ist.

Setzen wir die Informationen aus Tabelle 4-5 in die Praxis um, indem wir eine neue Tabellendefinition für den show system usersCLI-Befehl erstellen, den wir weiter oben in diesem Kapitel verwendet haben. Zunächst wird der show system users CLI-Befehl dem get-system-users-information XML RPC zugeordnet. Unsere Tabellendefinition enthält das RPC-Argument no-resolve, um DNS-Abfragen für die <from> Elemente in der RPC-Antwort zu vermeiden:

user@r0> show system users no-resolve | display xml rpc 
<rpc-reply xmlns:junos="http://xml.juniper.net/junos/15.1R2/junos">
    <rpc>
        <get-system-users-information>
                <no-resolve/>
        </get-system-users-information>
    </rpc>
    <cli>
        <banner></banner>
    </cli>
</rpc-reply>

Es ist hilfreich, sich bei der Erstellung der neuen Tabellen- und View-Definition auf die Struktur der erwarteten RPC-Antwort zu beziehen. Hier findest du eine gekürzte Version der get-system-users-information-Antwort als Referenz. Diese RPC-Antwort enthält sowohl systemweite Informationen (<up-time>, <active-user-count>, <load-average-1>, etc.) als auch loginspezifische Informationen (die <user-entry> Elemente):

user@r0> show system users no-resolve | display xml        
<rpc-reply xmlns:junos="http://xml.juniper.net/junos/15.1R2/junos">
    <system-users-information xmlns="http://xml.juniper.net/junos/15.1R2/junos">
        <uptime-information>
            <date-time junos:seconds="1437696318">5:05PM</date-time>
            <up-time junos:seconds="192060">2 days,  5:21</up-time>
            <active-user-count junos:format="8 users">8</active-user-count>
            <load-average-1>0.46</load-average-1>
            <load-average-5>0.50</load-average-5>
            <load-average-15>0.44</load-average-15>
            <user-table>
                <user-entry>
                    <user>root</user>
                    <tty>u0</tty>
                    <from>-</from>
                    <login-time junos:seconds="0">Wed08PM</login-time>
                    <idle-time junos:seconds="16860">4:41</idle-time>
                    <command>cli</command>
                </user-entry>
                <user-entry>
                    <user>user</user>
                    <tty>pts/2</tty>
                    <from>172.29.98.24</from>
                    <login-time junos:seconds="1437694518">4:35PM</login-time>
                    <idle-time junos:seconds="0">-</idle-time>
                    <command>-cli (cli)</command>
                </user-entry>
                <user-entry>
                    <user>user</user>
                    <tty>pts/3</tty>
                    <from>172.29.104.116</from>
                    <login-time junos:seconds="1437667698">9:08AM</login-time>
                    <idle-time junos:seconds="6420">1:47</idle-time>
                    <command>-cli (cli)</command>
                </user-entry>
                ... additional user-entry elements omitted ...
            </user-table>
        </uptime-information>
    </system-users-information>
    <cli>
        <banner></banner>               
    </cli>
</rpc-reply>

Die Tabellendefinition konzentriert sich auf die Login-spezifischen Informationen, indem sie die <user-entry>Elemente in Tabellenelemente extrahiert. Hier ist die Tabellendefinition mit Erklärungen für jedes Feld. Füge diesen Inhalt in eine users.yml-Datei ein. Erinnere dich daran, dass du Leerzeichen und keine Tabulatoren für die Einrückung verwendest:

---
### ------------------------------------------------------
### show system users no-resolve
### ------------------------------------------------------

UserTable:
    rpc: get-system-users-information
    args:
        no_resolve: True
    item: uptime-information/user-table/user-entry
    key:
        - user
        - tty
    view: UserView

Diese YAML-Datei definiert eine Tabelle namens UserTable. Die Tabelle wird durch die Ausführung des RPCs get-system-users-information mit dem Argument <no-resolve/>gefüllt. Die Tabelle enthält einen Eintrag für jeden XML-Eintrag, der dem XPath-Ausdruck uptime-information/user-table/user-entry entspricht. Und standardmäßig wird die Ansicht UserView auf jedes Tabellenelement angewendet. Dieses Beispiel zeigt ein Beispiel für einen Schlüssel mit mehreren Elementen. Jeder Teil des Schlüssels (tty und user) ist ein XPath-Ausdruck, der sich auf ein Tabellenelement (ein <user-entry>Element) bezieht. Der Schlüssel für jedes Tabellenelement ist ein Tupel, das aus den Werten user und tty gebildet wird.

Hinweis

In den meisten Fällen gibt es ein einziges XML-Element, das jedes Element eindeutig identifiziert. In diesem Fall identifiziert das Element <tty> jedes <user-entry> eindeutig und hätte als einfacher Schlüssel verwendet werden können. Zu Demonstrationszwecken haben wir uns jedoch für einen Schlüssel mit mehreren Elementen entschieden, der die Kombination der Elemente <user> und <tty> verwendet.

Der Schlüssel item in der vorherigen Tabellendefinition ist ein relativ einfacher XPath, der eine dreistufige Hierarchie uptime-information/user-table/user-entry angibt, um alle <user-entry>Elemente auszuwählen. Obwohl fortgeschrittene XPath-Ausdrücke nicht Gegenstand dieses Buches sind, kann es hilfreich sein zu sehen, wie ein komplizierterer XPath-Ausdruck verwendet wird, um die Auswahl der Tabellenelemente zu steuern. Wenn du zum Beispiel diesen XPath-Ausdruck einfügst, werden nur die Benutzeranmeldungen ausgewählt, die länger als 1 Stunde (3600 Sekunden) inaktiv waren:

item: "uptime-information/user-table/user-entry[idle-time[@seconds>3600]]"

Jedes Tabellenelement stellt weiterhin ein <user-entry> Element dar, aber nur bestimmte <user-entry> Elemente werden ausgewählt. Konkret werden die <user-entry> Elemente ausgewählt, die ein <idle-time> Kindelement haben und bei denen das <idle-time> Element ein seconds Attribut mit einem Wert größer als 3600 hat.

Hinweis

Der vorangehende XPath-Ausdruck muss in doppelte Anführungszeichen gesetzt werden. Er enthält Klammern, die YAML sonst als Inline-Liste interpretieren würde.

Nachdem wir nun gesehen haben, wie man eine komplexere Tabelle definiert, wollen wir uns nun der entsprechenden View-Definition widmen. Der einzige Zweck einer View-Definition ist die Zuordnung von Werten zu Schlüsseln in einem Python-View-Objekt. Der Wert für einen bestimmten Schlüssel ergibt sich aus einem entsprechenden XPath-Ausdruck. Der XPath-Ausdruck ist relativ zu jedem Tabellenelement und wählt normalerweise ein einzelnes Element aus dem Tabellenelement aus. Der Textknoten des ausgewählten Elements wird zum Wert des Schlüssels im Python-View-Objekt.

Füge diese View-Definition an dieselbe users.yml-Datei an, die auch die UserTable -Definition enthält:

UserView:
    fields:
        from: from
        login_time: login-time
        idle_time: idle-time
        command: command

Setze kein YAML-Dokumententrennzeichen (---) zwischen die Tabellen- und die View-Definition. Sie sind beide Teil desselben YAML-Dokuments.

Jede View-Definition beginnt mit dem Namen der View in der ersten Spalte. Auch hier werden View-Definitionen anhand ihrer Schlüssel von Tabellendefinitionen unterschieden. Wenn der Schlüssel und rpc vorhanden ist, nimmt PyEZ an, dass es sich um eine Tabellendefinition handelt. Wenn kein rpc vorhanden ist, nimmt PyEZ an, dass es sich um eine View-Definition handelt.

In diesem Beispiel lautet der Name der Ansicht UserView. Der View-Name wird zum Namen einer entsprechenden Python-Klasse, daher sollte der Name den Konventionen von Python für Klassennamen entsprechen. Außerdem muss der Name der View genau mit der Eigenschaft viewder Tabellendefinition übereinstimmen. Dies ist die Standardansicht für die Tabelle. Es können weitere Ansichten definiert werden, aber nur die Standardansicht ist erforderlich.

Beachte den Doppelpunkt nach UserView. Dieser Doppelpunkt zeigt an, dass UserView ein Schlüssel in einem assoziativen Array ist. Der Wert des Schlüssels UserView ist ein weiteres assoziatives Array, das vier Arten von Schlüsseln haben kann: fields, extends, groups, und fields_groupnameDas Beispiel verwendet den einfachsten dieser Schlüssel, fields. Der Wert des Schlüssels fields ist ein weiteres assoziatives Array.

Das assoziative Array fields definiert eine Reihe von Namen und entsprechenden XPath-Ausdrücken (die relativ zum Tabellenelement sind). Da die Namen zu Attributen des entsprechenden Python-View-Objekts werden, müssen sie den Python-Namenskonventionen für Variablen entsprechen. In diesem Beispiel sind from, login_time, idle_time und command die Feldnamen.

Warnung

View-Instanz-Attribute (Eigenschafts- oder Methodennamen) können nicht als Namen von Feldern verwendet werden. Zurzeit umfassen die View-Instanz-Attribute : asview, items, key, keys, name, refresh, to_json, updater, values, xml, D, FIELDS, GROUPS, ITEM_NAME_XPATH, und T. Verwende diese Namen nicht als Feldnamen. Du solltest auch Feldnamen vermeiden, die mit einem Unterstrich beginnen.

Die XPath-Ausdrücke sind from, login-time, idle-time und command. Jeder dieser XPath-Ausdrücke wählt ein einzelnes passendes untergeordnetes Element aus einem Tabellenelement aus. Schau dir die XML-Ausgabe an und beobachte, wie jedes <user-entry> Element <from> , <login-time>, <idle-time> und <command> Kindelemente enthält.

Es ist auch möglich, kompliziertere XPath-Ausdrücke für die Feldauswahl zu verwenden. Nimm diese Beispielfelddefinition aus dem vorgefertigten RouteTableView in der Datei routes.yml:

    via: "nh/via | nh/nh-local-interface"

Der Feldname lautet via. Der XPath-Ausdruck lautet nh/via | nh/nh-local-interface. Dieser XPath-Ausdruck wählt alle <via> und<nh-local-interface> Elemente unter den <nh> Kindelementen des Elements aus. Wenn nur ein passendes Element vorhanden ist, ist das Feld via der String-Wert des passenden Elements. Wenn der XPath-Ausdruck jedoch mehr als ein Element auswählt, ist der Wert des Feldes via eine Liste von String-Werten. Die String-Werte werden von jedem übereinstimmenden Element übernommen.

In der Standardeinstellung sind die Feldwerte Python-Strings. Es gibt jedoch Fälle, in denen die XML-Antwortelemente numerische Werte enthalten. In diesen Fällen kann der Wert des Feldes als Python int oder float definiert werden. Ein Beispiel dafür sind die Elemente <login-time> und <idle-time> jedes <user-entry>:

<login-time junos:seconds="0">Wed08PM</login-time>
<idle-time junos:seconds="16860">4:41</idle-time>

Der Wert jedes Elements ist ein Datum/Zeit-String, kein numerischer Wert. Diese Strings sind die Werte der Felder login_time und idle_time in der vorherigen Definition von UserView. Diese XML-Elemente enthalten jedoch auch ein secondsAttribut6 mit einem numerischen Wert. Definieren wir eine neue UserExtView, die ganzzahlige Werte für die Anmeldezeit und die Leerlaufzeit enthält. Füge diese neue Ansichtsdefinition an die gleiche users.yml-Datei an:

UserExtView:
    extends: UserView
    fields:
        login_seconds: {"login-time/@seconds": int}
        idle_seconds: {"idle-time/@seconds": int}
Warnung

Die YAML-Spezifikation reserviert das Zeichen @ für eine zukünftige Verwendung. Derzeit akzeptiert YAML ein @, das innerhalb eines Strings ohne Anführungszeichen steht. Es wird jedoch ein Fehler ausgegeben, wenn der String mit einem @Zeichen beginnt, wie z.B. @seconds. Das Einschließen dieser Strings in Anführungszeichen vermeidet mögliche Fehler in zukünftigen Versionen von YAML.

Die neue Definition der Ansicht UserExtView verdeutlicht mehrere Punkte. Schau dir zunächst die Werte der Felder login_seconds und idle_seconds an. Anstatt einen XPath-Ausdruck anzugeben, wird ein assoziatives Inline-Array verwendet. Dieses assoziative Array enthält einen XPath-Ausdruck und den Python-Datentyp, der für den resultierenden Wert verwendet werden soll (in diesem Fallint). In den Felddefinitionen werden auch komplexere XPath-Ausdrücke verwendet, die den Wert des Attributs seconds innerhalb jedes Elements auswählen.

Hinweis

Der Typ eines Ansichtsfeldes kann als int, float oder flag definiert werden. Der Typ flag setzt einen booleschen Wert, der True ist, wenn das Element vorhanden ist, und False, wenn das Element nicht vorhanden ist. Hier ist ein Beispiel aus dem vorgefertigten EthPortView in der Datei ethport.yml:

    running: { ifdf-running: flag }
    present: { ifdf-present: flag }

Unter findest du den Schlüssel extends in der Definition von UserExtView. Mit dem Schlüssel extends kannst du einfach eine neue Ansicht erstellen, die eine Obermenge einer anderen Ansicht ist. Die Zeile extends: UserView bewirkt, dass alle Felder aus der Definition UserView in UserExtView enthalten sind. Zusätzlich zu den Feldern aus UserView enthält UserExtView auch die neuen Felder login_seconds und idle_seconds, die unter dem Schlüssel fields definiert sind.

Die RPC-Antwort get-system-users-information enthält nicht nur <user-entry> Elemente für jede Anmeldung, sondern auch systemweite Informationen wie sowie die Elemente <active-user-count> und <load-average-1>. Es ist zwar etwas ungewöhnlich, aber es ist möglich, diese systemweiten Informationen in jedes UserExtView Objekt aufzunehmen. Dazu werden zwei zusätzliche Felder an die UserExtView Definition angehängt. Die vollständige UserExtView Definition lautet nun:

UserExtView:
    extends: UserView
    fields:
        login_seconds: {"login-time/@seconds": int}
        idle_seconds: {"idle-time/@seconds": int}
        num_users: {../../active-user-count: int}
        load_avg:  {../../load-average-1: float}

Wie alle Felddefinitionen geben auch die neuen Felder num_users und load_avg jeweils einen XPath-Ausdruck an, der sich auf die einzelnen Tabellenelemente bezieht. Diese XPath-Ausdrücke verwenden die Notation des übergeordneten Elements (..), um die XML-Hierarchie der Antwort zu durchlaufen und Knoten auszuwählen, die nicht im <user-entry>Element enthalten sind. Da jedes <user-entry> Element ein gemeinsames <user-table> übergeordnetes Element hat, hat das zur Folge, dass jedes UserExtView Objekt die Felder num_users und load_avg mit genau denselben Werten enthält. Die anderen Felder -from, login_time, idle_time, commands, login_seconds und idle_seconds- haben weiterhin anmeldungsspezifische Werte, da diese Felder aus dem <user-entry> Element pro Element ausgewählt werden. Beachte, dass das Feld load_avg als Python float definiert ist.

Für die Definition von Ansichten gibt es noch ein weiteres Werkzeug. Dieses Werkzeug ist groups. Gruppen sind völlig optional. Sie sind einfach eine Möglichkeit, eine Gruppe von Feldern zusammenzufassen, die XPath-Ausdrücke mit einem gemeinsamen Präfix verwenden. In der Definition von UserExtView teilen sich die Felder num_users und load_avg das Präfix ../... Hier ist eine alternative Definition von UserExtView, die das Tool groups verwendet:

UserExtView:
    groups:
        common: ../..
    fields_common:
        num_users: {active-user-count: int}
        load_avg:  {load-average-1: float}
    fields:
        from: from
        login_time: login-time
        idle_time: idle-time
        command: command
        login_seconds: {"login-time/@seconds": int}
        idle_seconds: {"idle-time/@seconds": int}

Jede Gruppe hat einen Namen und ein XPath-Präfix. In diesem Beispiel lautet der Name common und ../.. ist das XPath-Präfix. Ein entsprechender fields_groupname Schlüssel wird dann verwendet, um die Felder zu definieren, die das XPath-Präfix gemeinsam haben. In diesem Fall definiert der Schlüssel fields_common die Felder num_users und load_avg. Die vollständigen XPath-Ausdrücke für diese Felder lauten dann ../../active-user-count und ../../load-average-1.

Hinweis

Du hast vielleicht bemerkt, dass die vorangegangene UserExtView den Schlüssel extends nicht enthält. Stattdessen werden alle Felder von UserView in die Felder von UserExtView kopiert. Zurzeit schließen sich die Schlüsselwörter groups und extends gegenseitig aus. Du kannst nicht beide Schlüssel in einer View-Definition verwenden.

Bevor wir weitermachen, kombinieren wir die Tabellen- und View-Definitionen zu einer vollständigen users.yml-Datei. Diese Datei verwendet nicht groups in die UserExtViewDefinition:

---
### ------------------------------------------------------
### show system users no-resolve
### ------------------------------------------------------

UserTable:
    rpc: get-system-users-information
    args:
        no_resolve: True
    item: uptime-information/user-table/user-entry
    key:
        - user
        - tty
    view: UserView

UserView:
    fields:
        from: from
        login_time: login-time
        idle_time: idle-time
        command: command

UserExtView:
    extends: UserView
    fields:
        login_seconds: {"login-time/@seconds": int}
        idle_seconds: {"idle-time/@seconds": int}
        num_users: {../../active-user-count: int}
        load_avg:  {../../load-average-1: float}

Verwendung der neuen operativen Tabelle und Ansicht

Nachdem wir nun die vollständige Datei users.ymlerstellt haben, können wir diese Tabellen- und Ansichtsdefinitionen verwenden. Beginne mit dem Erstellen und Öffnen einer Geräteinstanz:

>>> from jnpr.junos import Device
>>> r0 = Device(host='r0',user='user',password='user123')
>>> r0.open()
Device(r0)

Die Tabellen- und View-Definitionen werden mit der Funktion loadyaml() aus dem Modul jnpr.junos.factorygeladen. Diese Funktion erstellt die Python-Klassen und gibt ein Wörterbuch zurück, das die Tabellen- und View-Namen den entsprechenden Klassenfunktionen zuordnet:

>>> from jnpr.junos.factory import loadyaml
>>> user_defs = loadyaml('users.yml')
>>> from pprint import pprint
>>> pprint(user_defs)
{'UserExtView': <class 'jnpr.junos.factory.View.UserExtView'>,
 'UserTable': <class 'jnpr.junos.factory.OpTable.UserTable'>,
 'UserView': <class 'jnpr.junos.factory.View.UserView'>}

Die Funktion loadyaml() benötigt einen Pfad zu der YAML-Datei, die die Tabellen- und View-Definitionen enthält. Dies kann ein absoluter oder relativer Dateipfad sein. Relative Pfade sind relativ zum aktuellen Arbeitsverzeichnis.

Sobald die Tabellen- und View-Definitionen erstellt wurden, wird eine Instanz der Tabellenklasse erstellt. Der Zugriff auf die Klassenfunktion erfolgt durch Indizierung des users_def Wörterbuchs mit dem Tabellennamen. Die Klassenfunktion nimmt eine Geräteinstanz als einziges Argument an:

>>> user_table = user_defs['UserTable'](r0)

Alternativ kannst du die Klassennamen auch in den globalen Namensraum kopieren und die Klassenfunktion mit dem Tabellennamen aufrufen:

>>> globals().update(user_defs)
>>> user_table = UserTable(r0)

Jede dieser Methoden führt zu einer leeren UserTable Instanz. Die Instanz wird der Variablen user_table zugewiesen. Genau wie bei einer vorgefertigten Tabelle füllt die Methode get()die Tabelle auf, indem sie den angegebenen RPC aufruft:

>>> user_table.get()
UserTable:r0: 8 items
>>> pprint(user_table.items())
[(('root', 'u0'),
  [('command', 'cli'),
   ('idle_time', '4:41'),
   ('from', '-'),
   ('login_time', 'Wed08PM')]),
 (('foo', 'pts/0'),
  [('command', '-cli (cli)'),
   ('idle_time', '7:56'),
   ('from', '172.29.104.116'),
   ('login_time', '9:08AM')]),
 (('bar', 'pts/1'),
  [('command', '-cli (cli)'),
   ('idle_time', '7:56'),
   ('from', '172.29.104.116'),
   ('login_time', '9:08AM')]),
 (('user', 'pts/2'),
  [('command', '-cli (cli)'),
   ('idle_time', '-'),
   ('from', '172.29.98.24'),
   ('login_time', '4:35PM')]),
 (('user', 'pts/3'),
  [('command', '-cli (cli)'),
   ('idle_time', '1:47'),
   ('from', '172.29.104.116'),
   ('login_time', '9:08AM')]),
 (('user', 'pts/4'),
  [('command', '-cli (cli)'),
   ('idle_time', '29'),
   ('from', '172.29.98.24'),
   ('login_time', '4:35PM')]),
 (('foo', 'pts/5'),
  [('command', '-cli (cli)'),
   ('idle_time', '29'),
   ('from', '172.29.98.24'),
   ('login_time', '4:35PM')]),
 (('bar', 'pts/6'),
  [('command', '-cli (cli)'),
   ('idle_time', '29'),
   ('from', '172.29.98.24'),
   ('login_time', '4:35PM')])]
>>>

Nimm dir Zeit, um die Ausgabe von pprint(user_table.items()) mit den Definitionen von UserTable und UserView in der Datei users.yml zu vergleichen. Achte auf die Ansichtsfelder und -werte. Achte auch auf den Schlüssel für jedes Tabellenelement. Es ist ein Tupel, das aus den Werten von user und tty gebildet wird. Du kannst auf eine Eigenschaft in einer bestimmten Ansicht zugreifen, indem du das Tupel als Tabellenschlüssel angibst:

>>> user_table[('foo','pts/5')]['from']
'172.29.98.24'

Eine andere Methode, um eine benutzerdefinierte operative Tabelle und Ansicht zu laden, besteht darin, eine entsprechende .py-Datei zu erstellen, die die Funktion loadyaml() ausführt und den globalen Namensraum aktualisiert. Speichere den folgenden Inhalt in einer users.py-Datei im selben Verzeichnis wie users.yml:

"""
Pythonifier for UserTable and UserView
"""
from jnpr.junos.factory import loadyaml
from os.path import splitext
_YAML_ = splitext(__file__)[0] + '.yml'
globals().update(loadyaml(_YAML_))

Dieser Code bestimmt die zu ladende YAML-Datei, indem er die Erweiterung . py durch .yml ersetzt. Anschließend werden die Funktionen loadyaml() und globals().update() verwendet, die bereits gezeigt wurden. Hier ein Beispiel für die Verwendung der Datei users.py zur Erstellung und Befüllung einer UserTable Instanz:

>>> from users import UserTable
>>> user_table = UserTable(r0)
>>> user_table.get()
UserTable:r0: 8 items
>>>

Durch die Hinzufügung der Datei <name>.py ist das Verfahren zur Erstellung einer neuen Tabelleninstanz dasselbe, unabhängig davon, ob die YAML-Definitionsdatei mit PyEZ vordefiniert oder benutzerdefiniert ist.

Hinweis

PyEZ-Nutzer sind aufgefordert, ihre eigenen Tabellen- und View-Definitionen über einen GitHub-Pull-Request an das PyEZ-Projekt zu senden. Je größer die Bibliothek der Tabellen und Ansichten wird, desto wahrscheinlicher ist es, dass du eine bestehende Tabelle oder Ansicht findest, die du wiederverwenden oder an deine Bedürfnisse anpassen kannst.

Eine andere Sichtweise anwenden

Erinnerst du dich an die UserExtView, die in der Datei users.ymldefiniert? Die UserExtView definiert zusätzliche Felder für jedes Tabellenelement. Wie wendest du diese Ansicht auf die Tabelleninstanz user_table an? Importiere einfach die View-Klasse und setze die Eigenschaft view von user_tableauf die neue Klasse UserExtView:

>>> from users import UserExtView
>>> user_table.view = UserExtView
>>> pprint(user_table.items())
[(('root', 'u0'),
  [('idle_seconds', 16860),
   ('from', '-'),
   ('idle_time', '4:41'),
   ('login_seconds', 0),
   ('num_users', 8),
   ('command', 'cli'),
   ('load_avg', 0.51),
   ('login_time', 'Wed08PM')]),
 (('foo', 'pts/0'),
  [('idle_seconds', 28560),
   ('from', '172.29.104.116'),
   ('idle_time', '7:56'),
   ('login_seconds', 1437667701),
   ('num_users', 8),
   ('command', '-cli (cli)'),
   ('load_avg', 0.51),
   ('login_time', '9:08AM')]),
 (('bar', 'pts/1'),
  [('idle_seconds', 28560),
   ('from', '172.29.104.116'),
   ('idle_time', '7:56'),
   ('login_seconds', 1437667701),
   ('num_users', 8),
   ('command', '-cli (cli)'),
   ('load_avg', 0.51),
   ('login_time', '9:08AM')]),
 (('user', 'pts/2'),
  [('idle_seconds', 0),
   ('from', '172.29.98.24'),
   ('idle_time', '-'),
   ('login_seconds', 1437694521),
   ('num_users', 8),
   ('command', '-cli (cli)'),
   ('load_avg', 0.51),
   ('login_time', '4:35PM')]),
 (('user', 'pts/3'),
  [('idle_seconds', 6420),
   ('from', '172.29.104.116'),
   ('idle_time', '1:47'),
   ('login_seconds', 1437667701),
   ('num_users', 8),
   ('command', '-cli (cli)'),
   ('load_avg', 0.51),
   ('login_time', '9:08AM')]),
 (('user', 'pts/4'),
  [('idle_seconds', 1740),
   ('from', '172.29.98.24'),
   ('idle_time', '29'),
   ('login_seconds', 1437694521),
   ('num_users', 8),
   ('command', '-cli (cli)'),
   ('load_avg', 0.51),
   ('login_time', '4:35PM')]),
 (('foo', 'pts/5'),
  [('idle_seconds', 1740),
   ('from', '172.29.98.24'),
   ('idle_time', '29'),
   ('login_seconds', 1437694521),
   ('num_users', 8),
   ('command', '-cli (cli)'),
   ('load_avg', 0.51),
   ('login_time', '4:35PM')]),
 (('bar', 'pts/6'),
  [('idle_seconds', 1740),
   ('from', '172.29.98.24'),
   ('idle_time', '29'),
   ('login_seconds', 1437694521),
   ('num_users', 8),
   ('command', '-cli (cli)'),
   ('load_avg', 0.51),
   ('login_time', '4:35PM')])]
>>>

Alternativ kannst du die Methode asview() auch auf eine einzelne View-Instanz (die ein einzelnes Tabellenelement darstellt) anwenden, wie im folgenden Beispiel gezeigt. Zunächst wird die Ansicht der Tabelle auf UserView zurückgesetzt:

>>> from users import UserView
>>> user_table.view = UserView

Als nächstes wird das erste Tabellenelement one_view zugewiesen:

>>> one_view = user_table[0]

Die Positionen innerhalb von one_view werden mit dem Standard UserView gedruckt, und dann noch einmal mit dem UserExtView:

>>> pprint(one_view.items())
[('command', 'cli'),
 ('idle_time', '4:41'),
 ('from', '-'),
 ('login_time', 'Wed08PM')]
>>> pprint(one_view.asview(UserExtView).items())
[('idle_seconds', 16860),
 ('from', '-'),
 ('idle_time', '4:41'),
 ('login_seconds', 0),
 ('num_users', 8),
 ('command', 'cli'),
 ('load_avg', 0.51),
 ('login_time', 'Wed08PM')]
>>>

Speichern und Laden von XML-Dateien aus Tabellen

Ein letzter Hinweis zu den Tabellen und Ansichten, bevor wir zur Konfiguration von Junos-Geräten mit PyEZ übergehen, ist, dass PyEZ-Tabellen normalerweise durch die Ausführung einer XML-RPC über eine offene NETCONF-Verbindung befüllt werden. Es ist jedoch auch möglich, die XML-RPC-Antwort zu speichern und später wiederzuverwenden. Um die XML-RPC-Antwort zu speichern, rufst du die Methode savexml() für die Tabelleninstanz auf. Das Argument path ist erforderlich und gibt an, wo (ein absoluter oder relativer Dateipfad) die XML-Ausgabe gespeichert werden soll. Die optionalen Flags hostname und timestamp können verwendet werden, um mehrere Tabellen in einzelnen XML-Dateien zu speichern:

>>> user_table.savexml(path='/tmp/user_table.xml',hostname=True,timestamp=True)
>>> quit()
user@h0$ ls /tmp/*.xml
/tmp/user_table_r0_20150723151807.xml

Später kann eine XML-Datei verwendet werden, um eine Tabelle aufzufüllen. Das Laden einer Tabelle aus einer XML-Datei erfordert keine NETCONF-Verbindung zu dem Gerät, das die RPC-Antwort ursprünglich erzeugt hat. Um die Tabelle aus einer XML-Datei zu laden, gibst du das Argument pathan die Klassenfunktion der Tabelle an:

>>> user_table = UserTable(path='/tmp/user_table_r0_20150723151807.xml')
>>> user_table.get()
UserTable:/tmp/user_table_r0_20150723151807.xml: 8 items
>>>
Hinweis

Die Methode get() muss noch aufgerufen werden, um die Tabelle aufzufüllen.

Nachdem wir nun gesehen haben, dass es mehrere Möglichkeiten gibt, operationale RPCs aufzurufen und die daraus resultierenden Antworten zu analysieren, wollen wir uns nun der Konfiguration des Geräts zuwenden.

Konfiguration

PyEZ bietet eine Config Klasse, die den Prozess des Ladens und Übertragens von Konfigurationsänderungen auf ein Junos-Gerät vereinfacht. Darüber hinaus ist PyEZ mit der Jinja2 Templating Engine integriert, um die Erstellung des eigentlichen Konfigurations-Snippets zu vereinfachen. Schließlich bietet PyEZ Dienstprogramme zum Vergleichen von Konfigurationen, zum Zurücknehmen von Konfigurationsänderungen und zum Sperren oder Entsperren der Konfigurationsdatenbank.

Der erste Schritt bei der Verwendung der Klasse Config ist die Erstellung einer Instanzvariable. Hier ist ein Beispiel für die Erstellung einer Konfigurationsinstanzvariable unter Verwendung der bereits geöffneten Geräteinstanzvariable r0:

>>> from jnpr.junos.utils.config import Config
>>> r0_cu = Config(r0)
>>> r0_cu
jnpr.junos.utils.Config(r0)

Alternativ kann die Konfigurationsinstanz auch als Eigenschaft der Geräteinstanz gebunden werden:

>>> from jnpr.junos.utils.config import Config
>>> r0.bind(cu=Config)
>>> r0.cu
jnpr.junos.utils.Config(r0)

Für den Rest dieses Abschnitts werden wir diese r0.cu Geräteeigenschaftssyntax verwenden. Der Name der Geräteeigenschaft ist jedoch nichts Besonderes. In unseren Beispielen haben wir den Namen cu gewählt, aber jeder gültige Python-Variablenname (der nicht bereits eine Eigenschaft der Geräteinstanz ist) kann an seiner Stelle verwendet werden. Beginnen wir damit, Konfigurationsschnipsel in die Kandidatenkonfiguration des Geräts zu laden.

Laden von Konfigurationsänderungen

Die Methode load() kann verwendet werden, um ein Konfigurations-Snippet oder eine vollständige Konfiguration in die Kandidatenkonfiguration des Geräts zu laden. Die Konfiguration kann in Text- (auch "geschweifte Klammer" genannt), Set- oder XML-Syntax angegeben werden. Alternativ kann die Konfiguration auch als lxml.etree.Element Objekt angegeben werden.

Konfigurationsschnipsel in Text-, Set- oder XML-Syntax können aus einer Datei auf dem Automatisierungshost oder einem Python-Stringobjekt geladen werden. Dateien werden mit dem Argument path in der Methode load() angegeben. Strings werden als erstes unbenanntes Argument an die Methode load() übergeben. Die Methode load() versucht, das Format des Konfigurationsinhalts automatisch zu bestimmen. Das Format eines Konfigurationsstrings wird durch den Inhalt des Strings bestimmt. Das Format einer Konfigurationsdatei wird durch die Dateinamenerweiterung des Pfades bestimmt (siehe Tabelle 4-6). In beiden Fällen kann das automatische Format außer Kraft gesetzt werden, indem das Argument format auf text, set oder xml setzt.

Tabelle 4-6. Zuordnung von Pfad-Dateinamenerweiterungen zum Konfigurationsformat
Pfad DateinamenerweiterungFormat der Konfiguration
.conf, .text, oder .txtText in "geschweifter Klammer" Konfigurationsformat
.setzenText im Konfigurationsformat einstellen
.xmlText im XML Konfigurationsformat

Die booleschen Flags overwrite und merge der Methode load() steuern, wie die neue Konfiguration die aktuelle Konfiguration beeinflusst. Das Standardverhalten entspricht dem Befehl load replace im CLI-Konfigurationsmodus. Wenn das overwrite Flag gesetzt ist, entspricht das Verhalten dem load override, und wenn das merge Flag gesetzt ist, entspricht das Verhalten dem load merge Befehl. Die overwrite und merge Flags schließen sich gegenseitig aus. Du kannst nicht beide zur gleichen Zeit setzen. Außerdem kannst du das Flag nicht setzen overwrite Flag nicht gesetzt werden, wenn die Konfiguration im Set-Format vorliegt.

Hier ist ein einfaches Beispiel, um den Hostnamen des Geräts zu ändern, indem du einen Konfigurationsstring im Set-Format übergibst:

>>> from lxml import etree
>>> new_hostname = "set system host-name r0.new" 
>>> result = r0.cu.load(new_hostname)
>>> etree.dump(result)
<load-configuration-results>
<ok/>
</load-configuration-results>

>>>

Die Methode load() gibt ein lxml.etree.Element Objekt zurück, das das Ergebnis anzeigt.

Hier sind einige weitere Beispiele für die Anwendung der Methode load():

# load merge set
result = r0.cu.load(path='hostname.set', merge=True)

# load override
result = r0.cu.load(path='new_config.txt', overwrite=True)

# load merge
result = r0.cu.load(path='hostname.conf', merge=True)

# load replace xml
result = r0.cu.load(path='hostname.xml')

Wenn ein Fehler auftritt, wird eine jnpr.junos.exception.ConfigLoadError Ausnahme ausgelöst:

>>> r0.cu.load('bad config syntax', format='set')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/local/lib/python2.7/dist-packages/jnpr/junos/utils/config.py", l...
    return try_load(rpc_contents, rpc_xattrs)
  File "/usr/local/lib/python2.7/dist-packages/jnpr/junos/utils/config.py", l...
    raise ConfigLoadError(cmd=err.cmd, rsp=err.rsp, errs=err.errs)
jnpr.junos.exception.ConfigLoadError: ConfigLoadError(severity: error, bad_el...
>>>

Standardmäßig ändert die Methode load() die gemeinsame Konfigurationsdatenbank. Das Äquivalent zum CLI-Befehl configure exclusive kann erreicht werden, indem zuerst die Methode lock() aufgerufen wird. Die Methode lock() gibt True zurück, wenn die Konfiguration erfolgreich gesperrt wurde, und löst eine jnpr.junos.exception.LockError Ausnahme aus, wenn die Sperrung der Konfigurationsdatenbank fehlschlägt, weil die Konfiguration bereits gesperrt ist:

>>> r0.cu.lock()
True
>>> result = r0.cu.load(path='hostname.xml')
>>> r0.cu.pdiff()

[edit system]
-  host-name r0;
+  host-name r0.new;

>>> r0.cu.unlock()
True
>>> r0.cu.pdiff()
None
>>>

Beachte, dass die Methode unlock() verwendet werden kann, um die Konfigurationsdatenbank zu entsperren und die Konfigurationsänderungen zu verwerfen. Die Methode pdiff()zeigt Konfigurationsunterschiede an und wird zur Fehlersuche verwendet. Sie wird unter im Abschnitt "Anzeigen von Konfigurationsunterschieden" näher erläutert .

Hinweis

Das Äquivalent zu , dem configure privateCLI-Befehl, kann durch erreicht werden, indem die open-configuration und close-configuration RPCs aufgerufen werden. Ein Beispiel findest du unter "Erstellen, Anwenden und Übertragen der Kandidatenkonfiguration".

Konfigurations-Templates

Mit Templates können große Konfigurationsblöcke mit minimalem Aufwand erstellt werden. PyEZ nutzt die Jinja2 Templating-Engine, um Konfigurationsdateien aus Vorlagen zu erstellen. Jinja2 ist eine beliebte, quelloffene Python-Bibliothek des Pocoo-Teams. Eine ausführliche Dokumentation zu Jinja2 findest du auf der Jinja2-Website.

Jinja2-Vorlagen kombinieren ein wiederverwendbares Textformular mit einer Reihe von Daten, die zum Ausfüllen des Formulars und zur Darstellung des Ergebnisses verwendet werden. Jinja2 bietet Variablensubstitution, Konditionale und einfache Schleifenkonstrukte. Jinja2 ist sozusagen eine eigene "Mini-Programmiersprache". Jinja2-Templates gibt es nicht nur in Junos; sie ermöglichen es, jede Textdatei aus einer "Vorlage" oder einem "Formular" und einer Reihe von Daten zu erstellen. Jede textbasierte Konfigurationsdatei, einschließlich Junos-Konfigurationsdateien in geschweifter Klammer-, Set- oder sogar XML-Syntax, kann aus einer Jinja2-Vorlage erstellt werden.

Schauen wir uns ein sehr einfaches Template-Beispiel an, das einen Hostnamen auf dem Router basierend auf einem Template konfiguriert. In diesem Beispiel wird die Konfiguration durch eine set Anweisung ausgedrückt. Sie hätte genauso gut im Text- oder XML-Format ausgedrückt werden können. Hier ist die Konfiguration set Anweisung um einen Hostnamen r0.new zu konfigurieren:

set system host-name r0.new

Speichere diese Anweisung in einer Datei namens hostname.set im aktuellen Arbeitsverzeichnis.

Wie du im vorherigen Abschnitt gesehen hast, wird eine Konfigurationsdatei mit der Methode load() in die Kandidatenkonfiguration des Geräts geladen:

>>> result = r0.cu.load(path='hostname.set')
>>>

Jinja2 verwendet doppelte geschweifte Klammern, um einen Ausdruck zu kennzeichnen, und der einfachste Jinja2-Ausdruck ist einfach der Name der Variablen:

{{ variable_name }}

Ein Jinja2-Variablenausdruck wertet den Wert der Variablen aus. Jinja2-Variablennamen folgen denselben Syntaxregeln wie Python-Variablennamen.

Wende diese Syntax auf die einzeilige Datei hostname.set an. Ersetze den fest codierten r0.new hostname durch einen Verweis auf die Variable host_name. Die aktualisierte Datei hostname.set lautet nun:

set system host-name {{ host_name }}

Wie du gerade gesehen hast, wird eine Konfigurationsdatei in die Kandidatenkonfiguration des Geräts geladen, indem das Argument path an die Methode load() übergeben wird. Auf ähnliche Weise wird eine Templating-Konfiguration in die Kandidatenkonfiguration des Geräts geladen, indem das Argument template_path an die Methode load() übergeben wird. Für Templates ist jedoch ein zusätzliches Argument für die Methode load()erforderlich. Das Argument template_vars nimmt ein Wörterbuch als Wert an. Jede Variable in der Jinja2-Vorlage muss ein Schlüssel in diesem Wörterbuch sein. Hier ist ein Beispiel, das die Vorlage hostname.set verwendet, um den Hostnamen foo zu konfigurieren:

>>> result = r0.cu.load(template_path='hostname.set',
...                     template_vars= {'host_name' : 'foo'})
>>> r0.cu.pdiff()

[edit system]
-  host-name r0;
+  host-name foo;

>>>

Das Argument template_path kann einen absoluten oder relativen Dateinamen angeben. Relative Dateinamen werden anhand des aktuellen Arbeitsverzeichnisses und eines Templating-Unterverzeichnisses des Modulpfads durchsucht.7 Genau wie beim Argument pathbestimmt die Dateierweiterung des Arguments template_path das erwartete Format der Vorlage. Die gleiche Zuordnung zwischen Dateinamenerweiterung und Konfigurationsformat, die in Tabelle 4-6 beschrieben ist, gilt auch für das Argument template_path.

Nachdem du nun die Grundlagen des Erstellens und Ladens einer Vorlage kennengelernt hast, wollen wir uns nun die weitere Syntax von Jinja2 ansehen. Zunächst kann ein Ausdruck null oder mehr Filter enthalten, die durch das Zeichen | getrennt sind. Ähnlich wie Unix-Pipeline- oder Junos-Pipe (|) -Anzeigefilter sind Jinja2-Filter Sätze von Funktionen, die verkettet werden können, um einen Ausdruck zu ändern. Jinja2-Filter ändern die Variable, die am Anfang eines Ausdrucks steht.

Du kannst zum Beispiel einen Jinja2-Filter verwenden, um einen Standardwert für eine Variable festzulegen. Beobachte, was mit der vorherigen Vorlage passiert, wenn kein host_name Schlüssel im template_vars Argument der load() Methode vorhanden ist:

>>> result = r0.cu.load(template_path='hostname.set',tempate_vars={})
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/local/lib/python2.7/dist-packages/jnpr/junos/utils/config.py", l...
    return try_load(rpc_contents, rpc_xattrs)
  File "/usr/local/lib/python2.7/dist-packages/jnpr/junos/utils/config.py", l...
    raise ConfigLoadError(cmd=err.cmd, rsp=err.rsp, errs=err.errs)
jnpr.junos.exception.ConfigLoadError: ConfigLoadError(severity: error, bad_el...

Die fehlende host_name Variable führt dazu, dass eine unvollständige set Konfigurationsanweisung erzeugt wird, die eine jnpr.junos.exception.ConfigLoadError Ausnahme auslöst. In diesem Fall kann eine Fehlermeldung eine angemessene Reaktion sein, um den Benutzer der Vorlage darauf hinzuweisen, dass host_name eine Pflichtvariable ist. In anderen Fällen ist es jedoch besser, einen Standardwert anzugeben, wenn eine Variable fehlt. Der von Jinja2 bereitgestellte Filter default() kann für diesen Zweck verwendet werden.

Hier wird der Filter default() verwendet, um einen Standardwert von Missing.Hostname zu erhalten. Der Filter Jinja2 lower wird auch verwendet, um den Hostnamen klein zu schreiben:

set system host-name {{ host_name | default('Missing.Hostname') | lower }}

Schau dir nun das Ergebnis an, wenn du den Schlüssel host_name aus dem Argument template_vars weglässt:8

>>> result = r0.cu.load(template_path='hostname.set')
>>> r0.cu.pdiff()

[edit system]
-  host-name r0;
+  host-name missing.hostname;

>>>

Der Hostname missing.hostnamewird als Ergebnis der Verkettung der Filter default() und lower konfiguriert.

Die Angabe eines Hostnamens von Foo in template_vars führt dazu, dass ein Hostname von foo konfiguriert wird:

>>> result = r0.cu.load(template_path='hostname.set',
...                     template_vars= {'host_name' : 'Foo'})
>>> r0.cu.pdiff()

[edit system]
-  host-name r0;
+  host-name foo;

>>>

In der Jinja2-Vorlagendokumentation findest du eine Liste der eingebauten Filter. Es ist auch möglich, eine eigene Python-Funktion zu schreiben, die als benutzerdefinierter Filter funktioniert. In der Jinja2-API-Dokumentationfindest du weitere Informationen zu benutzerdefinierten Filtern.

Neben der Variablenersetzung und den Filtern bietet Jinja2 auch Tags, die die Logik des Templatings steuern. Tags sind eingeschlossen in {% tag %} Begrenzungszeichen eingeschlossen. Ein solches Tag ist die bedingte Anweisung if. Mit einer if Anweisung können unterschiedliche Inhalte in die gerenderte Datei aufgenommen werden, je nachdem, ob ein Ausdruck als wahr oder falsch ausgewertet wird.

Das folgende Beispiel zeigt, wie der Hostname des Geräts konfiguriert wird, je nachdem, ob das Gerät über zwei Routing Engines verfügt oder nicht. Wenn zwei Routing Engines vorhanden sind, wird der Hostname in den speziellen Konfigurationsgruppen re0 und re1 konfiguriert und re0. oder re1. vorangestellt. Verfügt das Gerät über eine einzelne Routing Engine, wird der Hostname wie zuvor auf der Ebene der [edit system] Konfigurationshierarchie konfiguriert. Dieses Mal wird die Konfiguration in Textform (auch geschweifte Klammern genannt) angegeben und in der Datei hostname.conf im aktuellen Arbeitsverzeichnis gespeichert. Hier ist der Inhalt der Datei hostname.conf:

{% if dual_re %}
groups {
    re0 {
        system {
            host-name {{ "re0." + host_name }};
        }
    }
    re1 {
        system {
            host-name {{ "re1." + host_name }};
        }
    }
}
{% else %}
system {
    host-name {{ host_name }};
}
{% endif %}

Die Vorlage beginnt mit einer {% if expression %} Anweisung, die vom Wert der Variable dual_reabhängt. Die Anweisung {% else %} markiert das Ende des Textes, der wiedergegeben wird, wenn dual_re wahr ist; sie markiert auch den Anfang des Textes, der wiedergegeben wird, wenn dual_re falsch ist. Die Anweisung {% endif %} schließt den bedingten Block ab.

Beachte auch den Ausdruck, der verwendet wird, um den Hostnamen im Dual-RE-Fall darzustellen. Er verwendet eine statische Zeichenkette und Pythons + Zeichen für die String-Verkettung, um der Variablen host_name den entsprechenden Wert voranzustellen.

Hier ist ein Beispiel für das Rendern dieser Vorlage auf einem Gerät mit dual REs. Beachte, dass der Wert des Schlüssels dual_re aus dem Wert von r1.facts['2RE'] gesetzt wird, der von der Standard-Faktenerfassung von PyEZ geliefert wird:

>>> r1.facts['2RE']
True
>>> result = r1.cu.load(template_path='hostname.conf',
...                     template_vars= { 'dual_re' : r1.facts['2RE'],
...                                      'host_name' : 'r1' })
>>> r1.cu.pdiff()

[edit groups re0 system]
+   host-name re0.r1;
[edit groups re1 system]
+   host-name re1.r1;

>>>

Dieselbe Vorlage, die auf ein Gerät mit einer einzigen RE angewendet wird, legt den Hostnamen auf der Hierarchieebene [edit system]fest:

>>> r0.facts['2RE']
False
>>> result = r0.cu.load(template_path='hostname.conf',
...                     template_vars= { 'dual_re' : r0.facts['2RE'],
...                                      'host_name' : 'r0' })
>>> r0.cu.pdiff()

[edit system]
+  host-name r0;

>>>

Beachte, dass der Schlüssel dual_re wieder von den Fakteninformationen geliefert wird. Wenn du eine Vorlage mit einem bedingten Block verwendest, wird die richtige Konfiguration für das Gerät automatisch generiert, je nachdem, ob Dual Routing Engines vorhanden sind oder nicht.

Jinja2-Templates bieten eine Fülle zusätzlicher Möglichkeiten, aber zum Abschluss dieses Abschnitts wollen wir uns das {% for scalar_var in list_var %} Schleifen-Konstrukt. In diesem Beispiel durchläuft die Schleife die Einträge in einem Wörterbuch und verwendet den Schlüssel und die Werte des Wörterbuchs, um eine Reihe von IPv4-Adressen für eine Reihe von Schnittstellen zu konfigurieren. Dieses Mal wird die Konfiguration in XML-Syntax angegeben und in der Datei interface_ip.xml im aktuellen Arbeitsverzeichnis gespeichert. Hier ist der Inhalt von interface_ip.xml:

<interfaces>
{% for key, value in interface_ips.iteritems() %}
    <interface>
        <name>{{ key }}</name>
        <unit>
            <name>0</name>
            <family>
                <inet>
                    <address>{{ value }}</address>
                </inet>
            </family>
        </unit>
    </interface>
{% endfor %}
</interfaces>

Der Inhalt der Schleife for ist einer Python-Anweisung forsehr ähnlich. In diesem Beispiel wird die Dictionary-Methode iteritems() verwendet, um über jedes Schlüssel/Wert-Paar im Dictionary zu iterieren. In der Jinja2-Syntax endet die Schleife mit einer {% endfor %} -Anweisung.

Erstelle nun ein interface_ipsWörterbuch mit einem Satz von Schnittstellennamen als Schlüssel und einem Satz von IPv4-Adressen und Präfixlängen als Werte:

>>> interface_ips = {
...     'ge-0/0/0' : '10.0.4.1/30',
...     'ge-0/0/1' : '10.0.2.1/30',
...     'ge-0/0/2' : '10.0.3.1/30',
...     'ge-0/0/3' : '10.0.5.1/30' }

Verwenden wir dieses Wörterbuch und die Vorlage, um IP-Adressen auf r0 zu konfigurieren. Das interface_ips Wörterbuch wird der Wert des Schlüssels interface_ips im Argument template_vars:

>>> result = r0.cu.load(template_path='interface_ip.xml',
...                     template_vars= { 'interface_ips' : interface_ips })
>>> r0.cu.pdiff()

[edit interfaces ge-0/0/0 unit 0 family inet]
+       address 10.0.4.1/30;
[edit interfaces ge-0/0/1 unit 0 family inet]
+       address 10.0.2.1/30;
[edit interfaces ge-0/0/2 unit 0 family inet]
+       address 10.0.3.1/30;
[edit interfaces ge-0/0/3 unit 0 family inet]
+       address 10.0.5.1/30;

>>>

Wie erwartet, fügt die resultierende Konfiguration die richtigen Adressen auf mehreren Schnittstellen hinzu, indem sie die for Schleife verwendet.

Jinja2 Templates bieten viele zusätzliche Funktionen, die wir in diesem Buch nicht direkt behandeln können. Wirf einen Blick in die Jinja2-Dokumentation und experimentiere mit den Funktionen, die in deiner speziellen Umgebung anwendbar sein könnten.

Anzeigen von Konfigurationsunterschieden

Du hast gesehen, dass die Methode pdiff() in den vorherigen Beispielen verwendet wurde, um die Unterschiede anzuzeigen, die in der Kandidatenkonfiguration konfiguriert wurden. Die Methode pdiff() ist für Debugging-Zwecke gedacht, und so haben wir sie auch in diesen Beispielen verwendet. Sie gibt einfach die Unterschiede zwischen der Kandidatenkonfiguration und einer Rollback-Konfiguration aus. In der Standardeinstellung vergleicht die Methode pdiff() die Kandidatenkonfiguration mit der Konfiguration rollback 0. Die Konfiguration rollback 0ist die gleiche wie die aktuell übertragene Konfiguration:

>>> r0.cu.pdiff()

[edit system]
-  host-name r0;
+  host-name r0.new;

>>>

Anstatt die Unterschiede auszudrucken, gibt mit der Methode diff() einen String zurück, der die Unterschiede zwischen der Kandidaten- und der Rollback-Konfiguration enthält:

>>> r0.cu.diff()
'\n[edit system]\n-  host-name r0;\n+  host-name r0.new;\n'

Sowohl die Methoden diff() als auch pdiff() benötigen ein unbenanntes optionales Argument. Das Argument ist eine ganze Zahl, die die Rollback-ID angibt:

>>> r0.cu.pdiff(1)

[edit system]
-  host-name r0;
+  host-name r0.new;
[edit system login]
+    user bar {
+        uid 2002;
+        class super-user;
+        authentication {
+            encrypted-password "$5$0jDrGxJT$PXj0TWwtu5LPJ4Nvlc1YpmCKy7yAwOUH...
+        }
+    }
+    user foo {
+        uid 2003;
+        class super-user;
+        authentication {
+            encrypted-password "$5$WfsXdd11$4LXuWOIwQA5HsWvF8oxMZGyzodIHsnZP...
+        }
+    }

>>>

Nachdem wir nun gesehen haben, wie man Konfigurationsunterschiede anzeigt, schauen wir uns an, wie man die geänderte Kandidatenkonfiguration festschreibt.

Konfigurationsänderungen festschreiben

Die PyEZ Config Klasse bietet eine commit_check() Methode , um die Kandidatenkonfiguration zu überprüfen und eine commit() Methode, um die Änderungen zu übertragen. Die Methode commit_check() gibt True zurück, wenn die Kandidatenkonfiguration alle Überprüfungen bestanden hat, und löst eine Ausnahme jnpr.junos.exception.CommitError aus, wenn die Kandidatenkonfiguration bei den Überprüfungen fehlgeschlagen ist, einschließlich Warnungen:

>>> r0.cu.load("set system host-name r0")
<Element load-configuration-results at 0x7f6503180ea8>
>>> r0.cu.commit_check()
True
>>> r0.cu.load("set protocols bgp export FooBar")
<Element load-configuration-results at 0x7f650317d488>
>>> r0.cu.commit_check()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/local/lib/python2.7/dist-packages/jnpr/junos/utils/config.py", l...
    raise CommitError(cmd=err.cmd, rsp=err.rsp)
jnpr.junos.exception.CommitError: CommitError(edit_path: [edit groups junos-d...
>>>

Die Methode commit() nimmt mehrere optionale Argumente entgegen, die in Tabelle 4-7 aufgeführt sind. Sie gibt True zurück, wenn die Kandidatenkonfiguration erfolgreich übertragen wurde, und löst eine jnpr.junos.exception.CommitError Exception aus, wenn beim Übertragen der Konfiguration ein Fehler oder eine Warnung auftritt.

Tabelle 4-7. Argumente für die Methode commit()
ArgumentBeschreibung
commentDer Wert ist eine Kommentarzeichenkette, die die Übergabe beschreibt.
confirmDer Wert ist eine ganze Zahl, die die Anzahl der Minuten angibt, die auf eine Bestätigung der Übergabe gewartet werden soll. Die Übergabe wird bestätigt, indem commit() erneut aufgerufen wird, bevor der confirm Timer abläuft.
syncEin boolesches Flag. Wenn True, wird ein commit synchronize ausgeführt, der die neue Konfiguration auf beiden Routing-Engines eines Dual-RE-Systems festschreibt. Es ist möglich, dass ein commit synchronize trotzdem ausgeführt wird, wenn der Benutzer die synchronizeAnweisung auf der [edit system commit] Konfigurationshierarchieebene konfiguriert hat.
detailEin boolesches Flag. Wenn True, gibt die Methode commit() ein lxml.etree.Element Objekt mit zusätzlichen Details über den Übergabeprozess zurück. Dieses Argument sollte nur zu Debugging-Zwecken verwendet werden.
force_syncEin boolesches Flag. Wenn True, wird eine commit synchronize force durchgeführt. Dieses Argument sollte nur zu Debugging-Zwecken verwendet werden.
fullEin boolesches Flag. Wenn True, werden alle Junos-Daemons benachrichtigt und reparieren ihre vollständige Konfiguration, auch wenn keine Konfigurationsänderungen vorgenommen wurden, die sich auf die Daemons auswirken. Dieses Argument sollte nur zu Debugging-Zwecken verwendet werden.

Hier ist ein Beispiel für eine erfolgreiche und eine fehlgeschlagene Übertragung:

>>> r0.cu.load("set system host-name r0")
<Element load-configuration-results at 0x7f650317b5a8>
>>> r0.cu.commit(comment='New hostname',sync=True)
True
>>> r0.cu.load("set protocols bgp export FooBar")
<Element load-configuration-results at 0x7f65031765f0>
>>> r0.cu.commit()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/local/lib/python2.7/dist-packages/jnpr/junos/utils/config.py", l...
    raise CommitError(cmd=err.cmd, rsp=err.rsp)
jnpr.junos.exception.CommitError: CommitError(edit_path: [edit groups junos-d...
>>>
Hinweis

Wenn die Konfigurationsdatenbank mit der Methode lock() gesperrt wurde, wird sie durch den Aufruf von commit()nicht entsperrt. Du musst weiterhin die Methode unlock() aufrufen, um die Konfigurationssperre aufzuheben.

Verwenden der Rettungskonfiguration

Die Klasse Config bietet außerdem eine rescue()Methode, um Aktionen für die Rettungskonfiguration des Junos-Geräts auszuführen. Die Rettungskonfiguration soll die Wiederherstellung des Geräts im Falle einer fehlerhaften Konfigurationsänderung unterstützen. Die Idee ist, dass der Benutzer eine Minimalkonfiguration definiert, die benötigt wird, um das Gerät in einen bekannten guten Zustand zu versetzen, und diese Minimalkonfiguration als "Rettungskonfiguration" speichert. Die Methode rescue()nimmt ein unbenanntes Argument entgegen, das die Aktion angibt, die mit der Rettungskonfiguration durchgeführt werden soll. Die gültigen Werte für dieses Argument sind get, save, delete und reload. Wenn die Aktion get ist, kann ein zweites optionales benanntes Argument, format, angegeben werden. Das Standardformat ist text,, was bedeutet, dass die Konfiguration in der Textsyntax (auch geschweifte Klammer genannt) vorliegt. Das Argument format kann auch auf xml gesetzt werden, was die Konfiguration als lxml.etree.Element Objekt abruft.

Das folgende Beispiel demonstriert das Löschen, Speichern, Abrufen und erneute Laden der Rettungskonfiguration unter Verwendung des bestehenden r0.cu Gerätekonfigurationsattributs:

>>> r0.cu.rescue('delete')
True
>>> r0.cu.rescue('save')
True
>>> r0.cu.rescue('get',format='xml')
<Element rescue-information at 0x7f885ac76128>
>>> resp = r0.cu.rescue('reload')
>>> from lxml import etree
>>> etree.dump(resp)
<load-configuration-results>
<ok/>
</load-configuration-results>

>>>

Beachte tie save und delete Aktionen geben einen booleschen Wert zurück, der Erfolg oder Misserfolg anzeigt. Die Aktion save speichert die aktuelle bestätigte Konfiguration als Rettungskonfiguration, während die Aktion delete die aktuelle Rettungskonfiguration löscht. Die Aktion get gibt die aktuelle Rettungskonfiguration zurück. Im vorangegangenen Beispiel wird diese Konfiguration als lxml.etree.Element Objekt zurückgegeben, weil format='xml' angegeben wurde. Wäre das Argument format nicht angegeben worden, wäre stattdessen ein String mit der Rescue-Konfiguration zurückgegeben worden. Die Aktion reload lädt die Rescue-Konfiguration in die Kandidatenkonfiguration. Sie gibt die gleiche Antwort zurück wie die Methode load(). Wie die Methode load () ändert auch rescue('reload') nur die Kandidatenkonfiguration. Um die Rettungskonfiguration zu aktivieren, muss eine commit() ausgegeben werden.

Versorgungsunternehmen

PyEZ bietet außerdem eine Reihe von Dienstprogrammmethoden ( ), die zur Ausführung gängiger Aufgaben auf dem Junos-Betriebssystem verwendet werden können. Diese Aufgaben ermöglichen den Zugriff auf das Dateisystem oder auf die Junos-Shell auf Unix-Ebene, führen Sicherheitskopien durch und führen Junos-Software-Upgrades aus. Die Dateisystem-Dienstprogramme, die in der Klasse FS des Moduls jnpr.junos.utils.fs definiert sind, bieten allgemeine Befehle für den Zugriff auf das Dateisystem auf dem Junos-Gerät. Das Modul jnpr.junos.utils.scpdefiniert eine SCP Klasse, mit der sichere Kopien auf oder von dem Junos-Gerät durchgeführt werden können. Das jnpr.junos.utils.start_shell Modul stellt eine StartShell Klasse bereit, mit der eine SSH-Verbindung zu einem Junos-Gerät hergestellt werden kann. Zusätzliche StartShell Methoden werden bereitgestellt, um Befehle über die SSH-Verbindung auszuführen und auf eine erwartete Antwort zu warten. Schließlich bietet die Klasse SWim Modul jnpr.junos.utils.sw eine Reihe von Methoden, um die Junos-Software auf einem Gerät zu aktualisieren sowie das Gerät neu zu starten oder auszuschalten.

Da diese Hilfsprogramme nicht zur Grundfunktionalität von PyEZ gehören und mit jeder neuen PyEZ-Version erweitert werden, wird in diesem Buch nicht versucht, jedes einzelne Hilfsprogramm im Detail zu behandeln. Stattdessen solltest du die in Python eingebaute Funktion help() verwenden, um die Dokumentationsstrings für diese Klassen anzuzeigen. Hier ist ein Beispiel für die Anzeige der Dokumentationsstrings für die Klasse FS:

>>> from jnpr.junos.utils.fs import FS
>>> help(FS)
Help on class FS in module jnpr.junos.utils.fs:

class FS(jnpr.junos.utils.util.Util)
 |  Filesystem (FS) utilities:
 |  
 |  * :meth:`cat`: show the contents of a file
 |  * :meth:`checksum`: calculate file checksum (md5,sha256,sha1)
 |  * :meth:`cp`: local file copy (not scp)
 |  * :meth:`cwd`: change working directory
 |  * :meth:`ls`: return file/dir listing
 |  * :meth:`mkdir`: create a directory
 |  * :meth:`pwd`: get working directory
 |  * :meth:`mv`: local file rename
 |  * :meth:`rm`: local file delete
 |  * :meth:`rmdir`: remove a directory
 |  * :meth:`stat`: return file/dir information
 |  * :meth:`storage_usage`: return storage usage
 |  * :meth:`storage_cleanup`: perform storage storage_cleanup
...

Nach demselben Rezept kannst du auch die Dokumentationsstrings für die anderen PyEZ-Utility-Klassen anzeigen.

Ein PyEZ-Beispiel

Jetzt, wo du die Möglichkeiten von PyEZ kennengelernt hast, ist es an der Zeit, dieses Wissen in die Praxis umzusetzen, indem du das Beispiel lldp_interface_descriptions_rest.py, das in "Verwendung der RESTful APIs in Python" behandelt wurde, neu schreibst . Dieses Beispiel nutzt die RPC on Demand-Funktion von PyEZ, um die aktuellen LLDP-Nachbarn und Schnittstellenbeschreibungen abzufragen. Die Antworten werden mit den Bibliotheken lxml und jxmlease verarbeitet. Die neue Konfiguration wird mithilfe einer Jinja2-Vorlage angewendet.

Der Zweck und die allgemeine Architektur des Skripts lehnen sich eng an das vorherige Beispiel lldp_interface_descriptions_rest.py an. Das neue Skript heißt lldp_interface_descriptions_pyez.py und verwendet PyEZ, um die Netzwerktopologie eines oder mehrerer Junos-Geräte zu ermitteln und zu überwachen, die das Link Layer Discovery Protocol (LLDP) ausführen. Die von LLDP ermittelten Informationen werden in den Schnittstellenbeschreibungsfeldern der Gerätekonfiguration gespeichert. Die aktuellen LLDP-Informationen werden mit den vorherigen Informationen verglichen, die im Schnittstellenbeschreibungsfeld gespeichert sind, und der Benutzer wird über alle Änderungen des LLDP-Status (Up, Change oder Down) seit dem letzten Snapshot informiert.

Das Skript wird von einem Benutzer über die Befehlszeile aufgerufen und nimmt einen oder mehrere Gerätenamen oder IP-Adressen als Befehlszeilenargumente entgegen. Die Syntax lautet:

user@h0$ python lldp_interface_descriptions_pyez.py r0 r1 r2 r3 r4 r5

Damit der Dienst NETCONF-over-SSH erfolgreich ausgeführt werden kann, sollte auf jedem Gerät ein gemeinsamer Benutzername und ein Passwort mit entsprechender Berechtigung konfiguriert werden:

user@r0> show configuration system services 
netconf {
    ssh;
}

user@r0> show configuration system login                       
user user {
    uid 2001;
    class super-user;
    authentication {
        encrypted-password "$1$jCvocDbA$KeOycEvIDtSV/VOdPRHo5."; ## SECRET-DATA
    }
}

Das Skript fordert zur Eingabe des Benutzernamens und des Passworts auf, um sich mit den Geräten zu verbinden, und gibt dann seine Ausgabe auf dem Terminal des Benutzers aus. Es gibt für jedes überprüfte Gerät eine Benachrichtigung aus. Wenn das Skript seit dem letzten Snapshot Änderungen am LLDP-Status feststellt, werden diese Änderungen auf dem Terminal ausgegeben. Die neuen Schnittstellenbeschreibungen werden konfiguriert und eine Meldung zeigt an, ob die Konfiguration des Geräts erfolgreich aktualisiert wurde oder nicht. Das folgende Beispiel zeigt eine Beispielausgabe des Skripts:

user@h0$ ./lldp_interface_descriptions_pyez.py r0 r1 r2 r3 r4 r5
Device Username: user
Device Password: 
Connecting to r0...
Getting LLDP information from r0...
Getting interface descriptions from r0...
    ge-0/0/4 LLDP Change. Was: r7 ge-0/0/6 Now: r1 ge-0/0/0
    ge-0/0/3 LLDP Up. Now: r5 ge-0/0/0
    ge-0/0/2 LLDP Up. Now: r3 ge-0/0/2
    ge-0/0/0 LLDP Up. Now: r4 ge-0/0/2
    ge-0/0/5 LLDP Down. Was: r6 ge-0/0/8
    Successfully committed configuration changes on r0.
    Closing connection to r0.
Connecting to r1...
Getting LLDP information from r1...
Getting interface descriptions from r1...
    ge-0/0/2 LLDP Down. Was: r2 ge-0/0/0
    Successfully committed configuration changes on r1.
    Closing connection to r1.
Connecting to r2...
Getting LLDP information from r2...
Getting interface descriptions from r2...
    ge-0/0/2 LLDP Down. Was: r0 ge-0/0/1
    ge-0/0/1 LLDP Down. Was: r3 ge-0/0/0
    ge-0/0/0 LLDP Down. Was: r1 ge-0/0/2
    Successfully committed configuration changes on r2.
    Closing connection to r2.
Connecting to r3...
Getting LLDP information from r3...
Getting interface descriptions from r3...
    ge-0/0/0 LLDP Down. Was: r2 ge-0/0/1
    Successfully committed configuration changes on r3.
    Closing connection to r3.
Connecting to r4...
Getting LLDP information from r4...
Getting interface descriptions from r4...
    No LLDP changes to configure on r4.
    Closing connection to r4.
Connecting to r5...
Getting LLDP information from r5...
Getting interface descriptions from r5...
    ge-0/0/2 LLDP Up. Now: r4 ge-0/0/0
    ge-0/0/0 LLDP Up. Now: r0 ge-0/0/3
    Successfully committed configuration changes on r5.
    Closing connection to r5.
user@h0$

Während das Skript jedes in der Befehlszeile angegebene Gerät in einer Schleife durchläuft, führt es die folgenden Schritte aus:

  1. Sammeln Sie LLDP-Nachbarschaftsinformationen.

  2. Sammle Schnittstellenbeschreibungen; analysiere die LLDP-Nachbarschaftsinformationen, die zuvor in den Schnittstellenbeschreibungen gespeichert wurden.

  3. Aktuelle und frühere LLDP-Nachbarschaftsinformationen vergleichen; LLDP-Up-, Change- und Down-Meldungen drucken; neue Schnittstellenbeschreibungen berechnen.

  4. Erstelle, lade und übertrage eine Kandidatenkonfiguration mit aktualisierten Schnittstellenbeschreibungen.

Hinweis

Wie das RESTful-API-Beispielskript versucht auch dieses Beispiel, eine nützliche und realistische Funktion bereitzustellen und sich gleichzeitig auf Code zu konzentrieren, der PyEZ demonstriert. Es gelten die gleichen Vorbehalte. Das Beispielskript wird vom Benutzer über die Befehlszeile aufgerufen, aber ein komplexeres Programm könnte durch ein Ereignis oder nach einem Zeitplan aufgerufen werden. Außerdem könnte die Ausgabe in ein bestehendes Netzwerküberwachungs- oder Alarmierungssystem integriert werden, anstatt einfach auf dem Terminal ausgedruckt zu werden. Eine realistischere Implementierung würde die vom Gerät gesammelten LLDP-Informationen in einer geräteexternen Datenbank oder in geräteinternen apply-macro Konfigurationsanweisungen speichern und nicht in den Konfigurationsanweisungen der Schnittstelle description. Schließlich könnte jedes Gerät parallel abgefragt und konfiguriert werden, um die Ausführung des Programms zu beschleunigen.

Analysieren wir nun den Python-Code, mit dem jeder dieser Schritte ausgeführt wird. Auch hier empfehlen wir dir, jede Zeile der Programmauflistung in deine eigene Skriptdatei mit dem Namen lldp_interface_descriptions_pyez.py einzutippen. Nach Abschluss von "Putting It All Together" hast du ein funktionierendes Beispiel, das du in deinem eigenen Netzwerk ausführen kannst.

Die Präambel

Der erste Schritt in unserem Beispielskript besteht darin, die benötigten Bibliotheken zu importieren und eine einmalige Initialisierung durchzuführen. Die Callouts enthalten weitere Informationen zu jeder Zeile des Programmlistings:

#!/usr/bin/env python 1
"""Use interface descriptions to track the topology reported by LLDP.

This includes the following steps:
1) Gather LLDP neighbor information.
2) Gather interface descriptions.
3) Parse LLDP neighbor information previously stored in the descriptions.
4) Compare LLDP neighbor info to previous LLDP info from the descriptions.
5) Print LLDP Up / Change / Down events.
6) Store the updated LLDP neighbor info in the interface descriptions.

Interface descriptions are in the format:
[user-configured description ]LLDP: <remote system> <remote port>[(DOWN)]

The '(DOWN)' string indicates an LLDP neighbor which was previously
present, but is now not present.
"""

import sys 2
import getpass

import jxmlease 3

from jnpr.junos import Device 4
from jnpr.junos.utils.config import Config
import jnpr.junos.exception

TEMPLATE_PATH = 'interface_descriptions_template.xml' 5

# Create a jxmlease parser with desired defaults.
parser = jxmlease.EtreeParser() 6

class DoneWithDevice(Exception): pass 7
1

Die Zeile #! (manchmal auch Hashbang- oder Shebang-Zeile genannt) bietet die Möglichkeit, das Skript auszuführen, ohne den Befehl python anzugeben. (Dieser Mechanismus funktioniert auf Unix-ähnlichen Plattformen und auf Windows-Plattformen mit dem Python Launcher für Windows). Mit anderen Worten: Du könntest das Skript ausführen mit path/lldp_interface_descriptions_pyez.py r0 r1 r2 r3 r4 r5 anstelle von pythonpath/lldp_interface_descriptions_pyez.py r0 r1 r2 r3 r4 r5. Um das Skript direkt auszuführen, müssen die Ausführungsrechte für die Datei lldp_interface_descriptions_pyez.py gesetzt werden. Wenn du den Shell-Befehl /usr/bin/env zum Aufrufen von Python verwendest, ist das Skript nicht vom Speicherort des Befehls pythonabhängig.

2

Zwei Standard-Python-Module, sys und getpass, werden importiert. Das Modul sys ermöglicht den Zugriff auf Objekte, die vom Python-Interpreter verwaltet werden, und das Modul getpass ermöglicht dem Skript die interaktive Eingabeaufforderung des erforderlichen Passworts, ohne die Eingabe als Echo in das Terminal des Benutzers zu übertragen.

3

Die Bibliothek jxmlease parst XML in native Python-Datenstrukturen. Du musst sicherstellen, dass dieses Modul auf deinem System installiert ist.

4

Drei PyEZ-Module werden importiert. Die Klasse PyEZ Device wird verwendet, um eine Geräteinstanz zu erstellen. Dies ist die grundlegende PyEZ-Klasse für die Interaktion mit einem Junos-Gerät. Die Klasse PyEZ Config bietet Methoden für den Umgang mit der Junos-Gerätekonfiguration. Das Modul exception definiert mehrere PyEZ-spezifische Ausnahmen, die auf ein mögliches Problem hinweisen können.

5

Die Variable TEMPLATE_PATH wird als Konstante verwendet, um den Namen der Jinja2-Konfigurationsvorlage zu identifizieren, mit der die Konfiguration neuer Schnittstellenbeschreibungen erstellt wird.

6

Eine jxmlease Parser-Instanz wird erstellt, um lxml.etree.Element Objekte in Python-Datenstrukturen zu parsen. Die Methode jxmlease.EtreeParser() erzeugt eine Instanz der Klasse jxmlease.EtreeParser mit einer Reihe von Standardparametern. Während ein Parser, der mit der Methode jxmlease.Parser() erstellt wird, ein XML-Eingabedokument als String erwartet, erwartet eine Instanz der Klasse jxmlease.EtreeParser ein XML-Eingabedokument als lxml.etree.Element Objekt.

7

Es wird eine eigene Klasse DoneWithDeviceerstellt, die anzeigt, wann die Verarbeitung auf jedem Gerät in der Hauptgeräteschleife abgeschlossen ist. Diese neue Klasse ist eine Unterklasse von Exception.

Jeder der wichtigsten Schritte des Skripts wurde in eine Python-Funktion gekapselt, die von der Funktion main() der Datei lldp_interface_descriptions_pyez.pyausgeführt wird.

Schleife durch jedes Gerät

Beginnen wir mit , indem wir die Funktion main()analysieren, die eine Eingabeaufforderung für einen Benutzernamen und ein Passwort enthält und dann eine Schleife über jedes in der Befehlszeile angegebene Gerät zieht. Die Funktion main() ruft mehrere Funktionen auf, die in den folgenden Abschnitten analysiert werden:

def main():
    """The main loop.

    Prompt for a username and password.
    Loop over each device specified on the command line.
    Perform the following steps on each device:
    1) Get LLDP information from the current device state.
    2) Get interface descriptions from the device configuration.
    3) Compare the LLDP information against the previous snapshot of LLDP
       information stored in the interface descriptions. Print changes.
    4) Build a configuration snippet with new interface descriptions.
    5) Commit the configuration changes.

    Return an integer suitable for passing to sys.exit().
    """

    if len(sys.argv) == 1: 1
        print("\nUsage: %s device1 [device2 [...]]\n\n" % sys.argv[0])
        return 1

    rc = 0 2

    # Get username and password as user input.
    user = raw_input('Device Username: ')
    password = getpass.getpass('Device Password: ') 3

    for hostname in sys.argv[1:]: 4
        try:
            print("Connecting to %s..." % hostname)
            dev = Device(host=hostname, 5
                         user=user,
                         password=password,
                         normalize=True)
            dev.open() 6

            print("Getting LLDP information from %s..." % hostname)
            lldp_info = get_lldp_neighbors(device=dev) 7
            if lldp_info == None: 8
                print("    Error retrieving LLDP info on " + hostname +
                      ". Make sure LLDP is enabled.")
                rc = 1
                raise DoneWithDevice

            print("Getting interface descriptions from %s..." % hostname)
            desc_info = get_description_info_for_interfaces(device=dev) 9
            if desc_info == None:
                print("    Error retrieving interface descriptions on %s." %
                      hostname)
                rc = 1
                raise DoneWithDevice

            desc_changes = check_lldp_changes(lldp_info, desc_info) 10
            if not desc_changes:
                print("    No LLDP changes to configure on %s." % hostname)
                raise DoneWithDevice

            if load_merge_template_config( 11
                device=dev,
                template_path=TEMPLATE_PATH,
                template_vars={'descriptions': desc_changes}):
                print("    Successfully committed configuration changes on %s." %
                      hostname)
            else:
                print("    Error committing description changes on %s." %
                      hostname)
                rc = 1
                raise DoneWithDevice
        except jnpr.junos.exception.ConnectError as err: 12
            print("    Error connecting: " + repr(err))
            rc = 1
        except DoneWithDevice:
            pass
        finally: 13
            print("    Closing connection to %s." % hostname)
            try:
                dev.close()
            except:
                pass
    return rc
1

Das Skript erfordert, dass mindestens ein Gerät als Kommandozeilenargument angegeben wird. Die Liste sys.argv enthält den Namen des Skripts bei Index 0. Benutzerdefinierte Argumente beginnen bei Index 1. Wenn keine benutzerdefinierten Argumente vorhanden sind (len(sys.argv) == 1), wird die Benutzungsmeldung ausgegeben und das Skript wird mit dem Statuscode 1 beendet, um einen Fehler anzuzeigen.

2

Die Variable rc enthält den Statuscode, der am Ende des Skripts zurückgegeben wird. Der Wert wird auf 0 gesetzt, was einen Erfolg anzeigt. Wenn später ein Fehler auftritt, wird rc auf 1 gesetzt und die Verarbeitung wird mit dem nächsten in der Befehlszeile angegebenen Gerät fortgesetzt.

3

Diese Anweisung und die vorherige Anweisung fordern den Benutzer zur Eingabe des Benutzernamens und des Kennworts auf, die zum Aufbau der NETCONF-Sitzung mit den Junos-Geräten verwendet werden. Es wird davon ausgegangen, dass jedes Gerät mit denselben Benutzerauthentifizierungsdaten konfiguriert ist. Die Funktion getpass() aus der Python-Standardbibliothek fordert den Benutzer zur Eingabe eines Passworts auf und deaktiviert das Echo der Antwort auf dem Bildschirm.

4

Diese for Schleife durchläuft jeden Gerätenamen (oder jede IP-Adresse), der/die in den Befehlszeilenargumenten angegeben wurde. Da sys.argv[0] der Name des Skripts ist, wird das Slice sys.argv[1:] verwendet, um die Liste der Geräte zurückzugeben.

5

Es wird eine Geräteinstanz erstellt und der Variablen dev zugewiesen. Der vom Benutzer eingegebene Benutzername und das Passwort werden als Argumente user und password übergeben. Das Argument normalize=True stellt sicher, dass die Antwortnormalisierung auf jeden RPC on Demand-Aufruf angewendet wird, der die Geräteinstanz dev verwendet.

6

Die Methode open() wird auf der dev Geräteinstanz aufgerufen. Dadurch wird eine NETCONF-Sitzung mit dem Gerät aufgebaut und die Standard-Faktenerfassung von PyEZ aufgerufen. Wenn der Aufruf von open() fehlschlägt, wird eine jnpr.junos.exception.ConnectError (oder eine ihrer Unterklassen) ausgelöst. Diese mögliche Ausnahme wird später in der Funktion main()behandelt.

7

Die Funktion get_lldp_neighbors()wird für die aktuelle Geräteinstanz dev aufgerufen. Das Ergebnis wird im lldp_info Wörterbuch gespeichert.

8

Wenn get_lldp_neighbors()einen Fehler meldet (indem es den Wert None zurückgibt), wird eine Fehlermeldung gedruckt, rc wird auf 1 gesetzt, um den Fehler anzuzeigen, und die Ausnahme DoneWithDevice wird ausgelöst. Diese Ausnahme bewirkt, dass die Ausführung zur Zeile except DoneWithDevice am Ende der Funktion main() springt.

9

Die Funktion get_description_info_for_interfaces() wird auf dem aktuellen Gerät ausgeführt, dev. Das Ergebnis wird im desc_info Wörterbuch gespeichert. Ähnlich wie bei der Funktion get_lldp_neighbors() wird, wenn die get_description_info_for_interfaces() Funktion einen Fehler meldet (indem sie den Wert None zurückgibt), wird eine Fehlermeldung gedruckt, rc wird auf 1 gesetzt, um den Fehler anzuzeigen, und die DoneWithDevice Ausnahme ausgelöst. Diese Ausnahme bewirkt, dass die Ausführung zur Zeile except DoneWithDevice am Ende der Funktion main() springt.

10

Die Funktion get_lldp_description_changes() parst die Wörterbücher lldp_info und desc_info und gibt die neuen Schnittstellenbeschreibungen im Wörterbuch desc_changes zurück. Wenn sich keine Beschreibungen geändert haben, wird die DoneWithDevice Ausnahme ausgelöst. In diesem Fall weist die Ausnahme nicht auf einen Fehler hin. Sie überspringt einfach den Abschnitt des Codes, der die Konfigurationsänderung lädt und festschreibt, wenn es keine neue Konfiguration gibt, die angewendet werden muss.

11

Die Funktion load_merge_template_config() wird aufgerufen. Das Argument template_pathwird auf den Wert von TEMPLATE_PATH gesetzt und der Schlüssel descriptions des Arguments template_vars wird auf das Wörterbuch desc_changes gesetzt. Der Rückgabewert der Funktion zeigt an, ob die neue Konfiguration erfolgreich übertragen wurde. Tritt ein Fehler auf, wird eine Fehlermeldung ausgegeben, rc wird auf 1 gesetzt, um den Fehler anzuzeigen, und DoneWithDevice wird aufgerufen.

12

Eine Ausnahme von jnpr.junos.exception.ConnectError (oder einer Unterklasse) bedeutet, dass die NETCONF-Sitzung zum Gerät nicht geöffnet wurde. In diesem Fall wird der Fehler gedruckt, rc wird auf 1 gesetzt, um den Fehler anzuzeigen, und das Skript fährt mit dem finallyBlock fort.

13

Der Aufruf der Methode dev.close()in einem finally Block stellt sicher, dass die NETCONF-Sitzung ordnungsgemäß beendet wird, unabhängig davon, ob beim Sammeln der Informationen oder beim Übertragen der neuen Konfiguration eine Ausnahme aufgetreten ist. Da dev.close() selbst eine Ausnahme auslösen könnte, wird die Anweisung in einem try Block platziert. Der entsprechende except Block ignoriert einfach alle Ausnahmen, die von dev.close() ausgelöst wurden.

Schauen wir uns nun jede der Funktionen, die von der for Schleife aufgerufen werden, genauer an .

Sammeln von LLDP-Nachbarninformationen

Die erste Funktion, get_lldp_neighbors(), nutzt die PyEZ RPC on Demand Funktion, um die get-lldp-neighbors-information RPC abzufragen. Die RPC-Antwort ist das Standardformat der lxml Element Objekte. Die Funktion sammelt die System- und Portinformationen der LLDP-Nachbarn für jede lokale Schnittstelle mithilfe von lxml-Methoden und gibt die Informationen in einem Wörterbuch an den Aufrufer zurück:

def get_lldp_neighbors(device): 1
    """Get current LLDP neighbor information.

    Return a two-level dictionary with the LLDP neighbor information.
    The first-level key is the local port (aka interface) name.
    The second-level keys are 'system' for the remote system name
    and 'port' for the remote port ID. On error, return None.

    For example:
    {'ge-0/0/1': {'system': 'r1', 'port', 'ge-0/0/10'}}
    """

    lldp_info = {}
    try: 2
        resp = device.rpc.get_lldp_neighbors_information()
    except (jnpr.junos.exception.RpcError,
            jnpr.junos.exception.ConnectError)as err:
        print "    " + repr(err)
        return None

    for nbr in resp.findall('lldp-neighbor-information'): 3
        local_port = nbr.findtext('lldp-local-port-id') 4
        remote_system = nbr.findtext('lldp-remote-system-name')
        remote_port = nbr.findtext('lldp-remote-port-id')
        if local_port and (remote_system or remote_port):
            lldp_info[local_port] = {'system': remote_system, 5
                                     'port': remote_port}

    return lldp_info
1

Die Funktion get_lldp_neighbors()benötigt ein Argument device. Dieses Argument ist eine PyEZ-Geräteinstanz, die eine aktive NETCONF-Sitzung geöffnet hat.

2

Der RPC on Demand-Aufruf und die Verarbeitung der RPC-Antwort sind von einem try Block umgeben, um einen unerwarteten Abbruch des Skripts zu vermeiden. Wenn eine Ausnahme ausgelöst wird, wird eine Fehlermeldung ausgegeben und der Wert None an den Aufrufer zurückgegeben, um den Fehler anzuzeigen. Die PyEZ RPC on Demand Funktion ruft die XML RPC get-lldp-neighbors-information auf.

3

Die Methode lxml findall() gibt eine Liste aller lxml.etree.Element Objekte zurück, die dem XPath-Ausdruck lldp-neighbor-information entsprechen. Dies führt zu einer Schleife durch jeden LLDP-Nachbarn. Der Variable nbr wird das lxml.etree.Element Objekt zugewiesen, das den aktuellen LLDP-Nachbarn repräsentiert.

4

Die Methode lxml findtext() wählt das erste XML-Element aus, das einem XPath-Ausdruck entspricht. Sie wird verwendet, um die Werte der Variablen local_port, remote_system und remote_port auszuwählen.

5

Die Werte remote_system und remote_port werden im Wörterbuch lldp_info gespeichert. Dieses Wörterbuch ist mit dem local_port Wert verschlüsselt, der aus dem aktuellen LLDP Nachbarn extrahiert wurde.

Sammeln und Analysieren von Schnittstellenbeschreibungen

Die nächste Funktion, get_description_info_for_interfaces(), macht aus eine weitere PyEZ RPC on Demand Abfrage mit der get-interface-information RPC. Der Parameter descriptions wird der RPC hinzugefügt, um nur die Schnittstellenbeschreibungen abzufragen. Diesmal wird die Ausgabe von der jxmlease-Bibliothek geparst, um ein jxmlease.XMLDictNode Objekt zu erzeugen. Der Inhalt des Schnittstellenbeschreibungsfeldes wird dann aus dieser Antwort nach einer einfachen Konvention geparst, die für dieses Beispielskript gewählt wurde. Siehe "Sammeln und Parsen von Schnittstellenbeschreibungen", wenn du eine Auffrischung der verwendeten Konvention benötigst. Die Funktion ist wie folgt definiert:

def get_description_info_for_interfaces(device): 1
    """Get current interface description for each interface.

    Parse the description into the user-configured description, remote
    system, and remote port components.

    Return a two-level dictionary. The first-level key is the
    local port (aka interface) name. The second-level keys are
    'user_desc' for the user-configured description, 'system' for the
    remote system name, 'port' for the remote port, and 'down', which is
    a Boolean indicating if LLDP was previously down. On error, return None.

    For example:
    {'ge-0/0/1': {'user_desc': 'test description', 'system': 'r1',
                  'port': 'ge-0/0/10', 'down': True}}
    """

    desc_info = {}
    try: 2
        resp = parser(device.rpc.get_interface_information(descriptions=True))
    except (jnpr.junos.exception.RpcError,
            jnpr.junos.exception.ConnectError) as err:
        print "    " + repr(err)
        return None

    try:
        pi = resp['interface-information']['physical-interface'].jdict() 3
    except KeyError:
        return desc_info

    for (local_port, port_info) in pi.items(): 4
        try:
            (udesc, _, ldesc) = port_info['description'].partition('LLDP: ') 5
            udesc = udesc.rstrip()
            (remote_system, _, remote_port) = ldesc.partition(' ')
            (remote_port, down_string, _) = remote_port.partition('(DOWN)')
            desc_info[local_port] = {'user_desc': udesc,
                                     'system': remote_system,
                                     'port': remote_port,
                                     'down': True if down_string else False}
        except (KeyError, TypeError): 6
            pass
    return desc_info
1

get_description_info_for_interfaces() erfordert ein device Argument. Dieses Argument ist eine PyEZ-Geräteinstanz, die eine aktive NETCONF-Sitzung geöffnet hat.

2

Auch hier ist der RPC on Demand-Aufruf von einem try Block umgeben, um Ausnahmebedingungen zu behandeln. Die RPC on Demand-Funktion wird verwendet, um die XML-RPC get-interface-information auszuführen. Das Argument descriptions wird an die RPC übergeben und gibt an, dass nur Schnittstellenbeschreibungen zurückgegeben werden sollen. Das Objekt lxml.etree.Elementwird dann an die Instanz jxmlease.EtreeParser(), parser(), übergeben.

3

Der Variablen pi wird ein Wörterbuch zugewiesen, das auf den physischen Schnittstelleninformationen in der RPC-Antwort basiert. Die Methode jdict() (siehe "jxmlease objects") wird verwendet, um ein Wörterbuch aus den physischen Schnittstelleninformationen zu erstellen. Die Methode jdict() erstellt automatisch ein Wörterbuch, das auf den Werten der <name> Elemente in jedem <pyhsical-interface> Element der Antwort basiert. Eine KeyError Ausnahme zeigt an, dass entweder das <interface-information> oder das <physical-interface>Element in der Antwort fehlt. Dies bedeutet einfach, dass derzeit keine Schnittstellenbeschreibungen konfiguriert sind.

4

Die Schleife for durchläuft local_port, den Schlüssel des Wörterbuchs pi, und port_info, den Wert des Wörterbuchs pi.

5

In den nächsten Zeilen wird die vorhandene Schnittstellenbeschreibung in Komponenten zerlegt. Die Beschreibungen werden im Wörterbuch desc_info gespeichert, dessen Schlüssel der Wert local_port ist. Siehe "Schnittstellenbeschreibungen sammeln und analysieren" im RESTful API-Beispielskript, wenn dieser Code unklar ist.

6

Beim Zugriff auf die Daten wird eine KeyError Ausnahme ausgelöst, wenn der angegebene Schlüssel nicht existiert. Eine TypeError Ausnahme wird ausgelöst, wenn der Typ, auf den zugegriffen wird, nicht mit der Datenstruktur übereinstimmt. Wenn eine dieser Bedingungen eintritt, wird die Ausnahme ignoriert. Die Verarbeitung wird mit dem nächsten local_port in dem piWörterbuch fortgesetzt.

Aktuelle und vorherige LLDP-Nachbarninformationen vergleichen

Die Funktion check_lldp_changes()vergleicht die früheren LLDP-Informationen, die in den Beschreibungsfeldern gefunden wurden und jetzt im Wörterbuch desc_info gespeichert sind, mit den LLDP-Informationen, die jetzt im Wörterbuch lldp_infogespeichert sind. Da diese Funktion nur mit den Wörterbüchern desc_info und lldp_info arbeitet, die von den vorherigen Funktionen zurückgegeben wurden, ist ihr Inhalt genau derselbe wie bei der RESTful API-Version. Die Funktion ist hier enthalten, ohne die Erklärungen der einzelnen Zeilen zu wiederholen. Wenn du dir über den Zweck oder das Verhalten einer Zeile im Unklaren bist, lies die Erklärungen unter "Aktuelle und vorherige LLDP-Nachbarninformationen vergleichen". Die Funktionsdefinition lautet wie folgt:

def check_lldp_changes(lldp_info, desc_info):
    """Compare current LLDP info with previous snapshot from descriptions.

    Given the dictionaries produced by get_lldp_neighbors() and
    get_description_info_for_interfaces(), print LLDP up, change,
    and down messages.

    Return a dictionary containing information for the new descriptions
    to configure.
    """

    desc_changes = {}

    # Iterate through the current LLDP neighbor state. Compare this
    # to the saved state as retrieved from the interface descriptions.
    for local_port in lldp_info:
        lldp_system = lldp_info[local_port]['system']
        lldp_port = lldp_info[local_port]['port']
        has_lldp_desc = desc_info.has_key(local_port)
        if has_lldp_desc:
            desc_system = desc_info[local_port]['system']
            desc_port = desc_info[local_port]['port']
            down = desc_info[local_port]['down']
            if not desc_system or not desc_port:
                has_lldp_desc = False
        if not has_lldp_desc:
            print("    %s LLDP Up. Now: %s %s" %
                  (local_port,lldp_system,lldp_port))
        elif down:
            print("    %s LLDP Up. Was: %s %s Now: %s %s" %
                  (local_port,desc_system,desc_port,lldp_system,lldp_port))
        elif lldp_system != desc_system or lldp_port != desc_port:
            print("    %s LLDP Change. Was: %s %s Now: %s %s" %
                  (local_port,desc_system,desc_port,lldp_system,lldp_port))
        else:
            # No change. LLDP was not down. Same system and port.
            continue
        desc_changes[local_port] = "LLDP: %s %s" % (lldp_system,lldp_port)

    # Iterate through the saved state as retrieved from the interface
    # descriptions. Look for any neighbors that are present in the
    # saved state, but are not present in the current LLDP neighbor
    # state.
    for local_port in desc_info:
        desc_system = desc_info[local_port]['system']
        desc_port = desc_info[local_port]['port']
        down = desc_info[local_port]['down']
        if (desc_system and desc_port and not down and
            not lldp_info.has_key(local_port)):
            print("    %s LLDP Down. Was: %s %s" %
                  (local_port,desc_system,desc_port))
            desc_changes[local_port] = "LLDP: %s %s(DOWN)" % (desc_system,
                                                              desc_port)

    # Iterate through the list of interface descriptions we are going
    # to change. Prepend the user description, if any.
    for local_port in desc_changes:
        try:
            udesc = desc_info[local_port]['user_desc']
        except KeyError:
            continue
        if udesc:
            desc_changes[local_port] = udesc + " " + desc_changes[local_port]

    return desc_changes

Erstellen, Anwenden und Bestätigen der Kandidatenkonfiguration

Die Funktion load_merge_template_config() nimmt eine Geräteinstanz, eine Jinja2-Vorlage und Vorlagenvariablen als Argumente an. Sie nutzt RPC on Demand sowie die PyEZ-Konfigurationsmethoden load() und commit(), um das Äquivalent zu den CLI-Befehlen configure private, load merge und commit auszuführen. Sie überprüft die Ergebnisse auch auf mögliche Fehler:

def load_merge_template_config(device,
                               template_path,
                               template_vars): 1
    """Load templated config with "configure private" and "load merge".

    Given a template_path and template_vars, do:
        configure private,
        load merge of the templated config,
        commit,
        and check the results.

    Return True if the config was committed successfully, False otherwise.
    """

    class LoadNotOKError(Exception): pass 2

    device.bind(cu=Config) 3

    rc = False

    try:
        try:
            resp = device.rpc.open_configuration(private=True) 4
        except jnpr.junos.exception.RpcError as err: 5
            if not (err.rpc_error['severity'] == 'warning' and
                    'uncommitted changes will be discarded on exit' in
                    err.rpc_error['message']):
                raise

        resp = device.cu.load(template_path=template_path,
                              template_vars=template_vars,
                              merge=True) 6
        if resp.find("ok") is None: 7
            raise LoadNotOKError
        device.cu.commit(comment="made by %s" % sys.argv[0]) 8
    except (jnpr.junos.exception.RpcError, 9
            jnpr.junos.exception.ConnectError,
            LoadNotOKError) as err:
        print "    " + repr(err)
    except:
        print "    Unknown error occurred loading or committing configuration."
    else: 10
        rc = True
    try: 11
        device.rpc.close_configuration()
    except jnpr.junos.exception.RpcError as err:
        print "    " + repr(err)
        rc = False
    return rc
1

Die Argumente device, template_path und template_vars sind notwendige Parameter für die Funktion load_merge_template_config().

2

Es wird eine neue Unterklasse Exception definiert. Diese Klasse wird ausgelöst und behandelt, wenn es ein Problem beim Laden der neuen Konfiguration gibt.

3

Eine neue PyEZ-Konfigurationsinstanz wird erstellt und an das Attribut cu des Geräts gebunden.

4

Die XML-RPC zum Öffnen der Konfiguration wird von der RPC on Demand-Methode aufgerufen. Das Argument private macht diese RPC äquivalent zum CLI-Befehl configure private.

5

jnpr.junos.exception.RpcError Ausnahmen werden von diesem except Block abgefangen und behandelt. Es ist normal, dass der open-configuration XML RPC eine uncommitted changes will be discarded on exit Warnung zurückgibt, wenn das Argument private angegeben wird. Diese erwartete Warnung wird ignoriert. Alle anderen jnpr.junos.exception.RpcError Ausnahmen werden von dem umschließenden try/except Block abgefangen und behandelt.

6

Diese Anweisung übergibt die Argumente template_path und template_vars an die Methode load()der Konfigurationsinstanz. Die Konfiguration wird aus der Vorlage und den vom Benutzer eingegebenen Werten erstellt. Das Argument merge=True bewirkt, dass die neue Konfiguration mit der bestehenden Kandidatenkonfiguration zusammengeführt wird.

7

Ein <ok> XML-Element in der XML-Antwort der Methode load() zeigt an, dass die neue Konfiguration erfolgreich geladen wurde. Wenn das XML-Element <ok> in der Antwort nicht gefunden wird, wird eine LoadNotOKError Ausnahme ( ) ausgelöst, die weiter oben in dieser Funktion definiert wurde. In den meisten Fällen wird ein Fehler beim Laden der neuen Kandidatenkonfiguration eine Unterklasse der PyEZ-Ausnahme RpcError auslösen. Dieser Block behandelt lediglich die mögliche Situation, dass die Methode load() keine Ausnahme auslöst und eine unerwartete RPC-Antwort zurückgibt.

8

Die Methode commit() der Konfiguration wird verwendet, um die neue Kandidatenkonfiguration zu übertragen. Ein Kommentar, der den Namen des Skripts enthält, wird dem Commit mit dem Argument comment hinzugefügt.

9

jnpr.junos.exception.RpcError, jnpr.junos.exception.ConnectError und LoadNotOKError Ausnahmen und Unterklassen werden von diesem except Block abgefangen und behandelt. Der Inhalt dieser Ausnahmen wird einfach ausgedruckt. Für alle anderen Ausnahmen, die während des try Blocks aufgetreten sind, wird eine andere Meldung ausgegeben.

10

Dieser else Block wird nur ausgeführt, wenn keine Ausnahme ausgelöst wurde. Das bedeutet, dass die Konfiguration erfolgreich geladen und übertragen wurde. In diesem Fall wird rc der Wert von True zugewiesen, um den Erfolg anzuzeigen. (Zuvor wurde rc auf False initialisiert).

11

Unabhängig davon, ob eine vorherige Ausnahme ausgelöst wurde, wird dieser try Block ausgeführt und die private Kandidatenkonfiguration wird durch den close-configuration RPC geschlossen. Wenn ein Fehler beim Schließen der Konfiguration auftritt, wird die Ausnahme gedruckt und rc wird False zugewiesen, um den Fehler anzuzeigen.

Die Funktion load_merge_template_config() ist so geschrieben, dass sie unabhängig von der angewandten Konfiguration ist. Wenn du die Geräteinstanz, die Jinja2-Vorlagendatei und die Vorlagenvariablen übergibst, kannst du damit jede beliebige Konfiguration auf das Gerät anwenden. Die eigentliche Arbeit zur Erstellung der Konfiguration wird in der Jinja2-Vorlage erledigt. In diesem Beispiel befindet sich die Vorlage in der Datei interface_descriptions_template.xml im aktuellen Arbeitsverzeichnis. Wie die Dateinamenerweiterung .xml schon sagt, erzeugt diese Vorlage eine Konfiguration im XML-Format. Die Callouts erläutern jede Zeile der Vorlage:

<configuration>
    <interfaces> 1
    {% for key, value in descriptions.iteritems() %} 2
        <interface> 3
            <name>{{ key }}</name> 4
            <description>{{ value }}</description> 5
        </interface>
    {% endfor %} 6
    </interfaces>
</configuration>
1

Diese Vorlage erstellt eine Konfiguration in XML-Syntax. Es müssen nur die konfigurationsbezogenen XML-Tags eingefügt werden. Das XML-Tag <interfaces> entspricht der Ebene [edit interfaces] in der Junos-Konfigurationshierarchie.

2

Diese Zeile ist eine Jinja2 forSchleife, die über die Einträge im descriptions Wörterbuch iteriert. Der Schlüssel des descriptions Wörterbuchs ist der Name der Schnittstelle und der Wert ist die neu zu konfigurierende Beschreibung.

3

Der erste XML-Tag für jede Schnittstelle. Dieses Tag wird für jede Schnittstelle im descriptions Wörterbuch wiederholt.

4

Der Name der Schnittstelle. Der Ausdruck {{ key }} wird für jeden Schlüssel im Wörterbuch descriptions ausgewertet.

5

Die Beschreibung der Schnittstellen. Der Ausdruck {{ value }} wird für jeden Wert im Wörterbuch descriptionsausgewertet.

6

Das Tag {% endfor %} kennzeichnet das Ende der forSchleife, die über jede Schnittstelle im Wörterbuch descriptionsiteriert.

Alles zusammenfügen

Der letzte Codeblock im Beispielskript ruft die Funktion main() auf, wenn das Skript ausgeführt wird. Nach Eingabe dieses Codeblocks ist das Beispielskript funktionsfähig:

if __name__ == "__main__":
  sys.exit(main())

Jetzt ist das Skript fertig und kann auf einer Reihe von Junos-Geräten getestet werden, auf denen der NETCONF-over-SSH-Dienst und das LLDP-Protokoll laufen. Wenn du beim Ausführen des Skripts auf Fehler stößt, überprüfe deinen Code und vergleiche ihn sorgfältig mit dem Beispiel. Die vollständige Arbeitsdatei lldp_interface_descriptions_pyez.py ist auch unter auf GitHub verfügbar.

Einschränkungen

Wie alle Automatisierungstools, die in diesem Buch besprochen werden, erfüllt PyEZ eine sehr nützliche Rolle bei der Automatisierung von Junos-Netzwerken, aber es ist nicht die Lösung für jedes Netzwerkautomatisierungsproblem. Die erste und offensichtlichste Einschränkung ist, dass PyEZ Python-spezifisch ist. Wenn du Python fließend beherrschst oder ein bestehendes, in Python geschriebenes System erweiterst, kannst du das als Vorteil sehen. Wenn dein Projekt jedoch eine andere Sprache erfordert, gibt es vielleicht andere Möglichkeiten. Im nächsten Abschnitt findest du weitere Informationen darüber, wie du mit NETCONF in der Sprache deiner Wahl auf Junos-Geräte zugreifen kannst.

Eine weitere (fast ebenso offensichtliche) Einschränkung ist, dass PyEZ NETCONF benötigt. Da alle derzeit unterstützten Junos-Geräte NETCONF unterstützen, stellt diese Anforderung keine große Einschränkung dar. Allerdings muss der NETCONF-over-SSH-Dienst oder der eigenständige SSH-Dienst konfiguriert und von deinem Automatisierungshost aus erreichbar sein. Wenn du NETCONF über SSH verwendest, stelle sicher, dass der TCP-Zielport 830 auf dem Netzwerkpfad zwischen deinem Automatisierungshost und dem Junos-Zielgerät nicht gefiltert wird.

NETCONF-Bibliotheken für andere Sprachen

In diesem Buch wird zwar nicht näher auf eingegangen, aber es gibt verschiedene Bibliotheken für verschiedene Sprachen, die das NETCONF-Protokoll implementieren und eine gewisse Abstraktionsebene bieten, damit du keine direkten NETCONF-RPCs senden musst. Die meisten dieser Bibliotheken unterstützen nicht die höheren Abstraktionen wie Tabellen und Ansichten, die Junos PyEZ unterstützt, aber sie können trotzdem sehr nützlich sein.

Tabelle 4-8 gibt einen aktuellen Überblick über einige dieser Bibliotheken. Weitere Informationen zu den einzelnen Bibliotheken findest du unter dem entsprechenden Link.

Tabelle 4-8. Verfügbare NETCONF-Bibliotheken
SpracheBeschreibungLink
RubyBeliebte Open Source NETCONF-Bibliothek für Ruby. Einfach zu installieren, begrenzte Abhängigkeiten und aktiver Support.http://rubygems.org/gems/netconf
JavaOffene quelloffene NETCONF-Bibliothek für Java. Bereits im Einsatz bei Unternehmenskunden. Einfache Installation und keine Abhängigkeiten.http://www.juniper.net/support/downloads/?p=netconf
PerlUnterstützt von Juniper Networks JTAC. Älteste der NETCONF-Bibliotheken. Die Installation kann schwierig sein, da mehrere Abhängigkeiten bestehen.http://www.juniper.net/support/downloads/?p=netconf#sw
PHPOpen source NETCONF-Bibliothek für PHP. Sie wird aktiv weiterentwickelt und ist möglicherweise noch nicht bereit für den produktiven Einsatz.https://github.com/Juniper/netconf-php
PythonHerstellerunabhängige Open Source NETCONF-Bibliothek für Python. Wird von PyEZ verwendet.https://github.com/leopoul/ncclient

Diese Bibliotheken können hilfreich sein, wenn dein Projekt direkte NETCONF-Unterstützung oder eine bestimmte Entwicklungssprache erfordert; für die Entwicklung neuer Junos-Automatisierungen wird jedoch PyEZ dringend empfohlen.

Kapitel Zusammenfassung

Zusammenfassend lässt sich sagen, dass Junos PyEZ ein ausgewogenes Verhältnis zwischen Einfachheit und Leistungsfähigkeit bietet. Diese Ausgewogenheit macht es zur offensichtlichen Wahl für Benutzer, die bereits mit Python vertraut sind. PyEZ bietet Faktensammlungs- und Dienstprogrammfunktionen, die viele gängige Aufgaben vereinfachen, und es vereinfacht die RPC-Ausführung, indem es eine Abstraktionsschicht bereitstellt, die auf dem NETCONF-Protokoll aufbaut.

Ein großer Teil der Leistungsfähigkeit von PyEZ liegt im Umgang mit den resultierenden RPC-Antworten. Die größte Leistung und Flexibilität erhältst du, wenn du XPath-Ausdrücke mit der lxml-Bibliothek verwendest. Für ein schnelles und einfaches Mapping zwischen XML und nativen Python Datenstrukturen, verwende die jxmlease Bibliothek, um die lxml Antwort zu parsen. Für ein wiederverwendbares Mapping zwischen komplexen XML-Elementen und einfachen Python-Datenstrukturen wählst du den Tabellen- und View-Mechanismus.

Zusätzlich zu den operativen RPCs bietet PyEZ Werkzeuge, um Konfigurationsänderungen vorzunehmen. Zu diesen Werkzeugen gehört eine leistungsstarke Funktion zur Erstellung von Templates, mit der große und komplexe Konfigurationen mit minimalem Aufwand erstellt werden können.

1 ncclient ist ein von der Gemeinschaft geführtes und von der Gemeinschaft unterstütztes Open-Source-Projekt, das auf GitHub gehostet wird.

2 Zusätzlich zu den Anforderungen an die Junos PyEZ-Software muss für die Installation aus dem GitHub-Repository auch git installiert sein.

3 Die String-Darstellung einer Instanz der Klasse jnpr.junos.Device ist einfach Device(host).

4 Es ist sicherer, den privaten Schlüssel mit einer Passphrase zu schützen. In der Tat sollte eine Passphrase als bewährte Methode für Produktionsnetzwerke angesehen werden.

5 Beachte, dass PyEZ XML-Namensräume aus der Antwort entfernt. Während die CLI-Ausgabe also XML-Elemente mit Attributen im Namensraum junos enthält, wie z.B. <up-time junos:seconds="102120">1 day, 4:22​</up-time>, lautet das Attribut im Antwortobjekt seconds und nicht junos:seconds.

6 Erinnere dich daran, dass PyEZ Namensräume aus den XML-Antworten entfernt. Das Attribut junos:seconds in der rohen XML-Antwort wird also in PyEZ einfach als seconds angegeben.

7 Auf dem Beispiel-Automatisierungsrechner lautet der Modulpfad /usr/local/lib/python2.7/dist-packages/jnpr/junos, aber der Speicherort ist installationsabhängig und kann auf deinem Rechner anders sein. Du kannst den korrekten Speicherort für deinen Rechner mithilfe der Anweisungen am Anfang von "Vorgefertigte Betriebstabellen und -ansichten" ermitteln .

8 Wenn das Argument template_varskomplett weggelassen wird, wie in diesem Beispiel, ist es standardmäßig {}, ein leeres Python-Wörterbuch.

Get Junos-Verwaltung automatisieren now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.