Ansible shows error on wrong place

Hi,
I´m just starting with ansible and finishing my first playbook. It´s about downloading released packages from private Github Repositories. They can be either whl packages or packed tar.gz. Therefore I implemented different tasks for each case. The results seem to be ok, the file is downloaded and extracted properly. But there is a very confusing error message appearing when downloading a .whl file, if downloading a tar.gz file, there is no error message.

Here´s the code:

    - name: Extract tar.gz file
      ansible.builtin.unarchive:
        src: "{{ dowloaded_file.dest }}"
        dest: "{{ package_dir }}"
      register: file_to_copy
      when: dowloaded_file.dest.endswith('.tar.gz')
    
    - name: Set file_to_copy if whl package
      ansible.builtin.set_fact:
        file_to_copy: "{{ dowloaded_file.dest }}"
      when: dowloaded_file.dest.endswith('.whl')

    - name: Print filename 2
      ansible.builtin.debug:
        msg: "After unpacking: {{ file_to_copy }}"

    - name: Copy whl file
      ansible.builtin.copy:
        src: "{{ file_to_copy }}"
        dest: "{{ package_dir }}"
        mode: "0755"
      #register: files

    # there are multiple branches needed, depending on the file type
    - name: Print filename
      ansible.builtin.debug:
        msg: "Finished: {{ file_to_copy }}"

The error message is this:

TASK [Copy whl file] ***************************************************************************************
[ERROR]: Task failed: '_AnsibleTaggedDict' object has no attribute 'endswith'
Origin: /home/dw/code/openocd-tcl-controller/repo_config/ansible/download_release.yml:80:7

78         msg: "After unpacking: {{ file_to_copy }}"
79
80     - name: Copy whl file
         ^ column 7

fatal: [localhost]: FAILED! => {"changed": false, "msg": "Task failed: '_AnsibleTaggedDict' object has no attribute 'endswith'"}

PLAY RECAP *************************************************************************************************
localhost                  : ok=11   changed=0    unreachable=0   

Whats confusing is: the Task “Copy whl file” does not have any endswith functionality used from my point of view.
Can someone explain what is going on there? Even if it´s working out, I don´t like to have this error there and want to fix it.

BR
Dietrich

First things first, please mention the ansible-core version you’re using. I think there have been quite some changes to 2.19, so it might be worth knowing what exact version you’re running.

That said, you’re using file_to_copy both as a host fact (in Set file_to_copy if whl package) and as a variable within the play (register: file_to_copy). I’m not sure if they are really the same, so maybe Ansible gets confused by this. Just an idea.

A way to reproduce this (I used ansible-core devel, but likely everything >= 2.19 will work):

- hosts: localhost
  gather_facts: false
  vars:
    dowloaded_file:
      dest: foo.tar.gz
    package_dir: /tmp
    file_to_copy: /tmp/asdf
  tasks:
    - name: Extract tar.gz file
      debug:
        msg: "foo"
      register: file_to_copy
      when: dowloaded_file.dest.endswith('.tar.gz')

    - name: Set file_to_copy if whl package
      ansible.builtin.set_fact:
        file_to_copy: "{{ dowloaded_file.dest }}"
      when: dowloaded_file.dest.endswith('.whl')

    - name: Print filename 2
      ansible.builtin.debug:
        msg: "After unpacking: {{ file_to_copy }}"

    - name: Copy whl file
      ansible.builtin.copy:
        src: "{{ file_to_copy }}"
        dest: "{{ package_dir }}"
        mode: "0755"

The problem is that file_to_copy is a dictionary here (thanks to the “Extract tar.gz file” task) and not a string, and the exact error in the “Copy whl file” task is only visible when enabling tracebacks (for example by setting the environment variable ANSIBLE_DISPLAY_TRACEBACK=error):

TASK [Copy whl file] *******************************************************************************************************************************************************
[ERROR]: Task failed: '_AnsibleTaggedDict' object has no attribute 'endswith'
Origin: /path/to/test-error.yml:24:7

22         msg: "After unpacking: {{ file_to_copy }}"
23
24     - name: Copy whl file
         ^ column 7

Traceback (most recent call last):
  File "/path/to/ansible/executor/task_executor.py", line 423, in _execute
    result = self._execute_internal(templar, variables)
  File "/path/to/ansible/executor/task_executor.py", line 636, in _execute_internal
    result = self._handler.run(task_vars=vars_copy)
  File "/path/to/ansible/plugins/action/copy.py", line 469, in run
    trailing_slash = source.endswith(os.path.sep)
                     ^^^^^^^^^^^^^^^
AttributeError: '_AnsibleTaggedDict' object has no attribute 'endswith'

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/path/to/ansible/executor/task_executor.py", line 428, in _execute
    raise AnsibleTaskError(obj=self._task.get_ds()) from ex
ansible.errors.AnsibleTaskError: Task failed: '_AnsibleTaggedDict' object has no attribute 'endswith'

fatal: [localhost]: FAILED! => {"changed": false, "msg": "Task failed: '_AnsibleTaggedDict' object has no attribute 'endswith'"}

The error happens in the copy task, but it’s a crash in the action plugin. It’s basically a bug in the parameter validation of the copy action. (Actually it’s the missing error validation of the copy action.)

(This bug is triggered by a bug in the playbook, see what @mariolenz wrote.)

2 Likes
2 Likes

Version is
ansible [core 2.20.3]
installed with pip and running on Ubuntu WSL2 distribution.

I understood your second point. I assumed here that the same variable will be set with this approach, no matter which task is executed. Would something like pulling file_to_copy to the beginning of the play be more ansible-compliant?

Impressive.

Can I do something as a workaround, until it is fixed and released?

Pass a valid path instead of an invalid one.

What do you mean?
The paths are valid, files are downloaded and extracted as expected. Just the message is confusing.

@flowerysong If you really want to be helpful, you probably shouldn’t just say pass a valid path instead of an invalid one you’re holding it wrong :smirking_face:

You have

src: "{{ file_to_copy }}"

But you are registering file_to_copy as a task result

register: file_to_copy

You can’t pass in a dictionary value, select the key inside that dictionary that represents your path or use another variable that has your path.