Outils pour utilisateurs

Outils du site


infrastructure:ansible

Table des matières

Ansible 1.7.2 (Debian Jessie)

https://github.com/lorin/ansible-quickref

production                # inventory file for production servers
stage                     # inventory file for stage environment

group_vars/
   all                    # here we assign variables to particular groups
   boston                 # ""
host_vars/
   hostname1              # if systems need specific variables, put them here
   xyz.boston.example.com # ""

site.yml                  # master playbook
webservers.yml            # playbook for webserver tier
dbservers.yml             # playbook for dbserver tier

roles/
    common/               # this hierarchy represents a "role"
        defaults/
            main.yml      # variables par defaut du module, supplantées par tout le reste
        tasks/            #
            main.yml      #  <-- tasks file can include smaller files if warranted
        handlers/         #
            main.yml      #  <-- handlers file
        templates/        #  <-- files for use with the template resource
            ntp.conf.j2   #  <------- templates end in .j2
        files/            #
            bar.txt       #  <-- files for use with the copy resource
            foo.sh        #  <-- script files for use with the script resource
        vars/             #
            main.yml      #  <-- variables associated with this role -> valeurs spécifiques, dont on a besoin d'être sur, suplpanté seulement par '-e'
        meta/             #
            main.yml      #  <-- role dependencies

    webtier/              # same kind of structure as "common" was above, done for the webtier role
    monitoring/           # ""
    fooapp/               # ""

Exemple de fichier d'inventaire

Groupé par localisation (enfants) et par rôle (parents).

# file: production

[atlanta-webservers]
www-atl-1.example.com
www-atl-2.example.com

[boston-webservers]
www-bos-1.example.com
www-bos-2.example.com

[atlanta-dbservers]
db-atl-1.example.com
db-atl-2.example.com

[boston-dbservers]
db-bos-1.example.com

# webservers in all geos
[webservers:children]
atlanta-webservers
boston-webservers

# dbservers in all geos
[dbservers:children]
atlanta-dbservers
boston-dbservers

# everything in the atlanta geo
[atlanta:children]
atlanta-webservers
atlanta-dbservers

# everything in the boston geo
[boston:children]
boston-webservers
boston-dbservers

Exemple de variables d'inventaire

# file: group_vars/atlanta
  
ntp: ntp-atlanta.example.com
backup: backup-atlanta.example.com

# file: group_vars/webservers

apacheMaxRequestsPerChild: 3000
apacheMaxClients: 900

# file: group_vars/all

ntp: ntp-boston.example.com
backup: backup-boston.example.com

# file: host_vars/db-bos-1.example.com

foo_agent_port: 86
bar_agent_port: 99

site.yml - Un point d'entrée pour inclure les autres playbooks

# file: site.yml

- include: webservers.yml
- include: dbservers.yml

playbook : fait correspondre un groupe à des roles

# file: webservers.yml

- hosts: webservers
  roles:
    - common
    - webtier

Exemple de fichier de tasks pour un role

# file: roles/common/tasks/main.yml

- name: be sure ntp is installed
  yum: pkg=ntp state=installed
  tags: ntp

- name: be sure ntp is configured
  template: src=ntp.conf.j2 dest=/etc/ntp.conf
  notify:
    - restart ntpd
  tags: ntp

- name: be sure ntpd is running and enabled
  service: name=ntpd state=running enabled=yes
  tags: ntp

Exemple de handler

ATTENTION BUG

# file: roles/common/handlers/main.yml
- name: restart ntpd
  service: name=ntpd state=restarted

Exemples

ansible-playbook -i production site.yml
ansible-playbook -i production site.yml --tags ntp
ansible-playbook -i production webservers.yml
ansible-playbook -i production webservers.yml --limit boston
ansible-playbook -i production webservers.yml --limit boston[0-10]
ansible-playbook -i production webservers.yml --limit boston[10-20]
ansible boston -i production -m ping
ansible boston -i production -m command -a '/sbin/reboot'

Confirm what task names would be run if I ran this command and said “just ntp tasks”

ansible-playbook -i production webservers.yml --tags ntp --list-tasks

Confirm what hostnames might be communicated with if I said “limit to boston”

ansible-playbook -i production webservers.yml --limit boston --list-hosts

Information préalable

Si Red Hat Enterprise Linux (TM), CentOS, Fedora, Debian, or Ubuntu : prendre les derniers packages

Prérequis

  • serveur : Python 2.6 or 2.7
  • noeuds : ssh/sftp, Python 2.4 avec python-simplejson ou 2.5 ⇐ Python < 3 (attention à la version par défaut, si besoin, rensiegner la variable “ansible_python_interpreter”)

on peut bootstraper un noeud pour installer python 2.X en mode “raw” : ansible myhost –sudo -m raw -a “yum install -y python2 python-simplejson”

améliorations/changments notables sur les versions de Ansible

  • 1.9 : ANSIBLE_HOSTS désuet, voir ANSIBLE_INVENTORY
  • 2.x : fonctions plus puissantes pour les boucles

Installation

apt-get install ansible ansible-doc
sudo sed --in-place "s/^remote_port/#remote_port/g" /etc/ansible/ansible.cfg
iceweasel /usr/share/doc/ansible-doc/html/index.html

Par défaut, Ansible utilise l'authentification SSH par clef.
Par défaut, le serveur central se connecte sur les noeuds et lance des commandes/scripts. Mais cela peut être inversé (voir ansible-pull).

Premiers pas

variables d'environnement

BASEDIR="${HOME}/ansible"
INVENTORY=${BASEDIR}/localhost
LIMIT="localhost"
PARALLELISM=5

création du répertoire de référence

if [ ! -d ${BASEDIR} ]; then
    mkdir ${BASEDIR}
fi  

initialisation de l'inventaire avec l'hôte local

echo "localhost ansible_connection=local" > ${INVENTORY}

test avec un simple ls

ansible --inventory=${INVENTORY} --limit=${LIMIT} --forks=${PARALLELISM} --module-name=shell --args="ls" all

Mettre plus de machines dans un inventaire

Le nom de fichier correspond a l'environnement. On peut utiliser des sections [<nom_du_groupe>] pour creer des groupes d'hotes (conseille : par emplacement geographique).

RQ : on peut faire apparaitre un meme hote dans plusieurs groupes, mais attention a la determination des variables et leurs précédences !
RQ : mettre au carre ~/.ssh/config au prealable, et copier la clef publique SSH sur les serveurs !

Exemple

Host plop.liberasys.com
  Hostname plop.liberasys.com
  Port 2222
  User root

Exemple de fichier d'inventaire

mail.example.com

[webservers]
foo.example.com
bar.example.com

[dbservers]
one.example.com
two.example.com
three.example.com

Exemple d'utilisation d'ansible avec des machines de test

BASEDIR="${HOME}/ansible"
INVENTORY=${BASEDIR}/test
LIMIT=""
PARALLELISM=5

Pinger les machines :

ansible --inventory=${INVENTORY} --limit=${LIMIT} --forks=${PARALLELISM} --module-name=ping all

Voir si l'option UseDns est bien positionnee :

ansible --inventory=${INVENTORY} --limit=${LIMIT} --forks=${PARALLELISM} --module-name=command --args="bash -c 'cat /etc/ssh/sshd_config | grep -i dns'" all

Choses a voir

Si vous utilisez sudo, configurez le pour ne pas demander de mot de passe, et mettre en plus l'argument “–sudo” dans la ligne de commande ansible. Si besoin de spécifier un autre utilisateur, utiliser “–sudo-user=”.
Si vous ne voulez plus être embêté avec les vérifications de clefs connues des servuer SSH, éditer /etc/ansible/ansible.cfg et modifier : “host_key_checking = False”. Il y a aussi une variable d'environnement prévue : “export ANSIBLE_HOST_KEY_CHECKING=False”
Si vous voulez optimiser le temps de connexion SSH aux serveurs (à faire sur chaque serveur) : sudo echo “UseDNS no” » /etc/ssh/sshd_config && service ssh stop && sleep 3 && service ssh start
Si vous voulez tracer les changements de configuration, utilisez un système de gestion de version sur le répertoire de configuration ansible !

Inventaire

But : définir les hosts, des groupes de machines.

mail.example.com

[webservers]
foo.example.com
bar.example.com

[dbservers]
one.example.com
two.example.com
three.example.com

[targets]
localhost              ansible_connection=local
other1.example.com     ansible_connection=ssh        ansible_ssh_user=mpdehaan
other2.example.com     ansible_connection=ssh        ansible_ssh_user=mdehaan

Groupe de Groupe :
[southeast:children]
atlanta
raleigh

Avec des patterns :
[webservers]
www[01:50].example.com

Avec des variables de groupes :
[southeast:vars]
some_server=foo.southeast.example.com
halon_system_timeout=30
self_destruct_countdown=60
escape_pods=2

Avec des variables par host :
[atlanta]
host1 http_port=80 maxRequestsPerChild=808
host2 http_port=303 maxRequestsPerChild=909

Les variables dans l'inventaire

On peut les définir par hote, par groupe ([<nom_du_groupe>:vars]) ou par groupe de groupes (mais attention, cette derniere methode ne fonctionne pas avec ansible, seulement avec ansible-playbook).

Exemple :

[atlanta]
host1 http_port=80 maxRequestsPerChild=808

Certaines variables configurent le comportement d'interraction avec les hotes (on pourrait se passer de la configuration ssh_config) :

ansible_ssh_host
ansible_ssh_port
ansible_ssh_user
ansible_connection
ansible_ssh_private_key_file
ansible_shell_type
ansible_python_interpreter
ansible\_\*\_interpreter

Mais ! La bonne pratique veut que l'on sépare les variables “métier” du fichier d'inventaire. Hiérarchie conseillée :

${BASEDIR}/group_vars/groupe_1
${BASEDIR}/group_vars/groupe_2
${BASEDIR}/host_vars/host_a

RQ : ce sont alors des fichiers YAML

Example :

ntp_server: acme.example.org
database_server: storage.example.org

Inventaire automatique

RQ : on peut faire de l'inventaire automatique / dynamique, avec cobbler ou AWS EC2 par exemple, en passant par un script d'inventaire :

file:///usr/share/doc/ansible-doc/html/intro_dynamic_inventory.html

RQ : si l'option –inventory est utilisée avec un répertoire, toutes les sources présentes sont prises en compte (même les scripts .py).

Scripts d'inventaire automatique

BSD Jails Digital Ocean Google Compute Engine Linode OpenShift OpenStack Nova Red Hat's SpaceWalk Vagrant (not to be confused with the provisioner in vagrant, which is preferred) Zabbix

Patterns : choix des hostes à traiter

  1. tout : 'all' ou '*'
  2. host :

one.example.com

  one.example.com:two.example.com
  192.168.1.50
- hosts :
  192.168.1.*
  *.example.com
  *.com
- nom d'un groupe :
- opérations booléennes avec les groupes : 
  groupe1:groupe2 : OR (union)
  groupe1:!groupe2 : AND NOT (différence)
  groupa1:&groupe2 : ET (intersection)
  on peut combiner : webservers:dbservers:&staging:!phoenix (précédence de gauche à droite)
- mix :
  one*.com:dbservers
  ~(web|db).*\.example\.com
- limit flag :
  ansible-playbook site.yml --limit datacenter2

had-hoc commands

Utilisé pour les petites tâches qui n'ont pas besoin d'être réutiliser. Ex : rebooter les machines : ansible atlanta -a “/sbin/reboot” -f 10 copier un fichier : ansible webservers -m file -a “dest=/srv/foo/b.txt mode=600 owner=mdehaan group=mdehaan”

Polling :
lancer en arrière plan :

ansible all -B 3600 -a "/usr/bin/long_running_operation --do-stuff" (arrière plan)
ansible all -m async_status -a "jid=123456789" (status manuel)
ansible all -B 1800 -P 60 -a "/usr/bin/long_running_operation --do-stuff" (polling)

Fichier de configuration Ansible

Ordre de prise en compte des paramètres :

  • ANSIBLE_CONFIG (an environment variable) - voir : constants.py
  • ansible.cfg (in the current directory)
  • .ansible.cfg (in the home directory)
  • /etc/ansible/ansible.cfg

Chaine de caractères de tag de config par ansible : ansible_managed = Ansible managed: {file} modified on %Y-%m-%d %H:%M:%S by {uid} on {host}
Erreur si variable non renseignée : #error_on_undefined_vars=False ⇒ attention, par défaut à False (erreur silencieuse)
Logging : log_path = /var/log/ansible.log =⇒ par defaut, pas de logging !
Timeout SSH : par défaut à 10, peut être diminué sauf systèmes fortement chargés
Pipelining : a voir pour améliorer les perfs, par défaut à false, mais requiert des modification sur la configuration de sudo si il est utilisé (require tty)

Exemple de fichier personnalisé (~/.ansible.cfg)

[defaults]
log_path = ~/ansible/log/ansible.log
timeout = 1

[ssh_connection]
pipelining = True

[accelerate]
accelerate_port = 5099
accelerate_timeout = 30
accelerate_connect_timeout = 1.0
accelerate_multi_key = yes

Windows support

Via powershell en natif. Requiert pywinrm sur la machine de contrôle :

  pip install http://github.com/diyan/pywinrm/archive/master.zip#egg=pywinrm

Gestion de paquets via chocolatey : https://chocolatey.org
Il y a de smodules spécifiques windows (ex : ACLs).
Il faut toujours une machine de contrôle linux (et cygwin n'est pas supporté).

Playbooks

Langage de configuration, déploiement et orchestration.

Playbooks : on utilise des 'Modules' dans des 'Tasks' assemblées dans des 'Plays' contenus dans des 'Playbooks'.
Syntaxe = YAML

RQ : Ansible utilise des caractères spéciaux en interne. Si ces caractères sont utilisés pour la syntaxe YAML, il faut les escaper dans le playbook : [] {} : > | /. En fait Ansible a sa surcouche à YAML.

  • Un playbook est composé de plusieurs 'plays'.
  • Un 'play' fait concorder des groupes de machines avec des rôles bien définis, représentés par des 'tasks'. Basiquement, ce sont des appels à des modules Ansible.

Exemple :

  1. hosts: webservers

vars:

    http_port: 80
    max_clients: 200
  remote_user: root
  tasks:
  - name: ensure apache is at the latest version
    yum: pkg=httpd state=latest
  - name: write the apache config file
    template: src=/srv/httpd.j2 dest=/etc/httpd.conf
    notify:
    - restart apache
  - name: ensure apache is running
    service: name=httpd state=started
  handlers:
    - name: restart apache
      service: name=httpd state=restarted
  • Les 'tasks' sont exécutées dans l'ordre. Si une tâche échoue sur une hote, ce dernier est retiré de la liste d'exécution du playbook.
  • Une 'task' execute un module avec des arguments spécifiques.
  • Les modules sont idempotents. Cela signifie que l'on peut les lancer plusieurs fois sans effet de bord. Ils amènent les systèmes vers un état prédéfini.
  • Le champ 'name' est purement pour la sortie humaine.
  • La ligne d'action (par défaut : module) peut être divisée en plusieurs lignes

Système évènementiel : notify/handler, example :
ATTENTION BUG https://github.com/ansible/ansible/issues/11025

  1. name: template configuration file

template: src=template.j2 dest=/etc/foo.conf

  notify:
     - restart memcached
     - restart apache
handlers:
    - name: restart memcached
      service:  name=memcached state=restarted
    - name: restart apache
      service: name=apache state=restarted
  • Les handlers sont toujours lancés dans l'ordre de rédaction
  • Les handlers sont gérés dans : ‘pre_tasks’, ‘roles’, ‘tasks’, and ‘post_tasks’

Pour exécuter (flusher) les handlers immédiatement :

tasks:
   - shell: some tasks go here
   - meta: flush_handlers
   - shell: some other tasks

Lancement d'un playbook :

ansible-playbook playbook.yml -f 10

RQ : on peut inverser le sens de connection d'ansible, et lancer ansible-pull dans une crontab et l'asservir à un outil de gestion de version

TIPS :

Lister les hotes cibles :

ansible-playbook playbook.yml --list-hosts

vérifier la syntaxe :

ansible-playbook --syntax-check playbook.yml
  • On peut appeler des fichiers de 'tasks' au besoin afin de segmenter les choses. Sinon il y a les 'roles'
  • On peut inclure des 'plays' depuis d'autres playbooks.

Playbook inclusion

Abstractions réutilisables, combinant des fichiers d'inclusion.

Le but d'un 'play' dans un 'playbook' est de faire correspondre un groupe de machines à plusieurs 'roles'.

Fichier de 'tasks' à inclure :

# possibly saved as tasks/foo.yml
  1. name: placeholder foo

command: /bin/foo

  1. name: placeholder bar

command: /bin/bar

Inclusion d'un fichier de 'tasks' :

tasks:
  1. include: tasks/foo.yml

On peut donner des paramètres à des inclusions :

tasks:
  - include: wordpress.yml user=timmy
  - include: wordpress.yml user=alice
  - include: wordpress.yml user=bob
tasks:
 - { include: wordpress.yml, user: timmy, ssh_keys: [ 'keys/one.txt', 'keys/two.txt' ] }

Autre syntaxe pour le passage de variables :

tasks:
  - include: wordpress.yml
    vars:
        remote_user: timmy
        some_list_variable:
          - alpha
          - beta
          - gamma

On peut aussi le faire avec les handlers :

# this might be in a file like handlers/handlers.yml
- name: restart apache
  service: name=apache state=restarted

handlers:
  - include: handlers/handlers.yml

L'inclusion peut être utilisée avec les playbooks :

  1. name: this is a play at the top level of a file

hosts: all

  remote_user: root

  tasks:

  - name: say hi
    tags: foo
    shell: echo "hi..."

- include: load_balancers.yml
- include: webservers.yml
- include: dbservers.yml

MAIS :

  • on ne peut pas faire de substitution de variable
  • on ne peut pas conditionner le chemin d'un fichier inclu
  • voir fonction 'when'

Playbooks roles

Les roles sont juste une couche d'automatisation pour les directives d'inclusion.

Roles : outils d'organisation, définition de composants réutilisables et utilisant une structure de fichiers déterminée

Ex de structure :

site.yml
webservers.yml
fooservers.yml
roles/
   common/
     files/
     templates/
     tasks/
     handlers/
     vars/
     meta/
   webservers/
     files/
     templates/
     tasks/
     handlers/
     vars/
     meta/
   

Utilisation des 'roles' dans un playbook :

  1. hosts: webservers

roles:

  1. common
  2. webservers

Pour chaque 'role' x :

  • If roles/x/tasks/main.yml exists, tasks listed therein will be added to the play
  • If roles/x/handlers/main.yml exists, handlers listed therein will be added to the play
  • If roles/x/vars/main.yml exists, variables listed therein will be added to the play
  • If roles/x/meta/main.yml exists, any role dependencies listed therein will be added to the list of roles (1.3 and later)
  • Any copy tasks can reference files in roles/x/files/ without having to path them relatively or absolutely
  • Any script tasks can reference scripts in roles/x/files/ without having to path them relatively or absolutely
  • Any template tasks can reference files in roles/x/templates/ without having to path them relatively or absolutely
  • Any include tasks can reference files in roles/x/tasks/ without having to path them relatively or absolutely

RQ : on peut configurer le chemin des 'roles' ('roles_path').

On peut donner des variables aux roles :

  1. hosts: webservers

roles:

  1. common
  2. { role: foo_app_instance, dir: '/opt/a', port: 5000 }
  3. { role: foo_app_instance, dir: '/opt/b', port: 5001 }

On peut conditionner les roles :

  1. hosts: webservers

roles:

  1. { role: some_role, when: “ansible_os_family == 'RedHat'” }

On peut assigner des 'tags' aux roles :

  1. hosts: webservers

roles:

  1. { role: foo, tags: [“bar”, “baz”] }

RQ : les 'roles' sont executes avant les 'tasks' du playbook dans un 'play'. Mais on peut ordonner les choses :

  1. hosts: webservers
  pre_tasks:
    - shell: echo 'hello'
  roles:
    - { role: some_role }
  tasks:
    - shell: echo 'still busy'
  post_tasks:
    - shell: echo 'goodbye'

RQ : dans ce cas, les 'tags', si utilisés, doivent être présents également dans pre_tasks and post_tasks

Il est possible de définir un jeu de variables par défaut dans : defaults/main.yml

Les dépendances entre 'roles' gèrent automatiquement les séquences. Les dépendances sont rédigées dans le fichier meta/main.yml du répertoire du role. Dans ce fichier, ce sont les roles de dependance amont (antérieure, aka pré-conditions) :

dependencies:
  - { role: common, some_parameter: 3 }
  - { role: apache, port: 80 }
  - { role: postgres, dbname: blarg, other_parameter: 12 }
  - { role: '/path/to/common/roles/foo', x: 1 }

Les roles de dependance amont ne sont executés qu'une seule fois. On peut changer ce comportement au besoin (par exemple si on se sert d'une variable en condition), avec la directive 'allow_duplicates: yes' en début de meta/main.yml

RQ : on peut inclure des modules développés dans un 'role' afin de le distribuer.
RQ : avec cette méthode on peut patcher des modules existant

roles/
   my_custom_modules/
       library/
          module1
          module2

Repository de roles : https://galaxy.ansible.com/

Variables

nommage : lettres (minuscules), chiffres et '_' c'est tout !

Variables dans les inventaires : non recommandé

Variables en ligne dans un playbook :

  1. hosts: webservers

vars:

    http_port: 80

Variables dans les roles ou les fichiers inclus.

Jinja 2

Substitution des variables : nom_de_variable
Jinja2 permet des choses comme les boucles ou les donctions dans les templstes. Mais il n'est pas souhaitable de les utiliser dans les playbooks.
Jinja2 permet de filtrer, reformater et tansformer les expressions (voir : http://jinja.pocoo.org/docs/dev/templates/#builtin-filters).
Exemple pour le débugging :

{{ some_variable | to_nice_json }}
{{ some_variable | to_nice_yaml }}

Pour les filtres, Jinja2 utilise le pipe : <nom_de_variable>|<nom_filtre>(“<variable_du_filtre>”)
Exemple :

{{ listx|join(', ') }}

Voir : http://jinja.pocoo.org/docs/dev/templates/#builtin-filters

Affectation par défaut :

{{ some_variable | default(5) }}

Avec Ansible :

var|match : renvoie vrai si la variable correspond à une valeur
travail sur les listes :
  {{ list1 | unique }}
  {{ list1 | union(list2) }}
  {{ list1 | intersect(list2) }}
  {{ list1 | difference(list2) }}
  {{ list1 | symmetric_difference(list2) }}
  {{ list | join(" ") }}
random :
  {{ 59 |random}} * * * * root /script/from/cron
  {{ ['a','b','c']|random }} => 'c'
  {{ 100 |random(step=10) }}  => 70
  {{ 100 |random(1, 10) }}    => 31
  {{ 100 |random(start=1, step=10) }}    => 51
basename : {{ path | basename }}
dirname : {{ path | dirname }}
expansion chemin contenant ~ : {{ path | expanduser }}
base64 :
  {{ encoded | b64decode }}
  {{ decoded | b64encode }}
md5sum : {{ filename | md5 }}
casting : 
  some_string_value | bool
regexp match : when: url | match("http://example.com/users/.*/resources/.*")
regexp search : when: url | search("/users/.*/resources/.*")
regexp replace : {{ 'foobar' | regex_replace('^f.*o(.*)$', '\\1') }}
casting : <nom_variable>|<type>, ex : ansible_lsb.major_release|int

Les filtres sont améliorés sans cesse avec les versions et on peut en implémenter.
Attention aux caractères spéciaux de YAML : il faut escaper : [] {} : > | /

Facts

Relevé d'informations de l'hote distant = “locally supplied user values”.
On peut les afficher avec : ansible hostname -m setup
On les utilise comme des variables : ansible_hostname
Souvent utilisé dans les pré-conditions ('when')
On peut les utiliser pour créer des groupes dynamiques d'hotes ('group_by').
Si on a pas besoin des facts, on peut les désactiver (ce la accélère l'exécution) :

  1. hosts: whatever

gather_facts: no

On peut aussi créer des 'facts' dynamiquement, en utilisant le répertoire local des hotes : /etc/ansible/facts.d/preferences.fact
On peut aussi créer des modules spécifiques de 'facts'.
Voir doc pour l'application.

{{ ansible_eth0["ipv4"]["address"] }}
{{ ansible_eth0.ipv4.address }}

On peut ajouter un 'fact' dans un 'task' : set_fact

Example setting host facts using key=value pairs

- set_fact: one_fact="something" other_fact="{{ local_var * 2 }}"

Example setting host facts using complex arguments

- set_fact:
     one_fact: something
     other_fact: "{{ local_var * 2 }}"

As of 1.8, Ansible will convert boolean strings ('true', 'false', 'yes', 'no') to proper boolean values when using the key=value syntax, however it is still recommended that booleans be set using the complex argument style:

  1. set_fact:

one_fact: true

    other_fact: false

Registered variables

La sortie d'un module peut produire une variable utilisable.
Utiliser “-v” lors du lancement d'un playbook pour voir les valeurs utilisables. Exemple :

  1. hosts: web_servers

tasks:

     - shell: /usr/bin/foo
       register: foo_result
       ignore_errors: True

     - shell: /usr/bin/bar
       when: foo_result.rc == 5

Magic variables and other hosts variables access

On peut accéder aux variables des autres hosts : hostvars

{{ hostvars['test.example.com']['ansible_distribution'] }}

Pour lister les groupes qui incluent un hote : group_names

{% if 'webserver' in group_names %}
   # some part of a configuration file that only applies to webservers
{% endif %}

Pour lister les hotes et les groupes de l'inventaire : groups

{% for host in groups['app_servers'] %}
   # something that applies to all app servers.
{% endfor %}
{% for host in groups['app_servers'] %}
   {{ hostvars[host]['ansible_eth0']['ipv4']['address'] }}
{% endfor %}

Pour récupérer le FQDN tel que définit dans l'inventaire : inventory_hostname
Pour le hostnmae de l'inventaire : inventory_hostname_short
Pour avoir la liste des hosts concernés par le 'play' courrant : play_hosts
Pour le répertoire et le fichier d'inventaire : inventory_dir / inventory_file

Misc

Fichier de variables :

  1. hosts: all

remote_user: root

  vars:
    favcolor: blue
  vars_files:
    - /vars/external_vars.yml
  tasks:
  - name: this is just a placeholder
    command: /bin/echo foo

Le fichier de variables est un simple dictionnaire YAML :

# in the above example, this would be vars/external_vars.yml
somevar: somevalue
password: magic

On peut avoir des fichiers de variables par host, par groupe (voir patterns).

Passer des variables via la ligne de commande :

ansible-playbook release.yml --extra-vars "version=1.23.45 other_variable=foo"
ansible-playbook release.yml --extra-vars '{"pacman":"mrs","ghosts":["inky","pinky","clyde","sue"]}'
ansible-playbook release.yml --extra-vars "@some_file.json"

On peut donner des conditions aux inclusions de fichiers de variables :

  1. hosts: all

remote_user: root

  vars_files:
    - "vars/common.yml"
    - [ "vars/{{ ansible_os_family }}.yml", "vars/os_defaults.yml" ]
  tasks:
  1. name: make sure apache is running

service: name=apache state=running

Mais il faut facter ou ohai installés avant !
On gère ça normalement avec le 'group_by'.

On peut charger des variables dynamiquement dans une task: include_vars:

# Conditionally decide to load in variables when x is 0, otherwise do not.
- include_vars: contingency_plan.yml
  when: x == 0
# Load a variable file based on the OS type, or a default if not found.
- include_vars: "{{ item }}"
  with_first_found:
   - "{{ ansible_distribution }}.yml"
   - "{{ ansible_os_family }}.yml"
   - "default.yml"

ATTENTION : with_first_found cherche dans le répertoire files !!!

Précédence des variables :

  • -e variables always win
  • then comes “most everything else”
  • then comes variables defined in inventory
  • then comes facts discovered about a system
  • then “role defaults”, which are the most “defaulty” and lose in priority to everything.

Bien utiliser les fichier de variables :

# file: /etc/ansible/group_vars/all
# this is the site wide default
ntp_server: default-time.example.com
# file: /etc/ansible/group_vars/boston
ntp_server: boston-time.example.com
# file: /etc/ansible/host_vars/xyz.boston.example.com
ntp_server: override.example.com

Les groupes enfant supplantent les groupes parents.
Les hosts supplantent toujours leur groupes.

Supplanter les variables d'un role : appel avec parametres :

roles:
   - { name: apache, http_port: 8080 }

Conditionals

when

Utilisé comme clause dans une 'task'
Utilise les expressions jinja2

tasks:
  - name: "shutdown Debian flavored systems"
    command: /sbin/shutdown -t now
    when: ansible_os_family == "Debian"

tasks:
  - command: /bin/false
    register: result
    ignore_errors: True
  - command: /bin/something
    when: result|failed
  - command: /bin/something_else
    when: result|success
  - command: /bin/still/something_else
    when: result|skipped

tasks:
    - shell: echo "This certainly isn't epic!"
      when: not epic

tasks:
    - command: echo {{ item }}
      with_items: [ 0, 2, 4, 6, 8, 10 ]
      when: item > 5

Inclusion conditionnée (roles et taks) :

  1. include: tasks/sometasks.yml

when: “'reticulating splines' in output”

  
- hosts: webservers
  roles:
     - { role: debian_stock_config, when: ansible_os_family == 'Debian' }
  1. hosts: all

remote_user: root

  vars_files:
    - "vars/common.yml"
    - [ "vars/{{ ansible_os_family }}.yml", "vars/os_defaults.yml" ]
  tasks:
  - name: make sure apache is running
    service: name={{ apache }} state=running
# for vars/CentOS.yml
apache: httpd
somethingelse: 42

Templates conditionnnés :

  1. name: template a file

template: src=item dest=/etc/myapp/foo.conf

  with_first_found:
    - files:
       - {{ ansible_distribution }}.conf
       - default.conf
      paths:
       - search_location_one/somedir/
       - /opt/other_location/somedir/

Exemple contruit avec le multiligne sur register :

  1. name: registered variable usage as a with_items list

hosts: all

  tasks:

      - name: retrieve the list of home directories
        command: ls /home
        register: home_dirs

      - name: add home dirs to the backup spooler
        file: path=/mnt/bkspool/{{ item }} src=/home/{{ item }} state=link
        with_items: home_dirs.stdout_lines
        # same as with_items: home_dirs.stdout.split()

Loops

Simple : with_items
- name: add several users
  user: name={{ item }} state=present groups=wheel
  with_items:
     - testuser1
     - testuser2
   

Liste inclue : with_items: somelist

Items type liste :

  1. name: add several users

user: name=item.name state=present groups=item.groups

  with_items:
    - { name: 'testuser1', groups: 'wheel' }
    - { name: 'testuser2', groups: 'root' }

Boucle dans une boucle : with_nested

  1. name: here, 'users' contains the above list of employees

mysql_user: name=item_0 priv=item_1.*:ALL append_privs=yes password=foo

  with_nested:
    - users
    - [ 'clientdb', 'employeedb', 'providerdb' ]

Parsing de liste depuis un fichier de variables : with_dict

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 }})"
    with_dict: users

Boucle sur des fichiers : with_fileglob

Tous les fichiers d'un seul répertoire, non récursif, correspondant à un motif.

  1. hosts: all

tasks:

    # first ensure our target directory exists
    - file: dest=/etc/fooapp state=directory

    # copy each file over that matches the given pattern
    - copy: src={{ item }} dest=/etc/fooapp/ owner=root mode=600
      with_fileglob:
        - /playbooks/files/fooapp/*

RQ : si chemin relatif pour with_fileglob, la base est : roles/<rolename>/files

Listes parallèles: with_together

alpha: [ 'a', 'b', 'c', 'd' ]
numbers:  [ 1, 2, 3, 4 ]
tasks:
    - debug: msg="{{ item.0 }} and {{ item.1 }}"
      with_together:
        - alpha
        - numbers

Boucle sur des sous-elements : with_subelements, lookup

users:
  - name: alice
    authorized:
      - /tmp/alice/onekey.pub
      - /tmp/alice/twokey.pub
  - name: bob
    authorized:
      - /tmp/bob/id_rsa.pub
- user: name={{ item.name }} state=present generate_ssh_key=yes
  with_items: users

- authorized_key: "user={{ item.0.name }} key='{{ lookup('file', item.1) }}'"
  with_subelements:
     - users
     - authorized

Séquence numérique : with_sequence

  # create a series of directories with even numbers for some reason
  - file: dest=/var/stuff/{{ item }} state=directory
    with_sequence: start=4 end=16 stride=2

On peut formater en plus, faire de l'hexa, …

Choix aléatoire : with_random_choice

  1. debug: msg=item

with_random_choice:

  1. “go through the door”
  2. “drink from the goblet”
  3. “press the red button”
  4. “do nothing”

Boucle simple : until (+ retries, delay)

  1. action: shell /usr/bin/foo

register: result

  until: result.stdout.find("all systems go") != -1
  retries: 5
  delay: 10

Premier fichier trouvé : with_first_found

  1. name: some configuration template

template: src=item dest=/etc/file.cfg mode=0444 owner=root group=root

  with_first_found:
    - files:
       - "{{inventory_hostname}}/etc/file.cfg"
      paths:
       - ../../../templates.overwrites
       - ../../../templates
    - files:
        - etc/file.cfg
      paths:
        - templates

Boucle sur les résutlats d'un programme sur l'hote : with_lines

  1. name: Example of looping over a command result

shell: /usr/bin/frobnicate item

  with_lines: /usr/bin/frobnications_per_host --param {{ inventory_hostname }}

MAIS on est sensé plutot utiliser les inventaires dynamiques ou register :

  1. name: Example of looping over a REMOTE command result

shell: /usr/bin/something

  register: command_result
- name: Do something with each result
  shell: /usr/bin/something_else --param {{ item }}
  with_items: command_result.stdout_lines

Boucle avec l'index des entrées d'une liste : with_indexed_items

  1. name: indexed loop demo

debug: msg=“at array position item.0 there is a value item.1

  with_indexed_items: some_list

Boucle avec des listes mises à plat : with_flattened

# file: roles/foo/vars/main.yml
packages_base:
  - [ 'foo-package', 'bar-package' ]
packages_apps:
  - [ ['one-package', 'two-package' ]]
  - [ ['red-package'], ['blue-package']]
- name: flattened loop demo
  yum: name={{ item }} state=installed
  with_flattened:
     - packages_base
     - packages_apps

Les boucles et les registres : register

  1. shell: echo “item

with_items:

  1. one
  2. two

register: echo

C'est très détaillé :

{
    "changed": true,
    "msg": "All items completed",
    "results": [
        {
            "changed": true,
            "cmd": "echo \"one\" ",
            "delta": "0:00:00.003110",
            "end": "2013-12-19 12:00:05.187153",
            "invocation": {
                "module_args": "echo \"one\"",
                "module_name": "shell"
            },
            "item": "one",
            "rc": 0,
            "start": "2013-12-19 12:00:05.184043",
            "stderr": "",
            "stdout": "one"
        },
        {
            "changed": true,
            "cmd": "echo \"two\" ",
            "delta": "0:00:00.002920",
            "end": "2013-12-19 12:00:05.245502",
            "invocation": {
                "module_args": "echo \"two\"",
                "module_name": "shell"
            },
            "item": "two",
            "rc": 0,
            "start": "2013-12-19 12:00:05.242582",
            "stderr": "",
            "stdout": "two"
        }
    ]
}

Inspection :

  1. name: Fail if return code is not 0

fail:

    msg: "The command ({{ item.cmd }}) did not have a 0 return code"
  when: item.rc != 0
  with_items: echo.results

Fonctions de gestion complexe

Tâches en parallèle

Combien de hosts doivent être gérés en parallèle au maximum : serial
Cela permet de forcer l'exécution d'un 'play' sur n hosts avant d epasser aux prochaines 'plays'

Pourcentage d'échec

Permet d'arrêter une execution en cas de problèmes répétés :

  1. hosts: webservers

max_fail_percentage: 30

  serial: 10

Continue tant qu'il n'y a pas plus de 3 échecs sur 10 machines.

Execution sur un autre host : delegate_to

Particulièrement utile pour la gestion des cibles de load-balancer, la gestion des fenêtres de hors-service (outage) : delegate_to et local_action :

  1. hosts: webservers

serial: 5

  tasks:
  - name: take out of load balancer pool
    command: /usr/bin/take_out_of_pool {{ inventory_hostname }}
    delegate_to: 127.0.0.1
  - name: take out of load balancer pool v2 : shorthand
    local_action: command /usr/bin/take_out_of_pool {{ inventory_hostname }}
  - name: actual steps would go here
    yum: name=acme-web-stack state=latest
  - name: add back to load balancer pool
    command: /usr/bin/add_back_to_pool {{ inventory_hostname }}
    delegate_to: 127.0.0.1

Autre exemple : rsync vers une cible :

tasks:
- name: recursively copy files from management server to target
  local_action: command rsync -a /path/to/files {{ inventory_hostname }}:/path/to/target/

Lancer une task une seule fois : run_once

  1. command: /opt/application/upgrade_db.py

run_once: true

  delegate_to: web01.example.org

Ou sur un host particulier :

  1. command: /opt/application/upgrade_db.py

when: inventory_hostname == webservers[0]

Lancement local :

Variables d'environnement

En déclarer : environment

  1. hosts: all

remote_user: root

  tasks:
    - apt: name=cobbler state=installed
      environment:
        http_proxy: http://proxy.example.com:8080

En variable

  1. hosts: all

remote_user: root

  # here we make a variable named "proxy_env" that is a dictionary
  vars:
    proxy_env:
      http_proxy: http://proxy.example.com:8080
  tasks:
    - apt: name=cobbler state=installed
      environment: proxy_env

En variable dans un fichier

# file: group_vars/boston
ntp_server: ntp.bos.example.com
backup: bak.bos.example.com
proxy_env:
  http_proxy: http://proxy.bos.example.com:8080
  https_proxy: http://proxy.bos.example.com:8080

Gestion d'erreurs et de changement

Ignorer le code de retour : ignore_errors: yes

  1. name: this will not be counted as a failure

command: /bin/false

  ignore_errors: yes

Récupération particulière de l'état d'une commande : failed_when

  1. name: this command prints FAILED when it fails

command: /usr/bin/example-command -x -y -z

  register: command_result
  failed_when: "'FAILED' in command_result.stderr"

Etat de changement particulier : changed_when

tasks:
  - shell: /usr/bin/billybass --mode="take me to the river"
    register: bass_result
    changed_when: "bass_result.rc != 2"
  # this will never report 'changed' status
  - shell: wall 'beep'
    changed_when: False

Lookups

Sert à utiliser des informations externes (filesystem, centres de stockages externes, services, templates).
Sous forme de plugins.
Fait sur la machine locale.

Contenu de fichiers : contents

  1. hosts: all

vars:

     contents: "{{ lookup('file', '/etc/foo.txt') }}"
  tasks:
     - debug: msg="the value of foo.txt is {{ contents }}"

Génération de mot de passes et enregistrement dans un fichier : password

  1. hosts: all

tasks:

    # create a mysql user with a random password:
    - mysql_user: name={{ client }}
                  password="{{ lookup('password', 'credentials/' + client + '/' + tier + '/' + role + '/mysqlpassword length=15') }}"
                  priv={{ client }}_{{ tier }}_{{ role }}.*:ALL
    # create a mysql user with a random password using only ascii letters:
    - mysql_user: name={{ client }}
                  password="{{ lookup('password', '/tmp/passwordfile chars=ascii_letters') }}"
                  priv={{ client }}_{{ tier }}_{{ role }}.*:ALL
    # create a mysql user with a random password using only digits:
    - mysql_user: name={{ client }}
                  password="{{ lookup('password', '/tmp/passwordfile chars=digits') }}"
                  priv={{ client }}_{{ tier }}_{{ role }}.*:ALL
    # create a mysql user with a random password using many different char sets:
    - mysql_user: name={{ client }}
                  password="{{ lookup('password', '/tmp/passwordfile chars=ascii_letters,digits,hexdigits,punctuation') }}"
                  priv={{ client }}_{{ tier }}_{{ role }}.*:ALL

RQ : le module Vault permet d'enregistrer les mots de passes en chiffré

Recupérer des informations des hosts

  1. hosts: all

tasks:

  1. debug: msg=“lookup_env_home is an environment variable”
  2. debug: msg=“item is a line from the result of this command”

with_lines:

  1. cat /etc/motd
  2. debug: msg=“lookup_pipe_date is the raw result of running this command”
  1. debug: msg=“6379_somekey is value in Redis for somekey”
  1. debug: msg=“lookup_dnstxt_example.com is a DNS TXT record for example.com”
  1. debug: msg=“lookup_template_._some_template.j2 is a value from evaluation of this template”

Assigner des variables avec des lookup

vars:
  motd_value: "{{ lookup('file', '/etc/motd') }}"
tasks:
  - debug: msg="motd value is {{ motd_value }}"

Prompt

Récpérer une entrée utilisateur : vars_prompt

  1. hosts: all

remote_user: root

  vars:
    from: "camelot"
  vars_prompt:
    name: "what is your name?"
    quest: "what is your quest?"
    favcolor: "what is your favorite color?"

Avec valeur par défaut : vars_prompt, name:, prompt:, default:

vars_prompt:
  - name: "release_version"
    prompt: "Product release version"
    default: "1.0"

En cachant le texte saisi : private:

vars_prompt:
  - name: "some_password"
    prompt: "Enter password"
    private: yes

Avec hachage (requiert Passlib)

vars_prompt:
  - name: "my_password2"
    prompt: "Enter password2"
    private: yes
    encrypt: "md5_crypt"
    confirm: yes
    salt_size: 7

Tags

Permet de lancer une partie seulement d'un playbook.
Tags est supporté dans les plays, les tasks, les roles et les includes : tags:

tasks:
    - yum: name={{ item }} state=installed
      with_items:
         - httpd
         - memcached
      tags:
         - packages
    - template: src=templates/src.j2 dest=/etc/foo.conf
      tags:
         - configuration

roles:
  - { role: webserver, port: 5000, tags: [ 'web', 'foo' ] }
  1. include: foo.yml tags=web,foo

(l'include ajoute le tag sur toutes les tashs inclues)

ansible-playbook example.yml --tags "configuration,packages"
ansible-playbook example.yml --skip-tags "notification"

Vault

ansible-vault create foo.yml => demande un mot de passe
ansible-vault edit foo.yml
ansible-vault rekey foo.yml bar.yml baz.yml
ansible-vault encrypt foo.yml bar.yml baz.yml
ansible-vault decrypt foo.yml bar.yml baz.yml
ansible-playbook site.yml --ask-vault-pass => lance un playbook qui appelle des fichiers chiffrés (avec le même mot de passe)
ansible-playbook site.yml --vault-password-file ~/.vault_pass.txt
ansible-playbook site.yml --vault-password-file ~/.vault_pass.py

Modules

En ligne de commande : -m

ansible webservers -m service -a "name=httpd state=started"
ansible webservers -m ping
ansible webservers -m command -a "/sbin/reboot -t now"

En playbook :

  1. name: reboot the servers

action: command /sbin/reboot -t now

  1. name: reboot the servers

command: /sbin/reboot -t now

Les modules renvoient du JSON.
Ils sont idempotent : ils vont éviter de faire des changements si ce n'est pas nécessaire, on peut les lancer plusieurs fois sans problème.

Documentation en ligne de commande :

ansible-doc <nom_du_module>

Modules par catégorie :

file:///usr/share/doc/ansible-doc/html/modules_by_category.html

Tous les modules :

file:///usr/share/doc/ansible-doc/html/list_of_all_modules.html

Divers

Assertions

  1. assert: { that: “ansible_os_family != 'RedHat'” }
  2. assert:

that:

  1. “'foo' in some_command_result.stdout”
  2. “number_of_the_counting == 3”

Echec avec message d'erreur personnalisé

# Example playbook using fail and when together
- fail: msg="The system may not be provisioned according to the CMDB status."
  when: cmdb_status != "to-be-staged"

Debug

# Example that prints the loopback address and gateway for each host
- debug: msg="System {{ inventory_hostname }} has uuid {{ ansible_product_uuid }}"
- debug: msg="System {{ inventory_hostname }} has gateway {{ ansible_default_ipv4.gateway }}"
  when: ansible_default_ipv4.gateway is defined
- shell: /usr/bin/uptime
  register: result
- debug: var=result
- name: Display all variables/facts known for a host
  debug: var=hostvars[inventory_hostname]

Mode fireball

Utilise un module qui démarre un démon ZaroMQ temporaire.

Pause dans un playbook : pause

# Pause for 5 minutes to build app cache.
- pause: minutes=5
# Pause until you can verify updates to an application were successful.
- pause:
# A helpful reminder of what to look out for post-update.
- pause: prompt="Make sure org.foo.FooOverload exception is not present"

Attendre une condition : wait

# wait 300 seconds for port 8000 to become open on the host, don't start checking for 10 seconds
- wait_for: port=8000 delay=10
# wait until the file /tmp/foo is present before continuing
- wait_for: path=/tmp/foo
# wait until the string "completed" is in the file /tmp/foo before continuing
- wait_for: path=/tmp/foo search_regex=completed
# wait until the lock file is removed
- wait_for: path=/var/lock/file.lock state=absent
# wait until the process is finished and pid was destroyed
- wait_for: path=/proc/3466/status state=absent
# Wait 300 seconds for port 22 to become open and contain "OpenSSH", don't start checking for 10 seconds
- local_action: wait_for port=22 host="{{ inventory_hostname }}" search_regex=OpenSSH delay=10

Stratégies de test

Ajouter le flag '-check' permet de voir si Ansible a beaucoup de changements à faire (mais ne change rien).
Dans le playbook, ajouter always_run :

roles:
  - webserver
tasks:
  - script: verify.sh
    always_run: True

Modules utiles pour les tests

tasks:
  - wait_for: host={{ inventory_hostname }} port=22
    delegate_to: localhost
    
  - action: uri url=http://www.example.com return_content=yes
    register: webpage
  - fail: msg='service is not happy'
    when: "'AWESOME' not in webpage.content"
    
   - shell: /usr/bin/some-command --parameter value
     register: cmd_result
   - assert:
       that:
         - "'not ready' not in cmd_result.stderr"
         - "'gizmo enabled' in cmd_result.stdout"

   - stat: path=/path/to/something
     register: p
   - assert:
       that:
         - p.stat.exists and p.stat.isdir

Exemple de garde-fou : apply_testing_checks

  1. hosts: webservers

serial: 5

  pre_tasks:
    - name: take out of load balancer pool
      command: /usr/bin/take_out_of_pool {{ inventory_hostname }}
      delegate_to: 127.0.0.1
  roles:

     - common
     - webserver
     - apply_testing_checks
  post_tasks:

    - name: add back to load balancer pool
      command: /usr/bin/add_back_to_pool {{ inventory_hostname }}
      delegate_to: 127.0.0.1

Syntaxe YAML

Le fichier commence par “—”

Tous les membres d'une liste commencent par “-” :

# A list of tasty fruits
- Apple
- Orange
- Strawberry
- Mango

Tous les membres d'un dictionnaire (table de hachage) sont représentés par clef: valeur :

# An employee record
name: Example Developer
job: Developer
skill: Elite

On peut aussi représenter les doctionnaires sous une forme abrégée :

# An employee record
{name: Example Developer, job: Developer, skill: Elite}

Les booléens sont représentés de manière multiple :

create_key: yes
needs_agent: no
knows_oop: True
likes_emacs: TRUE
uses_cvs: false

Conseils

Lors des rolling releases, utiliser le paramètre “serial” : permet de conditionner au nombre d'hosts
en état avant de passer au 'play' suivant
Toujours mentionner l'état souhaité pour l'application d'un module : state=present
Grouper l'inventaire par roles.
Pour les différentes version d'OS, utiliser le group-by qui crée des groupes dynamiques :

# talk to all hosts just so we can learn about them
- hosts: all
  tasks:
     - group_by: key={{ ansible_distribution }}
# now just on the CentOS hosts...
- hosts: CentOS
  gather_facts: False
  tasks:
     - # tasks that only happen on CentOS go here

Les modules spécifiques à un playbook doivent être mis dans ./library/
Utiliser les lignes vides pour donner de l'air aux documents.
Utiliser # pour commenter
Toujours nommer les 'tasks'
KISS
Utiliser un gestionnaire de versions sur l'arborescence Ansible.

Exemples concrets

Exemple concret : file:/usr/share/doc/ansible-doc/html/guide_rolling_upgrade.html Exemples : https://github.com/ansible/ansible-examples Jinja2 : http://jinja.pocoo.org/docs/dev/templates/ debops : http://debops.org/ playbook super complet : https://gist.github.com/webstandardcss/9d1a293914d972399712 ===== Utilisation ===== ==== lancement ==== ansible-playbook ./install.yml –inventory-file=./test ==== lancement limité à un groupe ou à un host ==== ansible-playbook ./install.yml –inventory-file=./prod –limit=nommachine ==== limite en plus sur un tag ==== ansible-playbook install.yml -t staging -i prod -l ruche.alocean.com ==== facts ==== ansible -i prod -m setup ruche.alocean.com

infrastructure/ansible.txt · Dernière modification: 2019/01/10 15:46 par rguyader