Inline Jinja templates used with set_fact can be used to convert one complex data structure in to another without using a loop in Ansible, but I initially found doing so quite tricky.
The challenge
Today I had a requirement to read the data structure output from one module (the vmware_guest_disk_facts module), and refactor that in to a different data structure for input in to another module (the vmware_guest module).
I am deploying VMware VMs to a vCenter server using a template and the vmware_guest Ansible module. While doing so, I want to add one or more additional disks to the deployed VM. I can use the disks: option on the vmware_guest module to override the template’s disks, but to supplement them, I need to make sure I include the existing disks as well. Fortunately I can get details of the existing disks in the template using Ansible’s vmware_guest_disk_facts module.
Using the example given in the docs (https://docs.ansible.com/ansible/2.6/modules/vmware_guest_disk_facts_module.html) Here’s what the vmware_guest_disk_facts module might return:
{ '0': { 'backing_filename': '[datastore2] xxxx/xxxx.vmdk', 'capacity_in_kb': 10240, 'backing_disk_mode': 'persistent', 'backing_eagerlyscrub': False, 'backing_writethrough': False, 'label': 'Hard disk 1', 'backing_datastore': 'datastore2', 'key': 2000, 'capacity_in_bytes': 10485760, 'backing_thinprovisioned': False, 'controller_key': 1000, 'summary': '10,240 KB', 'unit_number': 0 } }
To add an additional 16Gb virtual hard disk to this VM during deployment, I need to pass the following to the vmware_guest module:
vmware_guest: ... hostname: "{{ vcenter_server_ip }}" username: "{{ vcenter_server_username }}" password: "{{ vcenter_server_password }}" name: "{{ inventory_hostname }}" template: "{{ template_name }}" disk: - size_kb: 10240 # <-- this is the disk in the template autoselect_datastore: true # - size_gb: 16 # <-- this is the disk I want to add autoselect_datastore: true # ...
The challenge I set myself was to work out what to pass in the disk option to make sure the existing disk does not get overridden with the additional disk I want to add.
The solution
I am able to grab information about the existing disks in a template with the following task:
- name: "Get facts for named template" vmware_guest_disk_facts: hostname: "{{ vcenter_server_ip }}" username: "{{ vcenter_server_username }}" password: "{{ vcenter_server_password }}" validate_certs: no datacenter: "{{ vcenter_datacenter_name }}" name: "{{ template_name }}" register: template_disk_facts delegate_to: localhost
Using set_fact in an Ansible loop to convert the output of this in to the format I need is relatively simple:
- name: "Define new disk structure" set_fact: vm_disks: (vm_disks|default([])) + {'size_kb': {{ item[1].capacity_in_kb }}, 'autoselect_datastore': true} loop: template_disk_facts
But like all Ansible loops, this results in running the task once for each value in the loop. For elegance, I want to create the data structure in a single pass. An inline Jinja template is the solution:
- name: "Define new disk structure" set_fact: # Create the list of disks to be deployed. This needs to include # the disks present in the template plus any additional disks # specified for this host in (for example) an attribute called # additional_disks specified in the host's host_vars file. vm_disks: >- [{% for disk in (template_disk_facts.guest_disk_facts|dictsort) %}{ 'size_kb': {{ disk[1].capacity_in_kb }}, 'autoselect_datastore': true}, {% endfor %} {% for disk in additional_disks|default([]) %}{ {% if disk.size_gb is defined %}'size_gb': {{ disk.size_gb }},{% endif %} {% if disk.size_mb is defined %}'size_mb': {{ disk.size_mb }},{% endif %} {% if disk.size_kb is defined %}'size_kb': {{ disk.size_kb }},{% endif %} 'autoselect_datastore': true}, {% endfor %}] delegate_to: localhost
Now, deploying the template complete with the additional disk, I just need to pass the set fact in the vmware_guest task:
- vmware_guest: ... hostname: "{{ vcenter_server_ip }}" username: "{{ vcenter_server_username }}" password: "{{ vcenter_server_password }}" name: "{{ inventory_hostname }}" template: "{{ template_name }}" disk: "{{ vm_disks }}" ...