Jinja2 for better Ansible playbooks and templates

3 Comments

There have been posts about Ansible on this blog before, so this one will not go into Ansible basics again, but focus on ways to improve your use of variables, often, but not only used together with the template module, showing some of the more involved features its Jinja 2-based implementation offers.

The examples are mostly taken right out of our Ansible provisioning for CenterDevice, with only slight adaptations for conciseness.

Basic Variable Access

The bare metal machines we use as the basis for our OpenStack infrastructure have different capabilities. We use this information to set up host aggregates.

The Ansible inventory sets a VENDOR_MODEL host variable for each machine:

[nodes]  
node01.baremetal VENDOR_MODEL="Dell R510"  
node02.baremetal VENDOR_MODEL="Dell R510"  
node03.baremetal VENDOR_MODEL="Dell R510"  
node04.baremetal VENDOR_MODEL="Dell R510"  
…
node0x.baremetal VENDOR_MODEL="Dell R420"

For use in playbooks (and in templates) Ansible automatically puts it into the hostvars dictionary. ansible_hostname is just another regular variable expansion.

shell: nova aggregate-add-host "{{ VENDOR_MODEL }}" "{{ ansible_hostname }}"

Sometimes, though, just expanding pre-defined variables is not good enough. So let’s move on.

Registering Variables for shell & command output

Running external programs via the shell and command modules often produces output and exit codes you may want to use in the subsequent flow of your playbook.

- name: Create temp file for some later task  
  command: mktemp /tmp/ansible.XXXXXXXXX  
  register: tmp_file

Using register like this captures the result of the command execution in a new dictionary variable called tmp_file. This contains, among other things, mktemp’s exit code and its standard out and standard err output. Knowing that mktemp prints the name of the created temp file to standard out lets us use it like so:

- name: Copy some file to the temp location  
  sudo: True  
  copy: src=sourcefile dest={{ tmp_file.stdout }}

Often you are interested in the exit code of a command to base decisions on. If, for example, you grep for some search term, grep informs you via its exit code if it found the term or not:

- name: check for gpg public key  
  sudo: true  
  shell: gpg --list-keys | grep {{ BACKUP_GPG_PUBLIC_KEY }}  
  register: find_gpg_public_key  
  always_run: true  
  failed_when: find_gpg_public_key.rc > 1

This snipped uses gpg --list-keys and grep to check if the key is already known in the gpg keychain. grep exits with exit code 0 when it found the search term and 1 when it did not. To let Ansible know that, we tell it to only treat the command as failed_when the registered output dictionary’s rc member (which stores the exit code) is greater than 1, as per grep’s man page.

Combined with the temporary file created shown earlier, the following snippet gracefully handles importing of the gpg key into the keychain on the target system if not present yet and to continue without interruption if it already is:

- name: check for gpg public key  
  sudo: true  
  shell: gpg --list-keys | grep {{ BACKUP_GPG_PUBLIC_KEY }}  
  register: find_gpg_public_key  
  always_run: true  
  failed_when: find_gpg_public_key.rc > 1
 
- name: Create temp file for gpg public key  
  command: mktemp /tmp/ansible.XXXXXXXXX  
  register: gpg_public_key_tmp_file  
  always_run: true  
  when: find_gpg_public_key.rc == 1
 
- name: Create gpg public key  
  sudo: true  
  copy: src=root/gnupg/backup@centerdevice.de.pub dest={{ gpg_public_key_tmp_file.stdout }} owner=root group=root mode=0600  
  when: find_gpg_public_key.rc == 1
 
- name: Import gpg public key  
  sudo: true  
  command: /usr/bin/gpg --import {{ gpg_public_key_tmp_file.stdout }}  
  when: find_gpg_public_key.rc == 1
 
- name: Delete temp file for gpg public key  
  sudo: true  
  file: path={{ gpg_public_key_tmp_file.stdout }} state=absent  
  when: find_gpg_public_key.rc == 1  
  always_run: true

Capturing other tasks’ output

Tasks other than command or shell also provide result output that can be registered into variables. See this example, where we set up several MySQL servers for replication automatically (the roles come from host variables, set up in the inventory):

Daniel Schneller has been designing and implementing complex software and database systems for more than 15 years and is the author of the MySQL Admin Cookbook. His current job title is Principal Cloud Engineer at CenterDevice GmbH, where he focuses on OpenStack and Ceph based cloud technologies. He has given talks at FroSCon, Data2Day and DWX Developer Week among others.

Share on FacebookGoogle+Share on LinkedInTweet about this on TwitterShare on RedditDigg thisShare on StumbleUpon

Kommentare

  • Maciej

    Useful and extensive, thanks!

  • gary siaw

    23. January 2015 von gary siaw

    thanks, i am finding ansible to be a steeper learning curve than i thought. you have some great examples here , but not exactly what i needed. I was wonder, is it possible to set variables based on the registered variable.

    if i have some task:
    – name: Find files for retrieving
    shell: find /tmp -type f | egrep “scripts_E3” | xargs -n 1 realpath
    register: stuff
    – debug: var=stuff.stdout_lines

    and i expect the output to be 2 or more lines, which i get from stuff.stdout_lines, is there a way to set variables based on array contents of stuff.stdout_lines ?

    like, i know i can get {{ stuff.stdout_lines[0] }} and {{ stuff.stdout_lines[1] }}, etc.

    but can i set them to a variable somehow, in the yaml file?

    like:
    file1={{ stuff.stdout_lines[0] }}
    file2={{ stuff.stdout_lines[1] }}

Comment

Your email address will not be published. Required fields are marked *