Fabric, le couteau suisse de l’automatisation (Publié dans Linux Pratique 122)

Posted by

Fabric est une bibliothèque Python et une interface en ligne de commande facilitant l’utilisation de SSH, que ce soit pour des applications ou dans le but d’automatiser certaines tâches répétitives d’administration système. La grande force de Fabric est d’être particulièrement simple à utiliser.

1 Fabric, késako?

Fabric, c’est tout le contraire des outils DevOps modernes tels que Salt ou Ansible pour ne citer que ceux qui opèrent au dessus du protocole SSH. Ce n’est pas un langage de haut niveau avec de nombreux modules faisant abstraction des couches systèmes inférieures. Point d’idempotence, de templates ou toutes les fonctionnalités qui font des outils de gestion de configuration le saint Graal de l’orchestration et pourtant je suis un fervent partisant de Puppet.
Pour autant, lorsqu’il s’agit d’interagir rapidement sur un grand nombre de serveurs, Puppet n’est pas la solution la plus efficace et cluster SSH encore moins. L’adoption de Fabric dans mon entreprise lorsque le produit est devenu mature a radicalement changé notre façon d’administrer nos nombreux systèmes en parallèle de l’utilisation de Puppet. Il m’est ainsi arrivé fréquemment d’utiliser Fabric pour bootstraper des agents Puppet.
Lorsque l’on écrit du code Fabric, on travaille avec les primitives natives de SSH et le shell et on fait moins qu’effleurer du Python. C’est cette simplicité qui fait de Fabric un véritable couteau suisse, utilisable par n’importe qui.
Comme Ansible donc, il se base sur SSH. Il n’y a donc aucun agent particulier à installer. L’authentification est gérée par SSH, on peut donc se logger par login/mot de passe ou par clé SSH. Sudo est géré pour la délégation de droits ainsi que les primitives de transfert de fichiers.

2 On en est à quelle version déjà ?

Fabric première version est la version historique, disponible dans les dépôts ubuntu 14.04 et 16.04 qui a comme grand défaut de n’être compatible que jusqu’à Python 2.7 qui n’est plus supporté par la fondation Python depuis un an mais peut être encore par votre distribution.
Fabric2 est une réécriture majeure qui supporte Python3 dont le but avoué des développeurs est d’être plus facilement maintenable. Cependant, cette nouvelle version apporte une syntaxe largement modifiée et dont il manque encore de nombreuses fonctionnalités pour un sysadmin comme les groupes de machine. Mais pour un développeur qui cherche une surcouche à plus haut niveau que le shell pour déployer une application ou qui a besoin d’une intégration SSH avec Django au dessus de ce que fourni le framework paramiko, le produit est tout à fait adapté.
Enfin, ma version de prédilection est Fabric3. C’est un fork amical de Fabric1 conçu pour fonctionner avec Python > 3.4. L’installation passe par le système de packages Python pip, tout ce qu’il y a de plus classique :

pip3 install Fabric3

3 Tout commence par un fabfile

Le lancement de fabric se fait par la commande fab qui cherche ses instructions dans un fichier fabfile.py dans le répertoire courant. Malgré l’extension du fichier, les connaissances requis en Python sont très faibles pour écrire un fabfile.
Voyons donc un premier fabfile avec une seule fonction :
from fabric.api import abort, cd, env, get, hide, hosts, local, prompt, \
put, require, roles, run, runs_once, settings, show, sudo, warn, open_shell

def host_details():
run('lsb_release -a')

Et allons exécuter notre fonction sur l’un de nos serveurs :

fab -H ldap1 host_details

[ldap1] Executing task 'host_details'
[ldap1] run: lsb_release -a
[ldap1] Login password for 'julien':
[ldap1] out: No LSB modules are available.
[ldap1] out: Distributor ID: Ubuntu
[ldap1] out: Description: Ubuntu 20.04 LTS
[ldap1] out: Release: 20.04
[ldap1] out: Codename: focal
[ldap1] out:

Quand il s’agit de simplement lancer une commande à usage unique, il est possible d’appeler directement fabric avec en paramètre notre commande sans avoir besoin de modifier le fabfile.

fab -H ldap2,ldap1 -- lsb_release -a
[ldap2] Executing task ''
[ldap2] run: lsb_release -a
[ldap2] Login password for 'julien':
[ldap2] out: No LSB modules are available.
[ldap2] out: Distributor ID: Ubuntu
[ldap2] out: Description: Ubuntu 18.04.4 LTS
[ldap2] out: Release: 18.04
[ldap2] out: Codename: bionic

[ldap1] Executing task ''
[ldap1] run: lsb_release -a
[ldap1] out: No LSB modules are available.
[ldap1] out: Distributor ID: Ubuntu
[ldap1] out: Description: Ubuntu 20.04 LTS
[ldap1] out: Release: 20.04
[ldap1] out: Codename: focal

4 On mutualise avec les rôles

Je parlais d’administration d’un parc de serveur donc ma commande fabric elle est jolie, elle me permet de spécifier plusieurs serveurs en utilisant fab -H ldap1,ldap2 mais ce n’est pas très pratique à utiliser. Et puis surtout je n’ai pas besoin de connaître et saisir le nom de tous les serveurs lorsque je déploie une nouvelle configuration SNMP, en particulier lorsque le parc est mouvant.
Pour cela, Fabric expose une variable d’environnements env.roledefs, en pratique un dictionnaire permettant d’organiser nos serveurs par rôles. Bien entendu, un serveur peut se retrouver dans plusieurs rôles mais il n’est pas possible d’utiliser nos définitions de rôles sous la forme imbriquée, d’union ou d’intersection.

env.roledefs = {
'dns': ['ns1', 'ns2'],
'ldap': ['ldap1', 'ldap2'],
'world': ['ldap1', 'ldap2', 'ns1', 'ns2'],
}

Comme pour les hôtes, on peut au besoin lancer une commande sur plusieurs rôles en les séparant par une virgule. Et quand le nombre de machines devient trop grand, il est même possible de paralléliser l’exécution avec -P.

fab -R ldap host_details

[ldap1] Executing task 'host_details'
[ldap1] run: lsb_release -a
[ldap1] Login password for 'julien':
[ldap1] out: No LSB modules are available.
[ldap1] out: Distributor ID: Ubuntu
[ldap1] out: Description: Ubuntu 20.04 LTS
[ldap1] out: Release: 20.04
[ldap1] out: Codename: focal
[ldap1] out:
[ldap2] Executing task 'host_details'
[ldap2] run: lsb_release -a
[ldap2] out: No LSB modules are available.
[ldap2] out: Distributor ID: Ubuntu
[ldap2] out: Description: Ubuntu 18.04.4 LTS
[ldap2] out: Release: 18.04
[ldap2] out: Codename: bionic

Personnellement, j’ai tendance à découper mes rôles par profil de serveur (DB, LDAP, SMTP,etc…), par environnement (recette, production…) et créer un rôle qui contient l’ensemble des serveurs.

5 Tout SSH dans un fabfile

Pour le premier exemple nous avons simplement utilisé la primitive run mais fabric en dispose d’une multitude d’autres pour couvrir l’essentiel des cas d’usages d’un sysadmin :
– sudo : forcément lorsqu’on parle d’administration systèmes se pose la question des droits. Pas question de tout faire en root, d’ailleurs le compte est probablement désactivé à la connexion SSH. L’utilisation se fait de la même façon que la commande run.
sudo(‘systemctl restart snmpd.service’)
Et il est aussi possible de préciser l’utilisateur sous lequel exécuter la commande en sudo :

sudo('/opt/zimbra/bin/zmlocalconfig ldap_host',user="zimbra")

– Transférer des fichiers avec get et put :

put('snmpd.conf','/etc/snmp/snmpd.conf',use_sudo=True,mirror_local_mode=True)

ou à l’inverse :

get('/etc/resolv.conf','resolv.conf')

mirror_local_mode, optionnel, est bien pratique pour répliquer les permissions locales sur la machine distante. Il est tout autant possible de spécifier les permissions avec mode=’0640’ ou ne rien indiquer.
– Redémarrer un serveur : reboot(), par défaut avec un timer de 120 secondes.
– Interraction avec le sysadmin : prompt :

def installpkg():
  pkg = prompt('Lequel monseigneur?')
  sudo('apt -y install '+pkg)

– Les contextes : ils permettent de redéfinir une partie de l’environnement pour fournir un contexte d’exécutions au commandes qui seront lancées.

def run_commande():
  with shell_env(PATH='/opt/zimbra/bin:$PATH'):
  run("commande")

lcd modifie le chemin de la machine sur laquelle le fabfile est exécuté et cd la machine sur laquelle on se connecte.

def update_git() :
  with cd("/srv/project"):
  run("git pull")

6 Environnement

Fabric propose également certaines dispositions pour interragir avec les variables d’environnement. De manière absolument pas sécurisée, les variables env.user et env.password permettent d’indiquer les identifiants de connexion de manière globale dans le fabfile, la dernière chose à montrer à son RSSI donc.
Néanmoins, d’autres aspects de l’environnement ont toute leur place sur un grand parc. Notamment, si je veux collecter tous les fichiers /etc/resolv.conf de mes serveurs pour les auditer, je peux facilement les récupérer localement nommés de manière appropriée :
def get_resolvconf():
get(‘/etc/resolv.conf’,’/tmp/resolv.conf-+env.host)
8 Un exemple dans le monde réel
Pour ne pas finir sur une bête liste de commandes, voici ce que nous donnerait un script fabric pour déployer un service snmp avec un fichier de configuration local. Il y a de fortes chances que du snmp soit standardisé sur un parc de machines et l’utilisation d’un outil d’automatisation tel que Fabric a toute sa place.

from fabric.api import abort, cd, env, get, hide, hosts, local, prompt, \
put, require, roles, run, runs_once, settings, show, sudo, warn, open_shell

env.roledefs = {
'dns': ['ns1', 'ns2'],
'ldap': ['master', 'slave'],
'world': ['ldap1', 'ldap2', 'ns1', 'ns2'],
}

def yumcleanup():
  sudo('yum clean all')

def inst_snmp():
  sudo('yum install net-snmp')
  sudo('systemctl enable snmpd.service')

def conf_snmp():
  put('snmpd.conf','/etc/snmp/snmpd.conf',use_sudo=True,mode=’0600’)
  sudo('systemctl restart snmpd.service')

def deploy_snmp():
  yumcleanup()
  inst_snmp()
  conf_snmp()

Il suffit ensuite d’appeler la commande deploy_snmp pour déclencher la configuration du SNMP sur les serveurs désirés. Et en quelques lignes de Python de plus, il est possible de rendre les commandes neutres sur le système de packages utilisé par la distribution.

Conclusion

Fabric n’est plus tout jeune dans le monde Linux et OpenSource désormais. Bien que largement dépassé par d’autres solutions autour de SSH comme Salt ou Ansible sur le terrain des fonctionnalités, ces logiciels ne doivent pas être opposés.
Je considère qu’un outil simple garde son intérêt pour sa souplesse et sa facilité d’utilisation. Il est en effet bien plus simple d’écrire une fonction de quelques lignes à exécuter sur plusieurs machines que l’équivalent Ansible, surtout quand c’est du code jetable.

Leave a Reply

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *