Livres de Jeu Ansible

Objectifs de certification

RHCE EX294 (RHEL8)

Si vous poursuivez des objectifs de certification, vous pourriez trouvez ici des bases qui vous aideront pour :

  • 2. Maîtrise des composants de base d’Ansible
    • 2.1. Inventaires
    • 2.2. Modules
    • 2.3. Variables
    • 2.4. Facts
    • 2.5. Jeux
    • 2.6. Playbooks
    • 2.8. Utiliser la documentation fournie pour trouver des informations spécifiques aux modules et commandes Ansible
  • 4. Configuration des noeuds gérés par Ansible
    • 4.1. Créer et distribuer des clés SSH aux noeuds gérés
  • 6. Création des jeux et des playbooks Ansible
    • 6.1. Utiliser des modules Ansible courants
    • 6.2. Utiliser des variables pour récupérer les résultats d’une commande exécutée
    • 6.3. Utiliser des conditions pour contrôler l’exécution des opérations
    • 6.4. Configurer la gestion des erreurs
    • 6.5. Créer des playbooks pour configurer des systèmes selon un état spécifique
  • 9. Utilisation des fonctions Ansible avancées
    • 9.3. Utilisation de variables et de facts Ansible

Références des sujets :

1. Comprendre le concept de livre de jeu

1.1. L’analogie d’un livre de jeu Ansible

Les livres de jeu (playbooks) sont écrits selon un langage d’automatisation simple et puissant. Les livres de jeu peuvent orchestrer avec précision plusieurs parties d’une topologie d’infrastructure avec un contrôle très détaillé du nombre de machines à traiter à la fois.

L’approche de Ansible en matière d’orchestration est une approche simple et précise : le code d’automatisation devrait être pérenne et il devrait y avoir très peu de choses à retenir sur la syntaxe ou des fonctionnalités spéciales.

Les livres de jeu sont écrits en langage YAML, Ain’t Markup Language. YAML expose un minimum de syntaxe et propose un modèle de configuration ou de processus plutôt qu’un langage de script ou de programmation.

Chaque livre de jeu est composé de un ou plusieurs “jeux (plays)” énoncés dans une liste.

Un jeu est une analogie sportive qui définit un état ou un modèle et qui se “rejoue” de différentes manières à d’autres moments.

Le but d’un “jeu” est de faire correspondre un “groupe” contenant des “hôtes (hosts)” dans des “roles” bien définis représentés par des objets Ansible appelés des “tâches (tasks)”. Sur le plan fondamental, une “tâche” Ansible n’est rien de plus qu’un appel à un module Ansible accompagné de paramètres. Un module est un programme Ansible en vue de réaliser certaines actions sur les “hôtes”.

Un module est généré sur le contrôleur, il est copié, exécuté et effacé sur les noeuds (dans le cadre de l’automation des serveurs). Par contre , quand il s’agit de gérer des périphériques du réseau, comme des commutateurs (switches) ou des routeurs, un module est généré et exécuté localement sur le contrôleur pour agir sur la cible.

En composant un “livre de jeu” avec plusieurs “jeux” en une forme de liste portant sur des groupes d’inventaire, il est possible d’orchestrer un déploiement multi-machines, en exécutant certaines tâches dans un groupe de routeurs, certaines tâches dans un groupe de commutateurs, et encore d’autres commandes dans un autre groupe de serveurs …

1.2. Aperçu d’un livre de jeu Ansible

En résumé, un “livre de jeu” consiste à :

  • exécuter des tâches (tasks) pour un inventaire (inventory) d’hôtes (hosts) rassemblés en groupes,
  • utilisant certains modules,
  • utilisant ou peuplant certaines variables,
  • notamment, dans des modèles (templates) de fichiers.

Un “livre de jeu” organise des tâches. Mais il de bonne pratique d’utiliser des roles. Un “role” ajoute un niveau d’abstraction dans l’exécution d’un livre de jeu. Le concept de rôle fait l’objet d’un chapitre ultérieur.

1.3. Programme ansible-playbook

ansible-playbook est le programme qui permet de lancer des livres de jeu. Avec tous les paramètres de connexion et d’inventaire définis, il demande en paramètres le ou les livres de jeu à lancer.

Options courantes

De manière non-exhaustive, on manquera pas d’évoquer et de tester quelques options utiles et courantes :

OptionsDescription
-iPrécise le fichier ou le script dynamique d’inventaire.
-eAttend une variable définie sous la forme clé="valeur".
-tExécute uniquement les tâches en liste marquées d’un “tag”, une balise, une étiquette.
--skip-tagsEvite des “tags”
-lLimite une liste d’hôtes ou de groupes d’un inventaire sur lesquels le livre de jeu est appliqué.
-v, -vvvDéfinit le niveau de verbosité : utile au déboggage.

Options informatives

On trouvera des options informatives, c’est-à-dire des options qui n’agissent pas sur les cibles et qui informent du livre de jeu invoqué :

OptionsDescription
--syntax-checkVérifie la syntaxe du livre de jeu sans l’éxécuter.
--list-hostsFournit la liste des hôtes concernés par le livre de jeu sans l’éxécuter.
--list-tagsFournit la liste des balises (“tags”) dans le livre de jeu sans l’éxécuter.
--list-tasksFournit la liste des tâches qui devraient être exécutées par un livre de jeu.

Options d’exécution/déboggage

OptionsDescription
-C, --checkCette option essaie de prédire certains changement sans les exécuter.
-D, --diffAffiche les changements des (petits) fichiers et modèles. Utile avec l’option --check.
--step“One-step-at-a-time” : chaque tâche est confirmée avant d’être exécutée.

Options de connexion

OptionsDescription
-f FORKSDéfinit le nombre de processus parallèles.
-k, --ask-passDemande le mot de passe de connexion.
--key-file=PRIVATE_KEY_FILEPrécise la clé privée à utiliser.
-uPrécise le nom d’utilisateur pour la connexion.
-cPrécise le type de connexion à utiliser.

Options d’élévation de privilèges

OptionsDescription
-b, --becomeIndique l’usage de l’élévation de privilège (su, sudo, …).
-K, --ask-become-passActive la demande mot de passe avec l’élévation de privilège.

Tous les livres de jeu qui suivent dans les exemples de ces documents sont fonctionnels et peuvent être joués par le programme ansible-playbook.

1.4. Une liste de jeux

Un livre de jeu (playbook) est un fichier YAML constitué d’une liste de jeux. Chaque jeu comporte des sections qui définissent de manière obligatoire sa portée en désignant les hôtes ou les groupes d’un inventaire (hosts:). De manière optionnelle, mais probablement recommandée, on trouvera au niveau du jeu des paramètres de connexion, des paramètres d’élévation de privilèges, des variables définies et, aussi heureusement, différentes actions qui comportent des listes de tâches (tasks:, roles:, handlers:, etc.).

Un premier exemple de livre de jeu empty-playbook.yaml pourrait être celui-ci :

#empty-playbook.yaml
#a playbook is a list of plays
---
- name: "PLAY 1"
  hosts: localhost
  tasks:
- name: "PLAY 2"
  hosts: localhost
  tasks:
- name: "PLAY 3"
  hosts: localhost
  tasks:

Ce livre de jeu porte sur l’hôte localhost soit dans notre cas sur le noeud de contrôle. localhost peut être sollicité par des tâches Ansible même s’il n’est pas précisé dans l’inventaire. Dans ce dernier cas, Ansible nous en informe avec des messages d’avertissement :

$ ansible-playbook empty-playbook.yaml
[WARNING]: No inventory was parsed, only implicit localhost is available
[WARNING]: provided hosts list is empty, only localhost is available. Note that
the implicit localhost does not match 'all'

1.5. Structure d’un jeu

En effet, un jeu dispose toujours au minimum de la clé hosts: qui désigne un hôte ou un groupe de l’inventaire, comme par exemple dans le fichier minimal-playbook.yaml :

#minimal-playbook.yaml
#a playbook with one play
---
- hosts: localhost

Il est toujours conseillé voir fortement recommandé de dénommer ses objets jeu, tâche, rôle, etc … avec la directive name:.

Dans l’exemple suivant helloworld-playbook.yaml, on trouve un livre de jeu “Hello World” d’un seul jeu avec une seule tâche qui affiche le message “Hello World!” :

#helloworld-playbook.yaml
#one play, one task
---
- name: "Hello World **play**"
  hosts: localhost
  tasks:
    - name: "Hello World **task**"
      debug:
        msg: "Hello World!"

Mais on pourrait aussi trouver d’autres clés dans le niveau racine du jeu. Il conviendrait de les définir pour chaque jeu selon les cas :

  • des paramètres de connexion (connexion:),
  • des variables (voir plus loin), l’activation ou la désactivation des facts (gather_facts: False),
  • des listes d’action de type pre_tasks:, post_tasks:, handlers:, roles:, qui sont des “sortes” de tâches,
  • des paramètres d’élévation de privilèges (become: True, become_user:), etc …

Voici une image de synthèse pour ceux qui préfèrent :

Livre de jeu Hello World

1.6. Valeurs de retour

Ansible génère des valeurs de retour à la suite de l’exécution des tâches et des modules.

Valeurs de retour, Return Values

Valeurs de retourSignication de la valeurs de retour 
backup_file (fichier_de_sauvegarde)Pour les modules qui implémentent backup=noyes lors de la manipulation de fichiers, un chemin vers le fichier de sauvegarde créé.
changed (modifié)Un booléen indiquant si la tâche a dû effectuer des modifications. 
failed (échoué)Un booléen qui indique si la tâche a échoué ou non. 
invocationInformations sur la manière dont le module a été invoqué. 
msgUne chaîne avec un message générique relayé à l’utilisateur. 
rcCertains modules exécutent des utilitaires en ligne de commande ou sont conçus pour exécuter des commandes directement (raw, shell, commande, etc), ce champ contient le ‘code de retour’ de ces utilitaires. 
resultsSi cette clé existe, elle indique qu’une boucle était présente pour la tâche et qu’elle contient une liste du module normal’résultat’ par élément. 
skipped (évité)Un booléen qui indique si la tâche a été ignorée ou non. 
stderrCertains modules exécutent des utilitaires en ligne de commande ou sont conçus pour exécuter des commandes directement (raw, shell, commande, etc), ce champ contient la sortie d’erreur de ces utilitaires. 
stderr_linesLorsque stderr est retourné, nous fournissons aussi toujours ce champ qui est une liste de chaînes de caractères, un élément par ligne de l’original. 
stdoutCertains modules exécutent des utilitaires en ligne de commande ou sont conçus pour exécuter directement des commandes (raw, shell, commande, etc). Cette zone contient l’édition normale de ces utilitaires. 
stdout_linesLorsque stdout est retourné, Ansible fournit toujours une liste de chaînes de caractères, chacune contenant un élément par ligne de la sortie originale. 

2. Concept de tâches

2.1. Appels à des tâches

On trouvera dans le livre de jeu tasks-playbook.yaml suivant des actions, dans l’ordre d’exécution :

  • pre_tasks:,
  • roles:,
  • tasks:,
  • post_tasks:,
  • handlers:.

qui peuvent contenir des listes de tâches.

#tasks-playbook.yaml
---
- name: "PLAY 1: tasks playbook"
  hosts: localhost
  connection: local
  pre_tasks:
  roles:
  tasks:
  post_tasks:
  handlers:

En toute logique, ce livre de jeu n’exécute aucune tâche.

2.2. Structure d’une tâche

Les tâches peuvent être directement appelées de manière séquentielle dans le livre de jeu selon la syntaxe formelle suivante (les modules présentés dans l’exemple n’existent pas) :

- name: "Task 1 example"
  module_x:
    argument1: foo
    argument2: bar
- name: "Task 2 example"
  module_y:
    argument1: foo
    argument2: bar

Les tâches font appel à une seule action, c’est-à-dire à un seul module et ses arguments, comme par exemple dans le livre de jeu playbook-with-simple-tasks.yaml :

#playbook-with-simple-tasks.yaml
---
- name: "PLAY 1: play with simple tasks"
  hosts: localhost
  connection: local
  gather_facts: False
  vars:
    test: True
  pre_tasks:
    - name: "PRE-TASK 1: collect facts"
      setup:
  tasks:
    - name: "TASK 1: check connectivity"
      ping:
    - name: "TASK 2: print hostname by var arg"
      debug:
        var: ansible_hostname
    - name: "TASK 3: print hostname by msg arg"
      debug:
        msg: "{{ ansible_hostname }}"
  post_tasks:
    - name: "POST-TASK 1: Succeed"
      command: /bin/true
    - name: "POST-TASK 2: Error"
      command: /bin/false

Dans ce dernier livre de jeu, on retrouve les modules suivants avec l’usage d’arguments :

  • setup : qui collecte les “facts” sur l’hôte.
  • ping : qui réalise un test de connectivité (selon la connexion définie).
  • debug : qui affiche des messages sur la sortie standard.
  • command : qui exécute des commandes système.

Il est parfaitement normal que ce livre de jeu se termine par un échec.

Veuillez être attentif au rapport d’état des tâches en fin de sortie d’exécution du livre de jeu :

ok=5 changed=1 unreachable=0 failed=1 skipped=0 rescued=0 ignored=0

Cinq tâches “ok”, un tâche “changed”, et une tâche “failed” sont dans le rapport.

Avec le module command quelque soit la réussite de la tâche, l’état est toujours “changed” ! Cet état de fait tient à l’usage du module command. En effet, comment peut-il savoir si quelque choses à changé suite à son exécution ? Il peut être utile de contrôler l’état de la tâche par la tâche elle-même pour la rendre idempotente, notamment en focntion de sonn résulat réel.

2.3. Idempotence des tâches

“Être idempotent permet à une tâche définie d’être exécutée une seule fois ou des centaines de fois sans créer un effet contraire sur le système cible, ne provoquant un changement à une seule reprise. En d’autres mots, si un changement est nécessaire pour obtenir le système dans un état désiré, alors le changement est réalisé ; par contre si le périphérique est déjà dans l’état désiré, aucun changement n’intervient. Ce comportement est différent des pratiques de scripts personnalisés et de copier/coller de lignes de commandes. Quand on exécute les mêmes commandes ou scripts sur un même système de manière répétée, le taux d’erreur est souvent élevé.”

Extrait de: Jason Edelman. « Network Automation with Ansible. », O’Reilly Media, 2016.

Ansible autorise l’idempotence, mais selon le module utilisé (les modules de commandes “brutes” comme command), il faudra le manipuler pour atteindre cette exigence de conception.

2.4. Contrôle sur les tâches

Dans le livre de jeu suivant playbook-with-tasks.yaml, on trouve du contrôle sur les tâches :

#playbook-with-tasks.yaml
---
- name: "PLAY 1: playbook with tasks"
  hosts: localhost
  connection: local
  gather_facts: False
  vars:
    test: True
  pre_tasks:
    - name: "PRE-TASK 1: collect facts"
      setup:
  tasks:
    - name: "TASK 1: check connectivity"
      ping:
      register: output
    - name: "TASK 2: print variable output"
      debug:
        var: output
      when: test == True
    - name: "TASK 3: print hostname"
      debug:
        msg: "{{ ansible_hostname }} is {{ output.ping }}ing"
  post_tasks:
    - name: "POST-TASK 1: Error handeling"
      command: /bin/false
      register: output
      ignore_errors: True
    - name: "POST-TASK 2: Error handeling and idempotency"
      command: /bin/false
      register: output
      ignore_errors: True
      changed_when: output.rc == 0
    - name: "POST-TASK 3: Idempotency with True"
      command: /bin/true
      register: output
      changed_when: output.rc == 1

Une tâche peut être contrôlée de différentes manières, dans cet exemple :

  • La clé register: enregistre la sortie d’une tâche dans une variable.
  • La clé when: place des conditions à l’exécution de la tâche.
  • La clé ignore_errors n’interrompt pas l’exécution du livre de jeu en cas d’erreur et marque la tâche comme “ignored”.
  • La clé changed_when modifiera la valeur de sortie changed sous condition “True”.

Ce livre de jeu exécute une tâche de collecte de facts avant toute autre, exécute en tâches le module ping, enregistre sa sortie dans une variable, affiche cette variable sous une condition de vérité et puis qui l’affiche dans un message, enfin, en toutes dernières tâches, il génère une erreur qui sera ignorée.

2.5. Inclusion de tâches

Si toutes les tâches étaient placées dans un fichier tasks.yaml, on pourrait parfaitement l’inclure dans le livre de jeu comme dans cet exemple playbook-with-tasks-included.yaml :

#playbook-with-tasks-included.yaml
---
- name: "PLAY 1: playbook with tasks included"
  hosts: localhost
  connection: local
  gather_facts: True
  vars:
    test: True
  tasks:
    - name: Include task list in play
      include_tasks: some-tasks.yaml

Ce dernier livre de jeu appelle le fichier de tâches some-tasks.yaml :

#some-tasks.yaml
---
- name: "PRE-TASK 1: check connectivity"
  ping:
  register: output
- name: "TASK 1: print variable output"
  debug:
    var: output
  when: test == True
- name: "TASK 2: print hostname"
  debug:
    msg: "{{ ansible_hostname }} is {{ output.ping }}ing"
  when: test == True
- name: "POST-TASK 1: Error handeling"
  command: /bin/false
  register: output
  ignore_errors: True
- name: "POST-TASK 2: Error handeling and idempotency"
  command: /bin/false
  register: output
  ignore_errors: True
  changed_when: output.rc == 0
- name: "POST-TASK 3: Idempotency with True"
  command: /bin/true
  register: output
  changed_when: output.rc == 1

Veuillez noter que la liste commence directement au début du fichier, contrairement au niveau des tâches dans le livre de jeu.

2.6. Gestionnaire (Handler)

Un “handler” (gestionnaire) est une tâche qui ne se sera réalisée qu’une seule fois à la suite d’un appel notify: associé à une tâche si la tâche rend une valeur de retour ‘changed’. Par exemple avec le livre de jeu playbook-with-handler.yaml :

#playbook-with-handler.yaml
---
- name: "PLAY 1: playbook with handler"
  hosts: localhost
  connection: local
  gather_facts: False
  vars_prompt:
    - name: "response"
      prompt: "Do you want execute the task?\n1- Yes\n2- no\n"
      private: no
  tasks:
    - name: "TASK 1: always true"
      command: "true"
      notify: print state
      when: response == "1"
    - name: "TASK 1: always true"
      debug:
        msg: "Goodbye"
      when: response == "2"
  handlers:
    - name: "print state"
      debug:
        msg: "CHANGED !!!"

2.7. Rôles

Définition d’un rôle.

Installation d’un rôle (emplacements, par Ansible-Galaxy,manuellement ou par un fichier requirements.yml, via github).

2.8. Utiliser des rôles

Usage d’un rôle.

2.9. Organiser un livre de jeu

playbooks
├── files
├── tasks
├── templates
└── vars

3. Variables

3.1. Introduction

Il existe de nombreuses manière de contrôler le contenu des livres de jeu grâce à l’usage de variables. Il est de bonne pratique d’organiser ses procédures avec des variables.

Les variables Ansible peuvent être déclarées dans l’inventaire, dans les dossiers group_vars/ ou host_vars/ par exemple, pour toutes valeurs dépendantes des cibles elle-mêmes.

Les variables peuvent aussi être définies dans un jeu sous des forme diverses :

  • en valorisation directe dans la tâche ou dans le jeu ou encore dans le rôle (vars:),
  • en référence à un fichier (vars_files:),
  • en les “incluant” (include_vars:)1,
  • en appelant des variables d’environnement,
  • en important d’autres livres de jeu (import_playbook:),
  • par génération ou récolte dynamique (facter, gather_facts:),
  • définies par des tâches du livre de jeu (set_facts:),
  • définies à partir de la sortie standard d’une tâche (register:),
  • définies à partir d’invites interactives (vars_prompt:),
  • A partir des dossiers default/ ou variables/ d’un rôle.

On peut aussi les définir sur la ligne de commande d’ansible-playbook -e ponctuellement comme argument supplémentaire et prépondérant. On peut aussi désigner un fichier contenant des variables : --extra-vars "@some_file.json".

Les variables sont appelées en format Jinja2 sous la forme :

  • {{ var }}
  • {{ var["key1"]["key2"]}}
  • {{ var.key1.key2 }}
  • {{ varlist[0] }}

3.2. Appel aux variables

Les variables sont appelées en format Jinja2 sous la forme : {{ interface }} ou encore {{ ipv4.address }} quand on dispose d’une présentation de données comme par exemple dans le fichier variables.yml :

#variables.yml
---
interface: eth0
ipv4:
  address: 192.168.1.1
ipv6:
  address:
    - fe80::1
    - fe80::11
    - fe80::111

Les guillemets sont nécessaires dans l’appel aux variables dans les paramètres car on appelle leur valeur.

Par contre dans les opérations logiques telles que les conditions cette mise entre guillemets n’est pas nécessaire.

Ansible est capable de récupérer des métadonnées sous forme de variables “dynamiques” ansible_*, aussi on peut aller chercher des variables d’autres hôtes de l’inventaire (hostvars, groups, group_names et inventory_hostname)

Enfin les modèles Jinja2 permet des traitement conditionnels, des boucles, des filtres …

Le livre de jeu suivant variables-playbook.yaml illustre l’utilisation de variables dans le livre de jeu lui-même (vars:), venant d’un fichier (vars_files:), d’une interaction utilisateur (vars_prompt:) et de la collecte des “Facts” (gather_facts: True).

#variables-playbook.yaml
---
- name: "PLAY 1: variables playbook"
  hosts: localhost
  gather_facts: True
  vars:
    variable: a new value
  vars_files:
    - variables.yml
  vars_prompt:
    name: response
    prompt: "What do to want to do?"
    private: no
  tasks:
    - name: "print variables"
      debug:
        msg:
          - "variable value: {{ variable }},"
          - "prompt value: {{ response }},"
          - "interface {{ interface }}: {{ ipv4.address }},"
          - "real hostname: {{ ansible_hostname }}"
          - "inventory hostname: {{ inventory_hostname }}"

On abordera ultérieurement dans le support la manipulation des variables. Notons aussi que Ansible-Vault permet de protéger ses variables. La protection des données sensibles avec ansible-vault est abordée dans un autre chapitre dédié.

Dans les exercices suivants nous allons utiliser les fonctionnalités :

  • vars: sur une tâche ou dans un jeu et vars_files: en premier niveau d’un jeu, qui permet de référencer des variables.
  • Le directive register: qui valorise une variable en cours de jeu avec la sortie générée par une tâche
  • Le module setfact qui permet de définir et de traiter des variables dans un jeu.

3.3. Créer un fichier de données YAML

Ansible pourra lire des fichiers contenant des variables qu’elles soient présentées en JSON, en XML, en CSV ou en YAML.

Ici, nous proposons de manipuler des données présentées en YAML.

Attention le nom de variables ne peuvent pas comporter d’espace, de tiret -, de point . ou commencer par un chiffre ! Il existe aussi une série de mots réservés.

Le fichier data.yml par exemple.

#data.yml
---
group: "omega"
users:
  - name: alfa
    password: testtest
  - name: beta
    password: testtest

3.4. Utiliser les données

#print-out-data.yaml
---
- name: print out data.yml
  hosts: localhost
  gather_facts: False
  vars_files:
    - data.yml
  tasks:
  - debug:
      msg: "{{ item.name }} with weak password {{ item.password }}"
    loop: "{{ users }}"
  - debug:
      msg: "{{ group }}"

3.5. Variables internes

  • hostvars (par exemple hostvars[other.example.com]['ansible_facts']['distribution'] alors que le jeu s’applique à un hôte différent)
  • group_names (groupes contenant l’hôte courant sous forme de liste)
  • groups (tous les groupes et tous les hôtes de l’inventaire sous forme de liste), ou plus précisément groups['app_servers'].
  • inventory_hostname (l’hôte courant dans l’inventaire)
  • inventory_hostname_short (le premier élément de l’hôte dans l’inventaire, p.ex. www de www.example.com)
  • play_hosts (les noms d’hôtes dans la portée du jeu)
  • inventory_dir (l’emplacement de l’inventaire)
  • inventoty_file (le nom du fichier d’inventaire)

!!! Exemples.

3.6. Facts

Les “Facts” sont des informations collectées par Ansible et que l’on peut appeller via des variables “magiques” ansible_*.

Par défaut, Ansible collecte les facts des cibles avant l’exécution des tâches du jeu. Ce comportement se contrôle sur le jeu avec la directive de jeu gather_facts: et une valeur booléenne. On peut faire en sorte que la collecte de “facts” doive toujours être paramétrée sur le jeu avec le paramètre gathering = explicit dans un fichier de configuration. Enfin le module setup permet de réaliser cette collecte à travers une tâche.

#print-out-operating-system.yaml
---
- name: print out operating system
  hosts: localhost
  gather_facts: False
  tasks:
    - name: "facts"
      setup:
      register: output
    - name: "print output"
      debug:
        msg: "{{ output }}"

On retiendra quelques unes de ces variables “magiques” ansible_* :

  • ansible_os_family ou ou ansible_facts['os_family']
  • ansible_distribution ou ansible_facts['distribution']
  • ansible_distribution_version ou ansible_facts['distribution_version']
  • ansible_distribution_version ou ansible_facts['distribution_version']
  • ansible_distribution_major_version ou ansible_facts['distribution_major_version']
  • ansible_fqdn ou ansible_facts['fqdn']
  • ansible_hostname ou ansible_facts['hostname']
  • ansible_pkg_mgr ou ansible_facts['hostname']
  • ansible_default_ipv4.address ou ansible_facts['default_ipv4.address']
  • ansible_default_ipv6.address ou ansible_facts['default_ipv6.address']

3.7. Obtenir la version de sa distribution

La variable ansible_os_family peut inclure les valeurs suivantes de manière non exhaustive :

  • Darwin
  • Debian
  • FreeBSD
  • RedHat
  • Slackware
  • Solaris
  • Windows

La variable ansible_distribution peut inclure les valeurs suivantes de manière non exhaustive :

  • Alpine
  • Altlinux
  • Amazon
  • Archlinux
  • ClearLinux
  • Coreos
  • CentOS
  • Debian
  • Fedora
  • OpenWrt
  • OracleLinux
  • RedHat
  • Slackware
  • SLES
  • SMGL
  • SUSE
  • Ubuntu
  • VMwareESX
#print-out-distribution.yaml
---
- name: print out distribution
  hosts: localhost
  gather_facts: True
  tasks:
    - name: "print output"
      debug:
       msg: |
            ansible_distribution: {{ ansible_os_family }}
            ansible_distribution: {{ ansible_distribution }}
            ansible_distribution_version: {{ ansible_distribution_version }}
            ansible_distribution_major_version: {{ ansible_distribution_major_version }}
            ansible_distribution_release: {{ ansible_distribution_release }}

3.8. Obtenir son adresse ip publique

En plusieurs tâches pour un résultat identique à :

ip_address=$(curl -s ipinfo.io/ip) ; echo ${ip_address}.xip.io
#get-ipv4-public.yaml
---
- name: "get ipv4 public address"
  hosts: localhost
  gather_facts: True
  tasks:
    - uri:
        url: https://ipinfo.io/ip
        return_content: yes
      register: ipinfo_content
    - set_fact:
        ip_address: "{{ ipinfo_content.content | replace('\n', '') }}"
    - set_fact:
        dns_name: "{{ ip_address }}.xip.io"
    - debug:
        msg: |
             ip address : {{ ip_address }}
             dns name : {{ dns_name }}
             real hostname : {{ ansible_hostname }}
             inventory hostname : {{ inventory_hostname }}

Pourquoi réinventer la roue ?

#get-ipv4-public-v2.yaml
---
- name: "get ipv4 public address"
  hosts: localhost
  gather_facts: True
  tasks:
    - ipinfoio_facts:
    - set_fact:
        dns_name: "{{ ansible_facts.ip }}.xip.io"
    - debug:
        msg: |
             ip address : {{ ansible_facts.ip }}
             dns name : {{ dns_name }}
             real hostname : {{ ansible_hostname }}
             inventory hostname : {{ inventory_hostname }}

3.9. Sortie brute placée en variable

#df-th.yaml
---
- name: "print df -Th"
  hosts: localhost
  gather_facts: True
  tasks:
    - shell: df -Th
      register: df_output
    - set_fact:
        df: "{{ df_output }}"
    - debug:
        msg: "{{ df.stdout_lines | list }}"

3.10. Prompts

Prompts

Le module prompts permet valoriser des variables en interagissant avec l’utilisateur.

---
- hosts: localhost
  vars_prompt:
    - name: "name"
      prompt: "what is your name?"
      private: no
    - name: "favcolor"
      prompt: "what is your favorite color?"
      private: no
    - name: "my_password"
      prompt: "Enter password"
      private: yes
      encrypt: "sha512_crypt"
      confirm: yes
      salt_size: 7
  tasks:
    - debug:
        msg: |
             {{ name }} loves {{ favcolor }}
             His password is {{ my_password }}

4. Boucles

4.1. Boucles à travers une liste

Ansible Loops

On peut appeler une variable valorisée en liste dans l’exécution d’un module :

#my-fruits.yaml
---
- name: "loop illustration"
  hosts: localhost
  vars:
    fruits:
      - apple
      - orange
      - pineapple
  tasks:
    - name: "Print my Fruits"
      debug:
        msg: "{{ item }}"
      loop: "{{ fruits }}"

Using register with a loop

#my-fruits-v2.yaml
---
- name: "loop with register"
  hosts: localhost
  vars:
    fruits:
      - apple
      - orange
      - pineapple
  tasks:
    - name: "loop + register"
      shell: "echo {{ item }}"
      loop: "{{ fruits }}"
      register: echo
    - name: "print result"
      debug:
        msg: "{{ echo }}"

Mieux :

#my-fruits-v2.1.yaml
---
- name: "loop with register"
  hosts: localhost
  vars:
    fruits:
      - apple
      - orange
      - pineapple
  tasks:
    - name: "loop register"
      shell: "echo {{ item }}"
      loop: "{{ fruits }}"
      register: echo
    - name: "print result"
      debug:
        msg: "{{ item.stdout_lines }}"
      loop: "{{ echo.results }}"

4.2. Boucle dans un dictionnaire

#other-my-fruits.yaml
---
- name: "other loop illustration"
  hosts: localhost
  vars:
    fruits:
      - name: apple
        color: green
      - name: orange
        color: orange
      - name: pineapple
        color: yellow
  tasks:
    - name: "Print my Fruits"
      debug:
        msg: "This {{ item.name }} is {{ item.color }}"
      loop: "{{ fruits }}"

4.3. Boucle imbriquée (nested loop)

#nested-my-fruits.yaml
---
- name: "nested loop illustration"
  hosts: localhost
  vars:
    fruits:
      - id: 0
        name: apple
      - id: 1
        name: orange
      - id: 2
        name: pineapple
    colors:
      - id: 0
        name: green
      - id: 1
        name: orange
      - id: 2
        name: yellow
  tasks:
    - name: "Print my Fruits"
      debug:
        msg: "This {{ item.0.name }} can have the {{ item.1.name }} color"
      loop: "{{ fruits|product(colors)|list }}"

4.4. Boucle “Over hashes”

#print-phone-records.yaml
---
- name: "over hashes loop illustration"
  hosts: localhost
  vars:
    users:
      alice:
        name: Alice Appleworth
        telephone: 123-456-7890
      bob:
        name: Bob Bananarama
        telephone: 987-654-3210
  tasks:
    - name: "Print phone records"
      debug:
        msg: "User {{ item.key }} is {{ item.value.name }} ({{ item.value.telephone }})"
      loop: "{{ users|dict2items }}"

4.5. Choix aléatoire dans une liste

Pas besoin de boucle ici ! On utilise le filtre random.

#random-my-fruits.yaml
---
- name: "loop with register"
  hosts: localhost
  vars:
    fruits:
      - apple
      - orange
      - pineapple
      - raspberry
  tasks:
    - name: "Print a random Fruit"
      debug:
        msg: "The chosen fruit is {{ fruits|random }}"

4.6. Boucle “Do-Until”

#do-until.yaml
---
- name: "Do-Until illustration"
  hosts: localhost
  tasks:
    - name: "do the task until all systems go is viewed"
      shell: "for x in {1..4}; do sleep 3 ; echo false ; done ; echo all systems go "
      register: result
      until: result.stdout.find("all systems go") != -1
      retries: 5
      delay: 10

5. Conditions et tests

Figures when: à retenir :

  • Opérateurs de comparaison : var == "Value", var >= 5, etc.
  • Tests booléens : var, où var correspond à un boléen (yes, true, True, TRUE, no, false, False)
  • Tests Jinja2 : var is defined, ! var is defined
  • Plusieurs conditions AND : <condition1> and <condition2>, liste
  • Plusieurs conditions OR : <condition1> or <condition2>
  • Groupement de conditions : (<condition1> and <condition2>) or (<condition3> and <condition4>)

La plus simple des conditions s’applique à une seule tâche. Il faut créer la tâche, puis ajouter une déclaration “when” qui applique un test. La clause when: est une expression Jinja2 brute sans double accolades. Lorsque vous exécutez la tâche ou le livre de jeu, Ansible évalue le test pour tous les hôtes. Sur tout hôte où le test est réussi (renvoie une valeur de True), Ansible exécute cette tâche.

Les tests en Jinja sont un moyen d’évaluer les modèles d’expression et de renvoyer Vrai ou Faux. Jinja est livré avec plusieurs de ces tests.

La principale différence entre les tests et les filtres est la suivante : les tests Jinja sont utilisés pour les comparaisons, alors que les filtres sont utilisés pour la manipulation des données. Ils ont des applications différentes en Jinja. Les tests peuvent également être utilisés dans les filtres de traitement de liste, comme map() et select() pour choisir les éléments de la liste.

En plus des tests Jinja2, Ansible en fournit quelques autres. Les utilisateurs peuvent aussi facilement créer les leurs.

La syntaxe pour l’utilisation d’un test Jinja dans une tâche Ansible est la suivante :

- debug:
    msg: "Test is True"
  when: variable is <test_name>

Comme ceci par exemple où l’évaluation donnera un résultat positif :

#...
---
- hosts: localhost
  gather_facts: False
  vars:
    result:
  tasks:
    - debug:
        var: result
      when: result is defined

Voir Jinja2 List of Builtin Tests :

  • boolean(value)
  • even(value)
  • integer(value)
  • ne(), !=
  • string(value)
  • callable()
  • false()
  • iterable()
  • none(value)
  • true()
  • defined(value)
  • float(value)
  • le(), <=
  • number()
  • undefined()
  • divisibleby(value, num)
  • ge(), >=
  • lower()
  • odd(value)
  • upper(value)
  • eq(), ==, equalto
  • gt(), >, greaterthan
  • lt(), <, lessthan
  • sameas()
  • escaped()
  • in(value, seq)
  • mapping()
  • sequence()

Voir aussi Jinja2 Filters in Ansible.

5.1. Test d’existence d’une variable

Dans ce jeu, les variables fruit et drink sont définies mais drink est vide. On constate aussi l’usage de la négation.

#test-defined-variables.yaml
---
- name: "test defined variables"
  hosts: localhost
  gather_facts: False
  vars:
    fruit: apple
    drink:
  tasks:
    - name: "Print a the Fruit when the var is defined"
      debug:
        msg: "The var fruit is {{ fruit }}"
      when: fruit is defined
    - name: "Print a the Drink when the var is empty"
      debug:
        msg: "The var drink is {{ drink }}"
      when: drink is defined
    - name: "Do not print a the Drink when the var is empty"
      debug:
        msg: "The var drink is {{ drink }}"
      when: drink != None
    - name: "Do not print the Snackbar when the var is not defined"
      debug:
        msg: "The var snackbar is {{ snackbar }}"
      when: ! snackbar is defined

5.2. Test booléen d’une variable

#test-boolean-variables.yaml
---
- name: "test boolean variables"
  hosts: localhost
  gather_facts: False
  vars:
    orange: False # Boolean
    pineapple: yes # Boolean
    apple: "yes" # String
  tasks:
    - name: "Test if the var orange is False"
      debug:
        msg: "The var is False"
      when: not orange
    - name: "Test if the var pineapple is True"
      debug:
        msg: "The var pineapple is True"
      when: pineapple
    - name: "Test if the var apple is True"
      debug:
        msg: "The var apple is True"
      when: apple|bool

5.3. Tests sur des équivalences de texte

#test-string-variables.yaml
---
- name: "test string variables"
  hosts: localhost
  gather_facts: True
  tasks:
    - name: "Debian Action"
      debug:
        msg: "{{ ansible_hostname }} Debian host"
      when: ansible_os_family == "Debian"
      # note that all variables can be directly in conditionals without double curly braces
    - name: "RedHat Action"
      debug:
        msg: "{{ ansible_hostname }} RedHat host"
      when: ansible_os_family == "RedHat"

5.4. Plusieurs conditions nécessaires

#test-multiple-and-conditionnals.yaml
---
- name: "and conditionnals"
  hosts: localhost
  gather_facts: True
  tasks:
    - name: "Test two conditions in one line"
      debug:
        msg: "{{ ansible_hostname }} CentOS 8"
      when: ansible_distribution == "CentOS" and ansible_distribution_major_version == "8"
    - name: "Test two conditions in two lines"
      debug:
        msg: "{{ ansible_hostname }} CentOS 8"
      when: ansible_distribution == "CentOS" and
            ansible_distribution_major_version == "8"
    - name: "Test multiple conditions in a list"
      debug:
        msg: "{{ ansible_hostname }} CentOS 8"
      when:
        - ansible_distribution == "CentOS"
        - ansible_distribution_major_version == "8"

5.5. Plusieurs conditions alternatives

#test-multiple-or-conditionnals.yaml
---
- name: "or conditionnals"
  hosts: localhost
  gather_facts: True
  tasks:
    - name: "Action on RedHat or Debian"
      debug:
        msg: "{{ ansible_hostname }} RedHat or Debian"
      when: ansible_os_family == "RedHat" or
            ansible_os_family == "Debian"

    - name: "Action on CentOS 8 or Ubuntu"
      debug:
        msg: "{{ ansible_hostname }} CentOS 8 or Ubuntu systems"
      when: (ansible_os_family == "RedHat" and
            ansible_facts['lsb']['major_release|int']x >= 6) or
            ansible_distribution == "Ubuntu"

5.6. Comparer des numéros de version

Quand on utilise la test version dans un livre de jeu ou un role, il ne faut pas écrire les accolades {{ }} :

#test-comparing-versions.yaml
---
- name: "Comparing versions"
  hosts: localhost
  gather_facts: False
  vars:
    my_version: 1.2.3
  tasks:
    - debug:
        msg: "my_version is higher than 1.0.0"
      when: my_version is version('1.0.0', '>')

5.7. Trouver des chaînes de caractères

Pour trouver une chaîne de caractères à partir d’une sous-chaîne ou d’un expression rationnelle (regular expression), on peut utiliser les tests match, search or regex :

#test-match-strings.yaml
---
- name: "Match strings"
  hosts: localhost
  gather_facts: False
  vars:
    url: "http://example.com/users/foo/resources/bar"
  tasks:
    - debug:
        msg: "matched pattern 1"
      when: url is match("http://example.com/users/.*/resources/")

    - debug:
        msg: "matched pattern 2"
      when: url is search("/users/.*/resources/.*")

    - debug:
        msg: "matched pattern 3"
      when: url is search("/users/")

    - debug:
        msg: "matched pattern 4"
      when: url is regex("example.com/\w+/foo")
  • match réussit si il trouve le motif au début de la ligne.
  • search réussit si il trouve le motif n’importe où dans la ligne.
  • regex réussit si il trouve le motif n’importe où dans la ligne par défaut, comme search. Toutefois, regex accepte d’autres tests en passant le mot-clé match_type.2
  • Tous ce tests sur les lignes acceptent les arguments optionnels ignorecase et multiline.

5.7. Tester si une liste contient une valeur

Ansible (version 2.8+) inclut un test de contenu qui fonctionne de manière similaire, mais en sens inverse du test Jinja2. Le test de contenu est conçu pour fonctionner avec les filtres : select, reject, selectattr et rejectattr :

#test-if-a-list-contains-a-value.yaml
---
- name: "Testing if a list contains a value"
  hosts: localhost
  gather_facts: False
  vars:
    lacp_groups:
      - master: lacp0
        network: 10.65.100.0/24
        gateway: 10.65.100.1
        dns4:
          - 10.65.100.10
          - 10.65.100.11
        interfaces:
          - em1
          - em2
      - master: lacp1
        network: 10.65.120.0/24
        gateway: 10.65.120.1
        dns4:
          - 10.65.100.10
          - 10.65.100.11
        interfaces:
            - em3
            - em4
  tasks:
    - debug:
        msg: "{{ (lacp_groups|selectattr('interfaces', 'contains', 'em1')|first).master }}"

5.8. Tester si une liste de valeur est “True”

Vous pouvez utiliser any ou all pour vérifier si n’importe quel élément ou tous les éléments d’une liste sont vrais ou non :

#test-if-a-list-value-is-true.yaml
---
- name: "Testing if a list value is True"
  hosts: localhost
  gather_facts: False
  vars:
    mylist:
        - 1
        - "{{ 3 == 3 }}"
        - True
    myotherlist:
        - False
        - True
tasks:
  - debug:
      msg: "all are true!"
    when: mylist is all

  - debug:
      msg: "at least one is true"
    when: myotherlist is any

5.9. Tester des chemins

On peut tester les chemins de fichier :

#...
---
- name: "Testing paths"
  hosts: localhost
  gather_facts: False
  tasks:
    - debug:
        msg: "path is a directory"
      when: mypath is directory

    - debug:
        msg: "path is a file"
      when: mypath is file

    - debug:
        msg: "path is a symlink"
      when: mypath is link

    - debug:
        msg: "path already exists"
      when: mypath is exists

    - debug:
        msg: "path is {{ (mypath is abs)|ternary('absolute','relative')}}"

    - debug:
        msg: "path is the same file as path2"
      when: mypath is same_file(path2)

    - debug:
        msg: "path is a mount"
      when: mypath is mount

5.10. Agir en fonction du résultat d’une commande

Agir en fonction du résultat d’une commande :

---
#ansible-playbook register-result.yaml -v
- hosts: localhost
  tasks:
    - command: /bin/false
      register: result
      ignore_errors: True

    - command: echo "print when result is failed"
      when: result is failed

    # In older versions of ansible use ``success``, now both are valid but succeeded uses the correct tense.
    - command: echo "print when result is succeed"
      when: result is succeeded

    - command: echo "print when result is skipped"
      when: result is skipped

    - command: echo "print when result is defined"
      when: result is defined

    - set_fact:
        result:

    - command: echo "print when result is not defined"
      when: ! result is defined

6. Filtres Ansible sur les variables

Les filtres permettent de transformer le contenu des variables.

6.1. Filtres de format

{{ var | to_nice_json }}
{{ var | to_json }}
{{ var | from_json }}
{{ var | to_nice_yml }}
{{ var | to_yml }}
{{ var | from_yml }}
{{ var | filesizeformat }}

PyYAML library, Formatting data: YAML and JSON

6.2. Fitres sur l’état des tâches

{{ result | failed }}
{{ result | changed }}
{{ result | success }}
{{ result | skipped }}

6.3. Filtre qui définit des valeurs par défaut ou obligatoire

{{ var | mandatory }}
{{ var | default(5) }}

6.4. Combiner et sélectionner des données

Combining and selecting data

6.5. Filtre de traitement d’un liste

{{ list1 | unique }}
{{ list1 | union(list2) }}
{{ list1 | intersect(list2) }}
{{ list1 | difference(list2) }}
{{ list1 | symmetric_difference(list2) }}

Managing list variables

6.6. Filtres de traitement des chemins

{{ path | basename }}
{{ path | dirname }}
{{ path | expanduser }}
{{ path | realpath }}

6.7. Filtres d’encodage

{{ var | b64decode }}
{{ var | b64encode }}
{{ filename | md5 }}
{{ var | bool }}
{{ var | int }}
{{ var | quote }}
{{ var | md5 }}
{{ var | fileglob }}

Manipulating strings

6.8. Filtres de recherche de texte

{{ var | match }}
{{ var | search }}
{{ var | regex }}

6.9. Filtres divers

Randomizing data

Encrypting and checksumming strings and passwords

Manipulating text

Managing UUIDs

Handling dates and times

7. Lookups

Les lookups Ansible permettent de récupérer des données en dehors du livre de jeu dans des sources externes.

ansible-doc -l -t lookup

7.1. Prendre le contenu d’un fichier

{{  lookup('file', '/etc/foo.txt')  }}

7.2. Prendre ou générer un mot de passe, stocké dans un fichier

{{  lookup('password', '/tmp/passwordfile chars=ascii')  }}

7.3. Prendre une variable d’environnement

{{  lookup('env','HOME')  }}

7.4. Prendre la sortie d’un commande

{{  lookup('pipe','date')  }}

7.5. Récupérer une entrée Redis

{{  lookup('redis', 'redis://localhost:6379,somekey')  }}

7.6. Prendre une entrée DNS

{{  lookup('dnstxt', 'example.com')  }}

7.7. Récupérer des fichiers après modélisation Jinja2

{{  lookup('template', './some_template.j2')  }}

8. Opérateurs logiques dans la modélisation Jinja2

Le module ansible.builtin.template permet de créer des fichiers sur les hôtes distants à partir de modèles qui sont exécutés avec le langage de modélisation (templating) Jinja2.

Un modèle contient des variables et/ou des expressions, qui sont remplacées par des valeurs lors du rendu d’un modèle, et des balises qui contrôlent la logique du modèle. La syntaxe du modèle Jinja2 est fortement inspirée de Django et de Python.

On a déjà vu différentes manières de manipuler des variables Jinja2 avec des filtres et des lookups ou encore des tests. On se concentrera ici sur la logique dans la modélisation de fichiers avec le module ansible.builtin.template.

8.1. Module ansible.builtin.template

8.2. Conditions

8.3. Boucles