mirror of
https://github.com/saltstack/salt.git
synced 2025-04-17 10:10:20 +00:00
356 lines
12 KiB
Python
356 lines
12 KiB
Python
"""
|
|
Fileserver backend which serves files pushed to the Master
|
|
|
|
The :mod:`cp.push <salt.modules.cp.push>` function allows Minions to push files
|
|
up to the Master. Using this backend, these pushed files are exposed to other
|
|
Minions via the Salt fileserver.
|
|
|
|
To enable minionfs, :conf_master:`file_recv` needs to be set to ``True`` in the
|
|
master config file (otherwise :mod:`cp.push <salt.modules.cp.push>` will not be
|
|
allowed to push files to the Master), and ``minionfs`` must be added to the
|
|
:conf_master:`fileserver_backends` list.
|
|
|
|
.. code-block:: yaml
|
|
|
|
fileserver_backend:
|
|
- minionfs
|
|
|
|
.. note::
|
|
``minion`` also works here. Prior to the 2018.3.0 release, *only*
|
|
``minion`` would work.
|
|
|
|
Other minionfs settings include: :conf_master:`minionfs_whitelist`,
|
|
:conf_master:`minionfs_blacklist`, :conf_master:`minionfs_mountpoint`, and
|
|
:conf_master:`minionfs_env`.
|
|
|
|
.. seealso:: :ref:`tutorial-minionfs`
|
|
|
|
"""
|
|
|
|
import logging
|
|
import os
|
|
|
|
import salt.fileserver
|
|
import salt.utils.files
|
|
import salt.utils.gzip_util
|
|
import salt.utils.hashutils
|
|
import salt.utils.path
|
|
import salt.utils.stringutils
|
|
import salt.utils.url
|
|
import salt.utils.versions
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
# Define the module's virtual name
|
|
__virtualname__ = "minionfs"
|
|
|
|
|
|
def __virtual__():
|
|
"""
|
|
Only load if file_recv is enabled
|
|
"""
|
|
if __virtualname__ not in __opts__["fileserver_backend"]:
|
|
return False
|
|
return __virtualname__ if __opts__["file_recv"] else False
|
|
|
|
|
|
def _is_exposed(minion):
|
|
"""
|
|
Check if the minion is exposed, based on the whitelist and blacklist
|
|
"""
|
|
return salt.utils.stringutils.check_whitelist_blacklist(
|
|
minion,
|
|
whitelist=__opts__["minionfs_whitelist"],
|
|
blacklist=__opts__["minionfs_blacklist"],
|
|
)
|
|
|
|
|
|
def find_file(path, tgt_env="base", **kwargs): # pylint: disable=W0613
|
|
"""
|
|
Search the environment for the relative path
|
|
"""
|
|
fnd = {"path": "", "rel": ""}
|
|
if os.path.isabs(path):
|
|
return fnd
|
|
if tgt_env not in envs():
|
|
return fnd
|
|
if os.path.basename(path) == "top.sls":
|
|
log.debug(
|
|
"minionfs will NOT serve top.sls for security reasons (path requested: %s)",
|
|
path,
|
|
)
|
|
return fnd
|
|
|
|
mountpoint = salt.utils.url.strip_proto(__opts__["minionfs_mountpoint"])
|
|
# Remove the mountpoint to get the "true" path
|
|
path = path[len(mountpoint) :].lstrip(os.path.sep)
|
|
try:
|
|
minion, pushed_file = path.split(os.sep, 1)
|
|
except ValueError:
|
|
return fnd
|
|
if not _is_exposed(minion):
|
|
return fnd
|
|
full = os.path.join(__opts__["cachedir"], "minions", minion, "files", pushed_file)
|
|
if os.path.isfile(full) and not salt.fileserver.is_file_ignored(__opts__, full):
|
|
fnd["path"] = full
|
|
fnd["rel"] = path
|
|
fnd["stat"] = list(os.stat(full))
|
|
return fnd
|
|
return fnd
|
|
|
|
|
|
def envs():
|
|
"""
|
|
Returns the one environment specified for minionfs in the master
|
|
configuration.
|
|
"""
|
|
return [__opts__["minionfs_env"]]
|
|
|
|
|
|
def serve_file(load, fnd):
|
|
"""
|
|
Return a chunk from a file based on the data received
|
|
|
|
CLI Example:
|
|
|
|
.. code-block:: bash
|
|
|
|
# Push the file to the master
|
|
$ salt 'source-minion' cp.push /path/to/the/file
|
|
$ salt 'destination-minion' cp.get_file salt://source-minion/path/to/the/file /destination/file
|
|
"""
|
|
ret = {"data": "", "dest": ""}
|
|
if not fnd["path"]:
|
|
return ret
|
|
ret["dest"] = fnd["rel"]
|
|
gzip = load.get("gzip", None)
|
|
fpath = os.path.normpath(fnd["path"])
|
|
|
|
# AP
|
|
# May I sleep here to slow down serving of big files?
|
|
# How many threads are serving files?
|
|
with salt.utils.files.fopen(fpath, "rb") as fp_:
|
|
fp_.seek(load["loc"])
|
|
data = fp_.read(__opts__["file_buffer_size"])
|
|
if data and not salt.utils.files.is_binary(fpath):
|
|
data = data.decode(__salt_system_encoding__)
|
|
if gzip and data:
|
|
data = salt.utils.gzip_util.compress(data, gzip)
|
|
ret["gzip"] = gzip
|
|
ret["data"] = data
|
|
return ret
|
|
|
|
|
|
def update():
|
|
"""
|
|
When we are asked to update (regular interval) lets reap the cache
|
|
"""
|
|
try:
|
|
salt.fileserver.reap_fileserver_cache_dir(
|
|
os.path.join(__opts__["cachedir"], "minionfs/hash"), find_file
|
|
)
|
|
except OSError:
|
|
# Hash file won't exist if no files have yet been served up
|
|
pass
|
|
|
|
|
|
def file_hash(load, fnd):
|
|
"""
|
|
Return a file hash, the hash type is set in the master config file
|
|
"""
|
|
path = fnd["path"]
|
|
ret = {}
|
|
|
|
if "env" in load:
|
|
# "env" is not supported; Use "saltenv".
|
|
load.pop("env")
|
|
|
|
if load["saltenv"] not in envs():
|
|
return {}
|
|
|
|
# if the file doesn't exist, we can't get a hash
|
|
if not path or not os.path.isfile(path):
|
|
return ret
|
|
|
|
# set the hash_type as it is determined by config-- so mechanism won't change that
|
|
ret["hash_type"] = __opts__["hash_type"]
|
|
|
|
# check if the hash is cached
|
|
# cache file's contents should be "hash:mtime"
|
|
cache_path = os.path.join(
|
|
__opts__["cachedir"],
|
|
"minionfs",
|
|
"hash",
|
|
load["saltenv"],
|
|
"{}.hash.{}".format(fnd["rel"], __opts__["hash_type"]),
|
|
)
|
|
# if we have a cache, serve that if the mtime hasn't changed
|
|
if os.path.exists(cache_path):
|
|
try:
|
|
with salt.utils.files.fopen(cache_path, "rb") as fp_:
|
|
try:
|
|
hsum, mtime = salt.utils.stringutils.to_unicode(fp_.read()).split(
|
|
":"
|
|
)
|
|
except ValueError:
|
|
log.debug(
|
|
"Fileserver attempted to read incomplete cache file. Retrying."
|
|
)
|
|
file_hash(load, fnd)
|
|
return ret
|
|
if os.path.getmtime(path) == mtime:
|
|
# check if mtime changed
|
|
ret["hsum"] = hsum
|
|
return ret
|
|
# Can't use Python select() because we need Windows support
|
|
except OSError:
|
|
log.debug("Fileserver encountered lock when reading cache file. Retrying.")
|
|
file_hash(load, fnd)
|
|
return ret
|
|
|
|
# if we don't have a cache entry-- lets make one
|
|
ret["hsum"] = salt.utils.hashutils.get_hash(path, __opts__["hash_type"])
|
|
cache_dir = os.path.dirname(cache_path)
|
|
# make cache directory if it doesn't exist
|
|
if not os.path.exists(cache_dir):
|
|
os.makedirs(cache_dir)
|
|
# save the cache object "hash:mtime"
|
|
cache_object = "{}:{}".format(ret["hsum"], os.path.getmtime(path))
|
|
with salt.utils.files.flopen(cache_path, "w") as fp_:
|
|
fp_.write(cache_object)
|
|
return ret
|
|
|
|
|
|
def file_list(load):
|
|
"""
|
|
Return a list of all files on the file server in a specified environment
|
|
"""
|
|
if "env" in load:
|
|
# "env" is not supported; Use "saltenv".
|
|
load.pop("env")
|
|
|
|
if load["saltenv"] not in envs():
|
|
return []
|
|
mountpoint = salt.utils.url.strip_proto(__opts__["minionfs_mountpoint"])
|
|
prefix = load.get("prefix", "").strip("/")
|
|
if mountpoint and prefix.startswith(mountpoint + os.path.sep):
|
|
prefix = prefix[len(mountpoint + os.path.sep) :]
|
|
|
|
minions_cache_dir = os.path.join(__opts__["cachedir"], "minions")
|
|
minion_dirs = os.listdir(minions_cache_dir)
|
|
|
|
# If the prefix is not an empty string, then get the minion id from it. The
|
|
# minion ID will be the part before the first slash, so if there is no
|
|
# slash, this is an invalid path.
|
|
if prefix:
|
|
tgt_minion, _, prefix = prefix.partition("/")
|
|
if not prefix:
|
|
# No minion ID in path
|
|
return []
|
|
# Reassign minion_dirs so we don't unnecessarily walk every minion's
|
|
# pushed files
|
|
if tgt_minion not in minion_dirs:
|
|
log.warning(
|
|
"No files found in minionfs cache for minion ID '%s'", tgt_minion
|
|
)
|
|
return []
|
|
minion_dirs = [tgt_minion]
|
|
|
|
ret = []
|
|
for minion in minion_dirs:
|
|
if not _is_exposed(minion):
|
|
continue
|
|
minion_files_dir = os.path.join(minions_cache_dir, minion, "files")
|
|
if not os.path.isdir(minion_files_dir):
|
|
log.debug(
|
|
"minionfs: could not find files directory under %s!",
|
|
os.path.join(minions_cache_dir, minion),
|
|
)
|
|
continue
|
|
walk_dir = os.path.join(minion_files_dir, prefix)
|
|
# Do not follow links for security reasons
|
|
for root, _, files in salt.utils.path.os_walk(walk_dir, followlinks=False):
|
|
for fname in files:
|
|
# Ignore links for security reasons
|
|
if os.path.islink(os.path.join(root, fname)):
|
|
continue
|
|
relpath = os.path.relpath(os.path.join(root, fname), minion_files_dir)
|
|
if relpath.startswith("../"):
|
|
continue
|
|
rel_fn = os.path.join(mountpoint, minion, relpath)
|
|
if not salt.fileserver.is_file_ignored(__opts__, rel_fn):
|
|
ret.append(rel_fn)
|
|
return ret
|
|
|
|
|
|
# There should be no emptydirs
|
|
# def file_list_emptydirs(load):
|
|
|
|
|
|
def dir_list(load):
|
|
"""
|
|
Return a list of all directories on the master
|
|
|
|
CLI Example:
|
|
|
|
.. code-block:: bash
|
|
|
|
$ salt 'source-minion' cp.push /absolute/path/file # Push the file to the master
|
|
$ salt 'destination-minion' cp.list_master_dirs
|
|
destination-minion:
|
|
- source-minion/absolute
|
|
- source-minion/absolute/path
|
|
"""
|
|
if "env" in load:
|
|
# "env" is not supported; Use "saltenv".
|
|
load.pop("env")
|
|
|
|
if load["saltenv"] not in envs():
|
|
return []
|
|
mountpoint = salt.utils.url.strip_proto(__opts__["minionfs_mountpoint"])
|
|
prefix = load.get("prefix", "").strip("/")
|
|
if mountpoint and prefix.startswith(mountpoint + os.path.sep):
|
|
prefix = prefix[len(mountpoint + os.path.sep) :]
|
|
|
|
minions_cache_dir = os.path.join(__opts__["cachedir"], "minions")
|
|
minion_dirs = os.listdir(minions_cache_dir)
|
|
|
|
# If the prefix is not an empty string, then get the minion id from it. The
|
|
# minion ID will be the part before the first slash, so if there is no
|
|
# slash, this is an invalid path.
|
|
if prefix:
|
|
tgt_minion, _, prefix = prefix.partition("/")
|
|
if not prefix:
|
|
# No minion ID in path
|
|
return []
|
|
# Reassign minion_dirs so we don't unnecessarily walk every minion's
|
|
# pushed files
|
|
if tgt_minion not in minion_dirs:
|
|
log.warning(
|
|
"No files found in minionfs cache for minion ID '%s'", tgt_minion
|
|
)
|
|
return []
|
|
minion_dirs = [tgt_minion]
|
|
|
|
ret = []
|
|
for minion in os.listdir(minions_cache_dir):
|
|
if not _is_exposed(minion):
|
|
continue
|
|
minion_files_dir = os.path.join(minions_cache_dir, minion, "files")
|
|
if not os.path.isdir(minion_files_dir):
|
|
log.warning(
|
|
"minionfs: could not find files directory under %s!",
|
|
os.path.join(minions_cache_dir, minion),
|
|
)
|
|
continue
|
|
walk_dir = os.path.join(minion_files_dir, prefix)
|
|
# Do not follow links for security reasons
|
|
for root, _, _ in salt.utils.path.os_walk(walk_dir, followlinks=False):
|
|
relpath = os.path.relpath(root, minion_files_dir)
|
|
# Ensure that the current directory and directories outside of
|
|
# the minion dir do not end up in return list
|
|
if relpath in (".", "..") or relpath.startswith("../"):
|
|
continue
|
|
ret.append(os.path.join(mountpoint, minion, relpath))
|
|
return ret
|