Contenu

Pattern de tâches *Ansible* pour vérifier la version d'une application à installer

Contenu

Lorsqu’on développe des rôles Ansible on peut être amené à installer un logiciel sans gestionnaire de packages (PKG, APT, YUM) mais en récupérant directement l’application (ou sa source) auprès de l’éditeur.

L’idempotence de nos rôles Ansible impose de ne pas re-récupérer (inutilement) un logiciel déjà installé : il va donc falloir préalablement tester si le logiciel est installé, si oui, dans quelle version et ensuite comparer à la version demandée par le rôle.

Je vous partage un petit modèle de tâches qui permet ceci.

Le processus complet est relativement simple :

  1. Quelle est la version demandée ?
    • Dernière version (voir explication plus bas) : on récupère ce numéro de version auprès d’un site/API/…
    • Version spécifique : on a donc le numéro de version.
  2. Logiciel déjà installé ?
    • Non : On télécharge la version demandée.
    • Oui :
      • Est à la version demandée ? : Rien à faire.
      • N’est pas à la version demandée ? : On télécharge la version demandée et on l’installe (à la place de la précédente installation).

Dans les explications suivantes j’utiliserais le logiciel Adminer comme exemple.

Tout d’abord, il faut avoir une variable Ansible qui indique la version souhaitée : adminer_version.

Afin de pouvoir aisément mettre à jour le logiciel via le même rôle Ansible, je fais le choix d’autoriser une valeur spéciale “latest” qui indiquera que, dans ce cas, on souhaite la toute dernière version disponible (un peu moins idempotent mais assez pratique).

1
adminer_version: latest

Maintenant qu’on sait quelle version on doit installer il nous fait savoir si le logiciel est déjà installé et si oui, à quelle la version.

Le test d’installation dépends du logiciel (existence de dossier/fichier, disponibilité d’une commande, résultat de dpkg -l). Dans mon cas il s’agit de vérifier qu’un fichier existe.

Pour simplifier la gestion/modularité du rôle j’utilise une variable pour définir l’emplacement où est/sera installé le logiciel : adminer_installation_filepath.

1
adminer_installation_filepath: /opt/adminer/adminer.php

La tâche de test est simpliste :

1
2
3
4
- name: Test if Adminer is installed
  ansible.builtin.stat:
    path: '{{ adminer_installation_filepath }}'
  register: __adminer_is_installed

Ensuite, si la version demandée est latest, on va interroger le site web de téléchargement du logiciel (ici GitHub) pour récupérer la dernière version.

Je pourrais utiliser community.general.github_release mais j’ai rencontré des problèmes à installer sa dépendance (github3.py) sur Debian 12, j’ai donc préféré utilisé la bonne vieille méthode du curl|grep de l’API publique de GitHub.

La tâche suivante récupère donc le numéro de version de la dernière release GitHub, uniquement lorsqu’il a été demandé d’installer la dernière version (adminer_version == 'latest') :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
- name: Get latest release of Adminer from repository
  # We use a basic `curl|grep` here because "community.general.github_release"
  # module requires "github3.py" module which cannot easily be installed on
  # Debian 12 "Bookworm" where the "externally-managed-environment" error occurs
  # if we try to install it using pip.
  ansible.builtin.shell: |
    curl --silent "https://api.github.com/repos/vrana/adminer/releases/latest" \
    | grep -Po '"tag_name": "\K.*?(?=")'    
  register: __adminer_release_to_install
  changed_when: false
  when: adminer_version == 'latest'

Le résultat (le nom de la release, qui est -ici- le numéro de version précédé d’un v) est stocké dans une variable temporaire __adminer_release_to_install qui désigne « la version qu’il faut avoir/installer ».

Dans le cas où une version spécifique à été désignée (adminer_version != 'latest'), la version à avoir/installé est presque le contenu de la variable adminer_version (le préfixe v diffère).

Pour simplifier le code Ansible et ne pas avoir à gérer séparément les 2 cas de adminer_version, je créer manuellement ma variable __adminer_release_to_install via un set_fact :

1
2
3
4
5
- ansible.builtin.set_fact:
    __adminer_release_to_install:
      failed: false
      stdout: 'v{{ adminer_version }}'
  when: adminer_version != 'latest'

Je trouve qu’il est pertinent d’avoir ici un petit test sur le bon déroulement de l’interrogation de GitHub avec message explicite, ce que permet la tâche assert suivante :

1
2
3
4
5
6
7
- ansible.builtin.assert:
    that:
      - __adminer_release_to_install is defined
      - not __adminer_release_to_install.failed
      - __adminer_release_to_install.stdout is defined
    fail_msg: 'Failed to fetch Adminer {{ adminer_version }} version information from GitHub.'
    success_msg: 'Fetched Adminer {{ adminer_version }} version information from GitHub.'

Maintenant qu’on sait quelle est la version à avoir/installer, on peut vérifier la version actuelle.

Vu le mode d’installation d’Adminer (une seule action) je peut placer cette tâche de test avant la tâche d’installation car cette dernière s’occupera, de fait, de désinstaller l’ancienne version.

Dans le cas d’Adminer, une des façons de savoir quelle est le numéro de version installé est de regarder le contenu du script PHP car celui-ci est indiqué dans le tag @version du DocBlock.

La tâche suivante regarde la version installée et la compare à la version attendue (__adminer_release_to_install) :

1
2
3
4
5
6
7
8
- name: Check if installed Adminer has correct version
  ansible.builtin.shell: |
    grep '^\* @version ' "{{ adminer_installation_filepath }}" | sed 's/^\* @version //' | grep -q "{{ __adminer_release_to_install.stdout | regex_replace("^v", "") }}"    
  ignore_errors: true
  register: __adminer_installed_with_correct_version
  changed_when: false
  when:
    - __adminer_is_installed.stat.exists

Le résultat se retrouvera dans la variable __adminer_installed_with_correct_version, indiquant au choix :

  • « Oui, Adminer a la bonne version. »
  • « Non, Adminer n’a pas la bonne version. »

Dans le cas spécifique d’Adminer et afin d’avoir un rôle plus complet je dois gérer le fait qu’Adminer existe en 2 variantes :

  • La variante complète qui gère plusieurs serveurs de base de données (SQL et NoSQL)
  • La variante ne gérant que MySQL/MariaDB

Je rajoute donc une variable qui permet de préciser quelle variante le rôle doit installer : adminer_mysql_variant.

Comme il n’existe que 2 variantes (pour l’instant ?), la variable peut n’être qu’un simple booléen :

1
adminer_mysql_variant: false

La tâche suivante est similaire à celle qui vérifie la version, elle regarde le code source. Malheureusement cette information n’est pas dans le DocBlock et donc la solution que j’ai retenu est de chercher si la chaîne PostgreSQL apparaît dans le code, signe d’une variante “complète”.

Bien entendu, inutile de faire ce test si la version installée n’est pas la bonne.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
- name: Check if installed Adminer has correct variant
  ansible.builtin.shell: |
    grep -q -F 'PostgreSQL' "{{ adminer_installation_filepath }}" && echo "full" || echo "mysql"    
  ignore_errors: true
  register: __adminer_installed_with_correct_variant
  changed_when: false
  failed_when: >
    (_adminer_installed_with_correct_variant.stdout == 'full') and adminer_mysql_variant    
  when:
    - __adminer_is_installed.stat.exists
    - not __adminer_installed_with_correct_version.failed

Le résultat se retrouvera dans la variable _adminer_installed_with_correct_variant, indiquant au choix :

  • « Oui, Adminer a la bonne variante. »
  • « Non, Adminer n’a pas la bonne variante. »

Maintenant qu’on connait :

  • La version et la variante attendues/demandées
  • Si le logiciel est installé et dans quelle version et variante

Il est temps d’installer (si besoin) la version et variante demandée.

S’agissait ici d’un simple script PHP à placer à l’endroit configuré, la tâche d’installation n’est qu’un simple get_url sur une URL construite en conséquence.

Cette tâche d’installation ne devant se déclencher que lorsque l’une des conditions suivante survient :

  • Le logiciel n’est pas déjà installé
  • Le logiciel est installé dans la mauvaise version
  • Le logiciel est installé dans la mauvaise variante
1
2
3
4
5
6
7
8
9
- name: Fetch Adminer release
  ansible.builtin.get_url:
    url: 'https://github.com/vrana/adminer/releases/download/{{ __adminer_release_to_install.stdout }}/adminer-{{ __adminer_release_to_install.stdout | regex_replace("^v", "") }}{{ "-mysql" if adminer_mysql_variant }}.php'
    dest: '{{ adminer_installation_filepath }}'
    force: true
  when: |
    (not __adminer_is_installed.stat.exists)
    or (__adminer_installed_with_correct_version.failed)
    or (__adminer_installed_with_correct_variant.failed)    

Et voici le bout de code Ansible complet qui applique ce pattern avec 2/3 tâches secondaires (mais nécessaires) :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
vars:
  # Path where Adminer will be installed
  adminer_installation_filepath: /opt/adminer/adminer.php

  # The version Adminer to install
  # Possible values: "latest" or the semver (without "v")
  adminer_version: latest

  # Tells if Adminer's MySQL variant should be installed or not
  adminer_mysql_variant: false

tasks:
  - name: Ensure various APT packages required by this Ansible role are installed
    ansible.builtin.apt:
      name:
        - curl
        - grep
        - sed
      state: present

  - name: Ensure Adminer install directory exists
    ansible.builtin.file:
      path: '{{ adminer_installation_filepath | dirname }}'
      state: directory

  - name: Get latest release of Adminer from repository
    # We use a basic `curl|grep` here because "community.general.github_release"
    # module requires "github3.py" module which cannot easily be installed on
    # Debian 12 "Bookworm" where the "externally-managed-environment" error occurs
    # if we try to install it using pip.
    ansible.builtin.shell: |
      curl --silent "https://api.github.com/repos/vrana/adminer/releases/latest" \
      | grep -Po '"tag_name": "\K.*?(?=")'      
    register: __adminer_release_to_install
    changed_when: false
    when: adminer_version == 'latest'

  - ansible.builtin.set_fact:
      __adminer_release_to_install:
        failed: false
        stdout: 'v{{ adminer_version }}'
    when: adminer_version != 'latest'

  - ansible.builtin.assert:
      that:
        - __adminer_release_to_install is defined
        - not __adminer_release_to_install.failed
        - __adminer_release_to_install.stdout is defined
      fail_msg: 'Failed to fetch Adminer {{ adminer_version }} version information from GitHub.'
      success_msg: 'Fetched Adminer {{ adminer_version }} version information from GitHub.'

  - name: Test if Adminer is installed
    ansible.builtin.stat:
      path: '{{ adminer_installation_filepath }}'
    register: __adminer_is_installed

  - name: Check if installed Adminer has correct version
    ansible.builtin.shell: |
      grep '^\* @version ' "{{ adminer_installation_filepath }}" | sed 's/^\* @version //' | grep -q "{{ __adminer_release_to_install.stdout | regex_replace("^v", "") }}"      
    ignore_errors: true
    register: __adminer_installed_with_correct_version
    changed_when: false
    when:
      - __adminer_is_installed.stat.exists

  - name: Check if installed Adminer has correct variant
    ansible.builtin.shell: |
      grep -q -F 'PostgreSQL' "{{ adminer_installation_filepath }}" && echo "full" || echo "mysql"      
    ignore_errors: true
    register: __adminer_installed_with_correct_variant
    changed_when: false
    failed_when: >
      (_adminer_installed_with_correct_variant.stdout == 'full') and adminer_mysql_variant      
    when:
      - __adminer_is_installed.stat.exists
      - not __adminer_installed_with_correct_version.failed

  - name: Fetch Adminer release
    ansible.builtin.get_url:
      url: 'https://github.com/vrana/adminer/releases/download/{{ __adminer_release_to_install.stdout }}/adminer-{{ __adminer_release_to_install.stdout | regex_replace("^v", "") }}{{ "-mysql" if adminer_mysql_variant }}.php'
      dest: '{{ adminer_installation_filepath }}'
      force: true
    when: |
      (not __adminer_is_installed.stat.exists)
      or (__adminer_installed_with_correct_version.failed)
      or (__adminer_installed_with_correct_variant.failed)      

Voilà, j’espère que ce pattern d’installation manuelle d’une version spécifique pour Ansible pourra vous servir.