Dans cet article je vais essayer d'illustrer simplement comment utiliser le module asyncssh de Python pour :
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.
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 :
username
password
client_keys
qui est une liste contenant la où les clés ssh ainsi que l'argument passphrase
qui est donc la passphrase de la clé ssh.client = await asyncssh.connect( server, username=username, password=password)
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.returncode
si différent de 0
y a un truc qui a planté quelque part ! out.stdout
la sortie standard out.stderr
la sortie d'erreurAlors 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
...
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
pass
Une 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
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 client
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 :
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@hostname
Bon 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 multiprocessing
p = multiprocessing.Process(target=standalone_tunnel, args=(hostname, username, password, local_post, remote_port, remote_ip))
p.start()
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 :