Ansible modules - Don't reinvent the wheel : use a module!

mail

file

mail

yum

The yum module is used to install / remove / upgrade / packages, but there is a little subtlety : could you tell the difference between these snippets ?
- name: "Do something with somePackage"
  yum:
    name: "somePackage"
    state: present
- name: "Do something with somePackage"
  yum:
    list: "somePackage"
    state: installed
mail

script

Transfer a local script to a slave, then run it
mail

assert inspects both success_msg and fail_msg whatever the result

---
#   ANSIBLE_LOCALHOST_WARNING=false ansible-playbook assert_successFailMsg.yml
- hosts: 127.0.0.1
  connection: local
  gather_facts: no

  tasks:

  - assert:
      that: true
      success_msg: "'true' is true"
      fail_msg: "Only Chuck Norris can do this : {{ 1/0 }}"
...
PLAY [127.0.0.1] *******************************************************************************************

TASK [assert] **********************************************************************************************
An exception occurred during task execution. To see the full traceback, use -vvv. The error was: ZeroDivisionError: division by zero
fatal: [127.0.0.1]: FAILED! => {"msg": "Unexpected failure during module execution.", "stdout": ""}

PLAY RECAP *************************************************************************************************
127.0.0.1                  : ok=0    changed=0    unreachable=0    failed=1    skipped=0    rescued=0    ignored=0
  • Even though the assertion is true, both success_msg and fail_msg directives are evaluated : fail_msg causes the error whereas success_msg is the one that would be displayed
  • The error message —referring to code within the fail_msg directive— is misleading as it suggests assert had a very unexpected result leading to the fail_msg line execution.
mail

find

see example in ad-hoc command
mail

tempfile

---
#	ANSIBLE_LOCALHOST_WARNING=false ansible-playbook test.yml
- hosts: 127.0.0.1
  connection: local
  gather_facts: no
  tasks:

  - name: create temporary file "/tmp/ansible.xxxxxxxx.tmp"
    tempfile:
      state: file
      suffix: '.tmp'
    register: myTempFile
  - debug:
      var: myTempFile.path

  - name: delete temporary file
    file:
      path: "{{ myTempFile.path }}"
      state: absent

  - name: check temp file is gone
    stat:
      path: "{{ myTempFile.path }}"
    register: checkTempFileIsGone
  - debug:
      var: checkTempFileIsGone.stat.exists
...
PLAY [127.0.0.1] ***********************************************************************************

TASK [create temporary file "/tmp/ansible.xxxxxxxx.tmp"] *******************************************
changed: [127.0.0.1]

TASK [debug] ***************************************************************************************
ok: [127.0.0.1] => {
    "myTempFile.path": "/tmp/ansible.ggx0tsvy.tmp"
}

TASK [delete temporary file] ***********************************************************************
changed: [127.0.0.1]

TASK [check temp file is gone] *********************************************************************
ok: [127.0.0.1]

TASK [debug] ***************************************************************************************
ok: [127.0.0.1] => {
    "checkTempFileIsGone.stat.exists": false
}

PLAY RECAP *****************************************************************************************
127.0.0.1                 : ok=5    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
mail

slurp

Usage

Retrieve the contents of a remote file as a base64 string
slurp fails if the remote file does not exist.
mail

uri

Usage

Interacts with webservices

Example

---
# ANSIBLE_LOCALHOST_WARNING=false ansible-playbook test.yml -D
- hosts: 127.0.0.1
  connection: local
  gather_facts: no
  tasks:

  - name: Make HTTP GET expecting HTTP status 200
    uri:
      url: http://www.example.com
    register: result
    until: result.status == 200		in check mode, 'result' does not exist and this task will fail

  - name: Make HTTP GET expecting HTTP status 404
    uri:
      url: http://www.example.com/nosuchpage.html
    register: result
    ignore_errors: yes

  - debug:
      var: result.status
...
mail

vmware_guest

As said in the requirements section, you'll need :

Python ≥ 2.6 :

python --version
Python 2.7.13

PyVmomi :

Is it already installed ?

pip list | grep -i pyvmomi
(nothing)
Nope, not installed yet.

Let's install it :

on Debianoids :
Long story short :
  1. I first installed python-pyvmomi with apt, got the version 5.5.0 which is packaged in Stretch
  2. later, while trying to connect to the vSphere API, Ansible failed :
    TASK [Create a virtual machine] ***********************************
    fatal: [localhost]: FAILED! => changed=false
      ansible_facts:
        discovered_interpreter_python: /usr/bin/python
      msg: 'Unknown error while connecting to vCenter or ESXi API at vSphere_host:443 : SmartConnect() got an unexpected keyword argument ''sslContext'''
  3. found out that PyVmomi > 6.0 is required to use sslContext
  4. so I installed python-pyvmomi from the backports (got version 6.7.1) and everything went extremely well
on Red Hatoids :
  1. python --version
    Python 2.7.5
    Whether Ansible relies on Python 2.x is a wild guess .
  2. yum search pyvmomi
    python2-pyvmomi.noarch : vSphere Python SDK
    python34-pyvmomi.noarch : vSphere SDK for Python3.4
    
  3. sudo yum install python2-pyvmomi.noarch
mail

assert

mail

fail

mail

assert : double-check what it really evaluates

For those unfamiliar with assert :

Even though we're dealing with assert in the context of Ansible, the overall concept is language-independent and is widely implemented. The bug/limitation/behavior we're discussing here _may be_ specific to Ansible.

How it works :
  1. I make a series of assertions
  2. assert checks these
  3. ...and yells at the wrong ones
Examples :
Technically, assert is close to an if then block, but it has some differences :
  • assert causes an error when an assertion is wrong, whereas if then can do this only if you trigger the error yourself explicitly in the then clause
  • assert generally raises an exception, causing the program to stop or even crash. This way, you can be sure a program continues only if all assertions pass.
  • listing assertions to be evaluated by assert
    • makes the code more readable
    • and documents the expected conditions for the programs to run normally
    more clearly than a series of if then

OK, so what's wrong with assert ?

I just found this at the beginning of a playbook —used in production for months / years :
  
  - name: Check variable is specified
    assert:
      that:
      - "{{ requiredVariable }} is undefined"
  
At first sight, the purpose of this snippet looks obvious : stop the playbook if the required variable is missing. But looking more thoroughly, you'll notice :
      - "{{ requiredVariable }} is undefined"
which is surprising, for 2 reasons :
  1. for this playbook to work right, the requiredVariable variable is mandatory, but this code ensures it's undefined
  2. the assertion requiredVariable is undefined should fail if the playbook is run while requiredVariable is actually defined (which is how it's always been run on our side)
So is going on ?

Explanations :

The following is based on the code of assert.yml, feel free to experiment on your side .
The code says :
  vars:
    myVariable: 'hello'



  - assert:
      that:
      - myVariable is defined
      - "{{ myVariable }} is undefined"
      - undefinedVariable is undefined
      - "{{ undefinedVariable }} is undefined"
And the 3 first assertions pass.

1st assertion : myVariable is defined

This is just checking a variable named myVariable is defined —which is true— so it passes.

2nd assertion : "{{ myVariable }} is undefined"

There are quotes and curly braces, and it seems it's doing the contrary but... it passes too ! Here is what happens :
  1. because of curly braces, the value of myVariable replaces {{ myVariable }}, which gives : "hello is undefined"
  2. "hello is undefined" is just a string, and is seen by assert as : hello is undefined
  3. hello is undefined is an assertion where we state the variable named hello is undefined. This is true, so it passes.

3rd assertion : undefinedVariable is undefined

Checking the undefinedVariable variable we defined nowhere is undefined : true.

4th assertion : "{{ undefinedVariable }} is undefined"

Quotes and curly braces again : the interpreter tries to replace {{ undefinedVariable }} with the value of undefinedVariable. There is no such value, hence the error :
The task includes an option with an undefined variable. The error was: 'undefinedVariable' is undefined

Conclusion

  • This subtlety can be the cause of hard-to-find bugs.
  • Using assert may lead to a false feeling of safety : the code I found at the beginning of our old playbook :
    • actually checks nothing (sh*t may happen someday...)
    • it doesn't even fail : this code WORKS. It just does not check what it was intended for
  • Failing to fail doesn't mean you succeeded !
mail

shell and command

If you're about to execute a script, consider the script module.

Their differences :

  • shell is executed by /bin/sh on the remote node, therefore lacks some functionalities you may be used to :
    ---
    #	ANSIBLE_LOCALHOST_WARNING=false ansible-playbook test.yml
    - hosts: 127.0.0.1
      connection: local
      gather_facts: no
      tasks:
    
      - shell: echo $RANDOM
        register: shellResult
      - debug:
          var: shellResult.stdout		empty string since $RANDOM is a Bash variable
    
      - shell: bash -c 'echo $RANDOM'
        register: shellResult
      - debug:
          var: shellResult.stdout		has a value
    ...
  • command is NOT executed in a shell on the remote node, so shell-specific variables (like $HOME) and commands (>, |, &, ...) are NOT supported.

Subtleties they share :

Since both behave mostly the same way and accept the same hacks, I won't be repeating shell or command hereafter .


skipped in check mode

Check mode is supported when passing creates or removes :
  • if running in check mode and either of these are specified, the module will check for the existence of the file and report the correct changed status
  • if these are not supplied, the task will be skipped
sources : shell (2.9), command (2.9), command (latest),
The shell (latest) manual page says about the same thing in a rather misleading way : the check mode is "partially supported" thanks to creates and removes, otherwise, the corresponding tasks are skipped.

LANG-dependent output

Depending on the value of the LANG environment variable, the output of a command can be displayed in a language or in another, which is problematic when parsing it with the usual tools : grep, sed, awk, cut, Check it :
for localeCode in fr_FR en_US es_MX de_DE ja_JP zh_CN; do echo -n "$localeCode : "; LANG=${localeCode}.utf8 man; done
fr_FR : Quelle page de manuel voulez-vous ?
en_US : What manual page do you want?
es_MX : ¿Qué página de manual desea?
de_DE : Welche Handbuchseiten möchten Sie haben?
ja_JP : マニュアルページを指定してください
zh_CN : 您需要什么手册页?
Workaround : force the output language to english by choosing the "default" C locale :
- name: "A task that makes great things"
  shell: "some shell command"
  environment:
    LANG: C

changed is always True

This is because Ansible has no mechanism for understanding whether the command run by shell actually changed anything (source).
To workaround this, you can instruct Ansible what to consider as a change with changed_when :
- name: check whether ...
  shell: someCommand
  register: myVariable
  changed_when: false				never report this task as changed
  ignore_errors: true
- shell: someCommand
  register: myVariable
  changed_when: "myVariable.rc != 2"	change based on return code
If you're running a shell command to decide whether or not to run a subsequent command (i.e. to guarantee idempotence) you _may_ have to use ignore_errors: true. This is required in such case because this shell command will return a success / failure return code. However, since hosts with failed tasks are removed from the targets of the playbook, we must instruct Ansible to ignore such errors and continue working on them.

shell warning : [WARNING]: Consider using yum module rather than running yum (source)

Ansible may warn when shell is used to perform an action that ought to be done via one of the numerous Ansible modules. This is right most of the time : using built-in modules is cleaner and is the best solution to achieve idempotence. But the warning is not always appropriate, since the suggested solution doesn't work (or doesn't exist (yet)). Regarding this yum example, the warning :
  • is perfectly legitimate when trying to install / remove / update packages : use yum instead of shell
  • falls flat if we're trying to run a command that is not (yet) supported by the yum module, such as listing repositories :
    
    shell: yum repolist enabled | grep "{{ redhat_repository_optional }}"
    
In the latter situation, there is no alternative to using shell, but we'd like to hide this warning anyway. To do so :
  • Ansible actually sends warnings based on a list of keywords following shell:, so let's fool it with which and the $() construct :
    shell: $(which yum) repolist enabled | grep "{{ redhat_repository_optional }}"
  • other solution :
    shell: yum repolist enabled | grep "{{ redhat_repository_optional }}" warn=no
    Does the job but poor readability
  • Better :
    shell: yum repolist enabled | grep "{{ redhat_repository_optional }}"
    args:
      warn: no
mail

lineinfile

Table of contents

Regexp-search + replace line only when the regexp matches (source)

  • use backrefs: yes
    - name: do something
      lineinfile:
        dest: /path/to/file
        regexp: '^what we are looking for$'
        line: 'the new line that will replace the whole line matched by the regexp above'
        backrefs: yes
  • OR update the regexp so that it matches both the original AND changed lines

unsupported parameter for module: path

Before Ansible 2.3, path was dest, which explains those frequent error messages. This is in the manual, actually, but VERY easy to miss if going too fast .