mirror of
https://github.com/ansible-collections/community.general.git
synced 2026-02-04 07:51:50 +00:00
[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 af99cc7deb)
Co-authored-by: Shahar Golshani <sgolshan@redhat.com>
Co-authored-by: Felix Fontein <felix@fontein.de>
This commit is contained in:
parent
649b32759b
commit
c9df20808d
12 changed files with 972 additions and 0 deletions
2
.github/BOTMETA.yml
vendored
2
.github/BOTMETA.yml
vendored
|
|
@ -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:
|
||||
|
|
|
|||
290
plugins/modules/file_remove.py
Normal file
290
plugins/modules/file_remove.py
Normal file
|
|
@ -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()
|
||||
5
tests/integration/targets/file_remove/aliases
Normal file
5
tests/integration/targets/file_remove/aliases
Normal 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
|
||||
6
tests/integration/targets/file_remove/defaults/main.yml
Normal file
6
tests/integration/targets/file_remove/defaults/main.yml
Normal 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"
|
||||
7
tests/integration/targets/file_remove/meta/main.yml
Normal file
7
tests/integration/targets/file_remove/meta/main.yml
Normal 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
|
||||
39
tests/integration/targets/file_remove/tasks/main.yml
Normal file
39
tests/integration/targets/file_remove/tasks/main.yml
Normal 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
|
||||
108
tests/integration/targets/file_remove/tasks/test_check_diff.yml
Normal file
108
tests/integration/targets/file_remove/tasks/test_check_diff.yml
Normal 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
|
||||
57
tests/integration/targets/file_remove/tasks/test_errors.yml
Normal file
57
tests/integration/targets/file_remove/tasks/test_errors.yml
Normal 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
|
||||
132
tests/integration/targets/file_remove/tasks/test_file_types.yml
Normal file
132
tests/integration/targets/file_remove/tasks/test_file_types.yml
Normal 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'
|
||||
102
tests/integration/targets/file_remove/tasks/test_glob.yml
Normal file
102
tests/integration/targets/file_remove/tasks/test_glob.yml
Normal 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'
|
||||
127
tests/integration/targets/file_remove/tasks/test_recursive.yml
Normal file
127
tests/integration/targets/file_remove/tasks/test_recursive.yml
Normal 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'
|
||||
97
tests/integration/targets/file_remove/tasks/test_regex.yml
Normal file
97
tests/integration/targets/file_remove/tasks/test_regex.yml
Normal 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'
|
||||
Loading…
Add table
Add a link
Reference in a new issue