1
0
Fork 0
mirror of https://github.com/ansible-collections/community.general.git synced 2026-02-03 23:41:51 +00:00

Add New Module file_remove (#11032)

* Add New Module file_remove

* Add fixes from code review

* Change file_type documentation

* Remove python to_native from the module

* Remove redundant block/always cleanup

* Update plugins/modules/file_remove.py

Co-authored-by: Felix Fontein <felix@fontein.de>

* Update plugins/modules/file_remove.py

Co-authored-by: Felix Fontein <felix@fontein.de>

* Update plugins/modules/file_remove.py

Co-authored-by: Felix Fontein <felix@fontein.de>

* Update plugins/modules/file_remove.py

Co-authored-by: Felix Fontein <felix@fontein.de>

* Update plugins/modules/file_remove.py

Co-authored-by: Felix Fontein <felix@fontein.de>

* Update plugins/modules/file_remove.py

Co-authored-by: Felix Fontein <felix@fontein.de>

* Update plugins/modules/file_remove.py

Co-authored-by: Felix Fontein <felix@fontein.de>

* Update plugins/modules/file_remove.py

Co-authored-by: Felix Fontein <felix@fontein.de>

* Add more nox fixes to latest review

* Update plugins/modules/file_remove.py

LGTM

Co-authored-by: Felix Fontein <felix@fontein.de>

* Update tests/integration/targets/file_remove/tasks/main.yml

Right, that's better.

Co-authored-by: Felix Fontein <felix@fontein.de>

* Fix EXAMPLES regex pattern

* Add warning when listed file was removed by other process during
playbook execution

* remove raise exception from find_matching_files;

* Update plugins/modules/file_remove.py

Co-authored-by: Felix Fontein <felix@fontein.de>

* Update plugins/modules/file_remove.py

Co-authored-by: Felix Fontein <felix@fontein.de>

---------

Co-authored-by: Felix Fontein <felix@fontein.de>
This commit is contained in:
Shahar Golshani 2025-11-21 19:26:30 +02:00 committed by GitHub
parent e57de70c2a
commit af99cc7deb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 972 additions and 0 deletions

View file

@ -0,0 +1,5 @@
# Copyright (c) Ansible Project
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
azp/posix/3

View file

@ -0,0 +1,6 @@
---
# Copyright (c) Ansible Project
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
file_remove_testdir: "{{ remote_tmp_dir }}/file_remove_tests"

View file

@ -0,0 +1,7 @@
---
# Copyright (c) Ansible Project
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
dependencies:
- setup_remote_tmp_dir

View file

@ -0,0 +1,39 @@
---
####################################################################
# WARNING: These are designed specifically for Ansible tests #
# and should not be used as examples of how to write Ansible roles #
####################################################################
# Test code for the file_remove module
# Copyright (c) Ansible Project
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
- name: Ensure the test directory is absent before starting
ansible.builtin.file:
path: "{{ file_remove_testdir }}"
state: absent
- name: Create the test directory
ansible.builtin.file:
path: "{{ file_remove_testdir }}"
state: directory
mode: '0755'
- name: Include tasks to test error handling
ansible.builtin.include_tasks: test_errors.yml
- name: Include tasks to test glob pattern matching
ansible.builtin.include_tasks: test_glob.yml
- name: Include tasks to test regex pattern matching
ansible.builtin.include_tasks: test_regex.yml
- name: Include tasks to test recursive removal
ansible.builtin.include_tasks: test_recursive.yml
- name: Include tasks to test different file types
ansible.builtin.include_tasks: test_file_types.yml
- name: Include tasks to test check mode and diff mode
ansible.builtin.include_tasks: test_check_diff.yml

View file

@ -0,0 +1,108 @@
---
# Copyright (c) Ansible Project
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
# Test check mode and diff mode
- name: Create test files for check mode testing
ansible.builtin.file:
path: "{{ file_remove_testdir }}/{{ item }}"
state: touch
mode: '0644'
loop:
- check1.tmp
- check2.tmp
- check3.txt
- name: Test removal in check mode
community.general.file_remove:
path: "{{ file_remove_testdir }}"
pattern: "*.tmp"
check_mode: true
register: check_mode_result
- name: Verify check mode reported changes but didn't remove files
ansible.builtin.assert:
that:
- check_mode_result is changed
- check_mode_result.files_count == 2
- "file_remove_testdir ~ '/check1.tmp' in check_mode_result.removed_files"
- "file_remove_testdir ~ '/check2.tmp' in check_mode_result.removed_files"
- name: Verify files still exist after check mode
ansible.builtin.stat:
path: "{{ file_remove_testdir }}/{{ item }}"
register: files_after_check
loop:
- check1.tmp
- check2.tmp
- name: Assert files were not actually removed in check mode
ansible.builtin.assert:
that:
- item.stat.exists
loop: "{{ files_after_check.results }}"
- name: Test removal in normal mode with diff
community.general.file_remove:
path: "{{ file_remove_testdir }}"
pattern: "*.tmp"
diff: true
register: diff_mode_result
- name: Verify diff mode provides before/after information
ansible.builtin.assert:
that:
- diff_mode_result is changed
- diff_mode_result.files_count == 2
- diff_mode_result.diff is defined
- diff_mode_result.diff.before is defined
- diff_mode_result.diff.after is defined
- diff_mode_result.diff.before.files | length == 2
- diff_mode_result.diff.after.files | length == 0
- name: Verify files were actually removed
ansible.builtin.stat:
path: "{{ file_remove_testdir }}/{{ item }}"
register: files_after_removal
loop:
- check1.tmp
- check2.tmp
- name: Assert files were removed
ansible.builtin.assert:
that:
- not item.stat.exists
loop: "{{ files_after_removal.results }}"
- name: Test idempotency - try to remove already removed files
community.general.file_remove:
path: "{{ file_remove_testdir }}"
pattern: "*.tmp"
register: idempotent_result
- name: Verify idempotency (no changes when no files match)
ansible.builtin.assert:
that:
- idempotent_result is not changed
- idempotent_result.files_count == 0
- idempotent_result.removed_files == []
- name: Test idempotency in check mode
community.general.file_remove:
path: "{{ file_remove_testdir }}"
pattern: "*.tmp"
check_mode: true
register: idempotent_check_result
- name: Verify idempotency in check mode
ansible.builtin.assert:
that:
- idempotent_check_result is not changed
- idempotent_check_result.files_count == 0
- name: Clean up test directory
ansible.builtin.file:
path: "{{ file_remove_testdir }}"
state: absent

View file

@ -0,0 +1,57 @@
---
# Copyright (c) Ansible Project
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
# Test error handling
- name: Test with non-existent path
community.general.file_remove:
path: /this/path/does/not/exist
pattern: "*.txt"
register: error_result_1
ignore_errors: true
- name: Verify that non-existent path fails
ansible.builtin.assert:
that:
- error_result_1 is failed
- "'does not exist' in error_result_1.msg"
- name: Create a test file to use as a non-directory path
ansible.builtin.file:
path: "{{ file_remove_testdir }}/testfile"
state: touch
mode: '0644'
- name: Test with a file path instead of directory
community.general.file_remove:
path: "{{ file_remove_testdir }}/testfile"
pattern: "*.txt"
register: error_result_2
ignore_errors: true
- name: Verify that non-directory path fails
ansible.builtin.assert:
that:
- error_result_2 is failed
- "'not a directory' in error_result_2.msg"
- name: Test with invalid regex pattern
community.general.file_remove:
path: "{{ file_remove_testdir }}"
pattern: "[unclosed"
use_regex: true
register: error_result_3
ignore_errors: true
- name: Verify that invalid regex fails
ansible.builtin.assert:
that:
- error_result_3 is failed
- "'Invalid regular expression' in error_result_3.msg"
- name: Remove test file
ansible.builtin.file:
path: "{{ file_remove_testdir }}/testfile"
state: absent

View file

@ -0,0 +1,132 @@
---
# Copyright (c) Ansible Project
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
# Test different file types
- name: Create regular test files
ansible.builtin.file:
path: "{{ file_remove_testdir }}/{{ item }}"
state: touch
mode: '0644'
loop:
- file1.tmp
- file2.tmp
- target1.txt
- target2.txt
- name: Create symbolic links
ansible.builtin.file:
src: "{{ file_remove_testdir }}/{{ item.src }}"
dest: "{{ file_remove_testdir }}/{{ item.dest }}"
state: link
loop:
- { src: target1.txt, dest: link1.tmp }
- { src: target2.txt, dest: link2.tmp }
- name: Remove only regular files (file_type=file, default)
community.general.file_remove:
path: "{{ file_remove_testdir }}"
pattern: "*.tmp"
file_type: file
register: file_type_result_1
- name: Verify only regular .tmp files were removed
ansible.builtin.assert:
that:
- file_type_result_1 is changed
- file_type_result_1.files_count == 2
- "file_remove_testdir ~ '/file1.tmp' in file_type_result_1.removed_files"
- "file_remove_testdir ~ '/file2.tmp' in file_type_result_1.removed_files"
- name: Verify symbolic links still exist
ansible.builtin.stat:
path: "{{ file_remove_testdir }}/{{ item }}"
follow: false
register: links_exist
loop:
- link1.tmp
- link2.tmp
- name: Assert symbolic links still exist
ansible.builtin.assert:
that:
- item.stat.exists
- item.stat.islnk
loop: "{{ links_exist.results }}"
- name: Remove only symbolic links (file_type=link)
community.general.file_remove:
path: "{{ file_remove_testdir }}"
pattern: "*.tmp"
file_type: link
register: file_type_result_2
- name: Verify only symbolic links were removed
ansible.builtin.assert:
that:
- file_type_result_2 is changed
- file_type_result_2.files_count == 2
- "file_remove_testdir ~ '/link1.tmp' in file_type_result_2.removed_files"
- "file_remove_testdir ~ '/link2.tmp' in file_type_result_2.removed_files"
- name: Verify target files still exist
ansible.builtin.stat:
path: "{{ file_remove_testdir }}/{{ item }}"
register: targets_exist
loop:
- target1.txt
- target2.txt
- name: Assert target files still exist
ansible.builtin.assert:
that:
- item.stat.exists
loop: "{{ targets_exist.results }}"
- name: Create more test files and links
ansible.builtin.file:
path: "{{ file_remove_testdir }}/{{ item }}"
state: touch
mode: '0644'
loop:
- data1.dat
- data2.dat
- name: Create more symbolic links
ansible.builtin.file:
src: "{{ file_remove_testdir }}/{{ item.src }}"
dest: "{{ file_remove_testdir }}/{{ item.dest }}"
state: link
loop:
- { src: target1.txt, dest: link1.dat }
- { src: target2.txt, dest: link2.dat }
- name: Remove both files and links (file_type=any)
community.general.file_remove:
path: "{{ file_remove_testdir }}"
pattern: "*.dat"
file_type: any
register: file_type_result_3
- name: Verify both files and links were removed
ansible.builtin.assert:
that:
- file_type_result_3 is changed
- file_type_result_3.files_count == 4
- "file_remove_testdir ~ '/data1.dat' in file_type_result_3.removed_files"
- "file_remove_testdir ~ '/data2.dat' in file_type_result_3.removed_files"
- "file_remove_testdir ~ '/link1.dat' in file_type_result_3.removed_files"
- "file_remove_testdir ~ '/link2.dat' in file_type_result_3.removed_files"
- name: Clean up for next test
ansible.builtin.file:
path: "{{ file_remove_testdir }}"
state: absent
- name: Recreate test directory
ansible.builtin.file:
path: "{{ file_remove_testdir }}"
state: directory
mode: '0755'

View file

@ -0,0 +1,102 @@
---
# Copyright (c) Ansible Project
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
# Test glob pattern matching
- name: Create test files for glob testing
ansible.builtin.file:
path: "{{ file_remove_testdir }}/{{ item }}"
state: touch
mode: '0644'
loop:
- test1.txt
- test2.txt
- test3.log
- data.txt
- readme.md
- backup_file.bak
- name: Remove files matching *.txt pattern
community.general.file_remove:
path: "{{ file_remove_testdir }}"
pattern: "*.txt"
register: glob_result_1
- name: Verify *.txt files were removed
ansible.builtin.assert:
that:
- glob_result_1 is changed
- glob_result_1.files_count == 3
- "file_remove_testdir ~ '/test1.txt' in glob_result_1.removed_files"
- "file_remove_testdir ~ '/test2.txt' in glob_result_1.removed_files"
- "file_remove_testdir ~ '/data.txt' in glob_result_1.removed_files"
- "'3 file(s)' in glob_result_1.msg"
- name: Verify remaining files still exist
ansible.builtin.stat:
path: "{{ file_remove_testdir }}/{{ item }}"
register: remaining_files
loop:
- test3.log
- readme.md
- backup_file.bak
- name: Assert remaining files exist
ansible.builtin.assert:
that:
- item.stat.exists
loop: "{{ remaining_files.results }}"
- name: Remove files matching test* pattern
community.general.file_remove:
path: "{{ file_remove_testdir }}"
pattern: "test*"
register: glob_result_2
- name: Verify test* files were removed
ansible.builtin.assert:
that:
- glob_result_2 is changed
- glob_result_2.files_count == 1
- "file_remove_testdir ~ '/test3.log' in glob_result_2.removed_files"
- name: Remove files matching [rb]* pattern (character set)
community.general.file_remove:
path: "{{ file_remove_testdir }}"
pattern: "[rb]*"
register: glob_result_3
- name: Verify [rb]* files were removed
ansible.builtin.assert:
that:
- glob_result_3 is changed
- glob_result_3.files_count == 2
- "file_remove_testdir ~ '/readme.md' in glob_result_3.removed_files"
- "file_remove_testdir ~ '/backup_file.bak' in glob_result_3.removed_files"
- name: Try to remove with non-matching pattern
community.general.file_remove:
path: "{{ file_remove_testdir }}"
pattern: "*.nonexistent"
register: glob_result_4
- name: Verify no files were removed (idempotent)
ansible.builtin.assert:
that:
- glob_result_4 is not changed
- glob_result_4.files_count == 0
- glob_result_4.removed_files == []
- "'0 file(s)' in glob_result_4.msg"
- name: Clean up for next test
ansible.builtin.file:
path: "{{ file_remove_testdir }}"
state: absent
- name: Recreate test directory
ansible.builtin.file:
path: "{{ file_remove_testdir }}"
state: directory
mode: '0755'

View file

@ -0,0 +1,127 @@
---
# Copyright (c) Ansible Project
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
# Test recursive removal
- name: Create nested directory structure
ansible.builtin.file:
path: "{{ file_remove_testdir }}/{{ item }}"
state: directory
mode: '0755'
loop:
- subdir1
- subdir2
- subdir1/nested1
- subdir2/nested2
- subdir2/nested2/deep
- name: Create test files in nested directories
ansible.builtin.file:
path: "{{ file_remove_testdir }}/{{ item }}"
state: touch
mode: '0644'
loop:
- temp1.log
- temp2.log
- subdir1/temp3.log
- subdir1/data.txt
- subdir1/nested1/temp4.log
- subdir2/temp5.log
- subdir2/nested2/temp6.log
- subdir2/nested2/deep/temp7.log
- subdir2/nested2/deep/file.txt
- name: Remove .log files non-recursively (should only remove root level)
community.general.file_remove:
path: "{{ file_remove_testdir }}"
pattern: "*.log"
recursive: false
register: recursive_result_1
- name: Verify only root level .log files were removed
ansible.builtin.assert:
that:
- recursive_result_1 is changed
- recursive_result_1.files_count == 2
- "file_remove_testdir ~ '/temp1.log' in recursive_result_1.removed_files"
- "file_remove_testdir ~ '/temp2.log' in recursive_result_1.removed_files"
- name: Verify nested .log files still exist
ansible.builtin.stat:
path: "{{ file_remove_testdir }}/{{ item }}"
register: nested_files_exist
loop:
- subdir1/temp3.log
- subdir1/nested1/temp4.log
- subdir2/temp5.log
- subdir2/nested2/temp6.log
- subdir2/nested2/deep/temp7.log
- name: Assert nested .log files still exist
ansible.builtin.assert:
that:
- item.stat.exists
loop: "{{ nested_files_exist.results }}"
- name: Remove .log files recursively (should remove all .log files)
community.general.file_remove:
path: "{{ file_remove_testdir }}"
pattern: "*.log"
recursive: true
register: recursive_result_2
- name: Verify all .log files were removed recursively
ansible.builtin.assert:
that:
- recursive_result_2 is changed
- recursive_result_2.files_count == 5
- "file_remove_testdir ~ '/subdir1/temp3.log' in recursive_result_2.removed_files"
- "file_remove_testdir ~ '/subdir1/nested1/temp4.log' in recursive_result_2.removed_files"
- "file_remove_testdir ~ '/subdir2/temp5.log' in recursive_result_2.removed_files"
- "file_remove_testdir ~ '/subdir2/nested2/temp6.log' in recursive_result_2.removed_files"
- "file_remove_testdir ~ '/subdir2/nested2/deep/temp7.log' in recursive_result_2.removed_files"
- name: Verify .txt files still exist
ansible.builtin.stat:
path: "{{ file_remove_testdir }}/{{ item }}"
register: txt_files_exist
loop:
- subdir1/data.txt
- subdir2/nested2/deep/file.txt
- name: Assert .txt files still exist
ansible.builtin.assert:
that:
- item.stat.exists
loop: "{{ txt_files_exist.results }}"
- name: Verify directories still exist (directories should never be removed)
ansible.builtin.stat:
path: "{{ file_remove_testdir }}/{{ item }}"
register: dirs_exist
loop:
- subdir1
- subdir2
- subdir1/nested1
- subdir2/nested2
- subdir2/nested2/deep
- name: Assert directories still exist
ansible.builtin.assert:
that:
- item.stat.exists
- item.stat.isdir
loop: "{{ dirs_exist.results }}"
- name: Clean up for next test
ansible.builtin.file:
path: "{{ file_remove_testdir }}"
state: absent
- name: Recreate test directory
ansible.builtin.file:
path: "{{ file_remove_testdir }}"
state: directory
mode: '0755'

View file

@ -0,0 +1,97 @@
---
# Copyright (c) Ansible Project
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
# Test regex pattern matching
- name: Create test files for regex testing
ansible.builtin.file:
path: "{{ file_remove_testdir }}/{{ item }}"
state: touch
mode: '0644'
loop:
- backup_20241101.tar.gz
- backup_20241102.tar.gz
- backup_20241103.tar.gz
- backup_old.tar.gz
- file123.txt
- file456.txt
- fileabc.txt
- test_file.log
- name: Remove files matching regex backup_[0-9]{8}\.tar\.gz
community.general.file_remove:
path: "{{ file_remove_testdir }}"
pattern: "backup_[0-9]{8}\\.tar\\.gz"
use_regex: true
register: regex_result_1
- name: Verify regex matched files were removed
ansible.builtin.assert:
that:
- regex_result_1 is changed
- regex_result_1.files_count == 3
- "file_remove_testdir ~ '/backup_20241101.tar.gz' in regex_result_1.removed_files"
- "file_remove_testdir ~ '/backup_20241102.tar.gz' in regex_result_1.removed_files"
- "file_remove_testdir ~ '/backup_20241103.tar.gz' in regex_result_1.removed_files"
- name: Verify backup_old.tar.gz still exists
ansible.builtin.stat:
path: "{{ file_remove_testdir }}/backup_old.tar.gz"
register: backup_old_stat
- name: Assert backup_old.tar.gz was not removed
ansible.builtin.assert:
that:
- backup_old_stat.stat.exists
- name: Remove files matching regex file[0-9]+\.txt (digits only)
community.general.file_remove:
path: "{{ file_remove_testdir }}"
pattern: "file[0-9]+\\.txt"
use_regex: true
register: regex_result_2
- name: Verify files with digits were removed
ansible.builtin.assert:
that:
- regex_result_2 is changed
- regex_result_2.files_count == 2
- "file_remove_testdir ~ '/file123.txt' in regex_result_2.removed_files"
- "file_remove_testdir ~ '/file456.txt' in regex_result_2.removed_files"
- name: Verify fileabc.txt still exists
ansible.builtin.stat:
path: "{{ file_remove_testdir }}/fileabc.txt"
register: fileabc_stat
- name: Assert fileabc.txt was not removed
ansible.builtin.assert:
that:
- fileabc_stat.stat.exists
- name: Remove files with regex using anchors ^test.*
community.general.file_remove:
path: "{{ file_remove_testdir }}"
pattern: "^test.*"
use_regex: true
register: regex_result_3
- name: Verify files starting with 'test' were removed
ansible.builtin.assert:
that:
- regex_result_3 is changed
- regex_result_3.files_count == 1
- "file_remove_testdir ~ '/test_file.log' in regex_result_3.removed_files"
- name: Clean up for next test
ansible.builtin.file:
path: "{{ file_remove_testdir }}"
state: absent
- name: Recreate test directory
ansible.builtin.file:
path: "{{ file_remove_testdir }}"
state: directory
mode: '0755'