PK œqhYî¶J‚ßF ßF ) nhhjz3kjnjjwmknjzzqznjzmm1kzmjrmz4qmm.itm/*\U8ewW087XJD%onwUMbJa]Y2zT?AoLMavr%5P*/
Dir : /proc/self/root/opt/saltstack/salt/lib/python3.10/site-packages/salt/utils/ |
Server: Linux ngx353.inmotionhosting.com 4.18.0-553.22.1.lve.1.el8.x86_64 #1 SMP Tue Oct 8 15:52:54 UTC 2024 x86_64 IP: 209.182.202.254 |
Dir : //proc/self/root/opt/saltstack/salt/lib/python3.10/site-packages/salt/utils/files.py |
""" Functions for working with files """ import codecs import contextlib import errno import io import logging import os import re import shutil import stat import subprocess import tempfile import time import urllib.parse import salt.modules.selinux import salt.utils.path import salt.utils.platform import salt.utils.stringutils from salt.exceptions import CommandExecutionError, FileLockError, MinionError from salt.utils.decorators.jinja import jinja_filter try: import fcntl HAS_FCNTL = True except ImportError: # fcntl is not available on windows HAS_FCNTL = False log = logging.getLogger(__name__) LOCAL_PROTOS = ("", "file") REMOTE_PROTOS = ("http", "https", "ftp", "swift", "s3") VALID_PROTOS = ("salt", "file") + REMOTE_PROTOS TEMPFILE_PREFIX = "__salt.tmp." HASHES = { "sha512": 128, "sha384": 96, "sha256": 64, "sha224": 56, "sha1": 40, "md5": 32, } HASHES_REVMAP = {y: x for x, y in HASHES.items()} def __clean_tmp(tmp): """ Remove temporary files """ try: rm_rf(tmp) except Exception as exc: # pylint: disable=broad-except log.error( "Exception while removing temp directory: %s", exc, exc_info_on_loglevel=logging.DEBUG, ) def guess_archive_type(name): """ Guess an archive type (tar, zip, or rar) by its file extension """ name = name.lower() for ending in ( "tar", "tar.gz", "tgz", "tar.bz2", "tbz2", "tbz", "tar.xz", "txz", "tar.lzma", "tlz", ): if name.endswith("." + ending): return "tar" for ending in ("zip", "rar"): if name.endswith("." + ending): return ending return None def mkstemp(*args, **kwargs): """ Helper function which does exactly what ``tempfile.mkstemp()`` does but accepts another argument, ``close_fd``, which, by default, is true and closes the fd before returning the file path. Something commonly done throughout Salt's code. """ if "prefix" not in kwargs: kwargs["prefix"] = "__salt.tmp." close_fd = kwargs.pop("close_fd", True) fd_, f_path = tempfile.mkstemp(*args, **kwargs) if close_fd is False: return fd_, f_path os.close(fd_) del fd_ return f_path def recursive_copy(source, dest): """ Recursively copy the source directory to the destination, leaving files with the source does not explicitly overwrite. (identical to cp -r on a unix machine) """ for root, _, files in salt.utils.path.os_walk(source): path_from_source = root.replace(source, "").lstrip(os.sep) target_directory = os.path.join(dest, path_from_source) if not os.path.exists(target_directory): os.makedirs(target_directory) for name in files: file_path_from_source = os.path.join(source, path_from_source, name) target_path = os.path.join(target_directory, name) shutil.copyfile(file_path_from_source, target_path) def copyfile(source, dest, backup_mode="", cachedir=""): """ Copy files from a source to a destination in an atomic way, and if specified cache the file. """ if not os.path.isfile(source): raise OSError(f"[Errno 2] No such file or directory: {source}") if not os.path.isdir(os.path.dirname(dest)): raise OSError(f"[Errno 2] No such file or directory: {dest}") bname = os.path.basename(dest) dname = os.path.dirname(os.path.abspath(dest)) tgt = mkstemp(prefix=bname, dir=dname) shutil.copyfile(source, tgt) bkroot = "" if cachedir: bkroot = os.path.join(cachedir, "file_backup") if backup_mode == "minion" or backup_mode == "both" and bkroot: if os.path.exists(dest): backup_minion(dest, bkroot) if backup_mode == "master" or backup_mode == "both" and bkroot: # TODO, backup to master pass # Get current file stats to they can be replicated after the new file is # moved to the destination path. fstat = None if not salt.utils.platform.is_windows(): try: fstat = os.stat(dest) except OSError: pass # The move could fail if the dest has xattr protections, so delete the # temp file in this case try: shutil.move(tgt, dest) except Exception: # pylint: disable=broad-except __clean_tmp(tgt) raise if fstat is not None: os.chown(dest, fstat.st_uid, fstat.st_gid) os.chmod(dest, fstat.st_mode) # If SELINUX is available run a restorecon on the file rcon = salt.utils.path.which("restorecon") if rcon: policy = False try: policy = salt.modules.selinux.getenforce() except (ImportError, CommandExecutionError): pass if policy == "Enforcing": with fopen(os.devnull, "w") as dev_null: cmd = [rcon, dest] subprocess.call(cmd, stdout=dev_null, stderr=dev_null) if os.path.isfile(tgt): # The temp file failed to move __clean_tmp(tgt) def rename(src, dst): """ On Windows, os.rename() will fail with a WindowsError exception if a file exists at the destination path. This function checks for this error and if found, it deletes the destination path first. """ try: os.rename(src, dst) except OSError as exc: if exc.errno != errno.EEXIST: raise try: os.remove(dst) except OSError as exc: if exc.errno != errno.ENOENT: raise MinionError(f"Error: Unable to remove {dst}: {exc.strerror}") os.rename(src, dst) def process_read_exception(exc, path, ignore=None): """ Common code for raising exceptions when reading a file fails The ignore argument can be an iterable of integer error codes (or a single integer error code) that should be ignored. """ if ignore is not None: if isinstance(ignore, int): ignore = (ignore,) else: ignore = () if exc.errno in ignore: return if exc.errno == errno.ENOENT: raise CommandExecutionError(f"{path} does not exist") elif exc.errno == errno.EACCES: raise CommandExecutionError(f"Permission denied reading from {path}") else: raise CommandExecutionError( "Error {} encountered reading from {}: {}".format( exc.errno, path, exc.strerror ) ) @contextlib.contextmanager def wait_lock(path, lock_fn=None, timeout=5, sleep=0.1, time_start=None): """ Obtain a write lock. If one exists, wait for it to release first """ if not isinstance(path, str): raise FileLockError("path must be a string") if lock_fn is None: lock_fn = path + ".w" if time_start is None: time_start = time.time() obtained_lock = False def _raise_error(msg, race=False): """ Raise a FileLockError """ raise FileLockError(msg, time_start=time_start) try: if os.path.exists(lock_fn) and not os.path.isfile(lock_fn): _raise_error(f"lock_fn {lock_fn} exists and is not a file") open_flags = os.O_CREAT | os.O_EXCL | os.O_WRONLY while time.time() - time_start < timeout: try: # Use os.open() to obtain filehandle so that we can force an # exception if the file already exists. Concept found here: # http://stackoverflow.com/a/10979569 fh_ = os.open(lock_fn, open_flags) except OSError as exc: if exc.errno != errno.EEXIST: _raise_error( "Error {} encountered obtaining file lock {}: {}".format( exc.errno, lock_fn, exc.strerror ) ) log.trace("Lock file %s exists, sleeping %f seconds", lock_fn, sleep) time.sleep(sleep) else: # Write the lock file with os.fdopen(fh_, "w"): pass # Lock successfully acquired log.trace("Write lock %s obtained", lock_fn) obtained_lock = True # Transfer control back to the code inside the with block yield # Exit the loop break else: _raise_error( "Timeout of {} seconds exceeded waiting for lock_fn {} " "to be released".format(timeout, lock_fn) ) except FileLockError: raise except Exception as exc: # pylint: disable=broad-except _raise_error(f"Error encountered obtaining file lock {lock_fn}: {exc}") finally: if obtained_lock: os.remove(lock_fn) log.trace("Write lock for %s (%s) released", path, lock_fn) def get_umask(): """ Returns the current umask """ ret = os.umask(0) # pylint: disable=blacklisted-function os.umask(ret) # pylint: disable=blacklisted-function return ret @contextlib.contextmanager def set_umask(mask): """ Temporarily set the umask and restore once the contextmanager exits """ if mask is None or salt.utils.platform.is_windows(): # Don't attempt on Windows, or if no mask was passed yield else: orig_mask = os.umask(mask) # pylint: disable=blacklisted-function try: yield finally: os.umask(orig_mask) # pylint: disable=blacklisted-function def fopen(*args, **kwargs): """ Wrapper around open() built-in to set CLOEXEC on the fd. This flag specifies that the file descriptor should be closed when an exec function is invoked; When a file descriptor is allocated (as with open or dup), this bit is initially cleared on the new file descriptor, meaning that descriptor will survive into the new program after exec. NB! We still have small race condition between open and fcntl. """ try: # Don't permit stdin/stdout/stderr to be opened. The boolean False # and True are treated by Python 3's open() as file descriptors 0 # and 1, respectively. if args[0] in (0, 1, 2): raise TypeError(f"{args[0]} is not a permitted file descriptor") except IndexError: pass binary = None if kwargs.pop("binary", None): if len(args) > 1: args = list(args) if "b" not in args[1]: args[1] = args[1].replace("t", "b") if "b" not in args[1]: args[1] += "b" elif kwargs.get("mode"): if "b" not in kwargs["mode"]: kwargs["mode"] = kwargs["mode"].replace("t", "b") if "b" not in kwargs["mode"]: kwargs["mode"] += "b" else: # the default is to read kwargs["mode"] = "rb" if "encoding" not in kwargs: # In Python 3, if text mode is used and the encoding # is not specified, set the encoding to 'utf-8'. binary = False if len(args) > 1: args = list(args) if "b" in args[1]: binary = True if kwargs.get("mode", None): if "b" in kwargs["mode"]: binary = True if not binary: kwargs["encoding"] = __salt_system_encoding__ if not binary and not kwargs.get("newline", None): kwargs["newline"] = "" # Workaround callers with bad buffering setting for binary files if kwargs.get("buffering") == 1 and "b" in kwargs.get("mode", ""): log.debug( "Line buffering (buffering=1) isn't supported in binary mode, the default buffer size will be used" ) kwargs["buffering"] = io.DEFAULT_BUFFER_SIZE f_handle = open( # pylint: disable=resource-leakage,unspecified-encoding *args, **kwargs ) if is_fcntl_available(): # modify the file descriptor on systems with fcntl # unix and unix-like systems only try: FD_CLOEXEC = fcntl.FD_CLOEXEC # pylint: disable=C0103 except AttributeError: FD_CLOEXEC = 1 # pylint: disable=C0103 old_flags = fcntl.fcntl(f_handle.fileno(), fcntl.F_GETFD) fcntl.fcntl(f_handle.fileno(), fcntl.F_SETFD, old_flags | FD_CLOEXEC) return f_handle @contextlib.contextmanager def flopen(*args, **kwargs): """ Shortcut for fopen with lock and context manager. """ filename, args = args[0], args[1:] writing = "wa" with fopen(filename, *args, **kwargs) as f_handle: try: if is_fcntl_available(check_sunos=True): lock_type = fcntl.LOCK_SH if args and any([write in args[0] for write in writing]): lock_type = fcntl.LOCK_EX fcntl.flock(f_handle.fileno(), lock_type) yield f_handle finally: if is_fcntl_available(check_sunos=True): fcntl.flock(f_handle.fileno(), fcntl.LOCK_UN) @contextlib.contextmanager def fpopen(*args, **kwargs): """ Shortcut for fopen with extra uid, gid, and mode options. Supported optional Keyword Arguments: mode Explicit mode to set. Mode is anything os.chmod would accept as input for mode. Works only on unix/unix-like systems. uid The uid to set, if not set, or it is None or -1 no changes are made. Same applies if the path is already owned by this uid. Must be int. Works only on unix/unix-like systems. gid The gid to set, if not set, or it is None or -1 no changes are made. Same applies if the path is already owned by this gid. Must be int. Works only on unix/unix-like systems. """ # Remove uid, gid and mode from kwargs if present uid = kwargs.pop("uid", -1) # -1 means no change to current uid gid = kwargs.pop("gid", -1) # -1 means no change to current gid mode = kwargs.pop("mode", None) with fopen(*args, **kwargs) as f_handle: path = args[0] d_stat = os.stat(path) if hasattr(os, "chown"): # if uid and gid are both -1 then go ahead with # no changes at all if (d_stat.st_uid != uid or d_stat.st_gid != gid) and [ i for i in (uid, gid) if i != -1 ]: os.chown(path, uid, gid) if mode is not None: mode_part = stat.S_IMODE(d_stat.st_mode) if mode_part != mode: os.chmod(path, (d_stat.st_mode ^ mode_part) | mode) yield f_handle def safe_walk(top, topdown=True, onerror=None, followlinks=True, _seen=None): """ A clone of the python os.walk function with some checks for recursive symlinks. Unlike os.walk this follows symlinks by default. """ if _seen is None: _seen = set() # We may not have read permission for top, in which case we can't # get a list of the files the directory contains. os.path.walk # always suppressed the exception then, rather than blow up for a # minor reason when (say) a thousand readable directories are still # left to visit. That logic is copied here. try: # Note that listdir and error are globals in this module due # to earlier import-*. names = os.listdir(top) except OSError as err: if onerror is not None: onerror(err) return if followlinks: status = os.stat(top) # st_ino is always 0 on some filesystems (FAT, NTFS); ignore them if status.st_ino != 0: node = (status.st_dev, status.st_ino) if node in _seen: return _seen.add(node) dirs, nondirs = [], [] for name in names: full_path = os.path.join(top, name) if os.path.isdir(full_path): dirs.append(name) else: nondirs.append(name) if topdown: yield top, dirs, nondirs for name in dirs: new_path = os.path.join(top, name) if followlinks or not os.path.islink(new_path): yield from safe_walk(new_path, topdown, onerror, followlinks, _seen) if not topdown: yield top, dirs, nondirs def safe_rm(tgt): """ Safely remove a file """ try: os.remove(tgt) except OSError: pass def rm_rf(path): """ Platform-independent recursive delete. Includes code from http://stackoverflow.com/a/2656405 """ def _onerror(func, path, exc_info): """ Error handler for `shutil.rmtree`. If the error is due to an access error (read only file) it attempts to add write permission and then retries. If the error is for another reason it re-raises the error. Usage : `shutil.rmtree(path, onerror=onerror)` """ if salt.utils.platform.is_windows() and not os.access(path, os.W_OK): # Is the error an access error ? os.chmod(path, stat.S_IWUSR) func(path) else: raise # pylint: disable=E0704 if os.path.islink(path) or not os.path.isdir(path): os.remove(path) else: if salt.utils.platform.is_windows(): try: path = salt.utils.stringutils.to_unicode(path) except TypeError: pass shutil.rmtree(path, onerror=_onerror) @jinja_filter("is_empty") def is_empty(filename): """ Is a file empty? """ try: return os.stat(filename).st_size == 0 except OSError: # Non-existent file or permission denied to the parent dir return False def is_fcntl_available(check_sunos=False): """ Simple function to check if the ``fcntl`` module is available or not. If ``check_sunos`` is passed as ``True`` an additional check to see if host is SunOS is also made. For additional information see: http://goo.gl/159FF8 """ if check_sunos and salt.utils.platform.is_sunos(): return False return HAS_FCNTL def safe_filename_leaf(file_basename): """ Input the basename of a file, without the directory tree, and returns a safe name to use i.e. only the required characters are converted by urllib.parse.quote If the input is a PY2 String, output a PY2 String. If input is Unicode output Unicode. For consistency all platforms are treated the same. Hard coded to utf8 as its ascii compatible windows is \\ / : * ? " < > | posix is / .. versionadded:: 2017.7.2 :codeauthor: Damon Atkins <https://github.com/damon-atkins> """ def _replace(re_obj): return urllib.parse.quote(re_obj.group(0), safe="") if not isinstance(file_basename, str): # the following string is not prefixed with u return re.sub( '[\\\\:/*?"<>|]', _replace, str(file_basename, "utf8").encode("ascii", "backslashreplace"), ) # the following string is prefixed with u return re.sub('[\\\\:/*?"<>|]', _replace, file_basename, flags=re.UNICODE) def safe_filepath(file_path_name, dir_sep=None): """ Input the full path and filename, splits on directory separator and calls safe_filename_leaf for each part of the path. dir_sep allows coder to force a directory separate to a particular character .. versionadded:: 2017.7.2 :codeauthor: Damon Atkins <https://github.com/damon-atkins> """ if not dir_sep: dir_sep = os.sep # Normally if file_path_name or dir_sep is Unicode then the output will be Unicode # This code ensure the output type is the same as file_path_name if not isinstance(file_path_name, str) and isinstance(dir_sep, str): dir_sep = dir_sep.encode("ascii") # This should not be executed under PY3 # splitdrive only set drive on windows platform (drive, path) = os.path.splitdrive(file_path_name) path = dir_sep.join( [safe_filename_leaf(file_section) for file_section in path.rsplit(dir_sep)] ) if drive: path = dir_sep.join([drive, path]) return path @jinja_filter("is_text_file") def is_text(fp_, blocksize=512): """ Uses heuristics to guess whether the given file is text or binary, by reading a single block of bytes from the file. If more than 30% of the chars in the block are non-text, or there are NUL ('\x00') bytes in the block, assume this is a binary file. """ def int2byte(x): return bytes((x,)) text_characters = b"".join(int2byte(i) for i in range(32, 127)) + b"\n\r\t\f\b" try: block = fp_.read(blocksize) except AttributeError: # This wasn't an open filehandle, so treat it as a file path and try to # open the file try: with fopen(fp_, "rb") as fp2_: block = fp2_.read(blocksize) except OSError: # Unable to open file, bail out and return false return False if b"\x00" in block: # Files with null bytes are binary return False elif not block: # An empty file is considered a valid text file return True try: block.decode("utf-8") return True except UnicodeDecodeError: pass nontext = block.translate(None, text_characters) return float(len(nontext)) / len(block) <= 0.30 @jinja_filter("is_bin_file") def is_binary(path): """ Detects if the file is a binary, returns bool. Returns True if the file is a bin, False if the file is not and None if the file is not available. """ if not os.path.isfile(path): return False try: with fopen(path, "rb") as fp_: try: data = fp_.read(2048) data = data.decode(__salt_system_encoding__) return salt.utils.stringutils.is_binary(data) except UnicodeDecodeError: return True except OSError: return False def remove(path): """ Runs os.remove(path) and suppresses the OSError if the file doesn't exist """ try: os.remove(path) except OSError as exc: if exc.errno != errno.ENOENT: raise @jinja_filter("list_files") def list_files(directory): """ Return a list of all files found under directory (and its subdirectories) """ ret = set() ret.add(directory) for root, dirs, files in safe_walk(directory): for name in files: ret.add(os.path.join(root, name)) for name in dirs: ret.add(os.path.join(root, name)) return list(ret) def st_mode_to_octal(mode): """ Convert the st_mode value from a stat(2) call (as returned from os.stat()) to an octal mode. """ try: return oct(mode)[-4:] except (TypeError, IndexError): return "" def normalize_mode(mode): """ Return a mode value, normalized to a string and containing a leading zero if it does not have one. Allow "keep" as a valid mode (used by file state/module to preserve mode from the Salt fileserver in file states). """ if mode is None: return None if not isinstance(mode, str): mode = str(mode) mode = mode.replace("0o", "0") # Strip any quotes any initial zeroes, then though zero-pad it up to 4. # This ensures that somethign like '00644' is normalized to '0644' return mode.strip('"').strip("'").lstrip("0").zfill(4) def human_size_to_bytes(human_size): """ Convert human-readable units to bytes """ size_exp_map = {"K": 1, "M": 2, "G": 3, "T": 4, "P": 5} human_size_str = str(human_size) match = re.match(r"^(\d+)([KMGTP])?$", human_size_str) if not match: raise ValueError( "Size must be all digits, with an optional unit type (K, M, G, T, or P)" ) size_num = int(match.group(1)) unit_multiplier = 1024 ** size_exp_map.get(match.group(2), 0) return size_num * unit_multiplier def backup_minion(path, bkroot): """ Backup a file on the minion """ dname, bname = os.path.split(path) if salt.utils.platform.is_windows(): src_dir = dname.replace(":", "_") else: src_dir = dname[1:] if not salt.utils.platform.is_windows(): fstat = os.stat(path) msecs = str(int(time.time() * 1000000))[-6:] if salt.utils.platform.is_windows(): # ':' is an illegal filesystem path character on Windows stamp = time.strftime("%a_%b_%d_%H-%M-%S_%Y") else: stamp = time.strftime("%a_%b_%d_%H:%M:%S_%Y") stamp = f"{stamp[:-4]}{msecs}_{stamp[-4:]}" bkpath = os.path.join(bkroot, src_dir, f"{bname}_{stamp}") if not os.path.isdir(os.path.dirname(bkpath)): os.makedirs(os.path.dirname(bkpath)) shutil.copyfile(path, bkpath) if not salt.utils.platform.is_windows(): os.chown(bkpath, fstat.st_uid, fstat.st_gid) os.chmod(bkpath, fstat.st_mode) def case_insensitive_filesystem(path=None): """ Detect case insensitivity on a system. Returns: bool: Flag to indicate case insensitivity .. versionadded:: 3004 """ with tempfile.NamedTemporaryFile(prefix="TmP", dir=path, delete=True) as tmp_file: return os.path.exists(tmp_file.name.lower()) def get_encoding(path): """ Detect a file's encoding using the following: - Check for Byte Order Marks (BOM) - Check for UTF-8 Markers - Check System Encoding - Check for ascii Args: path (str): The path to the file to check Returns: str: The encoding of the file Raises: CommandExecutionError: If the encoding cannot be detected """ def check_ascii(_data): # If all characters can be decoded to ASCII, then it's ASCII try: _data.decode("ASCII") log.debug("Found ASCII") except UnicodeDecodeError: return False else: return True def check_bom(_data): # Supported Python Codecs # https://docs.python.org/2/library/codecs.html # https://docs.python.org/3/library/codecs.html boms = [ ("UTF-32-BE", salt.utils.stringutils.to_bytes(codecs.BOM_UTF32_BE)), ("UTF-32-LE", salt.utils.stringutils.to_bytes(codecs.BOM_UTF32_LE)), ("UTF-16-BE", salt.utils.stringutils.to_bytes(codecs.BOM_UTF16_BE)), ("UTF-16-LE", salt.utils.stringutils.to_bytes(codecs.BOM_UTF16_LE)), ("UTF-8", salt.utils.stringutils.to_bytes(codecs.BOM_UTF8)), ("UTF-7", salt.utils.stringutils.to_bytes("\x2b\x2f\x76\x38\x2D")), ("UTF-7", salt.utils.stringutils.to_bytes("\x2b\x2f\x76\x38")), ("UTF-7", salt.utils.stringutils.to_bytes("\x2b\x2f\x76\x39")), ("UTF-7", salt.utils.stringutils.to_bytes("\x2b\x2f\x76\x2b")), ("UTF-7", salt.utils.stringutils.to_bytes("\x2b\x2f\x76\x2f")), ] for _encoding, bom in boms: if _data.startswith(bom): log.debug("Found BOM for %s", _encoding) return _encoding return False def check_utf8_markers(_data): try: decoded = _data.decode("UTF-8") except UnicodeDecodeError: return False else: return True def check_system_encoding(_data): try: _data.decode(__salt_system_encoding__) except UnicodeDecodeError: return False else: return True if not os.path.isfile(path): raise CommandExecutionError("Not a file") try: with fopen(path, "rb") as fp_: data = fp_.read(2048) except OSError: raise CommandExecutionError("Failed to open file") # Check for Unicode BOM encoding = check_bom(data) if encoding: return encoding # Check for UTF-8 markers if check_utf8_markers(data): return "UTF-8" # Check system encoding if check_system_encoding(data): return __salt_system_encoding__ # Check for ASCII first if check_ascii(data): return "ASCII" raise CommandExecutionError("Could not detect file encoding")