asyncssh : automatiser SSH et tunnels en Python
Introduction
Section intitulée « Introduction »Dans cet article je vais essayer d’illustrer simplement comment utiliser le module asyncssh de Python pour :
- Scripter des actions distantes
- Etablir un tunnel ssh permettant de faire du port forwarding
Alors si vous vous êtes déjà confronté à ces problématiques vous avez peut-être déjà croisé la route du module Python paramiko. Alors personellement je ne recommande pas l’usage de paramiko (en gros j’ai perdu un temps monstrueux avec paramiko, c’est peut-être ma faute attention, notamment lorsque je devais faire des portages d’application entre windows 7 / windows 10 et Linux) et du coup depuis ce jour je suis passé à asyncssh et ça fonctionne parfaitement.
La seule difficulté avec asyncssh vient du async … En effet c’est un module destiné à être utilisé dans un mode de programmation asynchrone avec asyncio il faut donc être un peu à l’aise avec les async/await dans Python 🤨 ! Si ce n’est pas le cas je vous invite à aller jeter un oeil sur le mooc Python 3 en semaine 8 vous avez une magnifique introduction à la programmation asynchrone.
Connection à un serveur
Section intitulée « Connection à un serveur »La première étape est bien évidemment la connection au serveur ssh. Pour cela on passe par la fonction connect de asyncssh qui prend entre autre arguments :
- Un nom d’utilisateur
username - Une authentification avec deux solutions possibles :
- mot de passe classique dans ce cas il suffit de spécifier l’argument
password - Clé ssh et dans ce cas il faut spécifier l’argument
client_keysqui est une liste contenant la où les clés ssh ainsi que l’argumentpassphrasequi est donc la passphrase de la clé ssh.
- mot de passe classique dans ce cas il suffit de spécifier l’argument
client = await asyncssh.connect( server, username=username, password=password)Une fois connecté, exécuter des commandes !
Section intitulée « Une fois connecté, exécuter des commandes ! »Une fois la connection avec notre serveur établie nous pouvons commencer à faire des choses intéressantes (enfin ça dépend de vous ça). Mais pour commencer la méthode essentielle à connaître est la méthode run qui comme son nom l’indique va nous permettre d’exécuter des commandes à distance. Dans sa version basique la méthode run prend une chaîne de caractère par exemple si je veux lancer la commande ls ~/Documents et bien c’est ce qu’on fait !
out = await client.run("ls ~/Documents")Alors le out que l’on récupère est une instance de asyncssh.SSHCompletedProcess on s’en fout un peu tout ce qu’il faut savoir c’est qu’il y a dans ce truc 3 attributs qui sont utiles :
out.returncodesi différent de0y a un truc qui a planté quelque part !out.stdoutla sortie standardout.stderrla sortie d’erreur
Alors juste vous le savez peut-être mais suivant la commande que vous exécutez ne regardez pas que dans le stderr si vous avez des erreurs car il existe tout un tas de logiciels qui écrivent les erreurs dans le stdout et certain ont même le génie d’ecrire des infos dans le stderr …
Ou bien lire et écrire des fichiers
Section intitulée « Ou bien lire et écrire des fichiers »De la même manière il est possible une fois la connection établie de lire et/ou écrire des fichiers via sftp si vous voulez tout savoir. Pour cela il suffit de créer un client sftp à partir du client ssh une fois la connexion établie.
async with client.start_sftp_client() as sftp: ## Read, write file, create directory, ... ## all on remote file system passUne fois le client sftp créé nous pouvons utiliser tout une ribambelle (oui j’aime bien ce mot) de méthodes par exemple :
sftp.exists( remote_path )sftp.mkdir( remote_path )sftp.open( fname, mode)sftp.chmod( remote_path, mode )- …
Pour la liste complète des méthodes RTFM
Et avec un petit rebond c’est tout aussi simple !
Section intitulée « Et avec un petit rebond c’est tout aussi simple ! »Alors le truc génial 🤩 de la fonction asyncssh.connect est qu’elle accepte un argument optionnel tunnel. Cet argument permet de spécifier un client ssh déjà connecté. En gros si pour accéder à la machine machine-C depuis machine-A on doit forcément passer par la machine machine-B et bien avec cet argument tunnel ce sera super simple il suffira de faire un truc du genre :
proxy = await asyncssh.connect( "machine-B", username=username, password=password)client = await asyncssh.connect( "machine-C", username=username, password=password, tunnel=proxy)Il existe également une version avec context manager qui prends la forme suivante :
async with asyncssh.connect( "machine-B", username=username, password=password) as proxy: async with asyncssh.connect( "machine-C", username=username, password=password, tunnel=proxy) as client: # do something crazy with my ssh clientPort forwarding et tunnel ssh
Section intitulée « Port forwarding et tunnel ssh »Pour finir je vais vous montrer l’autre truc super sympa de asyncssh à savoir que l’on peut depuis un boût de Python construire un tunnel ssh qui va nous permettre de faire du port forwarding. Deux trucs sympa :
- Le même code va fonctionner sous Linux, Mac et Windows donc pas de prise de tête
- On peut comme ça automatiser et surtout cacher plein d’opérations aux end-users. Car je sais pas vous mais moi si je demande aux gens de faire un tunnel ssh à la main avec OpenSSH ma boite mail va exploser avec les “ça marche pas” !!
async with asyncssh.connect(hostname, username=username) as client: listener = await client.forward_local_port("", local_port, remote_ip, remote_port) await listener.wait_closed()Le Python précédent est équivalent à la commande OpenSSH suivante pour les connaisseurs :
ssh -L local_port:remote_ip:remote_port username@hostnameBon alors par contre le code Python précédent est bloquant, c’est-à-dire qu’une fois le tunnel créé on ne peut rien faire d’autre ce qui peut s’avérer génant. Une solution assez simple pour ne pas se retrouver bloqué est simplement de déléguer la création du tunnel à un process séparé de notre process principal. Pour cela un petit coup de multiprocessing et ça roule !
def standalone_tunnel(hostname, username, password, local_post, remote_port, remote_ip ): async def do_job(hostname, username, password, local_post, remote_port, remote_ip ): async with asyncssh.connect(hostname, username=username, password=password) as client: listener = await client.forward_local_port("", local_port, remote_ip, remote_port) await listener.wait_closed() try: asyncio.get_event_loop().run_until_complete(do_job(hostname, username, password, local_post, remote_port, remote_ip)) except Exception as e: print("Port Forwarding Error : " + str(e))
import multiprocessingp = multiprocessing.Process(target=standalone_tunnel, args=(hostname, username, password, local_post, remote_port, remote_ip))p.start()Conclusion
Section intitulée « Conclusion »Je viens de vous montrer en mode express comment le module asyncssh de Python peut très facilement nous permettre de scripter des actions nécessitant des connections à des serveurs distants via ssh. Il y a évidemment plein d’applications possibles à cela mais je vais juste vous en présenter deux que j’ai eu à réaliser.
La première application a été la mise en place d’un outil pour faire de la visualisation distante. En effet à partir de Mars 2020 et une certaine pandémie mondiale il a fallu dans le labo où je suis que les utilisateurs puissent travailler à distance, notamment avec des softs disposant d’interface graphique un peu lourde. Pour cela j’ai développé une solution assez simple où chaque utilisateur lance sur un serveur du labo un serveur VNC et ensuite via un tunnel ssh (qui forward le port VNC de l’utilisateur vers un port local de son portable en faisant un rebond via la passerelle ssh) peut accéder à sa session graphique via le client VNC installé localement. Et bien évidemment si je demande aux utilisateurs de faire tout ça à la main 🤯 donc j’ai packagé tout ça dans une petite application Python à base de asyncssh et PyQt pour le graphique et en deux clics les utilisateurs peuvent se connecter !
La seconde application que j’ai pu avoir de asyncssh est le développement d’un utilitaire Python pour lancer un jupyter notebook sur un noeuds de calcul du cluster et faire le port forwarding qui va bien pour que l’utilisateur puisse ensuite ouvrir son notebook et travailler dans son navigateur en local. Pour cela le process est assez simple :
- Connexion à la frontal du cluster (besoin de deux rebonds dans mon cas mais facile avec asyncssh 😜)
- Ecriture sur le filesystem du cluster d’un script de soumission pour le gestionnaire de job (slurm)
- Soumission du job slurm
- Une fois le job slurm commencé, ouverture des fichiers de log du job pour récupérer le nom sur lequel le job est parti et le port sur lequel jupyter a démarré
- Connexion ssh au noeud du cluster où mon job tourne
- Création du tunnel ssh pour le port forwarding
- C’est bon il n’y a plus qu’à ouvrir son navigateur 📓
Pour aller plus loin :