salt/fileserver/hgfs.py
# -*- coding: utf-8 -*-
'''
Mercurial Fileserver Backend
To enable, add ``hgfs`` to the :conf_master:`fileserver_backend` option in the
Master config file.
.. code-block:: yaml
fileserver_backend:
- hgfs
.. note::
``hg`` also works here. Prior to the 2018.3.0 release, *only* ``hg`` would
work.
After enabling this backend, branches, bookmarks, and tags in a remote
mercurial repository are exposed to salt as different environments. This
feature is managed by the :conf_master:`fileserver_backend` option in the salt
master config file.
This fileserver has an additional option :conf_master:`hgfs_branch_method` that
will set the desired branch method. Possible values are: ``branches``,
``bookmarks``, or ``mixed``. If using ``branches`` or ``mixed``, the
``default`` branch will be mapped to ``base``.
.. versionchanged:: 2014.1.0
The :conf_master:`hgfs_base` master config parameter was added, allowing
for a branch other than ``default`` to be used for the ``base``
environment, and allowing for a ``base`` environment to be specified when
using an :conf_master:`hgfs_branch_method` of ``bookmarks``.
:depends: - mercurial
- python bindings for mercurial (``python-hglib``)
'''
# Import python libs
from __future__ import absolute_import, print_function, unicode_literals
import copy
import errno
import fnmatch
import glob
import hashlib
import logging
import os
import shutil
from datetime import datetime
from salt.exceptions import FileserverConfigError
VALID_BRANCH_METHODS = ('branches', 'bookmarks', 'mixed')
PER_REMOTE_OVERRIDES = ('base', 'branch_method', 'mountpoint', 'root')
# Import third party libs
from salt.ext import six
# pylint: disable=import-error
try:
import hglib
HAS_HG = True
except ImportError:
HAS_HG = False
# pylint: enable=import-error
# Import salt libs
import salt.utils.data
import salt.utils.files
import salt.utils.gzip_util
import salt.utils.hashutils
import salt.utils.stringutils
import salt.utils.url
import salt.utils.versions
import salt.fileserver
from salt.utils.event import tagify
log = logging.getLogger(__name__)
# Define the module's virtual name
__virtualname__ = 'hg'
def __virtual__():
'''
Only load if mercurial is available
'''
if __virtualname__ not in __opts__['fileserver_backend']:
return False
if not HAS_HG:
log.error('Mercurial fileserver backend is enabled in configuration '
'but could not be loaded, is hglib installed?')
return False
if __opts__['hgfs_branch_method'] not in VALID_BRANCH_METHODS:
log.error(
'Invalid hgfs_branch_method \'%s\'. Valid methods are: %s',
__opts__['hgfs_branch_method'], VALID_BRANCH_METHODS
)
return False
return __virtualname__
def _all_branches(repo):
'''
Returns all branches for the specified repo
'''
# repo.branches() returns a list of 3-tuples consisting of
# (branch name, rev #, nodeid)
# Example: [('default', 4, '7c96229269fa')]
return repo.branches()
def _get_branch(repo, name):
'''
Find the requested branch in the specified repo
'''
try:
return [x for x in _all_branches(repo) if x[0] == name][0]
except IndexError:
return False
def _all_bookmarks(repo):
'''
Returns all bookmarks for the specified repo
'''
# repo.bookmarks() returns a tuple containing the following:
# 1. A list of 3-tuples consisting of (bookmark name, rev #, nodeid)
# 2. The index of the current bookmark (-1 if no current one)
# Example: ([('mymark', 4, '7c96229269fa')], -1)
return repo.bookmarks()[0]
def _get_bookmark(repo, name):
'''
Find the requested bookmark in the specified repo
'''
try:
return [x for x in _all_bookmarks(repo) if x[0] == name][0]
except IndexError:
return False
def _all_tags(repo):
'''
Returns all tags for the specified repo
'''
# repo.tags() returns a list of 4-tuples consisting of
# (tag name, rev #, nodeid, islocal)
# Example: [('1.0', 3, '3be15e71b31a', False),
# ('tip', 4, '7c96229269fa', False)]
# Avoid returning the special 'tip' tag.
return [x for x in repo.tags() if x[0] != 'tip']
def _get_tag(repo, name):
'''
Find the requested tag in the specified repo
'''
try:
return [x for x in _all_tags(repo) if x[0] == name][0]
except IndexError:
return False
def _get_ref(repo, name):
'''
Return ref tuple if ref is in the repo.
'''
if name == 'base':
name = repo['base']
if name == repo['base'] or name in envs():
if repo['branch_method'] == 'branches':
return _get_branch(repo['repo'], name) \
or _get_tag(repo['repo'], name)
elif repo['branch_method'] == 'bookmarks':
return _get_bookmark(repo['repo'], name) \
or _get_tag(repo['repo'], name)
elif repo['branch_method'] == 'mixed':
return _get_branch(repo['repo'], name) \
or _get_bookmark(repo['repo'], name) \
or _get_tag(repo['repo'], name)
return False
def _failhard():
'''
Fatal fileserver configuration issue, raise an exception
'''
raise FileserverConfigError(
'Failed to load hg fileserver backend'
)
def init():
'''
Return a list of hglib objects for the various hgfs remotes
'''
bp_ = os.path.join(__opts__['cachedir'], 'hgfs')
new_remote = False
repos = []
per_remote_defaults = {}
for param in PER_REMOTE_OVERRIDES:
per_remote_defaults[param] = \
six.text_type(__opts__['hgfs_{0}'.format(param)])
for remote in __opts__['hgfs_remotes']:
repo_conf = copy.deepcopy(per_remote_defaults)
if isinstance(remote, dict):
repo_url = next(iter(remote))
per_remote_conf = dict(
[(key, six.text_type(val)) for key, val in
six.iteritems(salt.utils.data.repack_dictlist(remote[repo_url]))]
)
if not per_remote_conf:
log.error(
'Invalid per-remote configuration for hgfs remote %s. If '
'no per-remote parameters are being specified, there may '
'be a trailing colon after the URL, which should be '
'removed. Check the master configuration file.', repo_url
)
_failhard()
branch_method = \
per_remote_conf.get('branch_method',
per_remote_defaults['branch_method'])
if branch_method not in VALID_BRANCH_METHODS:
log.error(
'Invalid branch_method \'%s\' for remote %s. Valid '
'branch methods are: %s. This remote will be ignored.',
branch_method, repo_url, ', '.join(VALID_BRANCH_METHODS)
)
_failhard()
per_remote_errors = False
for param in (x for x in per_remote_conf
if x not in PER_REMOTE_OVERRIDES):
log.error(
'Invalid configuration parameter \'%s\' for remote %s. '
'Valid parameters are: %s. See the documentation for '
'further information.',
param, repo_url, ', '.join(PER_REMOTE_OVERRIDES)
)
per_remote_errors = True
if per_remote_errors:
_failhard()
repo_conf.update(per_remote_conf)
else:
repo_url = remote
if not isinstance(repo_url, six.string_types):
log.error(
'Invalid hgfs remote %s. Remotes must be strings, you may '
'need to enclose the URL in quotes', repo_url
)
_failhard()
try:
repo_conf['mountpoint'] = salt.utils.url.strip_proto(
repo_conf['mountpoint']
)
except TypeError:
# mountpoint not specified
pass
hash_type = getattr(hashlib, __opts__.get('hash_type', 'md5'))
repo_hash = hash_type(repo_url).hexdigest()
rp_ = os.path.join(bp_, repo_hash)
if not os.path.isdir(rp_):
os.makedirs(rp_)
if not os.listdir(rp_):
# Only init if the directory is empty.
hglib.init(rp_)
new_remote = True
try:
repo = hglib.open(rp_)
except hglib.error.ServerError:
log.error(
'Cache path %s (corresponding remote: %s) exists but is not '
'a valid mercurial repository. You will need to manually '
'delete this directory on the master to continue to use this '
'hgfs remote.', rp_, repo_url
)
_failhard()
except Exception as exc:
log.error(
'Exception \'%s\' encountered while initializing hgfs '
'remote %s', exc, repo_url
)
_failhard()
try:
refs = repo.config(names='paths')
except hglib.error.CommandError:
refs = None
# Do NOT put this if statement inside the except block above. Earlier
# versions of hglib did not raise an exception, so we need to do it
# this way to support both older and newer hglib.
if not refs:
# Write an hgrc defining the remote URL
hgconfpath = os.path.join(rp_, '.hg', 'hgrc')
with salt.utils.files.fopen(hgconfpath, 'w+') as hgconfig:
hgconfig.write('[paths]\n')
hgconfig.write(
salt.utils.stringutils.to_str(
'default = {0}\n'.format(repo_url)
)
)
repo_conf.update({
'repo': repo,
'url': repo_url,
'hash': repo_hash,
'cachedir': rp_,
'lockfile': os.path.join(__opts__['cachedir'],
'hgfs',
'{0}.update.lk'.format(repo_hash))
})
repos.append(repo_conf)
repo.close()
if new_remote:
remote_map = os.path.join(__opts__['cachedir'], 'hgfs/remote_map.txt')
try:
with salt.utils.files.fopen(remote_map, 'w+') as fp_:
timestamp = datetime.now().strftime('%d %b %Y %H:%M:%S.%f')
fp_.write('# hgfs_remote map as of {0}\n'.format(timestamp))
for repo in repos:
fp_.write(
salt.utils.stringutils.to_str(
'{0} = {1}\n'.format(repo['hash'], repo['url'])
)
)
except OSError:
pass
else:
log.info('Wrote new hgfs_remote map to %s', remote_map)
return repos
def _clear_old_remotes():
'''
Remove cache directories for remotes no longer configured
'''
bp_ = os.path.join(__opts__['cachedir'], 'hgfs')
try:
cachedir_ls = os.listdir(bp_)
except OSError:
cachedir_ls = []
repos = init()
# Remove actively-used remotes from list
for repo in repos:
try:
cachedir_ls.remove(repo['hash'])
except ValueError:
pass
to_remove = []
for item in cachedir_ls:
if item in ('hash', 'refs'):
continue
path = os.path.join(bp_, item)
if os.path.isdir(path):
to_remove.append(path)
failed = []
if to_remove:
for rdir in to_remove:
try:
shutil.rmtree(rdir)
except OSError as exc:
log.error(
'Unable to remove old hgfs remote cachedir %s: %s',
rdir, exc
)
failed.append(rdir)
else:
log.debug('hgfs removed old cachedir %s', rdir)
for fdir in failed:
to_remove.remove(fdir)
return bool(to_remove), repos
def clear_cache():
'''
Completely clear hgfs cache
'''
fsb_cachedir = os.path.join(__opts__['cachedir'], 'hgfs')
list_cachedir = os.path.join(__opts__['cachedir'], 'file_lists/hgfs')
errors = []
for rdir in (fsb_cachedir, list_cachedir):
if os.path.exists(rdir):
try:
shutil.rmtree(rdir)
except OSError as exc:
errors.append('Unable to delete {0}: {1}'.format(rdir, exc))
return errors
def clear_lock(remote=None):
'''
Clear update.lk
``remote`` can either be a dictionary containing repo configuration
information, or a pattern. If the latter, then remotes for which the URL
matches the pattern will be locked.
'''
def _do_clear_lock(repo):
def _add_error(errlist, repo, exc):
msg = ('Unable to remove update lock for {0} ({1}): {2} '
.format(repo['url'], repo['lockfile'], exc))
log.debug(msg)
errlist.append(msg)
success = []
failed = []
if os.path.exists(repo['lockfile']):
try:
os.remove(repo['lockfile'])
except OSError as exc:
if exc.errno == errno.EISDIR:
# Somehow this path is a directory. Should never happen
# unless some wiseguy manually creates a directory at this
# path, but just in case, handle it.
try:
shutil.rmtree(repo['lockfile'])
except OSError as exc:
_add_error(failed, repo, exc)
else:
_add_error(failed, repo, exc)
else:
msg = 'Removed lock for {0}'.format(repo['url'])
log.debug(msg)
success.append(msg)
return success, failed
if isinstance(remote, dict):
return _do_clear_lock(remote)
cleared = []
errors = []
for repo in init():
if remote:
try:
if not fnmatch.fnmatch(repo['url'], remote):
continue
except TypeError:
# remote was non-string, try again
if not fnmatch.fnmatch(repo['url'], six.text_type(remote)):
continue
success, failed = _do_clear_lock(repo)
cleared.extend(success)
errors.extend(failed)
return cleared, errors
def lock(remote=None):
'''
Place an update.lk
``remote`` can either be a dictionary containing repo configuration
information, or a pattern. If the latter, then remotes for which the URL
matches the pattern will be locked.
'''
def _do_lock(repo):
success = []
failed = []
if not os.path.exists(repo['lockfile']):
try:
with salt.utils.files.fopen(repo['lockfile'], 'w'):
pass
except (IOError, OSError) as exc:
msg = ('Unable to set update lock for {0} ({1}): {2} '
.format(repo['url'], repo['lockfile'], exc))
log.debug(msg)
failed.append(msg)
else:
msg = 'Set lock for {0}'.format(repo['url'])
log.debug(msg)
success.append(msg)
return success, failed
if isinstance(remote, dict):
return _do_lock(remote)
locked = []
errors = []
for repo in init():
if remote:
try:
if not fnmatch.fnmatch(repo['url'], remote):
continue
except TypeError:
# remote was non-string, try again
if not fnmatch.fnmatch(repo['url'], six.text_type(remote)):
continue
success, failed = _do_lock(repo)
locked.extend(success)
errors.extend(failed)
return locked, errors
def update():
'''
Execute an hg pull on all of the repos
'''
# data for the fileserver event
data = {'changed': False,
'backend': 'hgfs'}
# _clear_old_remotes runs init(), so use the value from there to avoid a
# second init()
data['changed'], repos = _clear_old_remotes()
for repo in repos:
if os.path.exists(repo['lockfile']):
log.warning(
'Update lockfile is present for hgfs remote %s, skipping. '
'If this warning persists, it is possible that the update '
'process was interrupted. Removing %s or running '
'\'salt-run fileserver.clear_lock hgfs\' will allow updates '
'to continue for this remote.', repo['url'], repo['lockfile']
)
continue
_, errors = lock(repo)
if errors:
log.error(
'Unable to set update lock for hgfs remote %s, skipping.',
repo['url']
)
continue
log.debug('hgfs is fetching from %s', repo['url'])
repo['repo'].open()
curtip = repo['repo'].tip()
try:
repo['repo'].pull()
except Exception as exc:
log.error(
'Exception %s caught while updating hgfs remote %s',
exc, repo['url'], exc_info_on_loglevel=logging.DEBUG
)
else:
newtip = repo['repo'].tip()
if curtip[1] != newtip[1]:
data['changed'] = True
repo['repo'].close()
clear_lock(repo)
env_cache = os.path.join(__opts__['cachedir'], 'hgfs/envs.p')
if data.get('changed', False) is True or not os.path.isfile(env_cache):
env_cachedir = os.path.dirname(env_cache)
if not os.path.exists(env_cachedir):
os.makedirs(env_cachedir)
new_envs = envs(ignore_cache=True)
serial = salt.payload.Serial(__opts__)
with salt.utils.files.fopen(env_cache, 'wb+') as fp_:
fp_.write(serial.dumps(new_envs))
log.trace('Wrote env cache data to %s', env_cache)
# if there is a change, fire an event
if __opts__.get('fileserver_events', False):
with salt.utils.event.get_event(
'master',
__opts__['sock_dir'],
__opts__['transport'],
opts=__opts__,
listen=False) as event:
event.fire_event(data, tagify(['hgfs', 'update'], prefix='fileserver'))
try:
salt.fileserver.reap_fileserver_cache_dir(
os.path.join(__opts__['cachedir'], 'hgfs/hash'),
find_file
)
except (IOError, OSError):
# Hash file won't exist if no files have yet been served up
pass
def _env_is_exposed(env):
'''
Check if an environment is exposed by comparing it against a whitelist and
blacklist.
'''
if __opts__['hgfs_env_whitelist']:
salt.utils.versions.warn_until(
'Neon',
'The hgfs_env_whitelist config option has been renamed to '
'hgfs_saltenv_whitelist. Please update your configuration.'
)
whitelist = __opts__['hgfs_env_whitelist']
else:
whitelist = __opts__['hgfs_saltenv_whitelist']
if __opts__['hgfs_env_blacklist']:
salt.utils.versions.warn_until(
'Neon',
'The hgfs_env_blacklist config option has been renamed to '
'hgfs_saltenv_blacklist. Please update your configuration.'
)
blacklist = __opts__['hgfs_env_blacklist']
else:
blacklist = __opts__['hgfs_saltenv_blacklist']
return salt.utils.stringutils.check_whitelist_blacklist(
env,
whitelist=whitelist,
blacklist=blacklist,
)
def envs(ignore_cache=False):
'''
Return a list of refs that can be used as environments
'''
if not ignore_cache:
env_cache = os.path.join(__opts__['cachedir'], 'hgfs/envs.p')
cache_match = salt.fileserver.check_env_cache(__opts__, env_cache)
if cache_match is not None:
return cache_match
ret = set()
for repo in init():
repo['repo'].open()
if repo['branch_method'] in ('branches', 'mixed'):
for branch in _all_branches(repo['repo']):
branch_name = branch[0]
if branch_name == repo['base']:
branch_name = 'base'
ret.add(branch_name)
if repo['branch_method'] in ('bookmarks', 'mixed'):
for bookmark in _all_bookmarks(repo['repo']):
bookmark_name = bookmark[0]
if bookmark_name == repo['base']:
bookmark_name = 'base'
ret.add(bookmark_name)
ret.update([x[0] for x in _all_tags(repo['repo'])])
repo['repo'].close()
return [x for x in sorted(ret) if _env_is_exposed(x)]
def find_file(path, tgt_env='base', **kwargs): # pylint: disable=W0613
'''
Find the first file to match the path and ref, read the file out of hg
and send the path to the newly cached file
'''
fnd = {'path': '',
'rel': ''}
if os.path.isabs(path) or tgt_env not in envs():
return fnd
dest = os.path.join(__opts__['cachedir'], 'hgfs/refs', tgt_env, path)
hashes_glob = os.path.join(__opts__['cachedir'],
'hgfs/hash',
tgt_env,
'{0}.hash.*'.format(path))
blobshadest = os.path.join(__opts__['cachedir'],
'hgfs/hash',
tgt_env,
'{0}.hash.blob_sha1'.format(path))
lk_fn = os.path.join(__opts__['cachedir'],
'hgfs/hash',
tgt_env,
'{0}.lk'.format(path))
destdir = os.path.dirname(dest)
hashdir = os.path.dirname(blobshadest)
if not os.path.isdir(destdir):
try:
os.makedirs(destdir)
except OSError:
# Path exists and is a file, remove it and retry
os.remove(destdir)
os.makedirs(destdir)
if not os.path.isdir(hashdir):
try:
os.makedirs(hashdir)
except OSError:
# Path exists and is a file, remove it and retry
os.remove(hashdir)
os.makedirs(hashdir)
for repo in init():
if repo['mountpoint'] \
and not path.startswith(repo['mountpoint'] + os.path.sep):
continue
repo_path = path[len(repo['mountpoint']):].lstrip(os.path.sep)
if repo['root']:
repo_path = os.path.join(repo['root'], repo_path)
repo['repo'].open()
ref = _get_ref(repo, tgt_env)
if not ref:
# Branch or tag not found in repo, try the next
repo['repo'].close()
continue
salt.fileserver.wait_lock(lk_fn, dest)
if os.path.isfile(blobshadest) and os.path.isfile(dest):
with salt.utils.files.fopen(blobshadest, 'r') as fp_:
sha = fp_.read()
if sha == ref[2]:
fnd['rel'] = path
fnd['path'] = dest
repo['repo'].close()
return fnd
try:
repo['repo'].cat(
['path:{0}'.format(repo_path)], rev=ref[2], output=dest
)
except hglib.error.CommandError:
repo['repo'].close()
continue
with salt.utils.files.fopen(lk_fn, 'w'):
pass
for filename in glob.glob(hashes_glob):
try:
os.remove(filename)
except Exception:
pass
with salt.utils.files.fopen(blobshadest, 'w+') as fp_:
fp_.write(ref[2])
try:
os.remove(lk_fn)
except (OSError, IOError):
pass
fnd['rel'] = path
fnd['path'] = dest
try:
# Converting the stat result to a list, the elements of the
# list correspond to the following stat_result params:
# 0 => st_mode=33188
# 1 => st_ino=10227377
# 2 => st_dev=65026
# 3 => st_nlink=1
# 4 => st_uid=1000
# 5 => st_gid=1000
# 6 => st_size=1056233
# 7 => st_atime=1468284229
# 8 => st_mtime=1456338235
# 9 => st_ctime=1456338235
fnd['stat'] = list(os.stat(dest))
except Exception:
pass
repo['repo'].close()
return fnd
return fnd
def serve_file(load, fnd):
'''
Return a chunk from a file based on the data received
'''
if 'env' in load:
# "env" is not supported; Use "saltenv".
load.pop('env')
ret = {'data': '',
'dest': ''}
if not all(x in load for x in ('path', 'loc', 'saltenv')):
return ret
if not fnd['path']:
return ret
ret['dest'] = fnd['rel']
gzip = load.get('gzip', None)
fpath = os.path.normpath(fnd['path'])
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 file_hash(load, fnd):
'''
Return a file hash, the hash type is set in the master config file
'''
if 'env' in load:
# "env" is not supported; Use "saltenv".
load.pop('env')
if not all(x in load for x in ('path', 'saltenv')):
return ''
ret = {'hash_type': __opts__['hash_type']}
relpath = fnd['rel']
path = fnd['path']
hashdest = os.path.join(__opts__['cachedir'],
'hgfs/hash',
load['saltenv'],
'{0}.hash.{1}'.format(relpath,
__opts__['hash_type']))
if not os.path.isfile(hashdest):
ret['hsum'] = salt.utils.hashutils.get_hash(path, __opts__['hash_type'])
with salt.utils.files.fopen(hashdest, 'w+') as fp_:
fp_.write(ret['hsum'])
return ret
else:
with salt.utils.files.fopen(hashdest, 'rb') as fp_:
ret['hsum'] = salt.utils.stringutils.to_unicode(fp_.read())
return ret
def _file_lists(load, form):
'''
Return a dict containing the file lists for files and dirs
'''
if 'env' in load:
# "env" is not supported; Use "saltenv".
load.pop('env')
list_cachedir = os.path.join(__opts__['cachedir'], 'file_lists/hgfs')
if not os.path.isdir(list_cachedir):
try:
os.makedirs(list_cachedir)
except os.error:
log.critical('Unable to make cachedir %s', list_cachedir)
return []
list_cache = os.path.join(list_cachedir, '{0}.p'.format(load['saltenv']))
w_lock = os.path.join(list_cachedir, '.{0}.w'.format(load['saltenv']))
cache_match, refresh_cache, save_cache = \
salt.fileserver.check_file_list_cache(
__opts__, form, list_cache, w_lock
)
if cache_match is not None:
return cache_match
if refresh_cache:
ret = {}
ret['files'] = _get_file_list(load)
ret['dirs'] = _get_dir_list(load)
if save_cache:
salt.fileserver.write_file_list_cache(
__opts__, ret, list_cache, w_lock
)
return ret.get(form, [])
# Shouldn't get here, but if we do, this prevents a TypeError
return []
def file_list(load):
'''
Return a list of all files on the file server in a specified environment
'''
return _file_lists(load, 'files')
def _get_file_list(load):
'''
Get 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 'saltenv' not in load or load['saltenv'] not in envs():
return []
ret = set()
for repo in init():
repo['repo'].open()
ref = _get_ref(repo, load['saltenv'])
if ref:
manifest = repo['repo'].manifest(rev=ref[1])
for tup in manifest:
relpath = os.path.relpath(tup[4], repo['root'])
# Don't add files outside the hgfs_root
if not relpath.startswith('../'):
ret.add(os.path.join(repo['mountpoint'], relpath))
repo['repo'].close()
return sorted(ret)
def file_list_emptydirs(load): # pylint: disable=W0613
'''
Return a list of all empty directories on the master
'''
# Cannot have empty dirs in hg
return []
def dir_list(load):
'''
Return a list of all directories on the master
'''
return _file_lists(load, 'dirs')
def _get_dir_list(load):
'''
Get a list of all directories on the master
'''
if 'env' in load:
# "env" is not supported; Use "saltenv".
load.pop('env')
if 'saltenv' not in load or load['saltenv'] not in envs():
return []
ret = set()
for repo in init():
repo['repo'].open()
ref = _get_ref(repo, load['saltenv'])
if ref:
manifest = repo['repo'].manifest(rev=ref[1])
for tup in manifest:
filepath = tup[4]
split = filepath.rsplit('/', 1)
while len(split) > 1:
relpath = os.path.relpath(split[0], repo['root'])
# Don't add '.'
if relpath != '.':
# Don't add files outside the hgfs_root
if not relpath.startswith('../'):
ret.add(os.path.join(repo['mountpoint'], relpath))
split = split[0].rsplit('/', 1)
repo['repo'].close()
if repo['mountpoint']:
ret.add(repo['mountpoint'])
return sorted(ret)