From c9df20808d322027749d985264d518a2e316de92 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Fri, 21 Nov 2025 18:33:37 +0100 Subject: [PATCH] [PR #11032/af99cc7d backport][stable-12] Add New Module file_remove (#11184) 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 * Update plugins/modules/file_remove.py * Update plugins/modules/file_remove.py * Update plugins/modules/file_remove.py * Update plugins/modules/file_remove.py * Update plugins/modules/file_remove.py * Update plugins/modules/file_remove.py * Update plugins/modules/file_remove.py * Add more nox fixes to latest review * Update plugins/modules/file_remove.py LGTM * Update tests/integration/targets/file_remove/tasks/main.yml Right, that's better. * 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 * Update plugins/modules/file_remove.py --------- (cherry picked from commit af99cc7deb36a37eed84a83c622d9db43b826b94) Co-authored-by: Shahar Golshani Co-authored-by: Felix Fontein --- .github/BOTMETA.yml | 2 + plugins/modules/file_remove.py | 290 ++++++++++++++++++ tests/integration/targets/file_remove/aliases | 5 + .../targets/file_remove/defaults/main.yml | 6 + .../targets/file_remove/meta/main.yml | 7 + .../targets/file_remove/tasks/main.yml | 39 +++ .../file_remove/tasks/test_check_diff.yml | 108 +++++++ .../targets/file_remove/tasks/test_errors.yml | 57 ++++ .../file_remove/tasks/test_file_types.yml | 132 ++++++++ .../targets/file_remove/tasks/test_glob.yml | 102 ++++++ .../file_remove/tasks/test_recursive.yml | 127 ++++++++ .../targets/file_remove/tasks/test_regex.yml | 97 ++++++ 12 files changed, 972 insertions(+) create mode 100644 plugins/modules/file_remove.py create mode 100644 tests/integration/targets/file_remove/aliases create mode 100644 tests/integration/targets/file_remove/defaults/main.yml create mode 100644 tests/integration/targets/file_remove/meta/main.yml create mode 100644 tests/integration/targets/file_remove/tasks/main.yml create mode 100644 tests/integration/targets/file_remove/tasks/test_check_diff.yml create mode 100644 tests/integration/targets/file_remove/tasks/test_errors.yml create mode 100644 tests/integration/targets/file_remove/tasks/test_file_types.yml create mode 100644 tests/integration/targets/file_remove/tasks/test_glob.yml create mode 100644 tests/integration/targets/file_remove/tasks/test_recursive.yml create mode 100644 tests/integration/targets/file_remove/tasks/test_regex.yml diff --git a/.github/BOTMETA.yml b/.github/BOTMETA.yml index 5bb9bd7bc8..76cc75bdbd 100644 --- a/.github/BOTMETA.yml +++ b/.github/BOTMETA.yml @@ -592,6 +592,8 @@ files: $modules/filesystem.py: labels: filesystem maintainers: pilou- abulimov quidame + $modules/file_remove.py: + maintainers: shahargolshani $modules/flatpak.py: maintainers: $team_flatpak $modules/flatpak_remote.py: diff --git a/plugins/modules/file_remove.py b/plugins/modules/file_remove.py new file mode 100644 index 0000000000..d0fb6892ea --- /dev/null +++ b/plugins/modules/file_remove.py @@ -0,0 +1,290 @@ +#!/usr/bin/python + +# Copyright (c) 2025, Shahar Golshani (@shahargolshani) +# 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 + +from __future__ import annotations + + +DOCUMENTATION = r""" +module: file_remove + +short_description: Remove files matching a pattern from a directory + +description: + - This module removes files from a specified directory that match a given pattern. + The pattern can include wildcards and regular expressions. + - By default, only files in the specified directory are removed (non-recursive). + Use the O(recursive) option to search and remove files in subdirectories. + +version_added: "12.1.0" + +author: + - Shahar Golshani (@shahargolshani) + +extends_documentation_fragment: + - community.general.attributes + +attributes: + check_mode: + support: full + diff_mode: + support: full + +options: + path: + description: + - Path to the directory where files should be removed. + - This must be an existing directory. + type: path + required: true + + pattern: + description: + - Pattern to match files for removal. + - Supports wildcards (V(*), V(?), V([seq]), V([!seq])) for glob-style matching. + - Use O(use_regex=true) to interpret this as a regular expression instead. + type: str + required: true + + use_regex: + description: + - If V(true), O(pattern) is interpreted as a regular expression. + - If V(false), O(pattern) is interpreted as a glob-style wildcard pattern. + type: bool + default: false + + recursive: + description: + - If V(true), search for files recursively in subdirectories. + - If V(false), only files in the specified directory are removed. + type: bool + default: false + + file_type: + description: + - Type of files to remove. + type: str + choices: + file: remove only regular files. + link: remove only symbolic links. + any: remove both files and symbolic links. + default: file + +notes: + - Directories are never removed by this module, only files and optionally symbolic links. + - This module will not follow symbolic links when O(recursive=true). + - Be careful with patterns that might match many files, especially with O(recursive=true). +""" + +EXAMPLES = r""" +- name: Remove all log files from /var/log + community.general.file_remove: + path: /var/log + pattern: "*.log" + +- name: Remove all temporary files recursively + community.general.file_remove: + path: /tmp/myapp + pattern: "*.tmp" + recursive: true + +- name: Remove files matching a regex pattern + community.general.file_remove: + path: /data/backups + pattern: "backup_[0-9]{8}\\.tar\\.gz" + use_regex: true + +- name: Remove both files and symbolic links + community.general.file_remove: + path: /opt/app/cache + pattern: "cache_*" + file_type: any + +- name: Remove all files starting with 'test_' (check mode) + community.general.file_remove: + path: /home/user/tests + pattern: "test_*" + check_mode: true +""" + +RETURN = r""" +removed_files: + description: List of files that were removed. + type: list + elements: str + returned: always + sample: ['/var/log/app.log', '/var/log/error.log'] + +files_count: + description: Number of files removed. + type: int + returned: success + sample: 2 + +path: + description: The directory path that was searched. + type: str + returned: success + sample: /var/log +""" + + +import os +import re +import glob +import typing as t + +from ansible.module_utils.basic import AnsibleModule + + +def find_matching_files( + path: str, pattern: str, use_regex: bool, recursive: bool, file_type: t.Literal["file", "link", "any"] +) -> list[str]: + """Find all files matching the pattern in the given path.""" + matching_files = [] + + if use_regex: + # Use regular expression matching + regex = re.compile(pattern) + if recursive: + for root, _dirs, files in os.walk(path, followlinks=False): + for filename in files: + if regex.match(filename) or regex.search(filename): + full_path = os.path.join(root, filename) + if should_include_file(full_path, file_type): + matching_files.append(full_path) + else: + for filename in os.listdir(path): + if regex.match(filename) or regex.search(filename): + full_path = os.path.join(path, filename) + if should_include_file(full_path, file_type): + matching_files.append(full_path) + else: + # Use glob pattern matching + if recursive: + glob_pattern = os.path.join(path, "**", pattern) + matching_files = [f for f in glob.glob(glob_pattern, recursive=True) if should_include_file(f, file_type)] + else: + glob_pattern = os.path.join(path, pattern) + matching_files = [f for f in glob.glob(glob_pattern) if should_include_file(f, file_type)] + + return sorted(matching_files) + + +def should_include_file(file_path: str, file_type: t.Literal["file", "link", "any"]) -> bool: + """Determine if a file should be included based on its type.""" + # Never include directories + if os.path.isdir(file_path): + return False + + is_link = os.path.islink(file_path) + is_file = os.path.isfile(file_path) + + if file_type == "file": + return is_file and not is_link + elif file_type == "link": + return is_link + else: + return is_file or is_link + + +def remove_files(module: AnsibleModule, files: list[str]) -> tuple[list[str], list[tuple[str, str]]]: + """Remove the specified files and return results.""" + removed_files = [] + failed_files = [] + + for file_path in files: + try: + if module.check_mode: + # In check mode, just verify the file exists + if os.path.exists(file_path): + removed_files.append(file_path) + else: + raise FileNotFoundError() + else: + # Actually remove the file + os.remove(file_path) + removed_files.append(file_path) + except FileNotFoundError: + module.warn(f"File not found (likely removed by other process): {file_path}") + except OSError as e: + failed_files.append((file_path, str(e))) + + return removed_files, failed_files + + +def main() -> None: + module = AnsibleModule( + argument_spec=dict( + path=dict(type="path", required=True), + pattern=dict(type="str", required=True), + use_regex=dict(type="bool", default=False), + recursive=dict(type="bool", default=False), + file_type=dict(type="str", default="file", choices=["file", "link", "any"]), + ), + supports_check_mode=True, + ) + + path: str = module.params["path"] + pattern: str = module.params["pattern"] + use_regex: bool = module.params["use_regex"] + recursive: bool = module.params["recursive"] + file_type: t.Literal["file", "link", "any"] = module.params["file_type"] + + # Validate that the path exists and is a directory + if not os.path.exists(path): + module.fail_json(msg=f"Path does not exist: {path}") + + if not os.path.isdir(path): + module.fail_json(msg=f"Path is not a directory: {path}") + + # Validate regex pattern if use_regex is true + if use_regex: + try: + re.compile(pattern) + except re.error as e: + module.fail_json(msg=f"Invalid regular expression pattern: {e}") + + # Find matching files + try: + matching_files = find_matching_files(path, pattern, use_regex, recursive, file_type) + except OSError as e: + module.fail_json(msg=str(e)) + + # Prepare diff information + diff = dict(before=dict(files=matching_files), after=dict(files=[])) + + # Remove the files + removed_files, failed_files = remove_files(module, matching_files) + + # Prepare result + changed = len(removed_files) > 0 + + result = dict( + changed=changed, + removed_files=removed_files, + files_count=len(removed_files), + path=path, + msg=f"Removed {len(removed_files)} file(s) matching pattern '{pattern}'", + ) + + # Add diff if in diff mode + if module._diff: + result["diff"] = diff + + # Report any failures + if failed_files: + failure_msg = "; ".join([f"{f}: {e}" for f, e in failed_files]) + module.fail_json( + msg=f"Failed to remove some files: {failure_msg}", + removed_files=removed_files, + failed_files=[f for f, e in failed_files], + ) + + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/tests/integration/targets/file_remove/aliases b/tests/integration/targets/file_remove/aliases new file mode 100644 index 0000000000..343f119da8 --- /dev/null +++ b/tests/integration/targets/file_remove/aliases @@ -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 diff --git a/tests/integration/targets/file_remove/defaults/main.yml b/tests/integration/targets/file_remove/defaults/main.yml new file mode 100644 index 0000000000..d41a4ba44f --- /dev/null +++ b/tests/integration/targets/file_remove/defaults/main.yml @@ -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" diff --git a/tests/integration/targets/file_remove/meta/main.yml b/tests/integration/targets/file_remove/meta/main.yml new file mode 100644 index 0000000000..982de6eb03 --- /dev/null +++ b/tests/integration/targets/file_remove/meta/main.yml @@ -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 diff --git a/tests/integration/targets/file_remove/tasks/main.yml b/tests/integration/targets/file_remove/tasks/main.yml new file mode 100644 index 0000000000..77ae55b7b6 --- /dev/null +++ b/tests/integration/targets/file_remove/tasks/main.yml @@ -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 diff --git a/tests/integration/targets/file_remove/tasks/test_check_diff.yml b/tests/integration/targets/file_remove/tasks/test_check_diff.yml new file mode 100644 index 0000000000..bdbcbd0a4a --- /dev/null +++ b/tests/integration/targets/file_remove/tasks/test_check_diff.yml @@ -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 diff --git a/tests/integration/targets/file_remove/tasks/test_errors.yml b/tests/integration/targets/file_remove/tasks/test_errors.yml new file mode 100644 index 0000000000..5b677ba08b --- /dev/null +++ b/tests/integration/targets/file_remove/tasks/test_errors.yml @@ -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 diff --git a/tests/integration/targets/file_remove/tasks/test_file_types.yml b/tests/integration/targets/file_remove/tasks/test_file_types.yml new file mode 100644 index 0000000000..2f2fd11fa0 --- /dev/null +++ b/tests/integration/targets/file_remove/tasks/test_file_types.yml @@ -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' diff --git a/tests/integration/targets/file_remove/tasks/test_glob.yml b/tests/integration/targets/file_remove/tasks/test_glob.yml new file mode 100644 index 0000000000..85ed6df969 --- /dev/null +++ b/tests/integration/targets/file_remove/tasks/test_glob.yml @@ -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' diff --git a/tests/integration/targets/file_remove/tasks/test_recursive.yml b/tests/integration/targets/file_remove/tasks/test_recursive.yml new file mode 100644 index 0000000000..5b32b577cc --- /dev/null +++ b/tests/integration/targets/file_remove/tasks/test_recursive.yml @@ -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' diff --git a/tests/integration/targets/file_remove/tasks/test_regex.yml b/tests/integration/targets/file_remove/tasks/test_regex.yml new file mode 100644 index 0000000000..3a0af37022 --- /dev/null +++ b/tests/integration/targets/file_remove/tasks/test_regex.yml @@ -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'