Auteur : PEB <becue@crans.org>
Date : 09/03/2015
Licence : GPLv3

What the fuck is happening?
===========================

Trigger est une sorte de librairie de remplacement de generate et des services
dans la base LDAP, qui fonctionnent avec bien trop de délai.

Trigger est le fruit d'une longue et intelligente (quelle modestie) réflexion,
et donc nous allons ici décrire son fonctionnement.

Mise à jour LDAP : the fuck is happening?
=========================================

Le binding envoit un tuple contenant en première entrée un hash, en deuxième entrée
un dico contenant les attributs avant modif par le binding, en troisième entrée un
dico contenant les attributs après modif, en quatrième entrée des données additionnelles
(inchangées durant tout le processing).

Le hash est utilisé pour marquer de façon « unique » un changement dans la base
LDAP (la résultante d'un .save()), et permet dans la suite de se repérer dans
les différentes étapes de régénération des services.

Documentation succincte de trigger
==================================

Tous les fichiers sont renseignés depuis /usr/scripts.

 * gestion/trigger/trigger.py est un script python, dont le rôle est de
   régénérer ponctuellement des services spécifiques, ou de tourner comme démon
   et de recevoir les changements effectués dans la base LDAP.  Ce script se
   comporte de façon différente sur chaque serveur, en fonction des services qui
   tournent sur ceux-ci. Qu'il tourne en mode démon ou bien qu'il soit appelé
   ponctuellement, il commence par importer les services définis pour le serveur
   courant dans gestion/config/trigger.py.

   Ensuite, selon le type de commande qu'on lui a passé, soit il se met en
   écoute, soit il régénère un, ou tous les services (--service régénère le
   service, --all les régénère tous, --daemon ignore les autres arguments et
   place le programme en écoute).

 * gestion/trigger/services/ contient l'ensemble des services existants. Les
   services chargés sont listés dans gestion/config/trigger.py pour chaque hôte.
   Chacun des fichiers .py dans ce dossier contient  une méthode décorée par la
   fonction record_service qui est contenue dans gestion/trigger/host.py. Cette
   fonction prend en argument un booléen, la variable ack, qui spécifie sur une
   fois le service exécuté, un ack doit être envoyé vers le serveur RabbitMQ
   (avec la clef de routage trigger.ack). La méthode décorée doit porter le nom
   du service, qui est déterminé dans les parseurs (voir ci-après).

 * gestion/trigger/parsers/ contient l'ensemble des parseurs existants. Il
   faut **nécessairement** un fichier .py par service. Comme les fichiers dans
   gestion/trigger/services, ces fichiers .py doivent porter le nom des services
   auxquels ils font référence. Ils peuvent contenir plusieurs fonctions
   décorées avec la fonction record_parser contenue dans gestion/trigger/host.py
   record_parser prend une infinité d'arguments, qui sont les attributs LDAP
   dont la modification doit appeler les fonctions décorées. Ces fonctions
   peuvent aussi être décorées avec la méthode chaining du fichier
   gestion/trigger/host.py (il faut d'abord décorer par chaining).

   chaining prend un unique argument, à savoir la position dans l'ordre de
   régénération pour une modif LDAP donnée. Typiquement, si on crée un home à un
   utilisateur, on veut d'abord appeler le service qui crée le home
   physiquement, puis celui qui envoit un mail de bienvenue à l'adhérent. Le
   second dépendant du premier, il faut mettre un indice inférieur au premier.
   Une fois qu'on a listé les trucs à régénérer et qu'on a créé une relation
   d'ordre pour les opérations dépendantes, la liste des choses à faire est
   stockée dans EventTracker (une Factory définie dans
   gestion/trigger/services/event.py) via le hash des modifs, et elle est
   dépilée dans l'ordre des indices (de 0 vers ...).

   Les messages pour un indice donné sont envoyés, puis on attend que des
   retours soient envoyés (les services chaînés doivent donc obligatoirement
   envoyer des acks, mais en fait, on évitera de mettre False pour d'autres
   services que event et ack). Une fois tous les acks reçus, on exécute les
   opérations de l'indice suivant s'il y en a et ainsi de suite jusqu'à avoir
   parcouru toute la liste.

Ajouter un nouveau service
==========================

Pour ajouter un service, il faut créer un fichier adapté dans trigger/services/,
et un dans trigger/parsers/.  Le nom des fichiers doit être celui des services.
Il faut écrire des fonctions adaptées (le nom est libre), par exemple, pour un
parser :

{{{
@record_parser(lc_ldap.attributs.macAddress.ldap_name, lc_ldap.attributs.ipHostNumber.ldap_name)
@chaining(0) # chaining en dessous du record_parser.
def send_mac_ip(ob_id, body, diff):
}}}

ob_id est le hash des modifications body est le tuple composé des dicos des
attributs de l'objet LDAP modifié, celui avant, et celui après modifs.  Diff est
le diff calculé à la volée sur les données. Les données peuvent être redontantes
parfois. Les parseurs doivent *impérativement* retourner un tuple à deux
élements, le premier est le nom du service à régénérer (qui matche donc le nom
du fichier, ou celui de gestion/trigger/services/nom_du_service.py, ou selui de
la méthode décorée avec record_service), le second est l'ensemble des opérations
à faire. Chaque parseur ne peut préparer l'appel que d'une fonction.
record_parser modifie la sortie des parsers en (nom_service, operations,
position) en tenant compte de la position mentionnée dans chaining. Si chaining
n'est pas appelé, la position par défaut est 0.

Ainsi, send_mac_ip décorée en l'état retournerait (nom_service, operations, 1).
Le troisième élément du tuple est enlevé par le service event quand il récupère
les retours des parseurs.

Pour un service, voici un exemple :

{{{
@record_service() #équivalent à @record_service(ack=True) équivalent à
def dhcp(ob_id, operations=None):
}}}

operations contient la liste des operations (généralement, un dico, avec "add",
"update", et "delete" et des trucs à ajouter, mettre à jour ou retirer… Le nom
de la fonction doit correspondre au nom du service (ici, dhcp).

Il faut ensuite référencer le service dans config/trigger.py pour les serveurs
où il est important, et relancer trigger sur ces machines (au moins civet pour
le parseur, et les serveurs concernés par le service). Lors des tests, il ne
faut pas hésiter à passer trigger en debug dans le fichier config/trigger.py.
###Utiliser testing.sh (en rajoutant une variable d'env pour ça), stp. -- Daniel

Parmi les choses importantes, l'idéal est d'avoir des dépendances les plus
paresseuses possibles d'un point de vue évaluation. Ainsi, civet qui ne fait
qu'importer le fichier et utiliser les fonctions d'analyse listées dans
changes_trigger peut éviter de jouer avec ce qui ne le concerne pas.

Enfin, si vous avez des questions, posez-les avant, pas après.

Pour chaque service, une "file d'attente" rabbitmq est créée ayant le nom
trigger-nomdel'hôte-nomduservice, et une clef de routage du type
trigger.nomduservice est mise en place pour que les messages envoyés vers
trigger.nomduservice soient dispatchés sur l'ensemble des queues
trigger-*-nomduservice.

Roadmap d'une modif
===================

Imaginons qu'on modifie la mac d'une machine dans gest_crans.

before est le dico des données de la machine avant modif, et after celui des
données de la machine après modif. Le hash (appelé ob_id ensuite) est calculé en
tenant compte du timestamp actuel, et des données dans ces deux dicos. Un
quatrième élément est généré : un dico (appelé more) contenant des données
additionnelles, non contenues dans la base LDAP, éventuellement utiles pour la
régénération.

On envoie alors (ob_id, before, after, more) avec la clef de routage
trigger.event, ce qui signifie que le seul service qui recevra ce message est
event, sur civet.

Event calcule alors un diff, qui ici ne reviendra qu'avec une mac ayant changé.
Il se présente sous la forme d'un dico, avec pour clef les attributs ayant
changé, et pour valeur un tuple avec la liste des attributs avant, et la liste
des attributs après.

Ce diff, ainsi que le couple (before, after), ob_id et more sont passé à
l'ensemble des parseurs réagissant à l'une des clefs du dico diff. Ces parseurs
retournent alors des tuples contenant le nom du service à régénérer, son
argument operations, et une posittion.

On construit alors EventTracker.event_chain[ob_id][position][nom_service]
à qui on donne la valeur operations. Event lance alors la première salve de
messages, à savoir ceux en position 0. Chaque message est envoyé avec comme clef
de routage le nom du service à régénérer, et comme contenu la suite d'opérations
à faire.

Sur les serveurs concernés, les messages sont reçus, et exécutés. Enfin, un ack
est envoyé si ack vaut true dans le décorateur. Ce ack permet au service ack de
marquer les opérations comme faites.

Une fois toutes les opérations de niveau 0 validées, ack envoie la pile des
messages de niveau 1, et rebelotte…
