Login und Logout auf einem Cisco-IOS-Device mit Netmiko

02.04.2021


In diesem Artikel hatte ich beschrieben, wie man in Python über die Netmiko-Bibliothek eine SSH-Verbindung zu einem Jumphost aufbaut. Hier geht es darum, wie man diese bestehende Verbindung dann nutzt, um sich auf einem Cisco IOS-Gerät einzuloggen.


Quellcode für den Device-Login

Der Code meiner Device-Login-Funktion sieht so aus:


1 def devicelogin(devicename):

2 """ Diese Funktion dient dem Login auf einem Network Device über die bestehende SSH-Hop-Verbindung."""

3

4 try:

5 # Umstellen auf Linux-CLI (Jumphost)

6 redispatch(net_connect, device_type='terminal_server')

7 # SSH-Befehl für Login

8 net_connect.write_channel("ssh -l "+ username +" "+devicename+"\n")

9 # Warten

10 time.sleep(1)

11 # Prüfen, ob das Device erwartungsgemäß reagiert

12 # - Der Substring '[Pp]assword' muss in der Antwort des Switches auftauchen

13 # - Mit Schleife darauf warten

14 output = ""

16 startzeit=time.time()

17 while (time.time() - startzeit < 30) and (not 'assword' in output) and not 'are you sure' in output):

18 output = net_connect.read_channel()

21 time.sleep(1)


22 # Falls das Device nicht innerhalb von 30 Sekunden reagiert hat -> Fehlermeldung

23 if (time.time() - startzeit > 30):

24 sys.stdout.write(RED)

25 print("[FEHLER] Das Networkdevice sendet keine Antwort auf den SSH-Login")

26 sys.stdout.write(RESET)

27 return 1


28 # Abfagen: Authentizität des Host kann nicht bestätigt werden (RSA Key Fingerprint)

29 if "are you sure you want to continue connecting" in output.lower():

30 net_connect.write_channel('yes\n')

31 output=""

32 time.sleep(1)

33 startzeit=time.time()

34 while (time.time() - startzeit < 30) and (len(output) < 3):

35 output=net_connect.read_channel().strip()

36 time.sleep(1)

37 # Falls das Device nicht innerhalb von 30 Sekunden reagiert hat -> Fehlermeldung

38 if (time.time() - startzeit > 30):

39 sys.stdout.write(RED)

40 print("[FEHLER] Der Jumphost sendet keine Antwort auf die Bestätigung der Akzeptanz des RSA Fingerprint")

41 sys.stdout.write(RESET)

42 return 1

43 # Passwort 'eingeben'

44 if 'password' in output.lower():

45 net_connect.write_channel(passwort+'\n')

46 output=""

47 time.sleep(1)

48 startzeit=time.time()

49 while (time.time() - startzeit < 30) and (len(output)<3):

50 output=net_connect.read_channel().strip()

51 time.sleep(1)

52 # Falls das Device nicht innerhalb von 30 Sekunden reagiert hat -> Fehlermeldung

53 if (time.time() - startzeit > 30):

54 sys.stdout.write(RED)

55 print("[FEHLER] Das Network Device reagiert nicht auf die Eingabe des Passworts")

56 sys.stdout.write(RESET)

57 return 1

58

59 # Wenn der Login auf dem Gerät erfolgreich war, muss das Device eine Antwort mit dem Hostname zurück liefern.

60 # Hostname aufbereiten (ersten Teil des FQDN extrahieren)

61 hostnameteile=devicename.split(".")

62 # Prüfen, ob der Hostname in der Antwort des Devices enthalten ist

63 if (hostnameteile[0].lower() in output.lower()):

64 # Auf Cisco-IOS-Syntax umschalten

65 redispatch(net_connect, device_type="cisco_ios")

66 #redispatch(net_connect, device_type="cisco_ios", session_prep=False)

67 #net_connect.send_command('\n')

68 #net_connect.send_command('terminal length 0')

69 # Falls das Device im EXEC Mode ist, in den ENABLE Mode wechseln

70 chk_enable = net_connect.check_enable_mode()

71 if chk_enable == False:

72 net_connect.enable()

73 return 0

74 else:

75 sys.stdout.write(RED)

76 print("[FEHLER] Das Network Device antwortet nicht mit dem erwarteten Prompt")

77 sys.stdout.write(RESET)

78 return 1

79

80 # Unerwartete Fehler abfangen

81 except Exception as e:

82 sys.stdout.write(RED)

83 print("[FEHLER] Die Anmeldung am zu sichernden Host ist misslungen.\n")

84 print("Es trat folgender unerwarteter Fehler beim Device Login auf:\n")

85 print(e)

86 print("-----\n\n\n")

87 sys.stdout.write(RESET)

88 return 1

Erklärung

Diese Funktion hat mich einige Zeit an ‚Bastelei‘ gekostet, bis sie ausreichend zuverlässig lief. Das Problem waren scheinbar zufällig auftauchende Probleme. Bei mehreren Logins hintereinander auf dem gleichen Cisco-Device lief meistens alles glatt, doch in manchen Fällen hing die Funktion auch plötzlich, bis sie dann in einen Timeout-Fehler lief. Anyway, here we go!


Als Parameter bekommt die Funktion den Devicenamen oder die IP-Adresse als String übergeben. Wird der Devicename verwendet, muss dieser natürlich auf dem Jumphost auflösbar sein.


Um unvorhergesehene Fehler abzufangen, ist der Code in einen try: except: -Block eingefasst – eine gute Praxis in Python.


Mit dem redispatch-Befehl in Zeile 6 wird Netmiko gesagt, dass es mit einem unixoiden System kommuniziert, siehe auch den letzten Blogpost.


Der net_connect.write_channel (Zeile 7) führt den ssh-Befehl auf dem Linux-Jumphost aus. Der time.sleep(1) sorgt dann für eine Pause von einer Sekunde.


In den Zeilen 14-21 wartet die Funktion dann darauf, dass das IOS-Device nach dem Passwort fragt. Festgemacht wird das daran, dass der Substring asswort in der Antwor des Cisco-Gerätes auftaucht. Das P habe ich weggelassen, um die Varianten Password und password zu berücksichtigen. Es gibt zwischen den IOS-Versionen subtile Unterschiede, so dass man immer genau überlegen muss, was man universell als Antwort von den Cisco-Geräten erwarten kann. Die While-Schleife schaut bis zu 30 Sekunden immer wieder nach, ob die erwartete Antwort gekommen ist. Falls nicht, wird eine rote Fehlermeldung ausgegeben und die Funktion mit einem Fehlercode als Rückgabewert verlassen. Das mit den Farben ist natürlich optional und Bedarf etwas zusätzlichen Programmcodes außerhalb der hier vorgestellten Funktion. Es kann sein, dass der Jumphost nicht nach dem Passwort fragt, sondern fragt, ob man dem RSA-Fingerprint des Cisco-Devices vertrauen möchte. Daher wird diese ebenfalls in die While-Schleife eingebaut ( ‚are you sure‚).


In den Zeilen 29.42 wird die optional auftretende Abfrage des Jumphosts bezüglich des RSA Fingerprints behandelt.


In den Zeilen 43 wird das Passwort ‚eingegeben‘, also an das Cisco-Gerät geschickt. Das sollte darauf ‚angemessen‘ reagieren. Angemessen habe ich im ersten Schritt so definiert, dass die Antwort länger als zwei Zeichen (ohne Whitespaces) ist (Zeilen 46-57, die While-Schleife wartet bis zu 30 Sekunden darauf).


Die Im darauf folgenden Programmcode (Zeilen 59-63) prüfe ich dann, ob das Cisco-Gerät einen Prompt zurück liefert. Das erkenne ich daran, dass der Devicename im Prompt enthalten ist (Annahme: der hostname ist auf dem Device entsprechend konfiguriert). Hier gibt es allerdings einen Haken: der hostname ist nur ein Teil des FQDNs, der Login erfolgte aber möglicherweise über den FQDN. Also muss der hostname aus dem übergebenen devicename extrahiert werden. Dies passiert in Zeile 61. Wenn der hostname dann im Prompt enthalten ist (Zeile 63), kann es weitergehen oder es wird ein Fehler gemeldet. Die lower-Funktion dient übrigens dazu, dass der Vergleich auch funktioniert, wenn die Groß- und Kleinschreibung unterschiedlich ist. Es wird jeweils der Text in Kleinbuchstaben verglichen. Man hätte hier natürlich auch Großbuchstaben verwenden können.


Im Zeile 65 bekommt Netmiko die Information, dass es jetzt das Verhalten eines Cisco IOS-Gerätes erwarten soll. Bei diesem Befehl hängt sich Netmiko manchmal auf. In den Zeilen 66-68 steht auskommentiert ein experimenteller Code, den man statt dem in Zeile 65 ausprobieren kann.


In den Zeilen 69-73 wird auf dem Cisco-Gerät in den Enable-Mode gewechselt, falls der nicht aktiv ist und dann die Funktion beendet.


In den Zeilen 80-88 werden unerwartete Fehler behandelt.


Der Code in dieser Funktion ist vermutlich noch nicht in seiner endgültigen Form, aber er ist schon ganz gut nutzbar.


Quellcode für den Device-Logout

def devicelogout():

""" Diese Funktion dient dem Abmelden von einem Network Device über die bestehende SSH-Hop-Verbindung."""


# Von Device abmelden. Es sollte eine Meldung kommen, die auf 'closed.' endet.

try:

output=net_connect.send_command_expect('exit\n',expect_string='closed.')

# Auf Linux-Syntax umschalten (SSH-Jumphost)

redispatch(net_connect,device_type="terminal_server")

except Exception as e:

sys.stdout(CYAN)

print("[HINWEIS] Es trat ein Fehler beim Abmelden vom Network Device auf. Dieser ist jedoch nicht relevant.\n")

sys.stdout.write(RESET)

Erklärung

In diesem Code wird der exit-Befehl auf der IOS-CLI ‚eingegeben‘, worauf das Cisco-Gerät die Verbindung beenden sollte und der ssh-Client auf dem Jumphost eine Connection Closed-Meldung ausgeben sollte. Das wird noch kurz geprüft und beim Ausbleiben als Hinweis kommentiert. Hier könnte man den Programmcode sicherlich noch etwas weiter ausbauen. Beispielsweise reich ein end nicht aus, wenn man gerade im Interface Configuration Mode auf dem IOS-Gerät steht.


Fazit

Der Zugriff auf Cisco IOS-Geräte über Netmiko ist zuweilen etwas tricky, funktioniert aber meist doch relativ gut.


Suchbegriffe: automatisierung