Skip to content

Asyncssh

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.

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_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’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 


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

Et avec un petit rebond c’est tout aussi simple !

Section titled “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 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 :

  • 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 :

Terminal window
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 :

  1. Connexion à la frontal du cluster (besoin de deux rebonds dans mon cas mais facile avec asyncssh 😜)
  2. Ecriture sur le filesystem du cluster d’un script de soumission pour le gestionnaire de job (slurm)
  3. Soumission du job slurm
  4. 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é
  5. Connexion ssh au noeud du cluster oĂč mon job tourne
  6. Création du tunnel ssh pour le port forwarding
  7. C’est bon il n’y a plus qu’à ouvrir son navigateur 📓