Synchroniser des tables MySQL avec un module Ansible sur mesure
Vincent Bernat
La collection community.mysql
d’Ansible Galaxy
fournit un module mysql_query
permettant d’exécuter des requêtes
MySQL arbitraires. Malheureusement, il ne supporte ni --check
, ni
--diff
. Il est aussi incapable de savoir s’il a provoqué un
changement. Écrivons un module Ansible ad hoc corrigeant ces
problèmes.
Note
Je vous recommande de lire « Écrire un module Ansible sur mesure » en introduction.
Code#
Le module a la signature suivante et exécute les instructions SQL dans une transaction. Il a besoin d’une liste des tables qui sont modifiées pour être capable de détecter et montrer les changements.
mysql_sync: sql: | DELETE FROM rules WHERE name LIKE 'CMDB:%'; INSERT INTO rules (name, rule) VALUES ('CMDB: check for cats', ':is(object, "CAT")'), ('CMDB: check for dogs', ':is(object, "DOG")'); REPLACE INTO webhooks (name, url) VALUES ('OpsGenie', 'https://opsgenie/something/token'), ('Slack', 'https://slack/something/token'); user: monitoring password: Yooghah5 database: monitoring tables: - rules - webhooks
Prérequis#
Le module n’est pas idempotent par lui-même, mais il est prévu que
l’utilisateur lui fournisse des instructions SQL adaptées. Dans
l’exemple ci-dessus, l’idempotence dérive du fait que le contenu de la
table rules
est supprimé et recréé de zéro tandis que les lignes de
la table webhooks
sont remplacées si elles existent déjà.
Vous avez besoin du paquet PyMySQL.
Définition du module#
En se basant sur le squelette présenté dans l’article précédent, nous définissons le module :
module_args = dict( sql=dict(type='str', required=True), user=dict(type='str', required=True), password=dict(type='str', required=True, no_log=True), database=dict(type='str', required=True), tables=dict(type='list', required=True, elements='str'), ) result = dict( changed=False ) module = AnsibleModule( argument_spec=module_args, supports_check_mode=True )
Le mot de passe est marqué avec no_log
pour s’assurer qu’il ne sera
ni affiché, ni stocké, notamment quand ansible-playbook
tourne en
mode verbeux. Il n’y a pas d’option host
car le module s’exécute
directement sur l’hôte faisant tourner MySQL. Il n’y a pas non plus de
possibilité d’authentification forte par certificat. Cela correspond à
notre philisophie du module sur mesure : n’implémenter que ce dont on
a strictement besoin.
Obtenir les données actuelles#
L’étape suivante consiste à récupérer les enregistrements qui se
trouvent actuellement dans la base de données. Le dictionnaire got
associe des noms de tables avec la liste des lignes qu’elles
contiennent :
got = {} tables = module.params['tables'] connection = pymysql.connect( user=module.params['user'], password=module.params['password'], db=module.params['database'], charset='utf8mb4', cursorclass=pymysql.cursors.DictCursor ) with connection.cursor() as cursor: for table in tables: cursor.execute("SELECT * FROM {}".format(table)) got[table] = cursor.fetchall()
Calculer les changements#
Construisons maintenant le dictionnaire wanted
. L’astuce consiste à
exécuter les instructions SQL dans une transaction sans la finaliser
avec COMMIT
. Les modifications seront invisibles1 pour
les autres lecteurs et nous pourrons comparer les lignes finales avec
les lignes collectées dans got
:
wanted = {} sql = module.params['sql'] statements = [statement.strip() for statement in sql.split(";\n") if statement.strip()] with connection.cursor() as cursor: for statement in statements: try: cursor.execute(statement) except pymysql.OperationalError as err: code, message = err.args result['msg'] = "MySQL error for {}: {}".format( statement, message) module.fail_json(**result) for table in tables: cursor.execute("SELECT * FROM {}".format(table)) wanted[table] = cursor.fetchall()
La première boucle for
exécute chaque instruction. En cas d’erreur,
nous retournons un message un brin détaillé. La seconde boucle remplit
le dictionnaire wanted
avec les enregistrements modifiés.
Appliquer les changements#
Pour revenir au squelette décrit dans l’article précédent, la dernière étape est d’appliquer les
changements quand il y a une différence entre got
et wanted
et
quand on n’est pas en mode simulation. L’objet diff
est un peu plus
élaboré car il est construit table par table. Ansible affiche alors le
nom de chaque table avant la représentation de la différence.
if got != wanted: result['changed'] = True result['diff'] = [dict( before_header=table, after_header=table, before=yaml.safe_dump(got[table]), after=yaml.safe_dump(wanted[table])) for table in tables if got[table] != wanted[table]] if module.check_mode or not result['changed']: module.exit_json(**result)
Appliquer les changements est plutôt simple : il suffit de finaliser la transaction ! Sinon, ils sont perdus quand le module finit son exécution.
connection.commit()
Le code complet est disponible sur GitHub. Par rapport au
module mysql_query
, celui-là supporte --diff
et --check
et
signale correctement s’il y a eu des changements. Toutefois, il ne
faut pas l’utiliser sur des tables gigantesques car elles sont
chargées en mémoire.
-
Les tables doivent utiliser le moteur de stockage InnoDB. De plus, MySQL ne sait pas comment utiliser les transactions avec les instructions de type DDL : ne modifiez pas les définitions des tables. ↩︎