William Shallum

Random Ansible "Tricks"

Posted Nov 12 2016, 09:08 by William Shallum [updated Nov 12 2016, 09:09]

Ansible is a thing used at work, it is sometimes rather annoying, but at least it’s somewhat easy to debug. Here are some things I found during experimenting.

No such thing as async include

The unit of reuse is the yml file. What I would like to do is something like this pseudocode:

jobs = []
for (i = 1 to 10) {
  jobs += async {
    do_some(i)
    long_running(i)
    tasks(i)
  }
}
wait_for_all(jobs)

async applies only to a task. Include is not a task, therefore you cannot do this:

- include: somefile.yml i={{ item }}
  with_items:
    - 1
    - 2
  async: 3600
  poll: 0
  register: jobs
- async_status: jid=item.ansible_job_id
  register: job
  until: job.finished
  retries: 3000
  with_items: "{{ jobs.results }}"

so as a workaround, if there is only one long running task in the included file:

- set_fact: jobs: []
- include: somefile.yml i={{ item }}
  with_items: "{{ items }}"
- async_status: jid=item.ansible_job_id
  register: job
  until: job.finished
  retries: 3000
  with_items: "{{ jobs }}"

somefile.yml:

- quick_pre_task: i={{ i }}
- long_running_task: i={{ i }}
  async: 3600
  poll: 0
  register: long_running
- set_fact:
    jobs: "{{ jobs|default([]) + [long_running] }}

This is equivalent to this pseudocode (i.e. quick_pre_task is still run synchronously and serially):

jobs = []
for (i = 1 to 10) { 
  quick_pre_task(i) 
  jobs += async {
    long_running_task(i)
  }
}
wait_for_all(jobs)

Ugh. Other workarounds may include just running another ansible-playbook command as async - say goodbye to your context.

Using Jinja2 control structures in the playbook

So let’s say you want the private IP address, no matter what interface it is on, as a parameter. Normally to use the value of a variable you would do:

- sometask: someparam={{ somevar }}

{{ somevar }} is of course evaluated using Jinja2 resulting in the value of the variable being substituted in. All very good, but the Jinja2 engine is used in its full glory here, so you can actually do:

- sometask: someparam="{% for ip in ansible_all_ipv4_addresses if ip.startswith('10.') %}{% if loop.first %}{{ ip }}{% endif %}{% endfor %}"

People will probably hate you for that.

As a sidenote, it seems that the basic structure (command option=argument) is parsed first before argument is passed to Jinja2 so you cannot do:

- set_fact: stuff: "option=argument"
- sometask: "{{ stuff }}"

Which is probably a good thing, otherwise we’ll be hearing people talk about “parameter injection” vulnerabilities.

Passing complex variable values on the command line

If you have a complex playbook with lots of parameters that have to be validated it may be easier to call it from another script that will check the parameters first to ensure they make sense. Variables can be set when calling ansible-playbook with -e, e.g.:

ansible-playbook -e 'somevar=somevalue' playbook.yml

However, the --help actually states:

-e EXTRA_VARS, --extra-vars=EXTRA_VARS
                        set additional variables as key=value or YAML/JSON

so this is valid and works as expected:

ansible-playbook -e '{"somearray": [1, 2, 3], "othervar":
{"objects": "too"}}' playbook.yml

JSON serialization is, of course, just an import away in most programming languages.