diff options
Diffstat (limited to 'ansible_collections/community/general/plugins/lookup/filetree.py')
-rw-r--r-- | ansible_collections/community/general/plugins/lookup/filetree.py | 225 |
1 files changed, 225 insertions, 0 deletions
diff --git a/ansible_collections/community/general/plugins/lookup/filetree.py b/ansible_collections/community/general/plugins/lookup/filetree.py new file mode 100644 index 000000000..f12cc4519 --- /dev/null +++ b/ansible_collections/community/general/plugins/lookup/filetree.py @@ -0,0 +1,225 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2016 Dag Wieers <dag@wieers.com> +# Copyright (c) 2017 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 +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = r''' +name: filetree +author: Dag Wieers (@dagwieers) <dag@wieers.com> +short_description: recursively match all files in a directory tree +description: +- This lookup enables you to template a complete tree of files on a target system while retaining permissions and ownership. +- Supports directories, files and symlinks, including SELinux and other file properties. +- If you provide more than one path, it will implement a first_found logic, and will not process entries it already processed in previous paths. + This enables merging different trees in order of importance, or add role_vars to specific paths to influence different instances of the same role. +options: + _terms: + description: path(s) of files to read + required: true +''' + +EXAMPLES = r""" +- name: Create directories + ansible.builtin.file: + path: /web/{{ item.path }} + state: directory + mode: '{{ item.mode }}' + with_community.general.filetree: web/ + when: item.state == 'directory' + +- name: Template files (explicitly skip directories in order to use the 'src' attribute) + ansible.builtin.template: + src: '{{ item.src }}' + # Your template files should be stored with a .j2 file extension, + # but should not be deployed with it. splitext|first removes it. + dest: /web/{{ item.path | splitext | first }} + mode: '{{ item.mode }}' + with_community.general.filetree: web/ + when: item.state == 'file' + +- name: Recreate symlinks + ansible.builtin.file: + src: '{{ item.src }}' + dest: /web/{{ item.path }} + state: link + follow: false # avoid corrupting target files if the link already exists + force: true + mode: '{{ item.mode }}' + with_community.general.filetree: web/ + when: item.state == 'link' + +- name: list all files under web/ + ansible.builtin.debug: + msg: "{{ lookup('community.general.filetree', 'web/') }}" +""" + +RETURN = r""" + _raw: + description: List of dictionaries with file information. + type: list + elements: dict + contains: + src: + description: + - Full path to file. + - Not returned when I(item.state) is set to C(directory). + type: path + root: + description: Allows filtering by original location. + type: path + path: + description: Contains the relative path to root. + type: path + mode: + description: The permissions the resulting file or directory. + type: str + state: + description: TODO + type: str + owner: + description: Name of the user that owns the file/directory. + type: raw + group: + description: Name of the group that owns the file/directory. + type: raw + seuser: + description: The user part of the SELinux file context. + type: raw + serole: + description: The role part of the SELinux file context. + type: raw + setype: + description: The type part of the SELinux file context. + type: raw + selevel: + description: The level part of the SELinux file context. + type: raw + uid: + description: Owner ID of the file/directory. + type: int + gid: + description: Group ID of the file/directory. + type: int + size: + description: Size of the target. + type: int + mtime: + description: Time of last modification. + type: float + ctime: + description: Time of last metadata update or creation (depends on OS). + type: float +""" +import os +import pwd +import grp +import stat + +HAVE_SELINUX = False +try: + import selinux + HAVE_SELINUX = True +except ImportError: + pass + +from ansible.plugins.lookup import LookupBase +from ansible.module_utils.common.text.converters import to_native, to_text +from ansible.utils.display import Display + +display = Display() + + +# If selinux fails to find a default, return an array of None +def selinux_context(path): + context = [None, None, None, None] + if HAVE_SELINUX and selinux.is_selinux_enabled(): + try: + # note: the selinux module uses byte strings on python2 and text + # strings on python3 + ret = selinux.lgetfilecon_raw(to_native(path)) + except OSError: + return context + if ret[0] != -1: + # Limit split to 4 because the selevel, the last in the list, + # may contain ':' characters + context = ret[1].split(':', 3) + return context + + +def file_props(root, path): + ''' Returns dictionary with file properties, or return None on failure ''' + abspath = os.path.join(root, path) + + try: + st = os.lstat(abspath) + except OSError as e: + display.warning('filetree: Error using stat() on path %s (%s)' % (abspath, e)) + return None + + ret = dict(root=root, path=path) + + if stat.S_ISLNK(st.st_mode): + ret['state'] = 'link' + ret['src'] = os.readlink(abspath) + elif stat.S_ISDIR(st.st_mode): + ret['state'] = 'directory' + elif stat.S_ISREG(st.st_mode): + ret['state'] = 'file' + ret['src'] = abspath + else: + display.warning('filetree: Error file type of %s is not supported' % abspath) + return None + + ret['uid'] = st.st_uid + ret['gid'] = st.st_gid + try: + ret['owner'] = pwd.getpwuid(st.st_uid).pw_name + except KeyError: + ret['owner'] = st.st_uid + try: + ret['group'] = to_text(grp.getgrgid(st.st_gid).gr_name) + except KeyError: + ret['group'] = st.st_gid + ret['mode'] = '0%03o' % (stat.S_IMODE(st.st_mode)) + ret['size'] = st.st_size + ret['mtime'] = st.st_mtime + ret['ctime'] = st.st_ctime + + if HAVE_SELINUX and selinux.is_selinux_enabled() == 1: + context = selinux_context(abspath) + ret['seuser'] = context[0] + ret['serole'] = context[1] + ret['setype'] = context[2] + ret['selevel'] = context[3] + + return ret + + +class LookupModule(LookupBase): + + def run(self, terms, variables=None, **kwargs): + self.set_options(var_options=variables, direct=kwargs) + + basedir = self.get_basedir(variables) + + ret = [] + for term in terms: + term_file = os.path.basename(term) + dwimmed_path = self._loader.path_dwim_relative(basedir, 'files', os.path.dirname(term)) + path = os.path.join(dwimmed_path, term_file) + display.debug("Walking '{0}'".format(path)) + for root, dirs, files in os.walk(path, topdown=True): + for entry in dirs + files: + relpath = os.path.relpath(os.path.join(root, entry), path) + + # Skip if relpath was already processed (from another root) + if relpath not in [entry['path'] for entry in ret]: + props = file_props(path, relpath) + if props is not None: + display.debug(" found '{0}'".format(os.path.join(path, relpath))) + ret.append(props) + + return ret |