homeserver/ansible_collections/community/general/plugins/lookup/filetree.py
mg c648a48c4c systemd: sanoid (#346)
Co-authored-by: Michael Grote <michael.grote@posteo.de>
Reviewed-on: mg/ansible#346
Co-authored-by: mg <michael.grote@posteo.de>
Co-committed-by: mg <michael.grote@posteo.de>
2022-04-03 11:04:27 +02:00

223 lines
7.1 KiB
Python

# -*- coding: utf-8 -*-
# (c) 2016 Dag Wieers <dag@wieers.com>
# (c) 2017 Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
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: yes
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):
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