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 ncclient
1 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.git
2 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-dev
Paket). 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
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(host='
r0
',user='user
',password='user123
')
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.
Bevor die Klasse
jnpr.junos.Device
verwendet werden kann, muss sie zunächst importiert werden. Diese Zeile importiert das Python-Paketjnpr.junos
und kopiert den NamenDevice
in den lokalen Namensraum, so dass du einfach aufDevice()
verweisen kannst. Eine alternative Syntax istimport jnpr.junos
. Auch hier wird das Python-Paketjnpr.junos
importiert, aber der NameDevice
wird nicht in den lokalen Namensraum kopiert. Bei dieser Syntax musst du die Klasse als Attribut vonjnpr.junos
mit der Syntaxjnpr.junos.Device()
referenzieren.Durch den Aufruf des Klassenobjekts
Device
mit der SyntaxDevice()
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 Namenr0
zugewiesen. Der Namer0
ist nichts Besonderes und kann durch jeden anderen gültigen Python-Variablennamen ersetzt werden. Die Parameter des AufrufsDevice()
legen die Anfangswerte der Attribute der Instanz fest. In diesem Beispiel wurden die Parameterhost
,user
undpassword
auf die entsprechenden Werte für das Junos-Gerät mit dem Hostnamenr0
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.
Parameter | Beschreibung | Standardwert |
---|---|---|
host | Ein 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.) |
port | Der 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 |
user | Der 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 $USER fü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:>>> |
password | Das 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_facts | Ein 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_probe | Mit 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_probe ist 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_probe kann für alle Geräteinstanzen geändert werden, indem er Device.auto_probe = vor der Instanziierung der Geräteinstanzen gesetzt wird.) |
ssh_config | Der 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_file | Der 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). |
normalize | Ein 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 Device
Klasse 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
uid 2001; class super-user; authentication { ssh-dsa "ssh-dss AAAAB3Nzauser
...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 user
auch 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.
Um diese Ausnahmen abzufangen und angemessen zu behandeln, solltest du die Methode open()
in einen try
/except
Block 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 except
referenziert 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 facts
Dictionary-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.ConnectClosedError
Ausnahme 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 rpc
aufgerufen 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('
/usr/local/lib/python2.7/dist-packages/jnpr/junos/devi... warnings.warn("CLI command is for debug use only!", ...) >>>show system uptime'
)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 timeout
der Instanz des Geräts zu setzen. Das Setzen der Eigenschaft timeout
wirkt 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.
Exception | Beschreibung |
---|---|
jnpr.junos. exception. ConnectClosedError | Wird 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.RpcTimeoutError | Wird 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.PermissionError | Wird ausgelöst, wenn die Autorisierung, wie in "Authentifizierung und Autorisierung" beschrieben, die Ausführung des RPCs nicht zulässt. |
jnpr.junos.exception.RpcError | Wird 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.PermissionError
Ausnahme 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.ConnectClosedError
Ausnahme 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
from
time
import
sleep
MAX_ATTEMPTS
=
3
WAIT_BEFORE_RECONNECT
=
10
# Assumes r0 already exists and is a connected device instance
for
attempt
in
range
(
MAX_ATTEMPTS
)
:
try
:
routes
=
r0
.
rpc
.
get_route_information
(
)
except
jnpr
.
junos
.
exception
.
ConnectClosedError
:
sleep
(
WAIT_BEFORE_RECONNECT
)
try
:
r0
.
open
(
)
except
jnpr
.
junos
.
exception
.
ConnectError
:
pass
else
:
# Success. No exception was raised.
# break will skip the for loop's else.
break
else
:
# 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 ...
Der Import des PyEZ-Exception-Moduls ist erforderlich, bevor bestimmte PyEZ-Exceptions von einer
except
-Anweisung abgefangen werden können. Importiere die Funktionsleep()
aus dem Modultime
in den lokalen Namespace.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.Die
for
Schleife versucht, den RPC bis zuMAX_ATTEMPTS
Mal auszuführen.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 diesestry
Blocks aus. Das Ergebnis des RPCs wird in der Variablenroutes
gespeichert. Diese RPC kann ohne Ausnahme erfolgreich sein oder eine der in Tabelle 4-3 aufgeführten Ausnahmen auslösen.Wenn die Anweisung im
try
Block eine Ausnahme auslöst, wird geprüft, ob sie mit einerjnpr.junos.exception.ConnectClosedError
Ausnahme übereinstimmt. Wenn die Ausnahme passt, werden die Anweisungen in diesemexcept
Block ausgeführt. Wenn die Ausnahme nicht zu diesemexcept
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. Jederexcept
Block wird der Reihe nach getestet, bis eine Übereinstimmung gefunden wird oder die Liste der Ausnahmen erschöpft ist.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ürWAIT_BEFORE_RECONNECT
Sekunden. Danach wird die Ausführung mit der nächsten Zeile fortgesetzt. Die Methodeopen()
ist in einentry
Block eingeschlossen, um alle Ausnahmen abzufangen, die auftreten, wenn der Aufruf fehlschlägt, weil die zugrunde liegende Netzwerkbedingung noch besteht. Die Anweisungexcept: pass
fängt allejnpr.junos.exception.ConnectError
Ausnahmen ab, die vonr0.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 vonjnpr.junos.
exception.
ConnectError
Wenn du diese eine Ausnahmeklasse abfängst, werden daher alle vonr0.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.Der
else
-Block einertry
/except
/else
-Verbundanweisung wird nur ausgeführt, wenn dertry
-Block keine Ausnahmen ausgelöst hat. Mit anderen Worten: Derelse
-Block wird nur ausgeführt, wennr0.rpc.get_route_information()
erfolgreich ausgeführt wurde. Diebreak
Anweisung verlässt diefor
Schleife, wenn der RPC erfolgreich war. Das Verlassen einerfor
Schleife mit einerbreak
Anweisung überspringt dieelse
Klausel der Schleife.Die
else
Klausel einer Pythonfor
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 r0
bereits 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.ConnectClosedError
lä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 users
Befehl 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 tag
Attributs 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.Element
Objekte 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 xml
Ausgabe 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 attrib
zugegriffen 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 bar
im Leerlauf war. Dies geschieht durch die Kombination der Methode find()
, eines XPath mit dem [
Prädikat und dem tag
='text
']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 KeyError
Ausnahme 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.Element
Objekt 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 lxml
findest 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 [
Teil des XPath-Ausdrucks genau übereinstimmen muss. Wenn du den Wert des tag
='value
']<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.Element
Objekt 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.XMLDictNode
Objekt 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_table
mit 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_table
mit 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_table
mit allen ARP-Tabelleneinträgen und zeigt dann arp_table
's type is jnpr.junos.
factory.
OpTable.ArpTable
ist, der eine Unterklasse von jnpr.junos.
factory.
OpTable
Wie 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.ArpView
Objekte 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 T
ist 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
:
rpc
:
get-arp-table-information
item
:
arp-table-entry
key
:
mac-address
view
:
ArpView
ArpView
:
fields
:
mac_address
:
mac-address
ip_address
:
ip-address
interface_name
:
interface-name
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.
Die Junos XML RPC(get-arp-table-information), die aufgerufen wird, um die Elementdaten der Tabelle abzurufen.
Ein XPath-Ausdruck, der verwendet wird, um jedes Tabellenelement aus der RPC-Antwort auszuwählen.
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.
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.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.
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.Der XPath-Ausdruck
mac-address
wird verwendet, um den Wert des Schlüsselsmac_address
im nativen Python-View-Objekt zu setzen. Damac_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).Auch hier ist
ip-address
ein XPath-Ausdruck undip_address
ist der Name des Schlüssels im Python-View-Objekt.Der letzte Schlüssel in jedem Python-View-Objekt ist
interface_name
. Der Wert des Schlüsselsinterface_name
wird durch den XPath-Ausdruckinterface-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.
Name des Schlüssels | Erforderlich oder optional | Beschreibung |
---|---|---|
rpc | Erforderlich | Der 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. |
args | Optional | Ein assoziatives Array, dessen Elemente als Standardargumente an rpc übergeben werden. Der Parameter args sollte nur angegeben werden, wenn rpc immer mit args Argumenten 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_key | Optional | Der Name eines optionalen, unbenannten ersten Arguments für die Methode Diese Definition ermöglicht es dem Benutzer, aufzurufen: >>> Dadurch wird die folgende RPC an das Junos-Gerät gesendet:
|
item | Erforderlich | Ein 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. |
key | Optional, 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 |
view | Erforderlicha | Der 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 |
Setzen wir die Informationen aus Tabelle 4-5 in die Praxis um, indem wir eine neue Tabellendefinition für den show system users
CLI-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 view
der 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_
Das Beispiel verwendet den einfachsten dieser Schlüssel, groupname
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 seconds
Attribut6 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_
Schlüssel wird dann verwendet, um die Felder zu definieren, die das XPath-Präfix gemeinsam haben. In diesem Fall definiert der Schlüssel groupname
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 UserExtView
Definition:
---
### ------------------------------------------------------
### 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.factory
geladen. 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 User
Table
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_table
auf 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 path
an 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.
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 private
CLI-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 path
bestimmt 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.hostname
wird 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 {%
Begrenzungszeichen eingeschlossen. Ein solches Tag ist die bedingte Anweisung tag
%}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
Anweisung, die vom Wert der Variable expression
%}dual_re
abhä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
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:scalar_var
in
list_var
%}
<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 for
sehr ä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_ips
Wö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 0
ist 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.
Argument | Beschreibung |
---|---|
comment | Der Wert ist eine Kommentarzeichenkette, die die Übergabe beschreibt. |
confirm | Der 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. |
sync | Ein 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 synchronize Anweisung auf der [edit system
commit] Konfigurationshierarchieebene konfiguriert hat. |
detail | Ein 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_sync | Ein boolesches Flag. Wenn True , wird eine commit synchronize force durchgeführt. Dieses Argument sollte nur zu Debugging-Zwecken verwendet werden. |
full | Ein 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... >>>
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.scp
definiert 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 SW
im 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:
Sammeln Sie LLDP-Nachbarschaftsinformationen.
Sammle Schnittstellenbeschreibungen; analysiere die LLDP-Nachbarschaftsinformationen, die zuvor in den Schnittstellenbeschreibungen gespeichert wurden.
Aktuelle und frühere LLDP-Nachbarschaftsinformationen vergleichen; LLDP-Up-, Change- und Down-Meldungen drucken; neue Schnittstellenbeschreibungen berechnen.
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
"""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
import
getpass
import
jxmlease
from
jnpr.junos
import
Device
from
jnpr.junos.utils.config
import
Config
import
jnpr.junos.exception
TEMPLATE_PATH
=
'
interface_descriptions_template.xml
'
# Create a jxmlease parser with desired defaults.
parser
=
jxmlease
.
EtreeParser
(
)
class
DoneWithDevice
(
Exception
)
:
pass
Die Zeile
#!
(manchmal auch Hashbang- oder Shebang-Zeile genannt) bietet die Möglichkeit, das Skript auszuführen, ohne den Befehlpython
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 mitpath
/lldp_interface_descriptions_pyez.py
r0 r1 r2 r3 r4 r5
anstelle vonpython
path
/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 Befehlspython
abhängig.Zwei Standard-Python-Module,
sys
undgetpass
, werden importiert. Das Modulsys
ermöglicht den Zugriff auf Objekte, die vom Python-Interpreter verwaltet werden, und das Modulgetpass
ermöglicht dem Skript die interaktive Eingabeaufforderung des erforderlichen Passworts, ohne die Eingabe als Echo in das Terminal des Benutzers zu übertragen.Die Bibliothek
jxmlease
parst XML in native Python-Datenstrukturen. Du musst sicherstellen, dass dieses Modul auf deinem System installiert ist.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 PyEZConfig
bietet Methoden für den Umgang mit der Junos-Gerätekonfiguration. Das Modulexception
definiert mehrere PyEZ-spezifische Ausnahmen, die auf ein mögliches Problem hinweisen können.Die Variable
TEMPLATE_PATH
wird als Konstante verwendet, um den Namen der Jinja2-Konfigurationsvorlage zu identifizieren, mit der die Konfiguration neuer Schnittstellenbeschreibungen erstellt wird.Eine
jxmlease
Parser-Instanz wird erstellt, umlxml.etree.Element
Objekte in Python-Datenstrukturen zu parsen. Die Methodejxmlease.EtreeParser()
erzeugt eine Instanz der Klassejxmlease.EtreeParser
mit einer Reihe von Standardparametern. Während ein Parser, der mit der Methodejxmlease.Parser()
erstellt wird, ein XML-Eingabedokument als String erwartet, erwartet eine Instanz der Klassejxmlease.EtreeParser
ein XML-Eingabedokument alslxml.etree.Element
Objekt.Es wird eine eigene Klasse
DoneWithDevice
erstellt, die anzeigt, wann die Verarbeitung auf jedem Gerät in der Hauptgeräteschleife abgeschlossen ist. Diese neue Klasse ist eine Unterklasse vonException
.
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
:
(
"
\n
Usage:
%s
device1 [device2 [...]]
\n
\n
"
%
sys
.
argv
[
0
]
)
return
1
rc
=
0
# Get username and password as user input.
user
=
raw_input
(
'
Device Username:
'
)
password
=
getpass
.
getpass
(
'
Device Password:
'
)
for
hostname
in
sys
.
argv
[
1
:
]
:
try
:
(
"
Connecting to
%s
...
"
%
hostname
)
dev
=
Device
(
host
=
hostname
,
user
=
user
,
password
=
password
,
normalize
=
True
)
dev
.
open
(
)
(
"
Getting LLDP information from
%s
...
"
%
hostname
)
lldp_info
=
get_lldp_neighbors
(
device
=
dev
)
if
lldp_info
==
None
:
(
"
Error retrieving LLDP info on
"
+
hostname
+
"
. Make sure LLDP is enabled.
"
)
rc
=
1
raise
DoneWithDevice
(
"
Getting interface descriptions from
%s
...
"
%
hostname
)
desc_info
=
get_description_info_for_interfaces
(
device
=
dev
)
if
desc_info
==
None
:
(
"
Error retrieving interface descriptions on
%s
.
"
%
hostname
)
rc
=
1
raise
DoneWithDevice
desc_changes
=
check_lldp_changes
(
lldp_info
,
desc_info
)
if
not
desc_changes
:
(
"
No LLDP changes to configure on
%s
.
"
%
hostname
)
raise
DoneWithDevice
if
load_merge_template_config
(
device
=
dev
,
template_path
=
TEMPLATE_PATH
,
template_vars
=
{
'
descriptions
'
:
desc_changes
}
)
:
(
"
Successfully committed configuration changes on
%s
.
"
%
hostname
)
else
:
(
"
Error committing description changes on
%s
.
"
%
hostname
)
rc
=
1
raise
DoneWithDevice
except
jnpr
.
junos
.
exception
.
ConnectError
as
err
:
(
"
Error connecting:
"
+
repr
(
err
)
)
rc
=
1
except
DoneWithDevice
:
pass
finally
:
(
"
Closing connection to
%s
.
"
%
hostname
)
try
:
dev
.
close
(
)
except
:
pass
return
rc
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 Statuscode1
beendet, um einen Fehler anzuzeigen.Die Variable
rc
enthält den Statuscode, der am Ende des Skripts zurückgegeben wird. Der Wert wird auf0
gesetzt, was einen Erfolg anzeigt. Wenn später ein Fehler auftritt, wirdrc
auf1
gesetzt und die Verarbeitung wird mit dem nächsten in der Befehlszeile angegebenen Gerät fortgesetzt.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.Diese
for
Schleife durchläuft jeden Gerätenamen (oder jede IP-Adresse), der/die in den Befehlszeilenargumenten angegeben wurde. Dasys.argv[0]
der Name des Skripts ist, wird das Slicesys.argv[1:]
verwendet, um die Liste der Geräte zurückzugeben.Es wird eine Geräteinstanz erstellt und der Variablen
dev
zugewiesen. Der vom Benutzer eingegebene Benutzername und das Passwort werden als Argumenteuser
undpassword
übergeben. Das Argumentnormalize=True
stellt sicher, dass die Antwortnormalisierung auf jeden RPC on Demand-Aufruf angewendet wird, der die Geräteinstanzdev
verwendet.Die Methode
open()
wird auf derdev
Geräteinstanz aufgerufen. Dadurch wird eine NETCONF-Sitzung mit dem Gerät aufgebaut und die Standard-Faktenerfassung von PyEZ aufgerufen. Wenn der Aufruf vonopen()
fehlschlägt, wird einejnpr.junos.exception.ConnectError
(oder eine ihrer Unterklassen) ausgelöst. Diese mögliche Ausnahme wird später in der Funktionmain()
behandelt.Die Funktion
get_lldp_neighbors()
wird für die aktuelle Geräteinstanzdev
aufgerufen. Das Ergebnis wird imlldp_info
Wörterbuch gespeichert.Wenn
get_lldp_neighbors()
einen Fehler meldet (indem es den WertNone
zurückgibt), wird eine Fehlermeldung gedruckt,rc
wird auf1
gesetzt, um den Fehler anzuzeigen, und die AusnahmeDoneWithDevice
wird ausgelöst. Diese Ausnahme bewirkt, dass die Ausführung zur Zeileexcept DoneWithDevice
am Ende der Funktionmain()
springt.Die Funktion
get_description_info_for_interfaces()
wird auf dem aktuellen Gerät ausgeführt,dev
. Das Ergebnis wird imdesc_info
Wörterbuch gespeichert. Ähnlich wie bei der Funktionget_lldp_neighbors()
wird, wenn dieget_description_
info_for_interfaces()
Funktion einen Fehler meldet (indem sie den WertNone
zurückgibt), wird eine Fehlermeldung gedruckt,rc
wird auf1
gesetzt, um den Fehler anzuzeigen, und dieDoneWith
Device
Ausnahme ausgelöst. Diese Ausnahme bewirkt, dass die Ausführung zur Zeileexcept DoneWithDevice
am Ende der Funktionmain()
springt.Die Funktion
get_lldp_description_changes()
parst die Wörterbücherlldp_info
unddesc_info
und gibt die neuen Schnittstellenbeschreibungen im Wörterbuchdesc_changes
zurück. Wenn sich keine Beschreibungen geändert haben, wird dieDoneWithDevice
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.Die Funktion
load_merge_template_config()
wird aufgerufen. Das Argumenttemplate_path
wird auf den Wert vonTEMPLATE_PATH
gesetzt und der Schlüsseldescriptions
des Argumentstemplate_vars
wird auf das Wörterbuchdesc_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 auf1
gesetzt, um den Fehler anzuzeigen, undDoneWithDevice
wird aufgerufen.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 auf1
gesetzt, um den Fehler anzuzeigen, und das Skript fährt mit demfinally
Block fort.Der Aufruf der Methode
dev.close()
in einemfinally
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. Dadev.close()
selbst eine Ausnahme auslösen könnte, wird die Anweisung in einemtry
Block platziert. Der entsprechendeexcept
Block ignoriert einfach alle Ausnahmen, die vondev.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
)
:
"""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
:
resp
=
device
.
rpc
.
get_lldp_neighbors_information
(
)
except
(
jnpr
.
junos
.
exception
.
RpcError
,
jnpr
.
junos
.
exception
.
ConnectError
)
as
err
:
"
"
+
repr
(
err
)
return
None
for
nbr
in
resp
.
findall
(
'
lldp-neighbor-information
'
)
:
local_port
=
nbr
.
findtext
(
'
lldp-local-port-id
'
)
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
,
'
port
'
:
remote_port
}
return
lldp_info
Die Funktion
get_lldp_neighbors()
benötigt ein Argumentdevice
. Dieses Argument ist eine PyEZ-Geräteinstanz, die eine aktive NETCONF-Sitzung geöffnet hat.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 WertNone
an den Aufrufer zurückgegeben, um den Fehler anzuzeigen. Die PyEZ RPC on Demand Funktion ruft die XML RPC get-lldp-neighbors-information auf.Die Methode lxml
findall()
gibt eine Liste allerlxml.etree.Element
Objekte zurück, die dem XPath-Ausdrucklldp-neighbor-information
entsprechen. Dies führt zu einer Schleife durch jeden LLDP-Nachbarn. Der Variablenbr
wird daslxml.etree.Element
Objekt zugewiesen, das den aktuellen LLDP-Nachbarn repräsentiert.Die Methode lxml
findtext()
wählt das erste XML-Element aus, das einem XPath-Ausdruck entspricht. Sie wird verwendet, um die Werte der Variablenlocal_port
,remote_system
undremote_port
auszuwählen.Die Werte
remote_system
undremote_port
werden im Wörterbuchlldp_info
gespeichert. Dieses Wörterbuch ist mit demlocal_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
)
:
"""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
:
resp
=
parser
(
device
.
rpc
.
get_interface_information
(
descriptions
=
True
)
)
except
(
jnpr
.
junos
.
exception
.
RpcError
,
jnpr
.
junos
.
exception
.
ConnectError
)
as
err
:
"
"
+
repr
(
err
)
return
None
try
:
pi
=
resp
[
'
interface-information
'
]
[
'
physical-interface
'
]
.
jdict
(
)
except
KeyError
:
return
desc_info
for
(
local_port
,
port_info
)
in
pi
.
items
(
)
:
try
:
(
udesc
,
_
,
ldesc
)
=
port_info
[
'
description
'
]
.
partition
(
'
LLDP:
'
)
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
)
:
pass
return
desc_info
get_description_info_for_interfaces()
erfordert eindevice
Argument. Dieses Argument ist eine PyEZ-Geräteinstanz, die eine aktive NETCONF-Sitzung geöffnet hat.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 Argumentdescriptions
wird an die RPC übergeben und gibt an, dass nur Schnittstellenbeschreibungen zurückgegeben werden sollen. Das Objektlxml.etree.Element
wird dann an die Instanzjxmlease.EtreeParser()
,parser()
, übergeben.Der Variablen
pi
wird ein Wörterbuch zugewiesen, das auf den physischen Schnittstelleninformationen in der RPC-Antwort basiert. Die Methodejdict()
(siehe "jxmlease objects") wird verwendet, um ein Wörterbuch aus den physischen Schnittstelleninformationen zu erstellen. Die Methodejdict()
erstellt automatisch ein Wörterbuch, das auf den Werten der<name>
Elemente in jedem<pyhsical-interface>
Element der Antwort basiert. EineKeyError
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.Die Schleife
for
durchläuftlocal_port
, den Schlüssel des Wörterbuchspi
, undport_info
, den Wert des Wörterbuchspi
.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 Wertlocal_port
ist. Siehe "Schnittstellenbeschreibungen sammeln und analysieren" im RESTful API-Beispielskript, wenn dieser Code unklar ist.Beim Zugriff auf die Daten wird eine
KeyError
Ausnahme ausgelöst, wenn der angegebene Schlüssel nicht existiert. EineTypeError
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ächstenlocal_port
in dempi
Wö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_info
gespeichert 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
:
(
"
%s
LLDP Up. Now:
%s
%s
"
%
(
local_port
,
lldp_system
,
lldp_port
))
elif
down
:
(
"
%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
:
(
"
%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
)):
(
"
%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
)
:
"""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
device
.
bind
(
cu
=
Config
)
rc
=
False
try
:
try
:
resp
=
device
.
rpc
.
open_configuration
(
private
=
True
)
except
jnpr
.
junos
.
exception
.
RpcError
as
err
:
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
)
if
resp
.
find
(
"
ok
"
)
is
None
:
raise
LoadNotOKError
device
.
cu
.
commit
(
comment
=
"
made by
%s
"
%
sys
.
argv
[
0
]
)
except
(
jnpr
.
junos
.
exception
.
RpcError
,
jnpr
.
junos
.
exception
.
ConnectError
,
LoadNotOKError
)
as
err
:
"
"
+
repr
(
err
)
except
:
"
Unknown error occurred loading or committing configuration.
"
else
:
rc
=
True
try
:
device
.
rpc
.
close_configuration
(
)
except
jnpr
.
junos
.
exception
.
RpcError
as
err
:
"
"
+
repr
(
err
)
rc
=
False
return
rc
Die Argumente
device
,template_path
undtemplate_vars
sind notwendige Parameter für die Funktionload_merge_template_config()
.Es wird eine neue Unterklasse
Exception
definiert. Diese Klasse wird ausgelöst und behandelt, wenn es ein Problem beim Laden der neuen Konfiguration gibt.Eine neue PyEZ-Konfigurationsinstanz wird erstellt und an das Attribut
cu
des Geräts gebunden.Die XML-RPC zum Öffnen der Konfiguration wird von der RPC on Demand-Methode aufgerufen. Das Argument
private
macht diese RPC äquivalent zum CLI-Befehlconfigure private
.jnpr.junos.exception.RpcError
Ausnahmen werden von diesemexcept
Block abgefangen und behandelt. Es ist normal, dass der open-configuration XML RPC eineuncommitted
changes will be discarded on exit
Warnung zurückgibt, wenn das Argumentprivate
angegeben wird. Diese erwartete Warnung wird ignoriert. Alle anderenjnpr.junos.exception.RpcError
Ausnahmen werden von dem umschließendentry
/except
Block abgefangen und behandelt.Diese Anweisung übergibt die Argumente
template_path
undtemplate_vars
an die Methodeload()
der Konfigurationsinstanz. Die Konfiguration wird aus der Vorlage und den vom Benutzer eingegebenen Werten erstellt. Das Argumentmerge=True
bewirkt, dass die neue Konfiguration mit der bestehenden Kandidatenkonfiguration zusammengeführt wird.Ein
<ok>
XML-Element in der XML-Antwort der Methodeload()
zeigt an, dass die neue Konfiguration erfolgreich geladen wurde. Wenn das XML-Element<ok>
in der Antwort nicht gefunden wird, wird eineLoadNotOKError
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-AusnahmeRpcError
auslösen. Dieser Block behandelt lediglich die mögliche Situation, dass die Methodeload()
keine Ausnahme auslöst und eine unerwartete RPC-Antwort zurückgibt.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 Argumentcomment
hinzugefügt.jnpr.junos.exception.RpcError
,jnpr.junos.exception.ConnectError
undLoadNotOKError
Ausnahmen und Unterklassen werden von diesemexcept
Block abgefangen und behandelt. Der Inhalt dieser Ausnahmen wird einfach ausgedruckt. Für alle anderen Ausnahmen, die während destry
Blocks aufgetreten sind, wird eine andere Meldung ausgegeben.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 wirdrc
der Wert vonTrue
zugewiesen, um den Erfolg anzuzeigen. (Zuvor wurderc
aufFalse
initialisiert).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 undrc
wirdFalse
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
>
{%
for
key
,
value
in
descriptions
.iteritems
(
)
%}
<interface
>
<name
>
{{
key
}}
</name>
<description
>
{{
value
}}
</description>
</interface>
{%
endfor
%}
</interfaces>
</configuration>
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.Diese Zeile ist eine Jinja2
for
Schleife, die über die Einträge imdescriptions
Wörterbuch iteriert. Der Schlüssel desdescriptions
Wörterbuchs ist der Name der Schnittstelle und der Wert ist die neu zu konfigurierende Beschreibung.Der erste XML-Tag für jede Schnittstelle. Dieses Tag wird für jede Schnittstelle im
descriptions
Wörterbuch wiederholt.Der Name der Schnittstelle. Der Ausdruck
{{ key }}
wird für jeden Schlüssel im Wörterbuchdescriptions
ausgewertet.Die Beschreibung der Schnittstellen. Der Ausdruck
{{ value }}
wird für jeden Wert im Wörterbuchdescriptions
ausgewertet.Das Tag
{% endfor %}
kennzeichnet das Ende derfor
Schleife, die über jede Schnittstelle im Wörterbuchdescriptions
iteriert.
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.
Sprache | Beschreibung | Link |
---|---|---|
Ruby | Beliebte Open Source NETCONF-Bibliothek für Ruby. Einfach zu installieren, begrenzte Abhängigkeiten und aktiver Support. | http://rubygems.org/gems/netconf |
Java | Offene 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 |
Perl | Unterstü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 |
PHP | Open 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 |
Python | Herstellerunabhä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_vars
komplett 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.