PolarSPARC

Introduction to Ansible - Part 3


Bhaskar S 07/08/2018


Overview

In Part 2 of the series, we introduced Ansible Playbooks and got our feet wet with some basic capabilities.

In this part, we will continue to explore some more features of Ansible Playbooks.

Hands-on with Ansible Playbooks

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

playbook-4.yaml
---
- name: Playbook with 2 tasks
  hosts: "{{ target_host }}"
  gather_facts: no

  vars:
    home_var: "HOME"
    shell_var: "SHELL"

  tasks:
  - name: Find user id
    command: id -un
    register: user
    
  - name: Display user id and shell
    debug:
      msg:
      - 'User -> {{ user.stdout }}'
      - 'Home -> {{ lookup("env", "{{ home_var }}") }}'
      - 'Shell -> {{ lookup("env", "{{ shell_var }}") }}'
      - 'Extra argument -> {{ name_var }}'

The above playbook defines four user-defined variables, namely, target_host, home_var, shell_var, and name_var respectively. User-defined variables can be defined in the vars section of the playbook as shown above. In addition, they can be passed in using the --extra-vars command-line argument. For our example, we will be passing in the values for target_host and name_var using the command-line argument.

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

$ ansible-playbook playbook-4.yaml --extra-vars "target_host=host1 name_var=Alice"

The following would be a typical output:

Output.1

PLAY [Playbook with 2 tasks] *******************************************************************************************************

TASK [Find user id] ****************************************************************************************************************
changed: [192.168.100.11]

TASK [Display user id and shell] ***************************************************************************************************
ok: [192.168.100.11] => {
    "msg": [
        "User -> vagrant", 
        "Home -> /home/bswamina", 
        "Shell -> /bin/bash", 
        "Extra argument -> Alice"
    ]
}

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

One could also use the short form for the command-line argument -e instead of the longer --extra-vars argument.

When passing user-defined variables from the command-line, they must be specified as key-value pairs that are space separated and enclosed in double quotes as was done above.

In the above playbook, we use the lookup plugin to query values from external sources such as the shell.

In our example, we query the environment variables HOME and SHELL.

Create and save a simple html file called custom_index.html, with the following contents, in the directory /home/alice/Ansible:

custom_index.html
<html>
    <head><title>Welcome to NGINX</title></head>
    <body>
        <h3 align='center'>
            <font color='red'>Welcome to NGINX</font>
        </h3>
    </body>
</html>

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

playbook-5.yaml
---
- name: Setup and start nginx
  hosts: "{{ target_host }}"
  become: yes
  
  vars:
    custom_html: "custom_index.html"

  tasks:
  - name: Install nginx
    apt: name=nginx state=present
    when: ansible_os_family == "Debian"
    
  - name: Start nginx
    service: name=nginx enabled=yes state=started
    notify: restart nginx

  - name: Customize nginx index.html
    copy:
      src={{ custom_html }}
      dest=/var/www/html/index.html
      mode=0644
    notify: restart nginx
    
  handlers:
  - name: restart nginx
    service: name=nginx state=restarted

The above playbook will install the popular open source webserver nginx, configure a custom index.html file, and restart nginx to reflect the changes.

If one is observant, they will notice the use of become clause at the very beginning of the playbook. Setting it to a value of yes means Ansible will execute every task in the context of the user root.

The first task uses the module apt to manage packages on Debian based target remote host(s). The apt module takes a package name and a package state. In our example, the package name is nginx with the state present. The first time this task executes, it installs the desired package. Any future runs will not reinstall the package since the package is already present.

The second task uses the module service to manage system services on target remote host(s). The service module takes a service name, a flag to indicate if the service will start on boot, and a service state. In our example, the package name is nginx, with enabled set to yes indicating it will start on boot and the service state of started.

The third task uses the module copy to copy a file to the target remote host(s) on a specified destination. The copy module takes a source file name, a destination location, and a file mode. In our example, the source file is custom_index.html that will be copied to the location /var/www/html/index.html on the target remote host.

We have a section called handlers that looks very similar to tasks in the playbook, except that they run only when notified. A handler runs only once (even when notified multiple times) at the end of a play after all the tasks in the play have completed execution.

A task notifies a handler using the notify clause and specifying the handlers name.

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

$ ansible-playbook playbook-5.yaml -e "target_host=host2"

The following would be a typical output:

Output.2

PLAY [Setup and start nginx] *******************************************************************************************************

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

TASK [Install nginx] ***************************************************************************************************************
changed: [192.168.100.12]

TASK [Start nginx] *****************************************************************************************************************
ok: [192.168.100.12]

TASK [Customize nginx index.html] **************************************************************************************************
changed: [192.168.100.12]

RUNNING HANDLER [restart nginx] ****************************************************************************************************
changed: [192.168.100.12]

PLAY RECAP *************************************************************************************************************************
192.168.100.12             : ok=5    changed=3    unreachable=0    failed=0

Open a browser and access the URL http://192.168.100.12. The following is an illustration:

Ansible Browser
Browser

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

playbook-6.yaml
---
- name: Setup sqlite and python-sqlite module
  hosts: "{{ target_host }}"
  become: yes
  
  vars:
    target_host: host1

  tasks:
  - name: Display OS family
    debug: msg="OS Family - {{ ansible_os_family }}"
    when: ansible_os_family is defined

  - name: Install debian packages
    ignore_errors: yes
    apt: name={{ item }} state=present
    when: ansible_os_family == "Debian"
    with_items:
    - sqlite5
    - python-sqlite5

The above playbook demonstrates a looping construct using the with_items clause, to install multiple packages. Each list element specified under with_items will be made available in Ansible via a pre-defined variable called item.

By default, Ansible will fail on a target host when it encounters a task failure. To be precise, if there are two target hosts - host1 and host2 and a task encounters an error only on host1, it will continue with the tasks on host2. To change the behavior to continue on errors, one can specify the ignore_errors clause with a value of yes. This is what we have done in the second task (since we have intentionally specified invalid package names to force failure).

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

$ ansible-playbook playbook-6.yaml

The following would be a typical output:

Output.3

PLAY [Setup sqlite and python-sqlite module] ***************************************************************************************

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

TASK [Display debian packages to install] ******************************************************************************************
ok: [192.168.100.11] => (item=None) => {
    "msg": "Debian package - sqlite"
}
ok: [192.168.100.11] => (item=None) => {
    "msg": "Debian package - python-sqlite"
}

TASK [Install debian packages] *****************************************************************************************************
failed: [192.168.100.11] (item=[u'sqlite5', u'python-sqlite5']) => {"changed": false, "item": ["sqlite5", "python-sqlite5"], "msg": "No package matching 'sqlite5' is available"}
...ignoring

PLAY RECAP *************************************************************************************************************************
192.168.100.11             : ok=3    changed=0    unreachable=0    failed=0

There will be situations when we want to forcibly stop execution of the Ansible playbook when an error is encounter on any of the target host(s). This is when we use the any_errors_fatal clause with a value of yes.

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

playbook-7.yaml
---
- name: Setup sqlite and python-sqlite module
  hosts: "{{ target_hosts }}"
  become: yes
  any_errors_fatal: yes
  
  vars:
    target_hosts: host1, host2

  tasks:
  - name: Install debian packages
    apt: name={{ item }} state=present
    when: ansible_os_family == "Debian"
    with_items:
    - sqlite5
    - python-sqlite5

  - name: Display debian packages to install
    debug: msg="Debian package - {{ item }}"
    with_items:
    - sqlite
    - python-sqlite

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

$ ansible-playbook playbook-7.yaml

The following would be a typical output:

Output.4

PLAY [Setup sqlite and python-sqlite module] ***************************************************************************************

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

TASK [Install debian packages] *****************************************************************************************************
failed: [192.168.100.12] (item=[u'sqlite5', u'python-sqlite5']) => {"changed": false, "item": ["sqlite5", "python-sqlite5"], "msg": "No package matching 'sqlite5' is available"}
failed: [192.168.100.11] (item=[u'sqlite5', u'python-sqlite5']) => {"changed": false, "item": ["sqlite5", "python-sqlite5"], "msg": "No package matching 'sqlite5' is available"}

NO MORE HOSTS LEFT *****************************************************************************************************************

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

To display the list of all the supported Ansible modules along with a short description, execute the following command:

$ ansible-doc -l

The following would be a typical output (truncated for brevity):

Output.5

a10_server                                           Manage A10 Networks AX/SoftAX/Thunder/vThunder devices' server object.    
a10_server_axapi3                                    Manage A10 Networks AX/SoftAX/Thunder/vThunder devices                    
a10_service_group                                    Manage A10 Networks AX/SoftAX/Thunder/vThunder devices' service groups.   
a10_virtual_server                                   Manage A10 Networks AX/SoftAX/Thunder/vThunder devices' virtual servers.  
accelerate                                           Enable accelerated mode on remote node                                    
aci_aaa_user                                         Manage AAA users (aaa:User)                                               
aci_aaa_user_certificate                             Manage AAA user certificates (aaa:UserCert)                               
aci_access_port_to_interface_policy_leaf_profile     Manage Fabric interface policy leaf profile interface selectors (infra:HPo...
aci_aep                                              Manage attachable Access Entity Profile (AEP) objects (infra:AttEntityP, i...
aci_aep_to_domain                                    Bind AEPs to Physical or Virtual Domains (infra:RsDomP)                   
aci_ap                                               Manage top level Application Profile (AP) objects (fv:Ap)                 
aci_bd                                               Manage Bridge Domains (BD) objects (fv:BD)                                
aci_bd_subnet                                        Manage Subnets (fv:Subnet)                                                
aci_bd_to_l3out                                      Bind Bridge Domain to L3 Out (fv:RsBDToOut)                               
aci_config_rollback                                  Provides rollback and rollback preview functionality (config:ImportP)     
aci_config_snapshot                                  Manage Config Snapshots (config:Snapshot, config:ExportP)                 
aci_contract                                         Manage contract resources (vz:BrCP)                                       
aci_contract_subject                                 Manage initial Contract Subjects (vz:Subj)                                
aci_contract_subject_to_filter                       Bind Contract Subjects to Filters (vz:RsSubjFiltAtt)                      
aci_domain                                           Manage physical, virtual, bridged, routed or FC domain profiles (phys:DomP...
aci_domain_to_vlan_pool                              Bind Domain to VLAN Pools (infra:RsVlanNs)                                
aci_encap_pool                                       Manage encap pools (fvns:VlanInstP, fvns:VxlanInstP, fvns:VsanInstP)      
aci_encap_pool_range                                 Manage encap ranges assigned to pools (fvns:EncapBlk, fvns:VsanEncapBlk)  
aci_epg                                              Manage End Point Groups (EPG) objects (fv:AEPg)                           
aci_epg_monitoring_policy                            Manage monitoring policies (mon:EPGPol)                                   
aci_epg_to_contract                                  Bind EPGs to Contracts (fv:RsCons, fv:RsProv)                             
aci_epg_to_domain                                    Bind EPGs to Domains (fv:RsDomAtt)                                        
aci_fabric_node                                      Manage Fabric Node Members (fabric:NodeIdentP)                            
aci_filter                                           Manages top level filter objects (vz:Filter)                              
aci_filter_entry                                     Manage filter entries (vz:Entry)                                          
aci_firmware_source                                  Manage firmware image sources (firmware:OSource)                          
aci_interface_policy_fc                              Manage Fibre Channel interface policies (fc:IfPol)                        
aci_interface_policy_l2                              Manage Layer 2 interface policies (l2:IfPol)                              
aci_interface_policy_leaf_policy_group               Manage fabric interface policy leaf policy groups (infra:AccBndlGrp, infra...
aci_interface_policy_leaf_profile                    Manage fabric interface policy leaf profiles (infra:AccPortP)             
aci_interface_policy_lldp                            Manage LLDP interface policies (lldp:IfPol)                               
aci_interface_policy_mcp                             Manage MCP interface policies (mcp:IfPol)                                 
aci_interface_policy_port_channel                    Manage port channel interface policies (lacp:LagPol)                      
aci_interface_policy_port_security                   Manage port security (l2:PortSecurityPol)                                 
aci_interface_selector_to_switch_policy_leaf_profile Bind interface selector profiles to switch policy leaf profiles (infra:RsA...
aci_l3out_route_tag_policy                           Manage route tag policies (l3ext:RouteTagPol)
...
...
...

To display the detailed documentation on a specific Ansible module, say for example the module debug, execute the following command:

$ ansible-doc debug

The following would be a typical output (truncated for brevity):

Output.6

> DEBUG    (/usr/lib/python2.7/dist-packages/ansible/modules/utilities/logic/debug.py)

        This module prints statements during execution and can be useful for debugging variables or
        expressions without necessarily halting the playbook. Useful for debugging together with the
        'when:' directive. This module is also supported for Windows targets.

  * note: This module has a corresponding action plugin.

OPTIONS (= is mandatory):

- msg
        The customized message that is printed. If omitted, prints a generic message.
        [Default: Hello world!]

- var
        A variable name to debug.  Mutually exclusive with the 'msg' option.
        [Default: (null)]

- verbosity
        A number that controls when the debug is run, if you set to 3 it will only run debug when -vvv or
        above
        [Default: 0]
        version_added: 2.1


NOTES:
      * This module is also supported for Windows targets.

AUTHOR: Dag Wieers (@dagwieers), Michael DeHaan
        METADATA:
          status:
          - stableinterface
          supported_by: core
        

EXAMPLES:
# 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
    verbosity: 2

- name: Display all variables/facts known for a host
  debug:
    var: hostvars[inventory_hostname]
    verbosity: 4

References

Introduction to Ansible - Part 1

Introduction to Ansible - Part 2

Official Ansible Documentation



© PolarSPARC