Search code examples
ansiblejinja2nested-loopswith-statement

How to use nested loops with sequence and a list and how get around of curly braces in Ansible


I have a code to backup config using the ios_config module. I used ios_facts to get the hostname of devices and I want to use it to put the backup file in a similarly named folder and also use it in the file name itself.

In the last task of my code, I need to loop through two items - the sequence from 0 to 1(or how many items are in my inventory) as I need to access the hostname in the results and use it in the backup options, and also loop through my inventory of devices which I extracted from a csv file. I am aware of the rule of double curly braces but I do not know how to get around it.

---
- hosts: localhost
  gather_facts: false

  tasks:
    - name: Block
      block:
          - name: Use CSV

            csv_to_facts:
              src: '{{playbook_dir}}/NEW/Inventory.csv'
              vsheets:
                - INFO:
                    - IP
                    - OS

          - debug:
              msg: '{{item.IP}}'
            loop: '{{INFO}}'

          - name: Create Inventory
            add_host:
              hostname: '{{item.IP}}'
              ansible_network_os: '{{item.OS}}'
              ansible_user: cisco
              ansible_ssh_pass: cisco
              ansible_connection: network_cli
              ansible_become: yes
              ansible_become_method: enable
              groups: group_01
            loop: '{{INFO}}'


          - name: Gather Facts (IOS)
            ios_facts:
            register: ios_facts_loop
            delegate_to: '{{item}}'
            loop: "{{groups['group_01']}}"


          - name: Backup Switch (IOS)
            ios_config:
              backup: yes
              backup_options:
                dir_path:  "tmp/backups/{{ ios_facts_loop.results.{{item[0]}}.ansible_facts.ansible_net_hostname }}"
                filename: "{{ios_facts_loop.results.item{{[0]}}.ansible_facts.ansible_net_hostname}} {{ lookup('pipe','date +%Y-%m-%d@%H:%M:%S')}}"
            register: backup_ios_location
            delegate_to: '{{item[1]}}'
            loop: 
              - with_sequence: "0-{{output|length - 3}}"
              - "{{groups['group_01']}}"

Solution

  • TLDR; for vars notation

    You cannot add double curly braces inside double curly braces like in your above code. You current var reference:

    ios_facts_loop.results.{{item[0]}}.ansible_facts.ansible_net_hostname
    

    should be turned to

    ios_facts_loop.results[item[0]].ansible_facts.ansible_net_hostname
    # or equivalent
    ios_facts_loop.results[item.0].ansible_facts.ansible_net_hostname
    

    Meanwhile, this will only fix your current syntax error (that you didn't share in your question) as the first element in your loop is a string 'with_sequence: "0-X"' which therefore has no index 0.

    Attempt to fix the logic

    If I understand correctly, for your last task, you just need to loop over the results of your ios_facts register and delegate the task to the server it was taken from. Luckilly, you should already have all the info you need in ios_facts_loop.results

    • It is a list so you can directly loop over it
    • Each element should contain an item key with the actual item that was used in the previous run at time of register (i.e. one of your groups['group_01'] element).

    So I would try to write your last task like this. Disclaimer this is a pure guess as I didn't see your exact datastructure.

    - name: Backup Switch (IOS)
      ios_config:
        backup: yes
        backup_options:
          dir_path:  "tmp/backups/{{ item.ansible_facts.ansible_net_hostname }}"
          filename: "{{ item.ansible_facts.ansible_net_hostname}}{{ lookup('pipe','date +%Y-%m-%d@%H:%M:%S')}}"
      register: backup_ios_location
      delegate_to: '{{item.item}}'
      loop: "{{ ios_facts_loop.results }}"
    

    Going further.

    I'm not really familiar with the ios_* modules but they should be really close to other stuff I use daily and I think you could really simplify your playbook taking advantage of more ansible feature (e.g. multiple plays in a playbook). I believe the following should actually do the job:

    ---
    - name: Construct inventory from CSV  
      hosts: localhost
      gather_facts: false
    
      tasks:
        - name: Use CSV
          csv_to_facts:
            src: '{{playbook_dir}}/NEW/Inventory.csv'
            vsheets:
              - INFO:
                  - IP
                  - OS
    
        - name: Create Inventory
          add_host:
            hostname: '{{item.IP}}'
            ansible_network_os: '{{item.OS}}'
            ansible_user: cisco
            ansible_ssh_pass: cisco
            ansible_connection: network_cli
            ansible_become: yes
            ansible_become_method: enable
            groups: group_01
          loop: '{{INFO}}'
    
    - name: Backup switches from created inventory
      hosts: group_01
      gather_facts: false
    
      tasks: 
        - name: Get facts from network os                                                                                            
          ios_facts:                                                                                                  
            gather_subset: all 
    
        - name: Backup Switch (IOS)
          ios_config:
            backup: yes
            backup_options:
              dir_path:  "tmp/backups/{{ ansible_net_hostname }}"
              filename: "{{ ansible_net_hostname }}{{ lookup('pipe','date +%Y-%m-%d@%H:%M:%S') }}"
    

    More background on dot and brackets notation for vars

    You can basically navigate a yaml datastructure with two notation which are equivalent.

    • the dot notation
    a_list_var.index_number
    a_hasmap_var.keyname
    
    • the brackets notation
    a_list_var[index_number]
    a_hashmap_var['key_name']
    

    If we take the following example:

    my_servers:
      hostA:
        ips:
          - x.x.x.x
          - y.y.y.y
        env:
          shell: bash
          home: somewhere
      hostB:
        ips:
          - a.a.a.a
          - b.b.b.b
        env:
          shell: sh
          home: elsewhere
    

    The following notation are all strictly equivalent:

    # all vars of hostA
    hostA_vars: "{{ my_servers.hostA }}"
    hostA_vars: "{{ my_server['hostA'] }}"
    # first IP of hostB
    hostB_ip: "{{ my_servers.hostB.0 }}"
    hostB_ip: "{{ my_servers.hostB[0] }}"
    hostB_ip: "{{ my_servers['hostB'].0 }}"
    hostB_ip: "{{ my_servers['hostB'][0] }}"
    

    As you can see, the dot notation tends to be less verbose and more readable. Meanwhile, you cannot use a variable identifier with the dot notation. So If you want to ave the home env of a variable server you would have to use:

    # set a var for server
    server: hostA
    # all equivalent again
    server_home: "{{ my_servers[server].env.home }}"
    server_home: "{{ my_servers[server]['env'].home }}"
    server_home: "{{ my_servers[server].env['home'] }}"
    server_home: "{{ my_servers[server]['env']['home'] }}"