ansible

Why cannot I get a loop to work with a dictionary replacement in Ansible?


I have an Ansible playbook that will be pulling a list of VMs from a Nutanix Prism Central, capture the vm_name and the UUID, parse the short name of the VM, come up with the FQDN and then replace the vm_name with the FQDN.

I have issues replacing the vm_name in the dictionary variable using a loop.

I get:

"The task includes an option with an undefined variable. The error was: str object has no element AnsibleUndefined(hint=None, obj=missing, name='loop')

I have created a minimal reproducible code:

---
- name: Test of value replacement
  hosts: all
  gather_facts: False
  vars:
    clone_vm:
      - {  uuid: "ac1fd960-e19a-48f3-935b-442901ef5fee", vm_name: "server1" }
      - {  uuid: "f44880f6-49aa-4ba3-8cba-403ff06f1c5e", vm_name: "server2" }
    fqdns:
         - "mymachine@mycompany.com"
         - "myothermachine@mycompany.com"


  tasks:
    - name: Show variable
      debug:
        msg:
        - "{{ clone_vm }}"
        - "{{ fqdns }}"

    - name: Replace vm_name with FQDN
      set_fact:
        clone_vm: "{{ clone_vm | map('combine', [{'vm_name': item[loop.index0]}]) }}"
      loop: "{{ fqdns }}"
      loop_control:
        loop_var: item

    - debug:
        msg: "{{ clone_vm }}"

If I replace [{'vm_name': item[loop.index0]}] with [{'vm_name': item.[loop.index0]}] I get a different error:

template error while templating string: expected name or number. String: {{ clone_vm | map('combine', [{'vm_name': item.[loop.index0]}]) }}. expected name or number

Is there another way to refer to the current value in fqdns to replace the current value in clone_vm?

From

- uuid: ac1fd960-e19a-48f3-935b-442901ef5fee
  vm_name: server1
- uuid: f44880f6-49aa-4ba3-8cba-403ff06f1c5e
  vm_name: server2

My desired output would be:

- uuid: ac1fd960-e19a-48f3-935b-442901ef5fee
  vm_name: myserver.mycompany.com
- uuid: f44880f6-49aa-4ba3-8cba-403ff06f1c5e
  vm_name: myotherserver.mycompany.com

Solution

  • Thanks to @U880C's answer, I finally got what you are actually trying to do.

    You do not need set_fact nor extended loop vars to achieve your goal (although you can use them, see example at bottom). The following self-explanatory playbook is an example that meets your requirement. You can run it with -v to get intermediate debugging to study the solution. Feel free to drop the intermediate vars to bring it down to a single jinja2 expression if you need.

    ---
    - name: Test of value replacement
      hosts: localhost
      gather_facts: False
      vars:
        clone_vm:
          - {  uuid: "ac1fd960-e19a-48f3-935b-442901ef5fee", vm_name: "vrtstart001.hs.local_clone1" }
          - {  uuid: "f44880f6-49aa-4ba3-8cba-403ff06f1c5e", vm_name: "vrtstsrs001.hs.local_clone1" }
        fqdns:
             - "vrtstart001.nadex.co"
             - "vrtstsrs001.nadex.co"
    
        _fqdns_dict_list: "{{ ['vm_name: '] | product(fqdns) | map('join') | map('from_yaml') }}"
        _synchronized_list: "{{ clone_vm | zip(_fqdns_dict_list) }}"
        clone_vm_transformed: "{{ _synchronized_list | map('combine') }}"
    
    
      tasks:
        - name: "Intermediate result: fqdns as a list of dicts"
          ansible.builtin.debug:
            var: _fqdns_dict_list
            verbosity: 1
    
        - name: "Intermediate result: synchronized list"
          ansible.builtin.debug:
            var: _synchronized_list
            verbosity: 1
    
        - name: "Final result: dict list with replaced vm_name"
          ansible.builtin.debug:
            var: clone_vm_transformed
    

    Playbook demo run:

    PLAY [Test of value replacement] ********************************************************************************************************************************************************************************************
    
    TASK [Intermediate result: fqdns as a list of dicts] ************************************************************************************************************************************************************************
    skipping: [localhost]
    
    TASK [Intermediate result: synchonized list] ********************************************************************************************************************************************************************************
    skipping: [localhost]
    
    TASK [Final result: dict list with replaced vm_name] ************************************************************************************************************************************************************************
    ok: [localhost] => {
        "clone_vm_transformed": [
            {
                "uuid": "ac1fd960-e19a-48f3-935b-442901ef5fee",
                "vm_name": "vrtstart001.nadex.co"
            },
            {
                "uuid": "f44880f6-49aa-4ba3-8cba-403ff06f1c5e",
                "vm_name": "vrtstsrs001.nadex.co"
            }
        ]
    }
    
    PLAY RECAP ******************************************************************************************************************************************************************************************************************
    localhost                  : ok=1    changed=0    unreachable=0    failed=0    skipped=2    rescued=0    ignored=0
    

    For completeness and learning sake, here is an example using your original approach. This will create an unnecessary task running in a loop and possibly for every host in your play loop (although this last point can be optimized). So I consider this a bad practice in this situation and the goal below is just to show it is feasible.

    The following playbook:

    ---
    - name: Test of value replacement
      hosts: localhost
      gather_facts: False
      vars:
        clone_vm:
          - {  uuid: "ac1fd960-e19a-48f3-935b-442901ef5fee", vm_name: "vrtstart001.hs.local_clone1" }
          - {  uuid: "f44880f6-49aa-4ba3-8cba-403ff06f1c5e", vm_name: "vrtstsrs001.hs.local_clone1" }
        fqdns:
             - "vrtstart001.nadex.co"
             - "vrtstsrs001.nadex.co"
    
      tasks:
        - name: Replace vm_name with FQDN
          vars:
            current_name:
              vm_name: "{{ item }}"
          ansible.builtin.set_fact:
            clone_vm_transformed: "{{ clone_vm_transformed | d([]) + [clone_vm[ansible_loop.index0] | combine(current_name)] }}"
          loop: "{{ fqdns }}"
          loop_control:
            extended: true
    
        - name: Show replacement result
          ansible.builtin.debug:
            var: clone_vm_transformed
    

    gives:

    PLAY [Test of value replacement] ********************************************************************************************************************************************************************************************
    
    TASK [Replace vm_name with FQDN] ********************************************************************************************************************************************************************************************
    ok: [localhost] => (item=vrtstart001.nadex.co)
    ok: [localhost] => (item=vrtstsrs001.nadex.co)
    
    TASK [Show replacement result] **********************************************************************************************************************************************************************************************
    ok: [localhost] => {
        "clone_vm_transformed": [
            {
                "uuid": "ac1fd960-e19a-48f3-935b-442901ef5fee",
                "vm_name": "vrtstart001.nadex.co"
            },
            {
                "uuid": "f44880f6-49aa-4ba3-8cba-403ff06f1c5e",
                "vm_name": "vrtstsrs001.nadex.co"
            }
        ]
    }
    
    PLAY RECAP ******************************************************************************************************************************************************************************************************************
    localhost                  : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0