Variables, Facts, Conditions et Boucles

On abordera les sujets suivants dans ce document :

Si vous poursuivez des objectifs de certification voici ceux qui sont suggérés ici :

  1. Comprendre les composants de base d’Ansible
    • Variables
    • “Facts”
  2. Créez des jeux et des playbooks Ansible
    • Savoir travailler avec les modules Ansible d’usage courant
    • Utiliser des variables pour récupérer les résultats de l’exécution d’une commande
    • Utiliser les conditions (“conditionals”) pour contrôler l’exécution de la lecture
    • Configurer la gestion des erreurs
  3. Créer et utiliser des modèles pour créer des fichiers de configuration personnalisés
  4. Travailler avec des variables et des “Facts” Ansibles

Tous les exemples opérationnels, c’est-à-dire que l’on peut exécuter tels que présentés, se trouvent dans le dossier demo du tutoriel interactif sur Ansible, 16) Exercices du support. Dans ce même tutoriel, on trouvera des leçons en rapport :

  • 6) Playbooks, pousser des fichiers sur les noeuds
  • 7) Playbooks et les erreurs
  • 8) Playbooks et les conditions
  • 9) Module Git
  • 10) Etendre le playbook à plusieurs hôtes
  • 11) Templates - Modèles
  • 12) Encore des variables

Ce document propose un exercice récapitulatif final, mais les parties plus spécifiques sur l’automation Linux ou réseau propose aussi des pratiques qui couvrent ces sujets.

1. Valeurs de retour (Rappel)

Ansible est très loquace et rend des sorties en format JSON. Ansible génère alors des valeurs de retour à la suite de l’exécution des tâches et des modules. Ces valeurs de retour peuvent être capturées et traitées. En voici une liste non exhaustive.

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=no
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. Variables

Il est de bonne pratique d’organiser ses procédures avec des variables. Les variables Ansible peuvent être déclarées à différents endroits : dans l’inventaire, dans les dossiers group_vars/ ou host_vars/ par exemple.

On peut aussi les définir sur la ligne de commande d’ansible-playbook -e comme argument supplémentaire et prépondérant, mais aussi dans le livre de 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

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

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

Dans les exercices suivant nous allons utiliser les fonctionnalités suivantes :

  • vars: sur une tâche ou dans un jeu et vars_files: en premier niveau d’un jeu.
  • 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.

Créer un fichier de données YAML

Attention le nom de variables ne peuvent 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

Utiliser les données

---
#print-out-data.yml
- 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. 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)

4. 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 controle sur le jeu avec la directive de jeu gather_facts:. On peut faire en sorte que la collecte de facts doivent 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.yml
- name: print out operating system
  hosts: localhost
  gather_facts: False
  tasks:
    - name: "facts"
      setup:
      register: output
    - name: "print output"
      debug:
        msg: "{{ output }}"
  • ansible_distribution
  • ansible_distribution_release
  • ansible_distribution_version
  • ansible_fqdn
  • ansible_hostname
  • ansible_os_family
  • ansible_pkg_mgr
  • ansible_default_ipv4.address
  • ansible_default_ipv6.address

Obtenir la version de sa distribution

---
#print-out-distribution.yml
- name: print out distribution
  hosts: localhost
  gather_facts: True
  tasks:
    - name: "print output"
      debug:
       msg: " {{ ansible_distribution }} {{ ansible_distribution_version }}"

Obtenir son adresse ip publique

---
#get-ipv4-public.yml
- 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}}"

5. Sortie brute à traiter

---
- 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 }}"

Exercice : Sortie à traiter.

tests

6. 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 }}

7. Les conditions

Ansible Conditionals

when:

  • var == "Value", var >= 5, etc.
  • var, où var correspond à un boléen (yes, true, True, TRUE)
  • var is defined, var is not defined, ! var is defined
  • <condition1> and <condition2>
  • <condition1> or <condition2>
  • (<condition1> and <condition2>) or (<condition3> and <condition4>)
  tasks:
    - name: "Debian Action"
      debug:
        msg: "{{ ansible_hostname }} Debian host"
      when: ansible_facts['os_family'] == "Debian"
      # note that all variables can be directly in conditionals without double curly braces
  tasks:
    - name: "Action on CentOS 7 and Debian 9 systems"
      debug:
        msg: "{{ ansible_hostname }} CentOS 7 and Debian 9 systems"
      when: (ansible_facts['distribution'] == "CentOS" and ansible_facts['distribution_major_version'] == "7") or
            (ansible_facts['distribution'] == "Debian" and ansible_facts['distribution_major_version'] == "9")
  tasks:
    - name: "shut down CentOS 7 systems"
      debug:
        msg: "{{ ansible_hostname }} CentOS 7"
      when:
        - ansible_facts['distribution'] == "CentOS"
        - ansible_facts['distribution_major_version'] == "7"
  tasks:
    - shell: echo "only on Red Hat 6, derivatives, and later"
      when: ansible_facts['os_family'] == "RedHat" and ansible_facts['lsb']['major_release']|int >= 6

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

---
#ansible-playbook register-result.yml -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

8. Filtres

{{ var | to_nice_json }}
{{ var | to_json }}
{{ var | from_json }}
{{ var | to_nice_yml }}
{{ var | to_yml }}
{{ var | from_yml }}
{{ result | failed }}
{{ result | changed }}
{{ result | success }}
{{ result | skipped }}
{{ var | manditory }}
{{ var | default(5) }}
{{ list1 | unique }}
{{ list1 | union(list2) }}
{{ list1 | intersect(list2) }}
{{ list1 | difference(list2) }}
{{ list1 | symmetric_difference(list2) }}
{{ path | basename }}
{{ path | dirname }}
{{ path | expanduser }}
{{ path | realpath }}
{{ var | b64decode }}
{{ var | b64encode }}
{{ filename | md5 }}
{{ var | bool }}
{{ var | int }}
{{ var | quote }}
{{ var | md5 }}
{{ var | fileglob }}
{{ var | match }}
{{ var | search }}
{{ var | regex }}

Voir aussi default jinja2 filters et Jinja2 Filters in Ansible.

9. Lookups

{{  lookup('file', '/etc/foo.txt')  }}
{{  lookup('password', '/tmp/passwordfile chars=ascii')  }}
{{  lookup('env','HOME')  }}
{{  lookup('pipe','date')  }}
{{  lookup('redis_kv', 'redis://localhost:6379,somekey')  }}
{{  lookup('dnstxt', 'example.com')  }}
{{  lookup('template', './some_template.j2')  }}

10. Boucles

Ansible Loops

Note : la boucle with_item est dépréciée au profit de la boucle loop.

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

---
#my-fruits.yml
- 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.yml
- 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.yml
- 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 }}"

Standard

---
#other-my-fruits.yml
- 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 }}"

Nested

---
#nested-my-fruits.yml
- 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 }}"

Over hashes

---
#print-phone-records.yml
- 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 }}"

Random choice

Pas besoin de boucle ici !

---
#random-my-fruits.yml
- 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 }}"

Do-Until

---
#do-until.yml
- 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

11. Exercice récapitulatif

En tant que gestionnaire d’une infrastructure, il vous est demandé de concevoir un livre de jeu qui :

  • crée un utilisateur “lambda” pour un client (un webmaster qui a besoin d’un accès à la ressource)
  • génère une clé privée dans son profil
  • envoie un courriel avec les informations de connexion à l’adresse du client associé à l’hôte
  • envoie la clé privée en formats “pem” (openssh) et/ou ppk (putty) en attachment, en format zip
  • place l’adresse du formateur en CC: pour livraison finale de l’exercice

Partons du principe que l’OS cible est un Ubuntu/Debian.

Modules à disposition :

  • apt
  • user
  • group
  • lineinfile
  • authorized_key
  • fetch
  • file
  • archive
  • mail avec comme serveur de messagerie smtp.gmail.com:587

Construction des données :

  • Données du clients (fichier : nom, adresse)
  • Données de la ressource (inventaire, nom d’hôte, paramètres de connexion, approvisionnement)
  • Données des tâches (jeu / rôle, substitution, paramètres par défaut)
  • Données dynamiques (adresse IP, capacités de la machine mise à disposition, …)

Optimisations utltérieures

  • Multi-OS : RHEL/Centos
  • Conversion en “role”
  • Protection Ansible-Vault
  • Intégration à un approvisionnement Terraform