ansible.utils icon indicating copy to clipboard operation
ansible.utils copied to clipboard

update_fact will prevent variable replacement under certain conditions

Open ipc-zpg opened this issue 3 years ago • 5 comments

SUMMARY

under certain conditions, update_facts will prevent variables being substituted within variables it manipulates

ISSUE TYPE
  • Bug Report
COMPONENT NAME

ansible.utils.update_fact

ANSIBLE VERSION
ansible [core 2.11.4]
  config file = /Users/pookey/.ansible.cfg
  configured module search path = ['/Users/pookey/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /opt/homebrew/Cellar/ansible/4.5.0/libexec/lib/python3.9/site-packages/ansible
  ansible collection location = /Users/pookey/.ansible/collections:/usr/share/ansible/collections
  executable location = /opt/homebrew/bin/ansible
  python version = 3.9.7 (default, Sep  3 2021, 04:31:11) [Clang 12.0.5 (clang-1205.0.22.9)]
  jinja version = 3.0.1
  libyaml = True
COLLECTION VERSION
ansible-galaxy collection list ansible.utils

# /opt/homebrew/Cellar/ansible/4.5.0/libexec/lib/python3.9/site-packages/ansible_collections
Collection    Version
------------- -------
ansible.utils 2.4.0

# /Users/pookey/.ansible/collections/ansible_collections
Collection    Version
------------- -------
ansible.utils 2.4.3
CONFIGURATION

OS / ENVIRONMENT

Tested on OSX and Linux

STEPS TO REPRODUCE
---

- hosts: localhost
  vars:
    env_name: moo
    datadog_checks:
      mysql:
        - host: "{{ env_name }}.moo"
      codedeploy:
        logs: []
  tasks:
    - debug:
        var: datadog_checks.mysql[0].host
    - name: update fact
      ansible.utils.update_fact:
        updates:
          - path: "datadog_checks['codedeploy']['logs']"
            value: "{{ datadog_checks['codedeploy']['logs'] + ['/path/to/logfile'] }}"
      register: new_dd
    - name: replace the datadog_checks array
      set_fact:
        datadog_checks: "{{ new_dd.datadog_checks }}"
    - debug:
        var: datadog_checks.mysql[0].host
EXPECTED RESULTS

the second debug should print 'moo.moo', in the same way the first debug does.

ACTUAL RESULTS

I get {{ env_name }}.moo as the output.

❯ ansible-playbook ./test.yaml
[WARNING]: No inventory was parsed, only implicit localhost is available
[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'

PLAY [localhost] *****************************************************************************************************************************************************************

TASK [Gathering Facts] ***********************************************************************************************************************************************************
ok: [localhost]

TASK [debug] *********************************************************************************************************************************************************************
ok: [localhost] => {
    "datadog_checks.mysql[0].host": "moo.moo"
}

TASK [update fact] ***************************************************************************************************************************************************************
changed: [localhost]

TASK [replace the datadog_checks array] ******************************************************************************************************************************************
ok: [localhost]

TASK [debug] *********************************************************************************************************************************************************************
ok: [localhost] => {
    "datadog_checks.mysql[0].host": "{{ env_name }}.moo"
}

PLAY RECAP ***********************************************************************************************************************************************************************
localhost                  : ok=5    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0


ipc-zpg avatar Jan 31 '22 11:01 ipc-zpg

@ipc-zpg Thanks for this, it took a few minutes to remember the subtleties of vars vs. facts in this case.

This is not entirely unexpected, at the time the update_fact task is run the datadog_checks variable has not had its references resolved yet. This can be seen in the following playbook:

- hosts: localhost
  gather_facts: false
  vars:
    env_name: moo
    datadog_checks:
      mysql:
        - host: "{{ env_name }}.moo"
      codedeploy:
        logs: []
  tasks:
    - name: Show delayed resolution of variables
      set_fact:
        env_name: A cow says
  
    - name: What does a cow say?
      debug:
        var: datadog_checks

TASK [What does a cow say?] ******************************************************************************************************************
ok: [localhost] => {
    "datadog_checks": {
        "codedeploy": {
            "logs": []
        },
        "mysql": [
            {
                "host": "A cow says.moo"
            }
        ]
    }
}

The workaround for this is to resolve all the references in datadog_checks by setting a fact referring to it, and updating that:

---
- hosts: localhost
  gather_facts: false
  vars:
    env_name: moo
    datadog_checks:
      mysql:
        - host: "{{ env_name }}.moo"
      codedeploy:
        logs: []
  tasks:

    - name: Set a fact, this will resolve references
      set_fact:
        resolved_datadog: "{{ datadog_checks }}"

    - name: Update the fact
      ansible.utils.update_fact:
        updates:
          - path: "resolved_datadog['codedeploy']['logs']"
            value: "{{ resolved_datadog['codedeploy']['logs'] + ['/path/to/logfile'] }}"
      register: updated_datadog
    
    - name: Show the new fact
      ansible.builtin.debug:
        var: updated_datadog

    - name: Replace the datadog_checks array
      set_fact:
        datadog_checks: "{{ updated_datadog.resolved_datadog }}"
    - debug:
        var: datadog_checks.mysql[0].host

TASK [debug] *********************************************************************************************************************************
ok: [localhost] => {
    "datadog_checks.mysql[0].host": "moo.moo"
}

In your example, ansible passes the unresolved var to the task, so that is what is being returned.

Let me know if this helps

-Brad

cidrblock avatar Apr 13 '22 11:04 cidrblock

@ipc-zpg Just checking in to see if ^^^ helped explain the behaviour

cidrblock avatar May 24 '22 18:05 cidrblock

So I have another example. It seems that if the path includes a variable representing a number, it also will not work.

This works fine:

- ansible.utils.update_fact:
    updates:
      - path: "vm_info['value']['cdroms']['3000']['backing']['type']"
        value: CLIENT_DEVICE

This works fine:

- ansible.utils.update_fact:
    updates:
      - path: "vm_info['value']['cdroms']['3000']['backing'][{{ lastpart }}]"
        value: CLIENT_DEVICE
  vars:
    lastpart: type

This fails:

- ansible.utils.update_fact:
    updates:
      - path: "vm_info['value']['cdroms'][{{ cdrom_id }}]['backing']['type']"
        value: CLIENT_DEVICE
  vars:
    cdrom_id: '3000'

TASK [ansible.utils.update_fact] ***********************************************
An exception occurred during task execution. To see the full traceback, use -vvv. The error was: KeyError: 3000
fatal: [XXX06-XX1-XXX29]: FAILED! => {"changed": false, "msg": "Error: the key '3000' was not found in {'3000': {'start_connected': False, 'backing': {'auto_detect': True, 'device_access_type': 'EMULATION', 'type': 'HOST_DEVICE'}, 'allow_guest_control': True, 'ide': {'master': True, 'primary': True}, 'label': 'CD/DVD drive 1', 'state': 'NOT_CONNECTED', 'type': 'IDE'}}."}

alice-rc avatar Feb 02 '23 00:02 alice-rc

I'm hitting this bug, any workaround?

Rockawear avatar Feb 09 '24 04:02 Rockawear

I also have the issue of not being able to use a variable that is set to equal an index number, but in my case, the error seems to indicate that update_fact interprets all variables as strings.

This works

- ansible.utils.update_fact:
    updates:
      - path: installation_media["{{sqlserver_version}}"][0]['product_id']
        value: ''

These both fail with the same error

- ansible.utils.update_fact:
    updates:
      - path: installation_media["{{sqlserver_version}}"]["{{idx}}"]['product_id']
        value: ''
  vars:
    idx: 0

- ansible.utils.update_fact:
    updates:
      - path: installation_media["{{sqlserver_version}}"]["{{idx | int}}"]['product_id']
        value: ''
  vars:
    idx: 0

The error

The error was: TypeError: list indices must be integers or slices, not str
FAILED! => {"changed": false, "msg": "Error: the key '0' was not found in....

danielleshoemake avatar Apr 17 '24 00:04 danielleshoemake