Search code examples
google-cloud-platformansibleyamljinja2ansible-template

Complex data structures in Ansible


I am attempting to automate an Ansible playbook that will run specific commands on a Docker container deployed on different servers.
For example, I have two VMs (vm1 and vm2 as shown in commands.yml), so, on vm1 I want to run command 1 and command 2 while on vm2 I want to run command 3 and command 4.
I just need to modify the loop to execute the commands one by one for each virtual machine, respectively. Currently, my configuration is structured as follows:

commands.yml

commands:
  - group: vm1
    commands:
     - command 1
     - command 2
  - group: vm2
    commands:
     - command 3
     - command 4

And my Ansible playbook looks like this:

- name: Deploy flex Docker image on GCP VM
  hosts: gcp_vms
  become: yes
  gather_facts: false
  vars_files:
    - vars.yml

  tasks:

    - name: Read commands from configuration file
      include_vars:
        file: commands.yml
        name: command_list

    - name: Run the commands
      community.docker.docker_container_exec:
        container: flex
        command: "{{ item.command }}"             
      loop: "{{ [command_list.commands].commands }}"

The inventory.ini is below

[gcp_vms]
vm1 ansible_host=ip
vm2 ansible_host= ip
; vm3 ansible_host= ip

Solution

  • To select the elements of your list that do correspond to your host, you can use the inventory_hostname special variable.
    Then you will have to use the selectattr and first filters of Jinja to get the corresponding list of commands.

    So, your loop would become:

    loop: >-
      {{ 
        (
          command_list.commands 
            | selectattr('group', '==', inventory_hostname) 
            | first
        ).commands
      }}
    

    Another option, to make it more simple, would be to transform your commands list into a dictionary:

    commands:
      vm1:
        commands:
         - command 1
         - command 2
      vm2:
        commands:
         - command 3
         - command 4
    

    Then, accessing the commands of a host becomes trivial:

    loop: "{{ command_list.commands[inventory_hostname].commands }}"
    

    Given the task:

    - debug:
        var: >-
          (
            command_list.commands
              | selectattr('group', '==', inventory_hostname)
              | first
          ).commands
      vars:
        command_list:
          commands:
            - group: vm1
              commands:
                - command 1
                - command 2
            - group: vm2
              commands:
                - command 3
                - command 4
    

    It would yield:

    ok: [vm1] => 
      ? |-
        (
          command_list.commands
            | selectattr('group', '==', inventory_hostname)
            | first
        ).commands
      : - command 1
        - command 2
    ok: [vm2] => 
      ? |-
        (
          command_list.commands
            | selectattr('group', '==', inventory_hostname)
            | first
        ).commands
      : - command 3
        - command 4
    

    Compare that to a simplified scenario with a dictionary:

    - debug:
        var: command_list.commands[inventory_hostname].commands
      vars:
        command_list:
          commands:
            vm1:
              commands:
              - command 1
              - command 2
            vm2:
              commands:
              - command 3
              - command 4
    

    Which yields:

    ok: [vm1] => 
      command_list.commands[inventory_hostname].commands:
      - command 1
      - command 2
    ok: [vm2] => 
      command_list.commands[inventory_hostname].commands:
      - command 3
      - command 4