Ewt
Ewt

Reputation: 3

ansible changing nested dict variable

After deployment of a VM with a DHCP IP I would like to get the IP and append it to the guests dictionary.

For the first VM (testvm2) the code perfoms as expected and updates the tempip variable for the testvm2 VM.

But with the second VM (testvm1), it updates the tempip variable of the first VM (testvm2) with the IP of the second VM (testvm1), and updates the tempip variable of the second VM (testvm1) with the code of the variable '{{ tempip_reg.stdout_lines }}'

Can anyone explain to me why this happens? I would appreciate the help.

I copied all the relevant code and output below:

guests dictionary:

---
guests:
  testvm1:
    mem: 512
    cpus: 1
    clone: template-centos
    vmid: 102
    tempip:

  testvm2:
    mem: 1536
    cpus: 2
    clone: template-centos
    vmid: 102
    tempip:

Ansible Playbook that starts the task:

---
- name: Provision VMs
  hosts: infra
  become: true
  vars_files:
    - group_vars/vms.yml
    - group_vars/vars.yml
  tasks:
    - include_tasks: roles/tasks/provision-tasks.yml
      with_dict: "{{ guests }}"

Ansible Tasks:

- name: Run proxmox-get-ip-linux.sh script to register DHCP IP of VM
  script: proxmox-get-ip-linux.sh "{{ item.key }}"
  register: tempip_reg

- name: temporary IP of VM "{{ item.key }}"
  debug:
    var: tempip_reg

- name: current host in item.key
  set_fact:
    current_host: "{{ item.key }}"

- name: current_host variable set to
  debug:
    var: current_host

- name: append item.value.tempip with the DHCP IP of the VM registered in 
 last task
  set_fact:
    guests: "{{ guests|combine({ current_host: {'tempip': '{{ tempip_reg.stdout_lines }}' }}, recursive=True) }}"

- name: temporary IP of "{{ item.key }}"
  debug: var=guests

Result first VM:

"tempip_reg": {
    "stdout": "192.168.1.21\r\n",
    "stdout_lines": [
        "192.168.1.21"
    }

"current_host": "testvm2"

"guests": {
    "testvm1": {
        "clone": "template-centos",
        "cpus": 1,
        "ip": "192.168.1.60",
        "mem": 512,
        "tempip": null,
        "vmid": 102
    },
    "testvm2": {
        "clone": "template-centos",
        "cpus": 2,
        "ip": "192.168.1.61",
        "mem": 1536,
        "tempip": [
            "192.168.1.21"
        ],
        "vmid": 102
    }
}

Result 2nd VM:

"tempip_reg": {
    "stdout": "192.168.1.22\r\n",
    "stdout_lines": [
        "192.168.1.22"
    }

    "current_host": "testvm1"

"guests": {
    "testvm1": {
        "clone": "template-centos",
        "cpus": 1,
        "ip": "192.168.1.60",
        "mem": 512,
        "tempip": "{{ tempip_reg.stdout_lines }}",
        "vmid": 102
    },
    "testvm2": {
        "clone": "template-centos",
        "cpus": 2,
        "ip": "192.168.1.61",
        "mem": 1536,
        "tempip": [
            "192.168.1.22"
        ],
        "vmid": 102
    }
}

Upvotes: 0

Views: 6092

Answers (1)

techraf
techraf

Reputation: 68629

TL;DR

Using Ansible code, you are trying to implement what Ansible already does for you.

Your attempts superimpose with built-in functionality and you get results which look nondeterministic.


Explanation:

  • The main problem with your code is a completely unnecessary loop declared with with_dict: "{{ guests }}" which causes to include the file 4 times.

    It runs 4 times because you change the guests dictionary, which it loops over inside the included tasks-file.

    In effect you get something which looks like an nondeterministic result.

  • The second problem is a trivial one: you always replace the value of tempip with a string {{ tempip_reg.stdout_lines }}.

  • Now, because of the unnecessary with_dict loop over a dictionary which you dynamically change, and because Jinja2 uses lazy variable evaluation, strings from previous iterations are interpreted as templates and get evaluated with incorrect values in subsequent iterations.

    The last iteration leaves the string {{ tempip_reg.stdout_lines }} intact.

  • You also define and print two different facts.


What you should do:

  • You should not declare arbitrary iterations at all. Ansible implements a loop for all hosts itself. That is, if you declare a task:

    - include_tasks: roles/tasks/provision-tasks.yml
    

    the file will be included for each of the hosts in infra group (twice in your example).

  • You seem to want to have a single copy of your data structure with updated values for each VM.

    At the same time, you create a fact, which is a separate data object maintained for each host separately.

    So you should refer to and modify (combine) a single fact - you can do it for example on localhost.


You should structure your code like this:

---
- name: Provision VMs
  hosts: infra
  become: true
  vars_files:
    - group_vars/vms.yml
    - group_vars/vars.yml
  tasks:
    - include_tasks: roles/tasks/provision-tasks.yml
    - debug:
        var: hostvars['localhost'].guests

and provision-tasks.yml:

- set_fact:
    guests: "{{ guests|combine({ current_host: {'tempip': tempip_reg.stdout_lines }}, recursive=True) }}"
  delegate_to: localhost

This will get you the following result:

"hostvars['localhost'].guests": {
    "testvm1": {
        "clone": "template-centos",
        "cpus": 1,
        "ip": "192.168.1.60",
        "mem": 512,
        "tempip": [
            "192.168.1.21"
        ],
        "vmid": 102
    },
    "testvm2": {
        "clone": "template-centos",
        "cpus": 2,
        "ip": "192.168.1.61",
        "mem": 1536,
        "tempip": [
            "192.168.1.22"
        ],
        "vmid": 102
    }
}

Finally, in the above play, you used group_vars and roles/tasks directories in wrong context. I left the paths intact and they will work for the above code, but basically you should never use them this way, because again, they have special meaning and treatment in Ansible.

Upvotes: 2

Related Questions