from __future__ import absolute_import # Import python libs import copy import logging import os # Import salt libs import salt.loader import salt.utils import salt.utils.gitfs import salt.utils.dictupdate import salt.pillar.git_pillar # Import third party libs import salt.ext.six as six try: from salt.utils.data import repack_dictlist except ImportError: from salt.utils import repack_dictlist PER_REMOTE_OVERRIDES = salt.pillar.git_pillar.PER_REMOTE_OVERRIDES PER_REMOTE_ONLY = tuple(set(list(salt.pillar.git_pillar.PER_REMOTE_ONLY) + ['stack'])) # Set up logging log = logging.getLogger(__name__) # Define the module's virtual name __virtualname__ = 'gitstack' def __virtual__(): ''' Only load if gitstack pillars are defined ''' gitstack_pillars = [x for x in __opts__['ext_pillar'] if 'gitstack' in x] if not gitstack_pillars: # No gitstack external pillars were configured return False return __virtualname__ def ext_pillar(minion_id, pillar, *repos, **single_repo_conf): # Checkout the ext_pillar sources opts = copy.deepcopy(__opts__) opts['pillar_roots'] = {} opts['__git_pillar'] = True stacks = [] invalid_repos_idx = [] ## legacy configuration with a plain dict under gitstack ext_pillar key if single_repo_conf and single_repo_conf.get('repo', None) is not None: branch = single_repo_conf.get('branch', 'master') repo = single_repo_conf['repo'] remote = ' '.join([branch, repo]) init_gitpillar_args = [ [remote], PER_REMOTE_OVERRIDES, PER_REMOTE_ONLY ] if 'stack' not in single_repo_conf: log.error('A stack key is mandatory in gitstack configuration') return {} ## new configuration way elif isinstance(repos, (list, tuple)) and len(repos) > 0: for repo_idx, repo in enumerate(repos): kw = repack_dictlist(repo[next(iter(repo))]) if 'stack' not in kw: # stack param is mandatory in gitstack repos configuration log.warning('Configuration for gitstack must contain a stack key for each repo.') log.warning('Configured gitstack repo %s (at position %d) will be ignored' % (next(iter(repo)), repo_idx)) invalid_repos_idx.append(repo_idx) continue stacks.append(kw['stack']) valid_repos = [repo for repo_idx, repo in enumerate(repos) if repo_idx not in invalid_repos_idx] init_gitpillar_args = [ valid_repos, PER_REMOTE_OVERRIDES, PER_REMOTE_ONLY ] else: ### Invalid configuration log.error('Configuration for gitstack must be a list of dicts or a single dict') return {} # check arguments to use with GitPillar, we could check also salt version if len(_get_function_varnames(salt.utils.gitfs.GitPillar.__init__)) > 2: # Include GLOBAL_ONLY args for Salt versions that require it if 'global_only' in _get_function_varnames(salt.utils.gitfs.GitPillar.__init__): init_gitpillar_args.append(salt.pillar.git_pillar.GLOBAL_ONLY) # Initialize GitPillar object gitpillar = salt.utils.gitfs.GitPillar(opts, *init_gitpillar_args, role="git_pillar") else: # Include GLOBAL_ONLY args for Salt versions that require it if 'global_only' in _get_function_varnames(salt.utils.gitfs.GitPillar.init_remotes): init_gitpillar_args.append(salt.pillar.git_pillar.GLOBAL_ONLY) # Initialize GitPillar object gitpillar = salt.utils.gitfs.GitPillar(opts, role="git_pillar") gitpillar.init_remotes(*init_gitpillar_args) if __opts__.get('__role') == 'minion': # If masterless, fetch the remotes. We'll need to remove this once # we make the minion daemon able to run standalone. gitpillar.fetch_remotes() gitpillar.checkout() # Prepend the local path of the cloned Git repo if not gitpillar.pillar_dirs: log.error('Repositories used by gitstack must be included in the git pillar configuration') return {} # Initialize variables stack_config = [] stack_config_kwargs = {} # Replace relative paths with the absolute path of the cloned repository if single_repo_conf: stack_config = _resolve_stack(single_repo_conf['stack'], list(gitpillar.pillar_dirs.items())[0][0]) else: pillar_dirs = list(gitpillar.pillar_dirs.keys()) for idx, stack in enumerate(stacks): try: pillar_dir = pillar_dirs[idx] except IndexError: log.warning('Ignoring gitstack stack configuration: %s' % stack) log.warning('Ignoring gitstack repo maybe failed checkout') continue if isinstance(stack, dict): # TODO: use salt.utils.dictupdate.merge stack_config_kwargs.update(_resolve_stack(stack, pillar_dir)) elif isinstance(stack, list): stack_config.extend(_resolve_stack(stack, pillar_dir)) else: stack_config.append(_resolve_stack(stack, pillar_dir)) # Load the 'stack' pillar module stack_pillar = salt.loader.pillars(__opts__, __salt__, __context__)['stack'] # Call the 'stack' pillar module if isinstance(stack_config, dict): return stack_pillar(minion_id, pillar, **stack_config) elif isinstance(stack_config, list): return stack_pillar(minion_id, pillar, *stack_config, **stack_config_kwargs) else: return stack_pillar(minion_id, pillar, stack_config) def _resolve_stack(x, path): ''' Resolve relative paths to the absolute path of the cloned Git repo ''' if isinstance(x, dict): y = {} for key, value in six.iteritems(x): y[key] = _resolve_stack(value, path) elif isinstance(x, list): y = [] for item in x: y.append(_resolve_stack(item, path)) elif isinstance(x, six.string_types): y = os.path.join(path, x) else: y = x return y def _get_function_varnames(fn): ''' Return the var names for a function ''' if six.PY2: return fn.im_func.func_code.co_varnames return fn.__code__.co_varnames