PolarSPARC

Introduction to Ansible - Part 2


Bhaskar S 07/04/2018


Overview

In Part 1 of the series, we touched on a brief overview of Ansible, basic terminology, installation and setup of the environment, and some basic ad-hoc task execution using Ansible.

In this part, we will get our hands dirty with Ansible Playbooks.

An Ansible playbook, written in yaml format, is a collection of one or more plays, which in turn consists of one or more tasks, which helps manage the configuration of the servers defined in the inventory file.

Hands-on with Ansible Playbooks

Create and save a playbook file called playbook-0.yaml, with the following contents, in the directory /home/alice/Ansible:

playbook-0.yaml
---
- name: Play-1
  hosts: host1
  tasks:
  - name: Display a message
    debug: msg="Hello, welcome to Ansible Playbooks"
    
- name: Play-2
  hosts: host2
  tasks:
  - name: Ping hosts
    ping:

The following is a pictorial representation of the above playbook playbook-0.yaml.

Ansible Playbook
Ansible Playbook

The above playbook file defines two plays, named, Play-1 and Play-2 respectively. The first play (Play-1) is targeted at host host1 and the second play (Play-2) is targeted at host host2.

The first play (Play-1) uses the module debug to display a message on the standartd output, while the second play (Play-2) uses the module ping to ping the host.

To test the above playbook playbook-0.yaml, execute the following command:

$ ansible-playbook -i hosts playbook-0.yaml

The following would be a typical output:

Output.1

 _______________
< PLAY [Play-1] >
 ---------------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

 ________________________
< TASK [Gathering Facts] >
 ------------------------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

ok: [192.168.100.11]
 __________________________
< TASK [Display a message] >
 --------------------------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

ok: [192.168.100.11] => {
    "msg": "Hello, welcome to Ansible Playbooks"
}
 _______________
< PLAY [Play-2] >
 ---------------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

 ________________________
< TASK [Gathering Facts] >
 ------------------------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

ok: [192.168.100.12]
 ___________________
< TASK [Ping hosts] >
 -------------------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

ok: [192.168.100.12]
 ____________
< PLAY RECAP >
 ------------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

192.168.100.11             : ok=2    changed=0    unreachable=0    failed=0   
192.168.100.12             : ok=2    changed=0    unreachable=0    failed=0

WOW !!! Whats with the cowsay display ??? They are distracting the results.

To disable the cowsay display, we need to create an Ansible configuration file called ansible.cfg in the directory /home/alice/Ansible, with the following contents:

ansible.cfg
[defaults]
nocows = yes
inventory = hosts

The nocows = yes option turns off displaying the cowsay.

With the inventory = hosts option, one does not need to specify the inventory file on the command line.

Execute the playbook playbook-0.yaml once again with the following command:

$ ansible-playbook playbook-0.yaml

The following would be a typical output:

Output.2

PLAY [Play-1] **********************************************************************************************************************

TASK [Gathering Facts] *************************************************************************************************************
ok: [192.168.100.11]

TASK [Display a message] ***********************************************************************************************************
ok: [192.168.100.11] => {
    "msg": "Hello, welcome to Ansible Playbooks"
}

PLAY [Play-2] **********************************************************************************************************************

TASK [Gathering Facts] *************************************************************************************************************
ok: [192.168.100.12]

TASK [Ping hosts] ******************************************************************************************************************
ok: [192.168.100.12]

PLAY RECAP *************************************************************************************************************************
192.168.100.11             : ok=2    changed=0    unreachable=0    failed=0   
192.168.100.12             : ok=2    changed=0    unreachable=0    failed=0

Looks much better now.

Before executing the first task in each play, Ansible connects to the target host and gathers various facts such as, the cpu, the memory, the storage disks, the operating system, the networking, etc., and stores them in pre-defined variables.

From the Output.2, the fact gathering is indicated by the lines starting with TASK [Gathering Facts].

Create and save a playbook file called playbook-1.yaml, with the following contents, in the directory /home/alice/Ansible:

playbook-1.yaml
---
- name: Simple playbook with 3 tasks
  hosts: myhosts
  tasks:
  - name: Find who am I
    command: whoami
    register: user
    
  - name: Display some facts
    debug: msg="OS Family - {{ ansible_os_family }}, User - {{user.stdout}}}"
    
  - name: Display some more facts
    debug:
      msg:
      - "Host Name: {{ ansible_hostname }}"
      - "Operating System: {{ ansible_lsb['description'] }}"
      - "Processor: {{ ansible_processor }}"
      - "Total Memory: {{ ansible_memtotal_mb }}"

The above playbook file defines just one play with 3 tasks, which is targeted at the hosts in the group myhosts.

The first task uses the module command to execute a command on the target remote host(s). The command module takes a command name followed by an optional list of space delimited arguments. In this example, we are invoking the whoami command.

Invoking the command whoami results in the display of the username of the currently logged in user on the standard output. How do we capture this value ???

This is where the register clause comes into play. The results of the command execution (which is a set of values) are captured in the variable called user. The username is in one of the returned values called stdout.

The other two tasks use the module debug to display Ansible gathered facts. Notice the use of the syntax {{ variable }} to display variables.

To test the above playbook playbook-1.yaml, execute the following command:

$ ansible-playbook -v playbook-1.yaml

Notice the use of the verbose flag -v when executing the playbook above to illustrate the fact that the results from the command module execution is a set of values.

The following would be a typical output:

Output.3

Using /home/bswamina/MyProjects/Ansible/ansible.cfg as config file

PLAY [Simple playbook with 3 tasks] ************************************************************************************************

TASK [Gathering Facts] *************************************************************************************************************
ok: [192.168.100.11]
ok: [192.168.100.12]

TASK [Find who am I] ***************************************************************************************************************
changed: [192.168.100.11] => {"changed": true, "cmd": ["whoami"], "delta": "0:00:00.016586", "end": "2018-07-04 14:49:44.588500", "rc": 0, "start": "2018-07-04 14:49:44.571914", "stderr": "", "stderr_lines": [], "stdout": "vagrant", "stdout_lines": ["vagrant"]}
changed: [192.168.100.12] => {"changed": true, "cmd": ["whoami"], "delta": "0:00:00.014920", "end": "2018-07-04 14:49:43.801986", "rc": 0, "start": "2018-07-04 14:49:43.787066", "stderr": "", "stderr_lines": [], "stdout": "vagrant", "stdout_lines": ["vagrant"]}

TASK [Display some facts] **********************************************************************************************************
ok: [192.168.100.11] => {
    "msg": "OS Family - Debian, User - vagrant}"
}
ok: [192.168.100.12] => {
    "msg": "OS Family - Debian, User - vagrant}"
}

TASK [Display some more facts] *****************************************************************************************************
ok: [192.168.100.11] => {
    "msg": [
        "Host Name: host1",
        "Operating System: Ubuntu 16.04.4 LTS",
        "Processor: ['0', 'AuthenticAMD', 'AMD FX(tm)-8350 Eight-Core Processor']",
        "Total Memory: 2000"
    ]
}
ok: [192.168.100.12] => {
    "msg": [
        "Host Name: host2",
        "Operating System: Ubuntu 16.04.4 LTS",
        "Processor: ['0', 'AuthenticAMD', 'AMD FX(tm)-8350 Eight-Core Processor']",
        "Total Memory: 2000"
    ]
}

PLAY RECAP *************************************************************************************************************************
192.168.100.11             : ok=4    changed=1    unreachable=0    failed=0   
192.168.100.12             : ok=4    changed=1    unreachable=0    failed=0

The following illustration highlights the set of values returned by the command module execution from the playbook playbook-1.yaml.

Ansible Results
Ansible Results

Ansible fact gathering is under-the-hood an additional task performed before the first task is executed. If one will not be using or referring any of the pre-defined Ansible variables from the fact gathering, then one can disable fact gathering as an optimization.

Create and save a playbook file called playbook-2.yaml, with the following contents, in the directory /home/alice/Ansible:

playbook-2.yaml
---
- name: Playbook with 3 tasks
  hosts: myhosts
  gather_facts: no
  tasks:
  - name: Execute free
    command: free -h
    register: free
    
  - name: Display free info
    debug: msg="Memory - {{free.stdout}}}"
    
  - name: Display OS family
    debug: msg="OS Family - {{ ansible_os_family }}"

The above playbook file defines just one play with 3 tasks, which is targeted at the hosts in the group myhosts.

To test the above playbook playbook-2.yaml, execute the following command:

$ ansible-playbook playbook-2.yaml

The following would be a typical output:

Output.4

PLAY [Playbook with 3 tasks] *******************************************************************************************************

TASK [Execute free] ****************************************************************************************************************
changed: [192.168.100.11]
changed: [192.168.100.12]

TASK [Display free info] ***********************************************************************************************************
ok: [192.168.100.11] => {
    "msg": "Memory -               total        used        free      shared  buff/cache   available\nMem:           2.0G         52M        1.6G        3.1M        328M        1.8G\nSwap:            0B          0B          0B}"
}
ok: [192.168.100.12] => {
    "msg": "Memory -               total        used        free      shared  buff/cache   available\nMem:           2.0G         52M        1.6G        3.1M        328M        1.8G\nSwap:            0B          0B          0B}"
}

TASK [Display OS family] ***********************************************************************************************************
fatal: [192.168.100.11]: FAILED! => {"msg": "The task includes an option with an undefined variable. The error was: 'ansible_os_family' is undefined\n\nThe error appears to have been in '/home/alice/Ansible/playbook-2.yaml': line 13, column 5, but may\nbe elsewhere in the file depending on the exact syntax problem.\n\nThe offending line appears to be:\n\n\n  - name: Display OS family\n    ^ here\n"}
fatal: [192.168.100.12]: FAILED! => {"msg": "The task includes an option with an undefined variable. The error was: 'ansible_os_family' is undefined\n\nThe error appears to have been in '/home/alice/Ansible/playbook-2.yaml': line 13, column 5, but may\nbe elsewhere in the file depending on the exact syntax problem.\n\nThe offending line appears to be:\n\n\n  - name: Display OS family\n    ^ here\n"}
  to retry, use: --limit @/home/alice/Ansible/playbook-2.retry

PLAY RECAP *************************************************************************************************************************
192.168.100.11             : ok=2    changed=1    unreachable=0    failed=1   
192.168.100.12             : ok=2    changed=1    unreachable=0    failed=1

As is evident from the Output.4 above, the 3rd task in the playbook playbook-2.yaml failed to execute as the pre-defined variable ansible_os_family is undefined. This is because we disabled fact gathering by specifying the clause gather_facts: no in the playbook.

Also, from the Output.4 above, the line to retry, use: --limit @/home/alice/Ansible/playbook-2.retry is interesting. If we look in the directory /home/alice/Ansible, we will see a file named playbook-2.retry. This file contains the names of the remote target host(s) on which the playbook playbook-2.yaml failed to execute.

To disable the creation of the retry file, modify the Ansible configuration file ansible.cfg located in the directory /home/alice/Ansible, with the following contents:

ansible.cfg
[defaults]
nocows = yes
retry_files_enabled = False
inventory = hosts

Is there a way to check if a variable is defined in Ansible before referencing it ???

This is where the when clause comes in handy for performing conditional checks prior to executing a task.

Create and save a playbook file called playbook-3.yaml, with the following contents, in the directory /home/alice/Ansible:

playbook-3.yaml
---
- name: Playbook with 3 tasks
  hosts: myhosts
  gather_facts: no
  tasks:
  - name: Execute free
    command: free -h
    register: free
    
  - name: Display free info
    debug: msg="Memory - {{free.stdout}}}"
    
  - name: Display OS family
    debug: msg="OS Family - {{ ansible_os_family }}"
    when: ansible_os_family is defined

To test the above playbook playbook-3.yaml, execute the following command:

$ ansible-playbook playbook-3.yaml

The following would be a typical output:

Output.5

PLAY [Playbook with 3 tasks] *******************************************************************************************************

TASK [Execute free] ****************************************************************************************************************
changed: [192.168.100.11]
changed: [192.168.100.12]

TASK [Display free info] ***********************************************************************************************************
ok: [192.168.100.11] => {
    "msg": "Memory -               total        used        free      shared  buff/cache   available\nMem:           2.0G         52M        1.7G        3.1M        191M        1.8G\nSwap:            0B          0B          0B}"
}
ok: [192.168.100.12] => {
    "msg": "Memory -               total        used        free      shared  buff/cache   available\nMem:           2.0G         52M        1.7G        3.1M        191M        1.8G\nSwap:            0B          0B          0B}"
}

TASK [Display OS family] ***********************************************************************************************************
skipping: [192.168.100.11]
skipping: [192.168.100.12]

PLAY RECAP *************************************************************************************************************************
192.168.100.11             : ok=2    changed=1    unreachable=0    failed=0   
192.168.100.12             : ok=2    changed=1    unreachable=0    failed=0

References

Introduction to Ansible - Part 1

Official Ansible Documentation



© PolarSPARC