Category Archives: Automation

Ansible tips’n’tricks: assessing your runtime environment

One thing that I frequently need to do is test for a certain condition, and fail if it is not met. After all, I want to write those playbooks in a safe way.

Here is an example: I need to ensure that my playbook only runs on Oracle Linux 7. How can I do this? Ansible offers a shell and a command module (make sure you read the notes in the command module documentation!), so I could simply write something testing for the output of, let’s say, /etc/os-release.

This is totally possible, although I believe it’s a bit messy and there is a more elegant way requiring far less coding. Ansible maintains a whole raft of variables it gathers when you run a playbook. Here is an example (I am again using the “debug” stdout_callback as described in my earlier blog posts):

[martin@controller ansible]$ ansible-playbook -i inventory.yml test.yml

PLAY [blogpost] ****************************************************************

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

TASK [say hello] ***************************************************************
ok: [server1] => {}

MSG:

hello world

PLAY RECAP *********************************************************************
server1                    : ok=2    changed=0    unreachable=0    failed=0   

When invoking the play “blogpost” (I have defined my host “server1” to be part of a group named “blogposts” in the local inventory file), I can see that the first task is to gather facts. This is unusual, since I haven’t defined any such task:

[martin@controller ansible]$ cat test.yml 
---
- hosts: blogpost

  tasks:
    - name: say hello
      debug:
        msg: hello world

Nevertheless, ansible gathers these facts, and they are most useful. But which of these exist, and what are their values? It is not too hard to find out. The following command invokes the setup module (using a local inventory mapping “server1” to group “blogpost”) specifically against host “server1”:

[martin@controller ansible]$ ansible -i inventory.yml -m setup server1 | head -n 25
server1 | SUCCESS => {
    "ansible_facts": {
        "ansible_all_ipv4_addresses": [
            "192.168.abc.24"
        ],
        "ansible_all_ipv6_addresses": [
            "fe80::5054:ff:fe91:abcd"
        ],
        "ansible_apparmor": {
            "status": "disabled"
        },
        "ansible_architecture": "x86_64",
        "ansible_bios_date": "04/01/2014",
        "ansible_bios_version": "1.10.2-2.fc27",
        "ansible_cmdline": {
            "BOOT_IMAGE": "/vmlinuz-4.1.12-94.3.9.el7uek.x86_64",
            "LANG": "en_US.UTF-8",
            "crashkernel": "auto",
            "quiet": true,
            "rd.lvm.lv": "rootvg/swaplv",
            "rhgb": true,
            "ro": true,
            "root": "/dev/mapper/rootvg-rootlv"
        },
        "ansible_date_time": {

There is a lot more output! You can query pretty much everything using these built-in variables. So what about our distribution? Let’s check the output of the setup module for a string containing the word “distribution”:

[martin@controller ansible]$ ansible -i inventory.yml -m setup server1 | grep -i distribution
        "ansible_distribution": "OracleLinux", 
        "ansible_distribution_file_parsed": true, 
        "ansible_distribution_file_path": "/etc/oracle-release", 
        "ansible_distribution_file_search_string": "Oracle Linux", 
        "ansible_distribution_file_variety": "OracleLinux", 
        "ansible_distribution_major_version": "7", 
        "ansible_distribution_release": "NA", 
        "ansible_distribution_version": "7.5", 

So it looks like there is something we could use here. I can re-write my playbook now to check for Oracle Linux 7 as follows:

[martin@controller ansible]$ cat test2.yml 
---
- hosts: blogpost

  tasks:
    - name: print debuginfo
      debug:
        var: "{{ item }}" 
      with_items:
        - ansible_distribution
        - ansible_distribution_major_version

    - name: ensure we run Oracle Linux 7
      fail:
        msg: This playbook only runs on Oracle Linux 7
      when: (ansible_distribution != "OracleLinux" and ansible_distribution_major_version|int != 7)

This does work all right when running against an Oracle Linux 7 system.

[martin@controller ansible]$ ansible-playbook -i inventory.yml test2.yml

PLAY [blogpost] ****************************************************************

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

TASK [print debuginfo] *********************************************************
ok: [server1] => (item=ansible_distribution) => {
    "ansible_distribution": "OracleLinux", 
    "item": "ansible_distribution"
}
ok: [server1] => (item=ansible_distribution_major_version) => {
    "ansible_distribution_major_version": "7", 
    "item": "ansible_distribution_major_version"
}

TASK [ensure we run Oracle Linux 7] ********************************************
skipping: [server1]

PLAY RECAP *********************************************************************
server1                    : ok=2    changed=0    unreachable=0    failed=0   

The “skipping” message indicates this task [ensure we run Oracle Linux 7] has not fired and we can proceed with life. Pointing this playbook to another environment not running Oracle Linux 7, the play fails:

[martin@controller ansible]$ ansible-playbook -i inventory.yml test2.yml

PLAY [blogpost] ****************************************************************

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

TASK [print debuginfo] *********************************************************
ok: [linuxdev] => (item=ansible_distribution) => {
    "ansible_distribution": "Fedora", 
    "item": "ansible_distribution"
}
ok: [linuxdev] => (item=ansible_distribution_major_version) => {
    "ansible_distribution_major_version": "28", 
    "item": "ansible_distribution_major_version"
}

TASK [ensure we run Oracle Linux 7] ********************************************
fatal: [linuxdev]: FAILED! => {
    "changed": false
}

MSG:

This playbook only runs on Oracle Linux 7


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

So there we go! This example can easily be extended to check for other things, such as a combination of running UEK on Oracle Linux, etc.

Happy scripting!

Advertisements

Ansible tips’n’tricks: even more output options

In my last post I wrote about the “debug” option to format Ansible output differently. I came across this setting simply by searching the usual developer forums for an alternative Ansible output option.

Having found out about the “debug” option made me curious, especially since there wasn’t an awful lot of documentation available about additional alternatives. Or so It thought before writing this post, there is actually, as you will see later. So to recap what I had so far: I noticed “skippy” in my distribution’s /etc/ansible/ansible.cfg although it is commented out. And I found the “debug” option via my favourite search engine, and there is the “default” as well.

There surely had to be more …

This wasn’t quite good enough for me and I started to wonder if there were more of these callbacks. Here is my Ansible version in case some of these callbacks might be quite recent:

[martin@controller environment]$ ansible --version | head -n 1
ansible 2.6.4

My first idea was to perform a file system search using find – I assumed trying to look out for “debug” or “default” would result in too many irrelevant hits, but “skippy” sounded unique enough a search term.

$ find / -iname "*skippy*" 2> /dev/null
/usr/lib/python2.7/site-packages/ansible/plugins/callback/skippy.py
/usr/lib/python2.7/site-packages/ansible/plugins/callback/skippy.pyc
/usr/lib/python2.7/site-packages/ansible/plugins/callback/skippy.pyo
[martin@linuxdev ~]$ 

This looks like a hit! I opened the first file, skippy.py, and found a documentation section:

DOCUMENTATION = '''
    callback: skippy
    callback_type: stdout
    requirements:
      - set as main display callback
    short_description: Ansible screen output that ignores skipped status
    version_added: "2.0"
    extends_documentation_fragment:
      - default_callback
    description:
        - This callback does the same as the default except it does not output skipped host/task/item status

So there you go, more information about the purpose of this callback! Are there any others that might be useful candidates for stdout_callback? Possibly! Let’s see (still in the callback directory)…

$ grep -i 'type: stdout' *.py
actionable.py:    type: stdout
debug.py:    type: stdout
default.py:    type: stdout
dense.py:type: stdout
full_skip.py:    type: stdout
json.py:    type: stdout
minimal.py:    type: stdout
null.py:    callback_type: stdout
oneline.py:    type: stdout
selective.py:    callback_type: stdout
skippy.py:    callback_type: stdout
stderr.py:    callback_type: stdout
unixy.py:    type: stdout
yaml.py:    type: stdout

Having unearthed this list I eventually found the relevant part of the documentation set! So instead of showing you the DOCUMENTATION variable for each of these callbacks, I point you to the official Ansible 2.6 documentation:

https://docs.ansible.com/ansible/2.6/plugins/callback.html#plugin-list

Happy scripting!

Ansible tips’n’tricks: a different output option

When running ansible scripts, occasionally you wonder why a given task has failed. I found out more than once that it’s commonly a problem with the script, not the engine ;) Finding out exactly where in the script I made the mistake can be more of a challenge.

With the default ansible settings, output can be a bit hard to read. Consider this example: I do quite a bit of patching in my lab, and this almost always requires an upgrade of OPatch (d’oh!). So instead of connecting to each of my hosts and performing the same unzip command over and over again, I thought of using something else. Why not use ansible for this task? It won’t get tired copying/unzipping OPatch to all the destinations I indicate in my configuration. And it won’t introduce a mistake when dealing with the fifth ORACLE_HOME on the third server…

Before replacing $ORACLE_HOME/OPatch with the new version, I want to take a backup of the current OPatch just in case. I don’t want to keep more than 1 backup around in this particular lab environment, so I decided to check for an existing backup first, before creating a new one. If one exists, I remove it. Or at least, that’s the plan.

So I was happily coding away and in my usual trial-and-error approach was ready to test the script I wrote for the first time. Here’s the result (as shown in my 80×24 terminal):

[martin@controller environment]$ ansible-playbook -i inventory.yml broken.yml

PLAY [blogpost] ****************************************************************

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

TASK [check if there is an old backup] *****************************************
ok: [server1]

TASK [remove old OPatch backup] ************************************************
fatal: [server1]: FAILED! => {"msg": "The conditional check 'backup_present.exis
ts' failed. The error was: error while evaluating conditional (backup_present.ex
ists): 'dict object' has no attribute 'exists'\n\nThe error appears to have been
 in '/home/martin/ansible/blogpost/environment/broken.yml': line 20, column 11, 
but may\nbe elsewhere in the file depending on the exact syntax problem.\n\nThe 
offending line appears to be:\n\n\n        - name: remove old OPatch backup\n  
        ^ here\n"}

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

[martin@controller environment]$ 

It doesn’t really matter what I was trying to do here, what matters though is the somewhat illegible formatting of the output. The listing above really shows how the error displayed in my terminal. I haven’t quite understood yet why there are linebreaks (\n) in the output that don’t result in a carriage return on screen.

So I did a little bit of digging around and found a global setting named stdout_callback. This is usually defined in /etc/ansible/ansible.cfg which would be bad news for developers if we couldn’t override it. Thankfully you can – using $HOME/.ansible.cfg or even an ansible.cfg file in your project directory. Setting stdout_callback to “debug” reveals a much more readable version of the error:

TASK [remove old OPatch backup] ************************************************
fatal: [server1]: FAILED! => {}

MSG:

The conditional check 'backup_present.exists' failed. The error was: error while
 evaluating conditional (backup_present.exists): 'dict object' has no attribute 
'exists'

The error appears to have been in '/home/martin/ansible/blogpost/environment/bro
ken.yml': line 20, column 11, but may
be elsewhere in the file depending on the exact syntax problem.

The offending line appears to be:


        - name: remove old OPatch backup
          ^ here


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

I find this much easier to read, and by setting stdout_callback to a non-default value in my project directory I don’t break anything inadvertently. It also immediately revealed I wasn’t checking backup_exists.stat.exists, I used backup_exists.exists. Pretty-printing the output helped me debug the mistake much quicker. Later on, when your script is ready to be deployed it’s probably a good idea not to use the debug callback ;)

The ansible version in this post is 2.6.4 by the way.

Happy scripting!

Ansible tips’n’tricks: a new series

In the past few months I have spent considerable amounts of time working on new technology (new as in “new to me”) and one of the things I have found a great interest-and more importantly-use cases with, is ansible. I don’t know how it is with you, but if I don’t have a practical use case for using a technology I find it hard to get familiar with it.

Ansible is a fantastic piece of technology with decent documentation and great community support. I didn’t find writing ansible code too hard. Most of the problems I have encountered while learning how to write ansible playbooks have-in some way or another-already been encountered and solved by other users. It does require a bit of Internet research, and you will need check if the solution offered somewhere on the Internet is still valid with the current version of the tool. As with a lot of other software, ansible evolves at a rather quick pace and keeping up can be a bit of a challenge.

I promise not to get into details about what ansible is, others have done so and I’m not a great fan of repeating what more clever people have said. Head over to the official documentation and see for yourself what ansible is. For the most part I consider it pretty straight forward and elegant. It sure beats my attempts at writing bash code ;)

Having said that it might require a minute or so to get used to the markup language; most so-called playbooks are written in YAML and you learn to appreciate the importance of white space in your scripts. Another key concept I personally found quite important to understand is idempotency. A script that is considered to be idempotent can be re-run multiple times safely. There are other important concepts to be aware of, and you should consult the documentation to learn more about these if you aren’t yet familiar with them.

My main reason for using ansible is to automate routine tasks in my personal lab environment, and this little series I’m planning on writing is primarily a “note to self” so I don’t forget how to do things. As with the Oracle posts I plan on writing I’ll add a “tested/written with version x” so later on when someone hits this article series via a search engine what it requires to do the same.

Things I’m planning on writing on are those that cost me a bit more time on <enter your favourite developer forum here> and I’m hoping these little snippets can help you with your automation project.