Cloud-Anbieter machen seit Jahren ein Vermögen mit sauber verpackten Managed Services. Ob Datenbanken oder Message-Broker, Entwickler wie wir scheinen kein Problem damit zu haben, ein bisschen mehr zu bezahlen, damit man sich um die Dinge kümmert. Aber Moment, sind wir nicht normalerweise die letzten, die sich für weniger Optimierung und weniger Kontrolle entscheiden? Warum ist das der Zeitpunkt, an dem wir uns anders entscheiden? Wenn ich eine Vermutung anstellen müsste, würde ich darauf wetten, dass es zum Teil daran liegt, dass serverseitige DevOps irgendwie scheiße sind.
Als Entwickler ist das Konfigurieren oder Debuggen eines VPS in der Regel Arbeit, die nicht einkalkuliert ist, und es ist nicht besonders lohnend. Im besten Fall wird Ihre Anwendung am Ende wahrscheinlich genauso laufen wie Ihre lokale Umgebung. Wie könnten wir diesen unvermeidlichen Teil unserer Arbeit besser machen? Nun, wir könnten es automatisieren.
Paramiko und SCP sind zwei Python-Bibliotheken, die wir zusammen verwenden können, um Aufgaben zu automatisieren, die wir auf einem entfernten Host ausführen möchten, wie das Neustarten von Diensten, das Durchführen von Aktualisierungen oder das Abrufen von Protokolldateien. Wir werden uns ansehen, wie das Skripting mit diesen Bibliotheken aussieht. Ich warne Sie vor: Es gibt eine beträchtliche Menge an Code in diesem Tutorial, was dazu führt, dass ich so aufgeregt bin, dass ich andere zu meinen manischen Code-Tutorial-Episoden zwinge. Wenn Sie anfangen, sich verloren zu fühlen, finden Sie das vollständige Repo hier:
Einrichten von SSH-Schlüsseln
Um eine SSH-Verbindung zu authentifizieren, müssen wir einen privaten RSA SSH-Schlüssel einrichten (nicht zu verwechseln mit OpenSSH). Mit folgendem Befehl können wir einen Schlüssel erzeugen:
Dieser Befehl fordert uns auf, einen Namen für unseren Schlüssel anzugeben. Nennen Sie ihn, wie Sie wollen:
Als Nächstes werden Sie aufgefordert, ein Kennwort einzugeben (Sie können dieses leer lassen).
Nun, da wir unseren Schlüssel haben, müssen wir diesen auf unseren Remote-Host kopieren. Am einfachsten geht das mit ssh-copy-id
:
Überprüfen unseres SSH-Schlüssels
Wenn Sie überprüfen möchten, welche Schlüssel Sie bereits haben, finden Sie diese im .ssh
Verzeichnis Ihres Systems:
Wir suchen nach Schlüsseln, die mit dem folgenden Header beginnen:
Fühlen Sie sich frei, das gleiche auf Ihrer FPS zu tun.
Start unseres Scripts
Lassen Sie uns unsere Bibliotheken installieren. Starten Sie die virtuelle Umgebung, die Sie bevorzugen, und lassen Sie es krachen:
Nur noch eine Sache, bevor wir sinnvollen Python-Code schreiben! Erstellen Sie eine Konfigurationsdatei, die die Variablen enthält, die wir für die Verbindung mit unserem Host benötigen. Hier sind die Grundzüge dessen, was wir für unseren Server benötigen:
- Host: Die IP-Adresse oder URL des Remote-Hosts, auf den wir zugreifen möchten.
- Benutzername: Dies ist der Benutzername, den Sie für die SSH-Verbindung zu Ihrem Server verwenden.
- Passphrase (optional): Wenn Sie bei der Erstellung Ihres SSH-Schlüssels eine Passphrase angegeben haben, geben Sie diese hier an. Denken Sie daran, dass die Passphrase Ihres SSH-Schlüssels nicht mit dem Passwort Ihres Benutzers identisch ist.
- SSH-Schlüssel: Der Dateipfad des Schlüssels, den wir zuvor erstellt haben. Unter OSX befindet sich dieser im Ordner ~/.ssh Ihres Systems. Der SSH-Schlüssel, den wir anvisieren, muss einen zugehörigen Schlüssel mit der Dateierweiterung .pub haben. Dies ist unser öffentlicher Schlüssel; wenn Sie früher mitgemacht haben, sollte dieser bereits für Sie generiert worden sein.
Wenn Sie versuchen, Dateien von Ihrem Remote-Host hoch- oder herunterzuladen, müssen Sie zwei weitere Variablen angeben:
- Remote Path: Der Pfad zum Remote-Verzeichnis, das wir für die Dateiübertragung anvisieren. Wir können entweder Dinge in diesen Ordner hochladen oder den Inhalt herunterladen.
- Lokaler Pfad: Die gleiche Idee wie oben, aber in umgekehrter Reihenfolge. Der Einfachheit halber ist der lokale Pfad, den wir verwenden werden, einfach /data und enthält Bilder von niedlichen Fuchs-Gifs.
Nun haben wir alles, was wir brauchen, um eine respektable config.py-Datei zu erstellen:
Erstellen eines SSH-Clients
Wir werden eine Klasse namens RemoteClient erstellen, um die Interaktionen mit unserem entfernten Host zu behandeln. Bevor wir uns zu sehr ins Zeug legen, beginnen wir einfach damit, die Klasse RemoteClient mit den Variablen zu instanziieren, die wir in config.py erstellt haben:
Bislang nichts Beeindruckendes: Wir haben nur ein paar Variablen gesetzt und sie an eine nutzlose Klasse übergeben. Lassen Sie uns die Dinge ein wenig vorantreiben, ohne unseren Konstruktor zu verlassen:
Wir haben drei neue Dinge hinzugefügt, die mit unserer Klasse instanziiert werden:
-
self.client
: self.client wird letztlich als Verbindungseinwand in unserer Klasse dienen, ähnlich wie man in Datenbankbibliotheken mit Begriffen wieconn
umgegangen ist. Unsere Verbindung wirdNone
sein, bis wir uns explizit mit unserem Remote-Host verbinden. -
self.scp
: Ähnlich wie self.client, behandelt aber ausschließlich Verbindungen zum Übertragen von Dateien. -
self.__upload_ssh_key()
ist keine Variable, sondern eine Funktion, die automatisch ausgeführt wird, wenn unser Client instanziert wird. Der Aufruf von__upload_ssh_key()
teilt unserem RemoteClient-Objekt mit, dass es sofort bei der Erstellung nach lokalen ssh-Schlüsseln suchen soll, damit wir versuchen können, diese an unseren entfernten Host zu übergeben. Andernfalls wären wir gar nicht in der Lage, eine Verbindung aufzubauen.
SSH-Schlüssel auf einen entfernten Host hochladen
Wir sind an dem Abschnitt dieser Übung angelangt, an dem wir einen verheerend unrühmlichen Boilerplate-Code herausschlagen müssen. Das ist typischerweise der Punkt, an dem emotional unterlegene Individuen der schieren, dumpfen Obskurität des Verständnisses von SSH-Schlüsseln und der Aufrechterhaltung von Verbindungen erliegen. Machen Sie keinen Fehler: Authentifizierung und Verwaltung von Verbindungen zu irgendetwas programmatisch ist überwältigend langweilig… es sei denn, Ihr Reiseführer ist zufällig ein bezaubernder Wortschöpfer, der als Ihr liebevoller Beschützer durch die gefährliche Unklarheit dient. Manche Leute nennen diesen Beitrag ein Tutorial. Ich habe vor, es Kunst zu nennen.
RemoteClient wird mit zwei privaten Methoden beginnen: __get_ssh_key()
und __upload_ssh_key()
. Erstere holt einen lokal gespeicherten öffentlichen Schlüssel ab, und letztere übergibt im Erfolgsfall diesen öffentlichen Schlüssel an unseren entfernten Host als Zugangsberechtigung. Sobald ein lokal erzeugter öffentlicher Schlüssel auf einem entfernten Rechner existiert, wird dieser Rechner uns dann für immer unsere Anfragen zur Verbindung mit ihm anvertrauen: keine Passwörter erforderlich. Wir werden auf dem Weg dorthin eine ordentliche Protokollierung einbauen, nur für den Fall, dass wir auf Probleme stoßen:
_get_ssh_key()
ist ganz einfach: Es prüft, ob ein SSH-Schlüssel in dem Pfad existiert, den wir in unserer Config angegeben haben und der für die Verbindung zu unserem Host verwendet werden soll. Wenn die Datei tatsächlich existiert, setzen wir fröhlich unsere self.ssh_key
-Variable, so dass dieser Schlüssel von nun an hochgeladen und von unserem Client verwendet werden kann. Paramiko stellt uns ein Submodul namens RSAKey zur Verfügung, um alles, was mit RSA-Schlüsseln zu tun hat, einfach zu handhaben, wie z.B. das Parsen einer privaten Schlüsseldatei in eine brauchbare Verbindungsauthentifizierung. Das bekommen wir hier:
Wenn unser RSA-Schlüssel unverständlicher Unsinn statt eines echten Schlüssels wäre, hätte Paramikos SSHException dies erkannt und frühzeitig eine Exception ausgelöst, die genau das erklärt. Die richtige Nutzung der Fehlerbehandlung einer Bibliothek nimmt eine Menge Rätselraten aus der Frage heraus, was schief gelaufen ist, besonders in Fällen, in denen es das Potenzial für zahlreiche Unbekannte in einem Nischenbereich gibt, mit dem keiner von uns oft zu tun hat.
_upload_ssh_key()
ist der Punkt, an dem wir unseren SSH-Schlüssel in die Kehle unseres entfernten Servers stopfen können, während wir schreien: „LOOK! IHR KÖNNT MIR JETZT FÜR IMMER VERTRAUEN!“ Um dies zu erreichen, gehe ich ein bisschen „old school“ vor, indem ich Bash-Befehle über das os.system
von Python übergebe. Solange mich niemand in den Kommentaren auf einen saubereren Ansatz aufmerksam macht, gehe ich davon aus, dass dies die knallharte Art ist, Schlüssel an einen entfernten Server zu übergeben.
Die standardmäßige Nicht-Python-Methode zur Übergabe von Schlüsseln an einen Host sieht wie folgt aus:
Das ist genau das, was wir in unserer Funktion in Python erreichen, die wie folgt aussieht:
system(f'ssh-copy-id -i {self.ssh_key_filepath} \ {self.user}@{self.host}>/dev/null 2>&1')
Ich nehme an, Sie lassen sich diesen /dev/null 2>&1
Teil nicht entgehen? Na gut. Wenn Sie es wissen müssen, hier ist ein Typ auf StackOverflow, der es besser erklärt, als ich es kann:
>
ist für Redirect/dev/null
ist ein schwarzes Loch, wo alle Daten, die gesendet werden, verworfen werden.2
ist der Dateideskriptor für Standardfehler.>
ist für den Redirect.&
ist das Symbol für den Dateideskriptor (ohne es würde das folgende1
als Dateiname gelten).1
ist der Dateideskriptor für Standard O.
Wir sagen also im Grunde unserem entfernten Server, dass wir ihm etwas geben, und er fragt: „Wo soll ich das Ding hinstellen?“, worauf wir antworten: „Nirgendwo im physischen Raum, denn dies ist kein Objekt, sondern ein ewiges Symbol unserer Freundschaft. Unser entfernter Gastgeber wird daraufhin von Dankbarkeit und Emotionen überflutet, denn ja, Computer haben Emotionen, aber das kann uns im Moment nicht stören.
Verbinden mit unserem Client
Wir fügen unserem Client eine Methode namens connect()
hinzu, um die Verbindung mit unserem Host zu handhaben:
Schauen wir uns das mal genauer an:
-
client = SSHClient()
legt die Grundlage für die Erstellung eines Objekts, das unseren SSH-Client repräsentiert. Die folgenden Zeilen konfigurieren dieses Objekt, um es nützlicher zu machen. -
load_system_host_keys()
weist unseren Client an, nach allen Hosts zu suchen, mit denen wir uns in der Vergangenheit verbunden haben, indem er die Datei known_hosts unseres Systems durchsucht und die SSH-Schlüssel findet, die unser Host erwartet. Wir haben uns in der Vergangenheit nie mit unserem Host verbunden, also müssen wir unseren SSH-Schlüssel explizit angeben. -
set_missing_host_key_policy()
sagt Paramiko, was im Falle eines unbekannten Schlüsselpaares zu tun ist. Dies erwartet eine in Paramiko eingebaute „Richtlinie“, der wir ein bestimmtesAutoAddPolicy()
zuweisen werden. Wenn wir unsere Richtlinie auf „auto-add“ setzen, bedeutet das, dass Paramiko automatisch den fehlenden Schlüssel lokal hinzufügt, wenn wir versuchen, uns mit einem nicht erkannten Host zu verbinden. -
connect()
ist die wichtigste Methode des SSHClient (wie Sie sich vielleicht vorstellen können). Wir sind endlich in der Lage, unseren Host, Benutzer und SSH-Schlüssel zu übergeben, um das zu erreichen, worauf wir alle gewartet haben: eine glorreiche SSH-Verbindung zu unserem Server! Dieconnect()
-Methode erlaubt eine Menge Flexibilität durch eine große Anzahl von optionalen Schlüsselwortargumenten. Ich übergebe hier zufällig ein paar: Das Setzen von look_for_keys aufTrue
gibt Paramiko die Erlaubnis, in unserem ~/.ssh-Ordner nach SSH-Schlüsseln zu suchen, und das Setzen von timeout wird automatisch Verbindungen schließen, die wir wahrscheinlich vergessen werden zu schließen. Wir könnten sogar Variablen für Dinge wie Port und Passwort übergeben, wenn wir uns auf diese Weise mit unserem Host verbinden würden.
Verbindung trennen
Wir sollten Verbindungen zu unserem entfernten Host schließen, wenn wir sie nicht mehr benutzen. Dies nicht zu tun, muss nicht unbedingt katastrophal sein, aber ich hatte schon ein paar Fälle, in denen genügend hängende Verbindungen schließlich den eingehenden Verkehr auf Port 22 ausreizten. Unabhängig davon, ob Ihr Anwendungsfall einen Neustart als Katastrophe oder milde Unannehmlichkeit ansieht, sollten wir unsere verdammten Verbindungen wie Erwachsene schließen, als ob wir uns nach dem Kacken den Hintern abwischen würden. Unabhängig von Ihrer Verbindungshygiene plädiere ich dafür, eine Timeout-Variable zu setzen (wie wir bereits gesehen haben). Wie auch immer: voila:
Spaßfakt: Das Setzen von self.client.close()
setzt self.client
tatsächlich gleich None
, was in Fällen nützlich ist, in denen Sie prüfen wollen, ob eine Verbindung bereits offen ist.
Unix-Befehle ausführen
Wir haben jetzt eine wunderbare Python-Klasse, die RSA-Schlüssel finden, sich verbinden und die Verbindung trennen kann. Allerdings fehlt ihr die Fähigkeit, etwas Nützliches zu tun.
Wir können das beheben und endlich anfangen, „Dinge“ zu tun, und zwar mit einer brandneuen Methode zum Ausführen von Befehlen, die ich treffend execute_commands()
nenne (das ist korrekt, „Befehle“ im Sinne von potenziell-mehr-als-einer, darauf gehen wir gleich noch ein). Die ganze Arbeit wird von der eingebauten exec_command()
-Methode des Paramiko-Clients erledigt, die einen einzelnen String als Befehl akzeptiert und ausführt:
Die gerade erstellte Funktion execute_commands()
erwartet eine Liste von Strings, die als Befehle ausgeführt werden sollen. Das ist zum Teil der Bequemlichkeit halber, aber auch, weil Paramiko zwischen den Befehlen keine „Zustands“-Änderungen (wie z.B. das Ändern von Verzeichnissen) durchführt, so dass jeder Befehl, den wir an Paramiko übergeben, davon ausgehen sollte, dass wir vom Stammverzeichnis unseres Servers aus arbeiten. Ich habe mir die Freiheit genommen, drei solcher Befehle wie folgt zu übergeben:
Ich kann den Inhalt eines Verzeichnisses durch Verkettung von cd path/to/dir && ls
anzeigen, aber die Ausführung von cd path/to/dir
gefolgt von ls
würde ins Leere laufen, weil ls
beim zweiten Mal die Liste der Dateien im Stammverzeichnis unseres Servers zurückgibt.
Sie werden feststellen, dass client.exec_command(cmd)
drei Werte anstelle von einem zurückgibt: Das kann nützlich sein, um zu sehen, welche Eingabe welche Ausgabe erzeugt hat. Hier sind zum Beispiel die vollständigen Protokolle für das von mir angegebene Beispiel, bei dem ich drei Befehle an remote.execute_commands()
übergeben habe:
Ein paar schöne Sachen hier. Jetzt können Sie sehen, welche Sites auf meinem Server sind, welche Bots mich spammen und wie viele Node-Prozesse ich laufen habe.
Ich möchte nicht noch mehr Zeit auf die Kunst der Befehlsausführung verschwenden, aber es ist erwähnenswert, warum wir nach jedem Befehl stdout.channel.recv_exit_status()
aufrufen. Das Warten auf recv_exit_status()
nach dem Aufruf von client.exec_command()
zwingt uns, unsere Befehle synchron auszuführen, da sonst die Wahrscheinlichkeit besteht, dass unser entfernter Rechner die Befehle nicht so schnell entschlüsseln kann, wie wir sie weitergeben.
Hochladen (und Herunterladen) von Dateien über SCP
SCP bezeichnet sowohl das Protokoll zum Kopieren von Dateien auf entfernte Rechner (Secure Copy Protocol) als auch die Python-Bibliothek, die dieses nutzt. Wir haben die SCP-Bibliothek bereits installiert, also importieren wir sie.
Die SCP- und Paramiko-Bibliotheken ergänzen sich gegenseitig und machen das Hochladen via SCP super einfach. SCPClient() erzeugt ein Objekt, das „transport“ von Paramiko erwartet, was wir mit self.conn.get_transport()
bereitstellen. Das Erstellen einer SCP-Verbindung ist von der Syntax her an unseren SSH-Client angelehnt, aber diese Verbindungen sind getrennt. Es ist möglich, eine SSH-Verbindung zu schließen und eine SCP-Verbindung offen zu lassen, also tun Sie das nicht. Öffnen Sie eine SCP-Verbindung wie folgt:
Das Hochladen einer einzelnen Datei ist langweilig, also lassen Sie uns stattdessen ein ganzes Verzeichnis mit Dateien hochladen. bulk_upload()
nimmt eine Liste von Dateipfaden entgegen und ruft dann __upload_single_file()
Unsere Methode erwartet zwei Zeichenketten: die erste ist der lokale Pfad zu unserer Datei, und die zweite ist der Pfad des entfernten Verzeichnisses, in das wir hochladen möchten.
SCP’s put()
Methode wird eine lokale Datei auf unseren entfernten Host hochladen. Dadurch werden vorhandene Dateien mit demselben Namen ersetzt, falls sie zufällig am angegebenen Zielort existieren. Das war’s schon!
Dateien herunterladen
Das Gegenstück zu SCP’s put()
ist die get()
Methode:
Unser großes, schönes Skript
Wir haben jetzt eine kranke Python-Klasse, um SSH und SCP mit einem entfernten Host zu handhaben… setzen wir sie in die Tat um! Das folgende Snippet ist ein schneller Weg, um zu testen, was wir bisher gebaut haben. Kurz gesagt, dieses Skript sucht nach einem lokalen Ordner, der mit Dateien gefüllt ist (in meinem Fall habe ich den Ordner mit Fox-Gifs gefüllt 🦊).
Schauen Sie sich an, wie einfach es ist, eine main.py zu erstellen, die dank unserer RemoteClient-Klasse komplexe Aufgaben auf entfernten Rechnern erledigt:
Hier ist die Ausgabe unserer Upload-Funktion:
Es hat funktioniert! Sie glauben mir nicht? Warum überprüfen wir es nicht selbst, indem wir remote.execute_commands()
ausführen?
Da haben Sie es. Direkt aus dem Mund des Fuchses.
Take It And Run With It
An dieser Stelle möchte ich mich kurz bei Ihnen allen bedanken und mich dafür entschuldigen, dass Sie noch hier sind. Ich habe mir geschworen, keine Tutorials mehr zu schreiben, die mehr als zweitausend Wörter lang sind, und dieses hier ist dabei, fünftausend Wörter Unsinn zu produzieren. Daran werde ich arbeiten. Neues Jahr, neues Ich.
Für Ihre Bequemlichkeit habe ich den Quellcode für dieses Tutorial auf Github hochgeladen. Fühlen Sie sich frei, diesen zu nehmen und damit zu arbeiten! Zum Abschluss möchte ich Ihnen noch den Kern der Client-Klasse zeigen, die wir zusammengestellt haben:
Den vollständigen Quellcode für dieses Tutorial finden Sie hier:
SiteGithubTwitter
Engineer mit einer andauernden Identitätskrise. Macht alles kaputt, bevor er Best Practices lernt. Völlig normal und emotional stabil.