jsondictionaryansiblecombineon-the-fly

Can you update ansible dictionary on the fly?


I have a dict/json type of object that I captured from the output of a REST API call, let's say it's in a variable/register named ouput. The data looks like below. For completeness, I shared the entire object and did some basic cleanup. Please feel free to only focus on that one line I discuss next;-)

            {
            "_create_time": 1724823837185,
            "_create_user": "system",
            "_last_modified_time": 1736218001919,
            "_last_modified_user": "nsx_policy",
            "_protection": "NOT_PROTECTED",
            "_revision": 1,
            "_system_owned": false,
            "display_name": "w1-vc1-c4n17.example.com",
            "host_switch_spec": {
                "host_switches": [
                    {
                        "cpu_config": [],
                        "ecmp_mode": "L3",
                        "host_switch_id": "50 01 cb 21 be 19 cb 38-78 bd 67 bc b3 0e 91 f1",
                        "host_switch_mode": "STANDARD",
                        "host_switch_name": "w1-vc1-c4-dvs",
                        "host_switch_profile_ids": [
                            {
                                "key": "UplinkHostSwitchProfile",
                                "value": "/infra/host-switch-profiles/6d7a0ed8-c86a-4190-9121-ce6f863226ae"
                            },
                            {
                                "key": "VtepHAHostSwitchProfile",
                                "value": "/infra/host-switch-profiles/0de8282e-7385-4e8e-a905-0c11960db728"
                            }
                        ],
                        "host_switch_type": "VDS",
                        "ip_assignment_spec": {
                            "ip_pool_id": "/infra/ip-pools/w1-nsx1-esxi-teps",
                            "resource_type": "StaticIpPoolSpec"
                        },
                        "is_migrate_pnics": false,
                        "not_ready": false,
                        "portgroup_transport_zone_id": "/infra/sites/default/enforcement-points/default/transport-zones/77166711-1f83-407f-8c27-a4e1b8b4e1e5",
                        "transport_zone_endpoints": [
                            {
                                "transport_zone_id": "/infra/sites/default/enforcement-points/default/transport-zones/4b367ddf-d4ac-460b-a884-24846ff4edf6",
                                "transport_zone_profile_ids": [
                                    {
                                        "profile_id": "/infra/transport-zone-profiles/52035bb3-ab02-4a08-9884-18631312e50a",
                                        "resource_type": "BfdHealthMonitoringProfile"
                                    }
                                ]
                            },
                            {
                                "transport_zone_id": "/infra/sites/default/enforcement-points/default/transport-zones/bc19b340-f0b5-46d8-9454-6d94d3288821",
                                "transport_zone_profile_ids": [
                                    {
                                        "profile_id": "/infra/transport-zone-profiles/52035bb3-ab02-4a08-9884-18631312e50a",
                                        "resource_type": "BfdHealthMonitoringProfile"
                                    }
                                ]
                            }
                        ],
                        "uplinks": [
                            {
                                "uplink_name": "uplink-2",
                                "vds_uplink_name": "uplink2"
                            },
                            {
                                "uplink_name": "uplink-1",
                                "vds_uplink_name": "uplink1"
                            }
                        ]
                    }
                ],
                "resource_type": "StandardHostSwitchSpec"
            },
            "id": "w1-vc1-c4n17-6adb838f-669c-48ca-b76f-e1238984ef93host-2171",
            "is_overridden": false,
            "maintenance_mode": "DISABLED",
            "marked_for_delete": false,
            "node_deployment_info": {
                "compute_collection_id": "6adb838f-669c-48ca-b76f-e1238984ef93:domain-c2147",
                "discovered_ip_addresses": [
                    "x.x.x.x",
                    "x.x.x.x"
                ],
                "discovered_node_id": "6adb838f-669c-48ca-b76f-e1238984ef93:host-2171",
                "fqdn": "w1-vc1-c4n17.example.com",
                "ip_addresses": [
                    "10.x.x.x"
                ],
                "managed_by_server": "10.x.x.x",
                "os_type": "ESXI",
                "os_version": "x.x.x"
            },
            "overridden": false,
            "owner_id": "1ae23909-d587-4683-a515-4c2001940803",
            "parent_path": "/infra/sites/default/enforcement-points/default",
            "path": "/infra/sites/default/enforcement-points/default/host-transport-nodes/w1-vc1-c4n17-6adb838f-669c-48ca-b76f-e1238984ef93host-2171",
            "realization_id": "c2c69a21-698a-4586-869a-7c0b0a651601",
            "relative_path": "w1-vc1-c4n17-6adb838f-669c-48ca-b76f-e1238984ef93host-2171",
            "remote_path": "",
            "resource_type": "HostTransportNode",
            "unique_id": "c2c69a21-698a-4586-869a-7c0b0a651601"
        }

I'd like to update this object by changing the following line:

"host_switch_mode": "STANDARD",

to:

"host_switch_mode": "LEGACY",

And I'll feed the result json as a message body to a POST API call. I've tried anislbe's combine filter but not getting the expected result. Something like:

- Name: example post/put API call
  uri:
    url: "https://{{ nsx }}/policy/api/v1/infra/host-transport-node-profiles/{{ id }}"
        force_basic_auth: yes
        validate_certs: no
        headers:
          Accept: "application/json"
          Content-Type: "application/json"
        user: "{{ username }}"
        password: "{{ password }}"
        method: PUT
        body: "{{ ouput | combine( {'host_switch_spec': {'host_switches': [ {'host_switch_mode': "LEGACY"},],}, }, recursive=true) }}"
        status_code: "200"
        body_format: json

With the combine filter, what I see is that instead of just merging in the relevant line, the entire host_switch_spec section is replaced (therefore losing other attributes such as host_switch_name, host_switch_id, etc), which is not what I want:

            "host_switch_spec": {
            "host_switches": [
                {
                    "host_switch_mode": "STANDARD"
                }
            ],
            "resource_type": "StandardHostSwitchSpec"
        },

What did I do wrong? I looked at the combine filter man page and tried with all the list_merge options and none of them helped either.


update:

To keep things simple, one of the complications that I did not mention in my initial post is that I have a list of those "output" data object and I need to process them in a loop like below. Say the list is in the variable "tn":

  # Modify the transport node json data on the fly and feed it to the API as message body
  - debug: 
      msg: 
        - "{{ item }}"
        - "{{ item | combine( {'host_switch_spec': {'host_switches': [ {'host_switch_mode': mode}, ], }, },) }}"
    with_items: "{{ tn }}"
    loop_control:
      label: "{{ item.display_name }}"

Summary:

Thank you all for your help and guidance. I have not completed the end to end testing yet, but here is what I came up with based on Vladimir Botka's fantastic examples:

      # how to modify the transport node json data on the fly and feed it to the API as message body
  - name: Testing switch mode update on the fly
    debug: 
      msg:
        #- "hs: {{ (item.host_switch_spec.host_switches + [ hs_update ]) | combine }}"
        #- "update: {{ {'host_switch_spec': {'host_switches': [ (item.host_switch_spec.host_switches + [ hs_update ]) | combine ]}} }}"
        #- "result: {{ item | combine({'host_switch_spec': {'host_switches': [ (item.host_switch_spec.host_switches + [ hs_update ]) | combine ]}}) }}"
        - "{{ item | combine({'host_switch_spec': {'host_switches': [ (item.host_switch_spec.host_switches + [ hs_update ]) | combine ]}}) }}"
    loop: "{{ tn }}"
    loop_control:
      label: "{{ item.display_name }}"
      index_var: counter
    vars:
      hs_update:
        host_switch_mode: "LEGACY"

And the result looks promising (showing last one- #24, and partial only to avoid clutter):

ok: [localhost] => (item=w11-vc1-c1n24.example.com) => {
"msg": [
    "24':' STANDARD",
    {
        "_create_time": 1743198123451,
        "_create_user": "system",
        "_last_modified_time": 1743445520238,
        "_last_modified_user": "system",
        "_protection": "NOT_PROTECTED",
        "_revision": 2,
        "_system_owned": false,
        "display_name": "w11-vc1-c1n24.example.com",
        "host_switch_spec": {
            "host_switches": [
                {
                    "cpu_config": [],
                    "ecmp_mode": "L3",
                    "host_switch_id": "50 1e 66 c9 c5 fb da b3-5e 1f 45 fc 2a 42 99 50",
                    "host_switch_mode": "LEGACY",
                    "host_switch_name": "w11-vc1-dvs",
                    "host_switch_profile_ids": [
                        {
                            "key": "UplinkHostSwitchProfile",
                            "value": "/infra/host-switch-profiles/ace9be64-9728-4340-968e-a62dae6a3d00"
                        },
                        {
                            "key": "VtepHAHostSwitchProfile",
                            "value": "/infra/host-switch-profiles/0de8282e-7385-4e8e-a905-0c11960db728"
                        }
                    ],
                    "host_switch_type": "VDS",

Solution

  • output.host_switch_spec.host_switches is a list with one item

        output.host_switch_spec.host_switches:
        -   cpu_config: []
            ecmp_mode: L3
            host_switch_id: 50 01 cb 21 be 19 cb 38-78 bd 67 bc b3 0e 91 f1
            host_switch_mode: STANDARD
            host_switch_name: w1-vc1-c4-dvs
        ...
    

    Put into a dictionary what you want to update. For example,

        hs_update:
          host_switch_mode: LEGACY
    

    combine the dictionaries

        hs: "{{ (output.host_switch_spec.host_switches + [hs_update]) | combine }}"
    

    gives

        hs:
            cpu_config: []
            ecmp_mode: L3
            host_switch_id: 50 01 cb 21 be 19 cb 38-78 bd 67 bc b3 0e 91 f1
            host_switch_mode: LEGACY
            host_switch_name: w1-vc1-c4-dvs
        ...
    

    Note: If there were more items you'd have to create a product and map the combine function.


    Create the dictionary update

        update:
          host_switch_spec:
            host_switches: "{{ [hs] }}"
    

    and combine the result

        result: "{{ output | combine(update) }}"
    

    gives

        result:
            _create_time: 1724823837185
            _create_user: system
            _last_modified_time: 1736218001919
            _last_modified_user: nsx_policy
            _protection: NOT_PROTECTED
            _revision: 1
            _system_owned: false
            display_name: w1-vc1-c4n17.example.com
            host_switch_spec:
                host_switches:
                -   cpu_config: []
                    ecmp_mode: L3
                    host_switch_id: 50 01 cb 21 be 19 cb 38-78 bd 67 bc b3 0e 91 f1
                    host_switch_mode: LEGACY
                    host_switch_name: w1-vc1-c4-dvs
        ...
    

    Example of a complete playbook for testing

    - hosts: localhost
    
      vars:
    
        hs_update:
          host_switch_mode: LEGACY
        hs: "{{ (output.host_switch_spec.host_switches + [hs_update]) | combine }}"
    
        update:
          host_switch_spec:
            host_switches: "{{ [hs] }}"
        result: "{{ output | combine(update) }}"
    
      tasks:
    
        - include_vars:
            file: data.json
            name: output
    
        - debug:
            var: output.host_switch_spec.host_switches
    
        - debug:
            var: hs
    
        - debug:
            var: result
    

    Q: "The 'host_switch_spec' has both a 'host_switches' and 'resource_type'"

    A: Combine the dictionaries recursively

        result: "{{ output | combine(update, recursive=True) }}"
    

    gives

        result:
            _create_time: 1724823837185
            _create_user: system
            _last_modified_time: 1736218001919
            _last_modified_user: nsx_policy
            _protection: NOT_PROTECTED
            _revision: 1
            _system_owned: false
            display_name: w1-vc1-c4n17.example.com
            host_switch_spec:
                host_switches:
                -   cpu_config: []
                    ecmp_mode: L3
                    host_switch_id: 50 01 cb 21 be 19 cb 38-78 bd 67 bc b3 0e 91 f1
                    host_switch_mode: LEGACY
                    host_switch_name: w1-vc1-c4-dvs
                    ...
                    uplinks:
                    -   uplink_name: uplink-2
                        vds_uplink_name: uplink2
                    -   uplink_name: uplink-1
                        vds_uplink_name: uplink1
                resource_type: StandardHostSwitchSpec
            id: w1-vc1-c4n17-6adb838f-669c-48ca-b76f-e1238984ef93host-2171
            is_overridden: false
            maintenance_mode: DISABLED
        ...
    

    Q: "List of those 'output' data objects."

    A: If there are more items, for example:

    output:
      bar: a
      foo: b
      hs_spec:
        hs:
          - {em: L3, hm: S3}
          - {em: L4, hm: S4}
          - {em: L5, hm: S5}
        rt: c
      mm: DISABLED
    

    update all items

    hs_update:
      hm: LEGACY
    hs: "{{ output.hs_spec.hs | product([hs_update])
                              | map('flatten')
                              | map('combine') }}"
    

    gives

    hs:
      - {em: L3, hm: LEGACY}
      - {em: L4, hm: LEGACY}
      - {em: L5, hm: LEGACY}
    

    Then, combine the dictionaries

    update:
      hs_spec:
        hs: "{{ hs }}"
    result: "{{ output | combine(update, recursive=True) }}"
    

    gives

    result:
      bar: a
      foo: b
      hs_spec:
        hs:
          - {em: L3, hm: LEGACY}
          - {em: L4, hm: LEGACY}
          - {em: L5, hm: LEGACY}
        rt: c
      mm: DISABLED