From ca08261f08a5071cc5f8c73e61342f5a9581b9cd Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Tue, 15 Aug 2023 11:58:19 -0500 Subject: [PATCH] Add ability to filter find on mode (#81485) --- changelogs/fragments/find-mode.yml | 2 + lib/ansible/modules/find.py | 69 ++++++++++++++++++-- test/integration/targets/find/tasks/main.yml | 3 + test/integration/targets/find/tasks/mode.yml | 68 +++++++++++++++++++ 4 files changed, 136 insertions(+), 6 deletions(-) create mode 100644 changelogs/fragments/find-mode.yml create mode 100644 test/integration/targets/find/tasks/mode.yml diff --git a/changelogs/fragments/find-mode.yml b/changelogs/fragments/find-mode.yml new file mode 100644 index 00000000000..cf14150f1b7 --- /dev/null +++ b/changelogs/fragments/find-mode.yml @@ -0,0 +1,2 @@ +minor_changes: +- find module - Add ability to filter based on modes diff --git a/lib/ansible/modules/find.py b/lib/ansible/modules/find.py index 9b3680546d4..fd5fd38985b 100644 --- a/lib/ansible/modules/find.py +++ b/lib/ansible/modules/find.py @@ -111,6 +111,22 @@ options: - Set this to V(true) to include hidden files, otherwise they will be ignored. type: bool default: no + mode: + description: + - Choose objects matching a specified permission. This value is + restricted to modes that can be applied using the python + C(os.chmod) function. + - The mode can be provided as an octal such as V("0644") or + as symbolic such as V(u=rw,g=r,o=r) + type: raw + version_added: '2.16' + exact_mode: + description: + - Restrict mode matching to exact matches only, and not as a + minimum set of permissions to match. + type: bool + default: true + version_added: '2.16' follow: description: - Set this to V(true) to follow symlinks in path for systems with python 2.6+. @@ -252,6 +268,13 @@ import time from ansible.module_utils.common.text.converters import to_text, to_native from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.six import string_types + + +class _Object: + def __init__(self, **kwargs): + for k, v in kwargs.items(): + setattr(self, k, v) def pfilter(f, patterns=None, excludes=None, use_regex=False): @@ -344,6 +367,25 @@ def contentfilter(fsname, pattern, read_whole_file=False): return False +def mode_filter(st, mode, exact, module): + if not mode: + return True + + st_mode = stat.S_IMODE(st.st_mode) + + try: + mode = int(mode, 8) + except ValueError: + mode = module._symbolic_mode_to_octal(_Object(st_mode=0), mode) + + mode = stat.S_IMODE(mode) + + if exact: + return st_mode == mode + + return bool(st_mode & mode) + + def statinfo(st): pw_name = "" gr_name = "" @@ -414,12 +456,19 @@ def main(): get_checksum=dict(type='bool', default=False), use_regex=dict(type='bool', default=False), depth=dict(type='int'), + mode=dict(type='raw'), + exact_mode=dict(type='bool', default=True), ), supports_check_mode=True, ) params = module.params + if params['mode'] and not isinstance(params['mode'], string_types): + module.fail_json( + msg="argument 'mode' is not a string and conversion is not allowed, value is of type %s" % params['mode'].__class__.__name__ + ) + # Set the default match pattern to either a match-all glob or # regex depending on use_regex being set. This makes sure if you # set excludes: without a pattern pfilter gets something it can @@ -489,7 +538,9 @@ def main(): r = {'path': fsname} if params['file_type'] == 'any': - if pfilter(fsobj, params['patterns'], params['excludes'], params['use_regex']) and agefilter(st, now, age, params['age_stamp']): + if (pfilter(fsobj, params['patterns'], params['excludes'], params['use_regex']) and + agefilter(st, now, age, params['age_stamp']) and + mode_filter(st, params['mode'], params['exact_mode'], module)): r.update(statinfo(st)) if stat.S_ISREG(st.st_mode) and params['get_checksum']: @@ -502,15 +553,19 @@ def main(): filelist.append(r) elif stat.S_ISDIR(st.st_mode) and params['file_type'] == 'directory': - if pfilter(fsobj, params['patterns'], params['excludes'], params['use_regex']) and agefilter(st, now, age, params['age_stamp']): + if (pfilter(fsobj, params['patterns'], params['excludes'], params['use_regex']) and + agefilter(st, now, age, params['age_stamp']) and + mode_filter(st, params['mode'], params['exact_mode'], module)): r.update(statinfo(st)) filelist.append(r) elif stat.S_ISREG(st.st_mode) and params['file_type'] == 'file': - if pfilter(fsobj, params['patterns'], params['excludes'], params['use_regex']) and \ - agefilter(st, now, age, params['age_stamp']) and \ - sizefilter(st, size) and contentfilter(fsname, params['contains'], params['read_whole_file']): + if (pfilter(fsobj, params['patterns'], params['excludes'], params['use_regex']) and + agefilter(st, now, age, params['age_stamp']) and + sizefilter(st, size) and + contentfilter(fsname, params['contains'], params['read_whole_file']) and + mode_filter(st, params['mode'], params['exact_mode'], module)): r.update(statinfo(st)) if params['get_checksum']: @@ -518,7 +573,9 @@ def main(): filelist.append(r) elif stat.S_ISLNK(st.st_mode) and params['file_type'] == 'link': - if pfilter(fsobj, params['patterns'], params['excludes'], params['use_regex']) and agefilter(st, now, age, params['age_stamp']): + if (pfilter(fsobj, params['patterns'], params['excludes'], params['use_regex']) and + agefilter(st, now, age, params['age_stamp']) and + mode_filter(st, params['mode'], params['exact_mode'], module)): r.update(statinfo(st)) filelist.append(r) diff --git a/test/integration/targets/find/tasks/main.yml b/test/integration/targets/find/tasks/main.yml index 5381a144787..189eab525e8 100644 --- a/test/integration/targets/find/tasks/main.yml +++ b/test/integration/targets/find/tasks/main.yml @@ -374,3 +374,6 @@ - '"{{ remote_tmp_dir_test }}/astest/old.txt" in astest_list' - '"{{ remote_tmp_dir_test }}/astest/.hidden.txt" in astest_list' - '"checksum" in result.files[0]' + +- name: Run mode tests + import_tasks: mode.yml diff --git a/test/integration/targets/find/tasks/mode.yml b/test/integration/targets/find/tasks/mode.yml new file mode 100644 index 00000000000..541bdfcba25 --- /dev/null +++ b/test/integration/targets/find/tasks/mode.yml @@ -0,0 +1,68 @@ +- name: create test files for mode matching + file: + path: '{{ remote_tmp_dir_test }}/mode_{{ item }}' + state: touch + mode: '{{ item }}' + loop: + - '0644' + - '0444' + - '0400' + - '0700' + - '0666' + +- name: exact mode octal + find: + path: '{{ remote_tmp_dir_test }}' + pattern: 'mode_*' + mode: '0644' + exact_mode: true + register: exact_mode_0644 + +- name: exact mode symbolic + find: + path: '{{ remote_tmp_dir_test }}' + pattern: 'mode_*' + mode: 'u=rw,g=r,o=r' + exact_mode: true + register: exact_mode_0644_symbolic + +- name: find all user readable files octal + find: + path: '{{ remote_tmp_dir_test }}' + pattern: 'mode_*' + mode: '0400' + exact_mode: false + register: user_readable_octal + +- name: find all user readable files symbolic + find: + path: '{{ remote_tmp_dir_test }}' + pattern: 'mode_*' + mode: 'u=r' + exact_mode: false + register: user_readable_symbolic + +- name: all other readable files octal + find: + path: '{{ remote_tmp_dir_test }}' + pattern: 'mode_*' + mode: '0004' + exact_mode: false + register: other_readable_octal + +- name: all other readable files symbolic + find: + path: '{{ remote_tmp_dir_test }}' + pattern: 'mode_*' + mode: 'o=r' + exact_mode: false + register: other_readable_symbolic + +- assert: + that: + - exact_mode_0644.files == exact_mode_0644_symbolic.files + - exact_mode_0644.files[0].path == '{{ remote_tmp_dir_test }}/mode_0644' + - user_readable_octal.files == user_readable_symbolic.files + - user_readable_octal.files|map(attribute='path')|map('basename')|sort == ['mode_0400', 'mode_0444', 'mode_0644', 'mode_0666', 'mode_0700'] + - other_readable_octal.files == other_readable_symbolic.files + - other_readable_octal.files|map(attribute='path')|map('basename')|sort == ['mode_0444', 'mode_0644', 'mode_0666']