Les fournisseurs de cloud ont fait un massacre avec des services gérés soigneusement emballés pendant des années. Que ce soit des bases de données ou des courtiers de messages, les développeurs comme nous ne semblent pas avoir de problème à payer un peu plus pour que les choses soient prises en charge. Mais attendez, ne sommes-nous pas généralement les dernières personnes à opter pour moins d’optimisation et moins de contrôle ? Pourquoi est-ce le moment de décider autrement ? Si je devais faire une supposition, je parierais que c’est en partie parce que le DevOps côté serveur est plutôt nul.
En tant que développeur, la configuration ou le débogage d’un VPS est généralement un travail non comptabilisé, et il n’est pas particulièrement gratifiant. Au mieux, votre application finira probablement par fonctionner de la même manière que votre environnement local. Comment pouvons-nous améliorer cette partie inévitable de notre travail ? Eh bien, nous pourrions l’automatiser.
Paramiko et SCP sont deux bibliothèques Python que nous pouvons utiliser ensemble pour automatiser les tâches que nous voudrions exécuter sur un hôte distant, comme le redémarrage des services, les mises à jour ou la saisie des fichiers journaux. Nous allons voir à quoi ressemble l’écriture de scripts avec ces bibliothèques. Je vous préviens : ce tutoriel contient une quantité non négligeable de code, ce qui a tendance à m’exciter suffisamment pour contraindre les autres à participer à mes épisodes maniaques de tutoriel de code. Si vous commencez à vous sentir perdu, le repo complet se trouve ici :
Configuration des clés SSH
Pour authentifier une connexion SSH, nous devons configurer une clé SSH RSA privée (à ne pas confondre avec OpenSSH). Nous pouvons générer une clé à l’aide de la commande suivante :
Cette commande nous invitera à fournir un nom pour notre clé. Donnez-lui le nom que vous voulez :
Puis, on vous demandera de fournir un mot de passe (n’hésitez pas à le laisser vide).
Maintenant que nous avons notre clé, nous devons la copier sur notre hôte distant. La façon la plus simple de le faire est d’utiliser ssh-copy-id
:
Vérification de notre clé SSH
Si vous souhaitez vérifier quelles clés vous possédez déjà, celles-ci peuvent être trouvées dans le répertoire .ssh
de votre système :
Nous recherchons des clés qui commencent par l’en-tête suivant :
N’hésitez pas à faire de même sur votre FPS.
Démarrer notre script
Installons nos bibliothèques. Lancez l’environnement virtuel que vous préférez et laissez-les s’exprimer :
Juste une dernière chose avant d’écrire du code Python significatif ! Créez un fichier de configuration pour contenir les variables dont nous aurons besoin pour nous connecter à notre hôte. Voici les bases de ce dont nous avons besoin pour entrer dans notre serveur:
- Hôte : L’adresse IP ou l’URL de l’hôte distant auquel nous essayons d’accéder.
- Nom d’utilisateur : Il s’agit du nom d’utilisateur que vous utilisez pour vous connecter en SSH à votre serveur.
- Passphrase (facultatif) : Si vous avez spécifié une phrase de passe lorsque vous avez créé votre clé ssh, indiquez-la ici. N’oubliez pas que la phrase de passe de votre clé SSH n’est pas la même que le mot de passe de votre utilisateur.
- Clé SSH : Le chemin de fichier de la clé que nous avons créée plus tôt. Sur OSX, celles-ci vivent dans le dossier ~/.ssh de votre système. La clé SSH que nous ciblons doit avoir une clé d’accompagnement avec une extension de fichier .pub. Il s’agit de notre clé publique ; si vous suiviez plus tôt, elle devrait déjà avoir été générée pour vous.
Si vous essayez de télécharger des fichiers depuis votre hôte distant, vous devrez inclure deux autres variables :
- Pathique distant : Le chemin vers le répertoire distant que nous cherchons à cibler pour les transferts de fichiers. Nous pouvons soit télécharger des choses vers ce dossier, soit en télécharger le contenu.
- Path local : Même idée que ci-dessus, mais à l’inverse. Pour notre commodité, le chemin local que nous utiliserons est simplement /data, et contient des images de gifs de renards mignons.
Maintenant nous avons tout ce dont nous avons besoin pour faire un fichier config.py respectable:
Création d’un client SSH
Nous allons créer une classe appelée RemoteClient pour gérer les interactions que nous aurons avec notre hôte distant. Avant d’être trop fantaisistes, commençons par instancier la classe RemoteClient avec les variables que nous avons créées dans config.py:
Rien d’impressionnant jusqu’ici : nous avons juste défini quelques variables et les avons passées dans une classe inutile. Passons à la vitesse supérieure sans quitter notre constructeur :
Nous avons ajouté trois nouvelles choses à instancier avec notre classe :
-
self.client
: self.client servira finalement d’objection de connexion dans notre classe, de manière similaire à la façon dont vous avez traité une terminologie commeconn
dans les bibliothèques de bases de données. Notre connexion seraNone
jusqu’à ce que nous nous connections explicitement à notre hôte distant. -
self.scp
: similaire à self.client, mais gère exclusivement les connexions pour le transfert de fichiers. -
self.__upload_ssh_key()
n’est pas une variable, mais plutôt une fonction à exécuter automatiquement chaque fois que notre client est instancié. L’appel de__upload_ssh_key()
indique à notre objet RemoteClient de vérifier les clés ssh locales dès leur création afin que nous puissions essayer de les transmettre à notre hôte distant. Sinon, nous ne serions pas en mesure d’établir une connexion du tout.
Transférer les clés SSH vers un hôte distant
Nous avons atteint la section de cet exercice où nous devons frapper un code passe-partout dévastateur et inglorieux. C’est typiquement là que les individus émotionnellement inférieurs succombent à l’obscurité pure et simple de la compréhension des clés SSH et du maintien des connexions. Ne vous y trompez pas : l’authentification et la gestion des connexions à tout ce qui est programmé sont excessivement ennuyeuses… à moins que votre guide touristique ne soit un orfèvre des mots, qui vous protège avec amour dans une obscurité périlleuse. Certaines personnes appellent ce post un tutoriel. J’ai l’intention de l’appeler de l’art.
RemoteClient va commencer avec deux méthodes privées : __get_ssh_key()
et __upload_ssh_key()
. La première va chercher une clé publique stockée localement, et en cas de succès, la seconde va livrer cette clé publique à notre hôte distant comme un rameau d’olivier d’accès. Une fois qu’une clé publique créée localement existe sur une machine distante, cette machine nous confiera à jamais nos demandes de connexion : aucun mot de passe n’est requis. Nous inclurons une journalisation appropriée en cours de route, au cas où nous rencontrerions des problèmes :
_get_ssh_key()
est assez simple : il vérifie qu’une clé SSH existe au chemin que nous avons spécifié dans notre config à utiliser pour se connecter à notre hôte. Si le fichier existe effectivement, nous définissons joyeusement notre variableself.ssh_key
, de sorte que cette clé puisse être téléchargée et utilisée par notre client à partir de maintenant. Paramiko nous fournit un sous-module appelé RSAKey pour gérer facilement tout ce qui concerne les clés RSA, comme l’analyse d’un fichier de clé privée en une authentification de connexion utilisable. C’est ce que nous obtenons ici :
Si notre clé RSA était un non-sens incompréhensible au lieu d’une vraie clé, la SSHException de Paramiko l’aurait attrapé et aurait soulevé une exception très tôt expliquant justement cela. L’utilisation appropriée de la gestion des erreurs d’une bibliothèque élimine une grande partie de la devinette de « ce qui s’est mal passé », en particulier dans des cas comme celui où il y a un potentiel pour de nombreuses inconnues dans un espace de niche avec lequel aucun d’entre nous ne déconne souvent.
_upload_ssh_key()
est le moment où nous obtenons de coincer notre clé SSH dans la gorge de notre serveur distant tout en criant, « LOOK ! TU PEUX ME FAIRE CONFIANCE POUR TOUJOURS MAINTENANT ! » Pour ce faire, je fais un peu « vieille école » en passant les commandes bash via la os.system
de Python. À moins que quelqu’un me signale une approche plus propre dans les commentaires, je vais supposer que c’est la façon la plus badass de gérer le passage des clés à un serveur distant.
La façon standard non-Python de passer des clés à un hôte ressemble à ceci :
C’est précisément ce que nous accomplissons dans notre fonction en Python, qui ressemble à ceci :
system(f'ssh-copy-id -i {self.ssh_key_filepath} \ {self.user}@{self.host}>/dev/null 2>&1')
Je suppose que vous ne me laisserez pas glisser ce /dev/null 2>&1
bit par vous ? Bien. Si vous devez le savoir, voici un gars sur StackOverflow qui l’explique mieux que moi :
>
est pour la redirection/dev/null
est un trou noir où toute donnée envoyée, sera rejetée.2
est le descripteur de fichier pour l’erreur standard.>
est pour la redirection.&
est le symbole du descripteur de fichier (sans lui, le1
suivant serait considéré comme un nom de fichier).1
est le descripteur de fichier pour Standard O.
Donc, en gros, nous disons à notre serveur distant que nous lui donnons quelque chose, et il est du genre » où est-ce que je mets ce truc ? « , ce à quoi nous répondons » nulle part dans l’espace physique, car ce n’est pas un objet, mais plutôt un symbole éternel de notre amitié « . Notre hôte distant est alors inondé de gratitude et d’émotion, car oui, les ordinateurs ont des émotions, mais nous ne pouvons pas être dérangés par cela pour le moment.
Connexion à notre client
Nous allons ajouter une méthode à notre client appelée connect()
pour gérer la connexion à notre hôte :
Décomposons cela:
-
client = SSHClient()
prépare le terrain pour créer un objet représentant notre client SSH. Les lignes suivantes vont configurer cet objet pour le rendre plus utile. -
load_system_host_keys()
demande à notre client de rechercher tous les hôtes auxquels nous nous sommes connectés dans le passé en regardant le fichier known_hosts de notre système et en trouvant les clés SSH que notre hôte attend. Nous ne nous sommes jamais connectés à notre hôte dans le passé, nous devons donc spécifier notre clé SSH explicitement. -
set_missing_host_key_policy()
indique à Paramiko ce qu’il faut faire en cas de paire de clés inconnue. Ceci attend une « politique » intégrée à Paramiko, à laquelle nous allons spécifierAutoAddPolicy()
. Définir notre politique à « auto-add » signifie que si nous tentons de nous connecter à un hôte non reconnu, Paramiko ajoutera automatiquement la clé manquante localement. -
connect()
est la méthode la plus importante de SSHClient (comme vous pouvez l’imaginer). Nous sommes enfin en mesure de passer notre hôte, notre utilisateur et notre clé SSH pour obtenir ce que nous attendions tous : une glorieuse connexion SSH à notre serveur ! La méthodeconnect()
permet une tonne de flexibilité via une vaste gamme d’arguments de mots-clés optionnels également. Il se trouve que j’en passe quelques-uns ici : définir look_for_keys àTrue
donne à Paramiko la permission de regarder dans notre dossier ~/.ssh pour découvrir les clés SSH par lui-même, et définir timeout fermera automatiquement les connexions que nous oublierons probablement de fermer. Nous pourrions même passer des variables pour des choses comme le port et le mot de passe, si nous avions choisi de nous connecter à notre hôte de cette façon.
Déconnexion
Nous devrions fermer les connexions à notre hôte distant dès que nous avons fini de les utiliser. Ne pas le faire ne serait pas nécessairement désastreux, mais j’ai eu quelques cas où suffisamment de connexions suspendues finissaient par maximiser le trafic entrant sur le port 22. Indépendamment du fait que votre cas d’utilisation puisse considérer un redémarrage comme un désastre ou un léger désagrément, fermons nos fichues connexions comme des adultes, comme si nous nous essuyions les fesses après avoir fait caca. Quelle que soit votre hygiène de connexion, je préconise de définir une variable de timeout (comme nous l’avons vu précédemment). Bref, voilà :
Fun fact : le réglage de self.client.close()
met en fait self.client
à égalité avec None
, ce qui est utile dans les cas où vous pourriez vouloir vérifier si une connexion est déjà ouverte.
Exécution de commandes Unix
Nous avons maintenant une merveilleuse classe Python qui peut trouver des clés RSA, se connecter et se déconnecter. Il lui manque cependant la capacité de faire, eh bien, quoi que ce soit d’utile.
Nous pouvons corriger cela et enfin commencer à faire des » trucs » avec une toute nouvelle méthode pour exécuter des commandes, que je vais judicieusement baptiser execute_commands()
(c’est exact, » commandes » comme dans potentiellement plus qu’une, nous en parlerons dans un moment). Le travail de fond de tout cela est effectué par la méthode intégrée du client Paramiko exec_command()
, qui accepte une seule chaîne de caractères comme commande et l’exécute :
La fonction que nous venons de créer execute_commands()
attend une liste de chaînes à exécuter comme commandes. C’est en partie pour des raisons de commodité, mais c’est aussi parce que Paramiko n’exécutera aucun changement d' »état » (comme le changement de répertoire) entre les commandes, donc chaque commande que nous passons à Paramiko doit supposer que nous travaillons à partir de la racine de notre serveur. J’ai pris la liberté de passer trois de ces commandes comme ceci:
Je peux afficher le contenu d’un répertoire en enchaînant cd path/to/dir && ls
, mais l’exécution de cd path/to/dir
suivie de ls
aboutirait au néant car ls
la deuxième fois renvoie la liste des fichiers de la racine de notre serveur.
Vous remarquerez que client.exec_command(cmd)
renvoie trois valeurs plutôt qu’une : cela peut être utile pour voir quelle entrée a produit quelle sortie. Par exemple, voici les journaux complets pour l’exemple que j’ai fourni où j’ai passé trois commandes à remote.execute_commands()
:
De belles choses ici. Maintenant, vous pouvez voir quels sites sont sur mon serveur, quels bots me spamment et combien de processus de nœuds je fais tourner.
Je ne veux pas perdre beaucoup plus de temps sur l’art d’exécuter des commandes, mais il faut mentionner la présence de la raison pour laquelle nous appelons stdout.channel.recv_exit_status()
après chaque commande. Le fait d’attendre que recv_exit_status()
revienne après l’exécution de client.exec_command()
force nos commandes à être exécutées de manière synchrone, sinon il y a de fortes chances que notre machine distante ne soit pas en mesure de déchiffrer les commandes aussi rapidement que nous les transmettons.
Chargement (et téléchargement) de fichiers via SCP
SCP désigne à la fois le protocole de copie de fichiers vers des machines distantes (secure copy protocol) ainsi que la bibliothèque Python, qui l’utilise. Nous avons déjà installé la bibliothèque SCP, alors importez cette merde.
Les bibliothèques SCP et Paramiko se complètent pour rendre le téléchargement via SCP super facile. SCPClient() crée un objet qui attend « transport » de Paramiko, que nous fournissons avec self.conn.get_transport()
. La création d’une connexion SCP s’inspire de notre client SSH en termes de syntaxe, mais ces connexions sont distinctes. Il est possible de fermer une connexion SSH et de laisser une connexion SCP ouverte, alors ne le faites pas. Ouvrez une connexion SCP comme ceci :
Télécharger un seul fichier est ennuyeux, alors téléchargeons plutôt un répertoire entier de fichiers. bulk_upload()
accepte une liste de chemins de fichiers, puis appelle __upload_single_file()
Notre méthode s’attend à recevoir deux chaînes de caractères : la première étant le chemin local de notre fichier, et la seconde étant le chemin du répertoire distant vers lequel nous souhaitons télécharger.
La méthode put()
deSCP va télécharger un fichier local vers notre hôte distant. Cela remplacera les fichiers existants portant le même nom s’il se trouve qu’ils existent à la destination que nous avons spécifiée. C’est tout ce qu’il faut !
Téléchargement de fichiers
L’homologue de la méthode put()
de SCP est la méthode get()
:
Notre grand et beau script
Nous avons maintenant une classe Python malade pour gérer SSH et SCP avec un hôte distant… mettons-la au travail ! Le snippet suivant est un moyen rapide de tester ce que nous avons construit jusqu’à présent. En bref, ce script recherche un dossier local rempli de fichiers (dans mon cas, j’ai rempli le dossier avec des fox gifs 🦊).
Voyez comme il est facile de créer un main.py qui gère des tâches complexes sur des machines distantes grâce à notre classe RemoteClient :
Voici la sortie de notre fonction de téléchargement :
Cela a marché ! Vous ne me croyez pas ? Pourquoi ne pas vérifier par nous-mêmes en exécutant remote.execute_commands()
?
Voilà. Tout droit sorti de la bouche du renard.
Take It And Run With It
C’est ici que je voudrais prendre un moment pour vous remercier tous, et m’excuser que vous soyez encore là. J’ai fait le serment d’arrêter de poster des tutoriels de plus de deux mille mots, et celui-ci cherche à pousser cinq mille mots d’absurdité. Je vais travailler là-dessus. Nouvelle année, nouveau moi.
Pour vous faciliter la tâche, j’ai téléchargé la source de ce tutoriel sur Github. N’hésitez pas à prendre cela et à courir avec ! Pour terminer, je vous laisse avec la viande et les pommes de terre de la classe Client que nous avons mise en place :
Le code source complet de ce tutoriel se trouve ici :
SiteGithubTwitter
Ingénieur avec une crise d’identité permanente. Casse tout avant d’apprendre les meilleures pratiques. Tout à fait normal et émotionnellement stable.
.