ansiblejinja2

How can I transform a list of dictionaries containing two keys into a string?


I created an Ansible role that is setting up cron jobs for a user in a wide variety of Linux distros and versions. The cron jobs to be set up are defined in crontab_entries variable. Also there are some env variables that should be set to every cron job.

One clean solution would be to use ansible.builtin.cron module with env: true, which sets up env vars for the entire crontab file. Although there are hosts in the inventory with cron implementations which do not support this, so the fallback solution would be to insert variable declarations into the cron jobs. For example, replacing

job: db/bin/start.sh with job: "export HOME=/tmp; export SHELL=/bin/bash ; {{ script_root }}/db/bin/start.sh"

crontab_entries:
  - name: "OS settings"
    job: "os-linux/bin/start.sh"
  - name: "app settings"
    job: "app-linux/bin/start.sh"
  - name: "db settings"
    job: "db/bin/start.sh"

cron_env:
  - var: HOME
    value: /tmp
  - var: shell
    value: /bin/bash

So basically I would need to convert contents of cron_env dict into a string like this:

"export HOME=/tmp; export SHELL=/bin/bash ; "

Can it be done with simple Jinja filters?


Solution

  • Given a list of dictionaries of key var and of value value, you can use a combination of the filters map, zip and join to construct the string you are looking for.

    The logic would play as such:

    1. we use a map(attribute='...') to break the dictionaries into two lists, one of the var, the other one of the value.
      With
      cron_env | map(attribute='var')
      
      and
      cron_env | map(attribute='value')
      
      we get:
      - HOME
      - shell
      
      on one side and
      - /tmp
      - /bin/bash
      
      on the other.
    2. we zip those two list back together in order to have a list of list with each var and its corresponding value.
      So
      cron_env | map(attribute='var')
        | zip(cron_env | map(attribute='value'))
      
      will give
      - - HOME
        - /tmp
      - - shell
        - /bin/bash
      
    3. we join in map, in order to have a list of strings var=value.
      On the previous result, we apply
      map('join', '=')
      
      to get
      - HOME=/tmp
      - shell=/bin/bash
      
    4. we join each elements with ; export .
      From the previous step, we apply
      join('; export ')
      
      to get
      HOME=/tmp; export shell=/bin/bash
      
    5. we add an export at the beginning and a ; at the end.
      Remember that the tilde (~) is the concatenation symbol in Jinja.
      'export ' ~ all_the_above ~ '; '
      
      will finally give
      export HOME=/tmp; export shell=/bin/bash; 
      

    All together, the task:

    - debug:
        msg: >-
          {{
            'export ' ~
            cron_env
              | map(attribute='var')
              | zip(cron_env | map(attribute='value'))
              | map('join', '=')
              | join('; export ')
            ~ '; '
          }}
      vars:
        cron_env:
          - var: HOME
            value: /tmp
          - var: shell
            value: /bin/bash
    

    Would yield:

    ok: [localhost] => 
      msg: 'export HOME=/tmp; export shell=/bin/bash; '
    

    Another way could be to add the export and ; to the sub-list with the help of the product filter, then flatten the list of lists.
    The advantage of this approach is that, if the cron_env list happens to be empty, the resulting string would be an empty string too.

    Here is a task achieving this:

    - debug:
        msg: >-
          {{
            ['export '] | product(
              cron_env
                | map(attribute='var')
                | zip(cron_env | map(attribute='value'))
                | map('join', '=')
            ) | product(['; '])
              | flatten
              | join
          }}
      vars:
        cron_env:
          - var: HOME
            value: /tmp
          - var: shell
            value: /bin/bash