'How to copy files that match a glob pattern with Ansible

I have the following local set of files:

$ find . -maxdepth 2
.
./.DS_Store
./radio
./radio/server.properties
./radio/recordings/
./radio/requirements.txt
./radio/__pycache__
./radio/radio.iml
./radio/radio.py
./radio/audio.py
./ansibleplaybook.yml
./inventory

I would like to copy only the .py files such that my remote server looks like this:

$ find ~/radio/
/home/pi/radio/
/home/pi/radio/radio.py
/home/pi/radio/audio.py

I have used examples found here and in the Ansible documentation to come up with this playbook:

---
- hosts: all
  tasks:
    - name: Copy radio source
      copy:
        src: "radio"
        dest: "~/radio"
      with_fileglob:
        - "*.py"

However, run I run this no directory is created and nothing is copied.

$ ansible-playbook -i inventory ansibleplaybook.yml -u pi

PLAY [pi] **************************************************************************************************************************************************

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

TASK [Copy radio source] **********************************************************************************************************************************************

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

If I specify the .py files individually then the radio directory is created and the .py file is copied. I know I only have two .py files now, but having to update my playbook every time I add a new python module will obviously start to get very tedious. How can I get the above playbook to work so that all the files that match the pattern are copied?



Solution 1:[1]

I think part of your confusion is that with_fileglob doesn't know anything about the parameters to the copy module, so it doesn't know you want files relative to the radio directory. Consider, for example, what happens if you run this shell command from your home directory:

ls *.py

It won't find those *.py files, either, because they're inside the radio directory.

Because with_fileglob returns no results, the task never runs.

There are a number of ways of tackling this problem. Note that in the following examples I'm using a destination of /tmp/target to avoid cluttering up my home directory with test files.

Recursive copy

If you remove the with_fileglob loop, then Ansible will recursively copy the contents of the local radio directory to ~/radio on the target host. This will copy all the files, not just the Python sources.

Use find module to generate a list of files

If you really want only the Python sources, you can use the find module to generate a list of matching files, and then use that as input to the copy module. In this case, you will need to take care of creating target directories yourself, because copy won't do that for you.

Something like this:

- hosts: localhost
  gather_facts: false
  tasks:
    - find:
        paths: radio
        patterns: "*.py"
      register: python_files

    - name: Create target directories
      file:
        path: "/tmp/target/{{ item.path|dirname }}"
        state: directory
      loop: "{{ python_files.files }}"
      loop_control:
        label: "{{ item.path }}"

    - name: Copy files
      copy:
        src: "{{ item.path }}"
        dest: "/tmp/target/{{ item.path }}"
      loop: "{{ python_files.files }}"
      loop_control:
        label: "{{ item.path }}"

Use the synchronize module

You could use the synchronize module to copy the files instead. synchronize is a wrapper around rsync so in many ways it's more appropriate to the task at hand. That might look like this:

- hosts: localhost
  gather_facts: false
  tasks:
    - synchronize:
        src: radio/
        dest: /tmp/target/
        rsync_opts:
          - '--include=*.py'
          - '--include=*/'
          - '--exclude=*'

See the rsync man page for more information about the --include and --exclude options.

Solution 2:[2]

What you need is actually this:

---
- hosts: all
  tasks:
  - name: Copy radio source
    copy:
      src: "{{ item }}"
      dest: "~/radio/"
    with_fileglob:
      - "radio/*.py"

Note the trailing slash "/" on the dest to ensure writing to a directory. And as larsks says the "with" part do not know anything about the parameters to Copy. Actually the with_fileglob is used to create the parameters to copy, thus the "{{ item }}" as src that reffers to the items provided by the "with" part, you can almost look at it as a loop over the task.

Ref: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/fileglob_lookup.html

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 larsks
Solution 2 Samuel Åslund