ansibleyaml

yedit fails on a key that is a list of dict, can it be done?


I have a yaml file witch contains a instance of a key with dicts.

Here's an example:

INSTANCES:
  - NAME: hosttype1
    HOST_NAME: mysevername1
  - NAME: hosttype2
    HOST_NAME: myservername2
  - NAME: hosttype2
    HOST_NAME: myservername3

And the code i tried in my role to change all NAME's and HOST_NAME's are like this:

- name: Update property file {{ environment_env_property_file }} 
yedit:
  src: "{{ environment_env_property_file }}"
  state: present
  edits:
- key: INSTANCES.NAME[0]
  value: "CM01"
- key: INSTANCES.NAME[0].HOST_NAME
  value: "ap-2001c.domain.net"
- key: INSTANCES.NAME[1]
  value: "BP01"
- key: INSTANCES.NAME[1].HOST_NAME
  value: "ap-2002c.domain.net"
- key: INSTANCES.NAME[2]
  value: "BP01"
- key: INSTANCES.NAME[2].HOST_NAME
  value: "ap-2003c.domain.net"

Error from ansible

An exception occurred during task execution. To see the full traceback, use -vvv. The error was: main.YeditException: Unexpected item type found while going through key path: INSTANCES.NAME[0] (at key: NAME)

So what is the correct way to change the list of dictionaries?

Or should it be?

- key: INSTANCES[0].NAME
  value: "hosttype1"
- key: INSTANCES[0].HOST_NAME
  value: "myservername1"
- key: INSTANCES[1].NAME
  value: "hosttype2"
- key: INSTANCES[1].HOST_NAME
  value: "myservername2"
- key: INSTANCES[2].NAME
  value: "hosttype3"
- key: INSTANCES[2].HOST_NAME
  value: "myservername3"

Solution

  • After just dropping the source code from yedit.py under my local Ansible /library, for a property_file.yml with content of

    INSTANCES:
      - NAME: hosttype1
        HOST_NAME: mysevername1
      - NAME: hosttype2
        HOST_NAME: myservername2
      - NAME: hosttype2
        HOST_NAME: myservername3
    

    a minimal example playbook

    ---
    - hosts: localhost
      become: false
      gather_facts: false
    
      vars:
    
        property_file: property_file.yml
    
      tasks:
    
        - name: Update property file {{ property_file }}
          yedit:
            src: "{{ property_file }}"
            state: present
            edits:
              - key: INSTANCES[0].NAME
                value: "CM01"
              - key: INSTANCES[0].HOST_NAME
                value: "ap-2001c.example.net"
              - key: INSTANCES[1].NAME
                value: "BP01"
              - key: INSTANCES[1].HOST_NAME
                value: "ap-2002c.example.net"
              - key: INSTANCES[2].NAME
                value: "BP01"
              - key: INSTANCES[2].HOST_NAME
                value: "ap-2003c.example.net"
    

    will result into a changed YML file with content of

    INSTANCES:
    - HOST_NAME: ap-2001c.example.net
      NAME: CM01
    - HOST_NAME: ap-2002c.example.net
      NAME: BP01
    - HOST_NAME: ap-2003c.example.net
      NAME: BP01
    

    If that's the Use Case, changing all key value pairs, template or copy might be simpler.


    If the Use Case is to change a subset only, a few list elements from a huge list , a minimal example playbook with loop and based on index

    ---
    - hosts: localhost
      become: false
      gather_facts: false
    
      vars:
    
        NEW_INSTANCES:
          - TYPE: CM01
            FQDN: ap-2001c.example.net
          - TYPE: BP01
            FQDN: ap-2002c.example.net
    
        property_file: property_file.yml
    
      tasks:
    
        - name: Update property file {{ property_file }}
          yedit:
            src: "{{ property_file }}"
            state: present
            edits:
              - key: "INSTANCES[{{ ansible_loop.index0 }}].NAME"
                value: "{{ item.TYPE }}"
              - key: "INSTANCES[{{ ansible_loop.index0 }}].HOST_NAME"
                value: "{{ item.FQDN }}"
          loop: "{{ NEW_INSTANCES }}"
          loop_control:
            extended: true
    

    will result into an output of

    TASK [Update property file property_file.yml] ****************************
    ok: [localhost] => (item={'TYPE': 'CM01', 'FQDN': 'ap-2001c.example.net'})
    ok: [localhost] => (item={'TYPE': 'BP01', 'FQDN': 'ap-2002c.example.net'})
    

    and a file content of

    INSTANCES:
    - HOST_NAME: ap-2001c.example.net
      NAME: CM01
    - HOST_NAME: ap-2002c.example.net
      NAME: BP01
    - HOST_NAME: myservername3
      NAME: hosttype2
    

    Please take note in this second example the module in question, yedit expect the key to edit as string:

    def main():
    ...
        module = AnsibleModule(
            argument_spec=dict(
    ...
                key=dict(default='', type='str'),
                value=dict(),
    ...
    

    Therefore one can't just write INSTANCES[ansible_loop.index0].HOST_NAME. Instead it is necessary to write "INSTANCES[{{ ansible_loop.index0 }}].HOST_NAME" which resolves to INSTANCES[0].HOST_NAME. Also "INSTANCES[ {{ ansible_loop.index0 }} ].HOST_NAME" would not work as that resolves to INSTANCES[ 0 ].HOST_NAME.