File: //usr/lib/python2.7/site-packages/salt/fileserver/minionfs.py
# -*- coding: utf-8 -*-
'''
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`
'''
from __future__ import absolute_import, print_function, unicode_literals
# Import python libs
import os
import logging
# Import salt libs
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
# Import third party libs
from salt.ext import six
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 six.PY3 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 os.error:
# 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'],
'{0}.hash.{1}'.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 os.error:
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 = '{0}:{1}'.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