Ansible Omnibus Examples – Filtering, Loops, Conditionals, Tips
The great sales leader Tony Owens once said “Steal the wheel, don’t reinvent it.” With that in mind, here’s my github repo; feel free to grab and modify when/where you can.
I plan to revisit and update this article on a semi regular basis to keep adding new content, so keep checking back.
Also I welcome any and all feedback.
Variables
Saving task output to variable
This is done by registering the output to a variable. As you can see in the below you simply issue a call to a module and use the register keyword along with the variable name you want the output saved to.
1 2 3 4 5 6 7 8 9 10 | --- - name: Register variables hosts: localhost tasks: - name: get system time shell: clock register: sys_clock - debug: var: sys_clock |
The playbook output looks like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | ok: [localhost] => { "sys_clock": { "changed": true, "cmd": "clock", "delta": "0:00:00.202137", "end": "2020-09-23 10:41:50.977857", "failed": false, "rc": 0, "start": "2020-09-23 10:41:50.775720", "stderr": "", "stderr_lines": [], "stdout": "2020-09-23 10:41:50.805184-05:00", "stdout_lines": [ "2020-09-23 10:41:50.805184-05:00" ] } } |
Note all of the things returned on a registered variable…it’s not simply the results of the run.
If there was an error detected then stderr and stderr_lines would have values, but since this completed correctly, they are empty.
My out put is in stdout and stdout_lines. Lines will have the information formatted into consecutive lines whenever it detects a new line or line break. Stdout will just be all of the things smashed together.
rc stands for return code, which some commands will utilize(though not all do).
If I wanted to print just stdout I could do it like this:
1 2 3 | - name: print stdout debug: msg: "{{ sys_clock.stdout }}" |
Note how I referenced the variable: variablename (dot) variable.
I can also reference the exact same variable with slightly different syntax:
1 2 3 | - name: print stdout debug: msg: "{{ sys_clock['stdout'] }}" |
They both produce the same output:
1 2 3 | TASK [print stdout] ************************************************************************************************************************ ok: [localhost] => { "msg": "2020-09-23 10:51:04.258207-05:00" |
Setting variables in a playbook
This is easily done via the set_facts module. In essence it says “set this variable to this”. Also notice that in the example I’m pulling something from the hostvars. This is the gathered_facts info and in this instance the current host should have a “color” field.
1 2 3 4 5 6 7 8 9 10 11 | - name: Set variables hosts: localhost tasks: - name: set facts, yo set_facts: cust_desc: Bob's Burgers cust_int: gi0/1 cust_color: "{{ hostvars[host]['color'] }}" - debug: msg: "{{cust_desc}},{{cust_int}},{{cust_color}}" |
Prompting for variables
A playbook can prompt the user at runtime for them to input data. I don’t use engine itself very often(in tower these prompts can be done with the “survey” option via the GUI).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | - name: Prompt for variables hosts: localhost vars_prompt: - name: cust_desc prompt: "what is the customer description?" private: no - name: cust_int prompt: "what is the customer interface?" private: no - name: cust_color prompt: "what is the customer color?" private: no - debug: msg: "{{cust_desc}},{{cust_int}},{{cust_color}}" |
Loops
Loops give you the ability to iterate through a list which makes task completion much more efficient(or can at any rate).
In this example I use the yum module with a list, to install/update packages on a RHEL based device. Notice the variable “{{ item }}” here. By default when you define a loop, the product of that loop is referred to as item in variable form. This means at first iteration item will equal httpd, then at the next iteration it will equal dovecot.
1 2 3 4 5 6 | - yum: name: "{{ item }}" state: latest loop: - httpd - dovecot |
Have a look here at version two of the above loop. This is what you are more likely to encounter when working with other modules to gather information. The variable mail_services is a list(which equates to an array). Here I specify the loop should use mail_services and iterate through each member.
1 2 3 4 5 6 7 8 9 10 | vars: mail_services: - httpd - dovecot tasks: - yum: name: "{{ item }}" state: latest loop: "{{ mail_services }}" |
Efficiency Note
Looping in modules isn’t always the most efficient way of processing information, though. The yum module allows you to specify multiple services in the form of a list like this:
1 2 3 4 5 | - yum: name: - httpd - dovecot state: latest |
The difference seems subtle, does it not? When you perform a loop it has to recall the module for each item in the array, which can take quite a while. When you just specify all of the modules in the name section(which can just be an list variable), it processes that block of code at once, making it much more efficient. Having said all of this, not every module allows you to specify multiple items like this, so sometimes a loop is the only option; be sure to check the documentation to see what options are available to you.
Looping through lists of dictionaries:
This adds a little more complication, and you may see it defined in multiple forms. Here’s two version of the same thing:
1 2 3 4 5 6 7 | - user: name: "{{ item.name }}" state: present groups: "{{ item.groups }}" loop: - { name: 'greg', groups: 'tacos' } - { name: 'sowell', groups: 'robot' } |
1 2 3 4 5 6 7 8 9 10 11 | vars: sys_users: - name: greg groups: tacos - name: sowell groups: robot - user: name: "{{ item.name }}" state: present groups: "{{ item.groups }}" loop: "{{ sys_users }}" |
Looking at the above two options I actually think version two makes more sense(this is how you are most likely to encounter this information). Most often you won’t statically define this stuff, rather it will be returned from some other task.
Make note how the name and groups are referenced in the loop as “item.keyvalue”.
That’s one of the tricky bits about Ansible, there are a lot of ways to accomplish the same task 🙂
Nested Loops
This is a bit of a trick question 🙂 Loops can’t be nested, BUT what you can do is have a loop that calls a role or another task yaml file. Take for example the following task
1 2 3 4 5 6 7 8 9 10 11 12 13 | --- - name: nested loop test hosts: localhost gather_facts: faluse tasks: - name: loop through items and include task file for further looping include_tasks: tasks/2nd_loop.yml loop: - Greg - Jimmy loop_control: loop_var: outter_names |
Contents of tasks/2nd_loop.yml
1 2 3 4 5 6 7 8 9 10 | --- - name: the inner loop debug: msg: "{{ outter_names }} loves {{ inner_food }}" loop: - burgers - pizza - waffle fries loop_control: loop_var: inner_food |
You’ll notice some new stuff here; namely “loop_control”. This gives you multiple new abilities on loops, but here you see that I use “loop_var”. This allows me to rename how the loop is referenced in the resulting task(or called tasks). In the original playbook I name the outer loop variables outer_names and on the called task I rename them inner_food. The reason this is so important is that by default loop results are named “item”. If you are nesting loops, then both loops would attempt to use the same name of “item” and that would create a conflict. If I rename my loop variables, then everything keeps working correctly and is also much easier to read.
This will result in:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | [root@ansible1 ansible]# ansible-playbook outerloop.yml PLAY [nested loop test] *************************************************************************************************************************************************************** TASK [loop through items and include task file for further looping] ******************************************************************************************************************* included: /etc/ansible/tasks/2nd_loop.yml for localhost included: /etc/ansible/tasks/2nd_loop.yml for localhost TASK [the inner loop] ***************************************************************************************************************************************************************** ok: [localhost] => (item=burgers) => { "msg": "Greg loves burgers" } ok: [localhost] => (item=pizza) => { "msg": "Greg loves pizza" } ok: [localhost] => (item=waffle fries) => { "msg": "Greg loves waffle fries" } TASK [the inner loop] ***************************************************************************************************************************************************************** ok: [localhost] => (item=burgers) => { "msg": "Jimmy loves burgers" } ok: [localhost] => (item=pizza) => { "msg": "Jimmy loves pizza" } ok: [localhost] => (item=waffle fries) => { "msg": "Jimmy loves waffle fries" } PLAY RECAP **************************************************************************************************************************************************************************** localhost : ok=4 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 |
Code Block Loops
Here’s another gotcha; code blocks like this can’t be looped. The following fails:
1 2 3 4 5 6 7 8 | - name: why can't we loop this? block: - name: print stuff debug: msg: "{{ item }}" loop: - too bad - so sad |
Lookups
Lookup plugins allow access to outside data sources. This can be pulling info from files on the local system to say pulling the time from your ansible control node. Keep in mind that they run on the control node only(not on the remote host). There are SO MANY lookup plugins. I suppose I’ll add my favorites from time to time here.
Lookup plugins are usually called something like this:
1 2 | vars: file_contents: "{{lookup('file', 'path/to/file.txt')}}" |
In the above notice it is called within the mustaches “{{}}”. This tells ansible to break out into jinja2 for processing. Here the lookup plugin calls for a file, then supplies the file to pull it from.
Lookup plugins also have error handling in the form of the errors option:
1 2 | - name: file doesnt exist, but i dont care .. file plugin itself warns anyways ... debug: msg="{{ lookup('file', '/idontexist', errors='ignore') }}" |
Options are ignore, warn, or strict(strict is the default and will cause the task to fail).
By default a lookup plugin returns a comma separated list of values, which if you want to use it in a loop isn’t great. You can add the option wantlist=True or you can substitute the lookup plugin for the query plugin if you want a list returned. The query plugin returns a list by default. Here are the options side by side:
1 2 3 4 5 | lookup('dict', dict_variable, wantlist=True) query('dict', dict_variable) q('dict', dict_variable) |
q = query in the above.
Filters
Stuff here soon.
1 |
Conditionals
Conditionals allow for checking “when” something is true. This way I can check for something like OS type or firmware version.
Searching for a string in a variable is as simple as adding this to a playbook:
1 | when: variable is search("my string") |
An example would be:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | --- - name: testing conditionals hosts: localhost vars: test_var: this contains my string tasks: - name: will match when: test_var is search("my string") debug: msg: It did contain the string - name: will NOT match when: test_var is not search("my string") debug: msg: It did NOT contain the string |
In the above I created a variable named test_var.
In the following two tasks I have a conditional(a “when” parameter) that is searching for my string in a variable.
A when condition will match when whatever is being checked returns true. For example, here are some other valid conditionals:
1 2 3 4 5 6 | when: true when: false when: 1 == 1 when: 2 != 4 when: variable1 == variable2 when: variable1 >= 3 |
Once a when condition is true, it will process the task. If the condition returns false, it will “skip” processing of that task for the designated host.
Random Tips
Assert Module
The assert module gives you the ability to verify certain conditions at any point in your playbook. So half way through my playbook if all conditions aren’t ideal, I can have an assert task kill the playbook. Take the following playbook for example.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | --- - name: testing assert hosts: localhost gather_facts: false vars: tasks: - name: assert 1 assert: that: - 1 == 1 - name: assert 2 assert: that: - 1 == 2 - name: assert 3 assert: that: - 1 == 1 |
Notice that assert 1 passes(1 does equal 1). Assert two, however, will fail, and my playbook will stop:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | [root@localhost general]# ansible-playbook assert-test.yml [WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all' PLAY [testing assert] ********************************************************** TASK [assert 1] **************************************************************** ok: [localhost] => { "changed": false, "msg": "All assertions passed" } TASK [assert 2] **************************************************************** fatal: [localhost]: FAILED! => { "assertion": "1 == 2", "changed": false, "evaluated_to": false, "msg": "Assertion failed" } PLAY RECAP ********************************************************************* localhost : ok=1 changed=0 unreachable=0 failed=1 skipped=0 rescued=0 ignored=0 |
Spanning Multiple Lines with Values
You can quickly test what it does with this website(don’t even have to do it within ansible).
The pipe keeps everything just as seen on multiple lines(it puts line breaks after each line).
1 2 3 4 5 6 7 | multiple_lines: | this line will read just how you see it here. Where these will all be multiple lines. |
The greater than will concatenate everything into a single line
1 2 3 4 5 6 7 | single_line: > all of this shows up as a single line. This allows you to just break up long lines. |
Both of the above | and > have “\n” new lines at the end of them. You can add the – to the end of them to remove new lines as in >- or |-.
1 2 3 4 5 | single_line_no_newline: >- this is all one line with no new line at the end of it. |
1 |
1 |