saltstack/salt

View on GitHub
salt/runner.py

Summary

Maintainability
D
1 day
Test Coverage
# -*- coding: utf-8 -*-
'''
Execute salt convenience routines
'''

# Import python libs
from __future__ import absolute_import, print_function, unicode_literals
import os
import logging

# Import salt libs
import salt.exceptions
import salt.loader
import salt.minion
import salt.utils.args
import salt.utils.event
import salt.utils.files
import salt.utils.user
import salt.defaults.exitcodes
from salt.client import mixins
from salt.output import display_output
from salt.utils.lazy import verify_fun

log = logging.getLogger(__name__)


class RunnerClient(mixins.SyncClientMixin, mixins.AsyncClientMixin, object):
    '''
    The interface used by the :command:`salt-run` CLI tool on the Salt Master

    It executes :ref:`runner modules <all-salt.runners>` which run on the Salt
    Master.

    Importing and using ``RunnerClient`` must be done on the same machine as
    the Salt Master and it must be done using the same user that the Salt
    Master is running as.

    Salt's :conf_master:`external_auth` can be used to authenticate calls. The
    eauth user must be authorized to execute runner modules: (``@runner``).
    Only the :py:meth:`master_call` below supports eauth.
    '''
    client = 'runner'
    tag_prefix = 'run'

    def __init__(self, opts):
        self.opts = opts
        self.context = {}

    @property
    def functions(self):
        if not hasattr(self, '_functions'):
            if not hasattr(self, 'utils'):
                self.utils = salt.loader.utils(self.opts)
            # Must be self.functions for mixin to work correctly :-/
            try:
                self._functions = salt.loader.runner(
                    self.opts, utils=self.utils, context=self.context)
            except AttributeError:
                # Just in case self.utils is still not present (perhaps due to
                # problems with the loader), load the runner funcs without them
                self._functions = salt.loader.runner(
                    self.opts, context=self.context)

        return self._functions

    def _reformat_low(self, low):
        '''
        Format the low data for RunnerClient()'s master_call() function

        This also normalizes the following low data formats to a single, common
        low data structure.

        Old-style low: ``{'fun': 'jobs.lookup_jid', 'jid': '1234'}``
        New-style: ``{'fun': 'jobs.lookup_jid', 'kwarg': {'jid': '1234'}}``
        CLI-style: ``{'fun': 'jobs.lookup_jid', 'arg': ['jid="1234"']}``
        '''
        fun = low.pop('fun')
        verify_fun(self.functions, fun)

        eauth_creds = dict([(i, low.pop(i)) for i in [
            'username', 'password', 'eauth', 'token', 'client', 'user', 'key',
        ] if i in low])

        # Run name=value args through parse_input. We don't need to run kwargs
        # through because there is no way to send name=value strings in the low
        # dict other than by including an `arg` array.
        _arg, _kwarg = salt.utils.args.parse_input(
                low.pop('arg', []), condition=False)
        _kwarg.update(low.pop('kwarg', {}))

        # If anything hasn't been pop()'ed out of low by this point it must be
        # an old-style kwarg.
        _kwarg.update(low)

        # Finally, mung our kwargs to a format suitable for the byzantine
        # load_args_and_kwargs so that we can introspect the function being
        # called and fish for invalid kwargs.
        munged = []
        munged.extend(_arg)
        munged.append(dict(__kwarg__=True, **_kwarg))
        arg, kwarg = salt.minion.load_args_and_kwargs(
            self.functions[fun],
            munged,
            ignore_invalid=True)

        return dict(fun=fun, kwarg={'kwarg': kwarg, 'arg': arg},
                **eauth_creds)

    def cmd_async(self, low):
        '''
        Execute a runner function asynchronously; eauth is respected

        This function requires that :conf_master:`external_auth` is configured
        and the user is authorized to execute runner functions: (``@runner``).

        .. code-block:: python

            runner.eauth_async({
                'fun': 'jobs.list_jobs',
                'username': 'saltdev',
                'password': 'saltdev',
                'eauth': 'pam',
            })
        '''
        reformatted_low = self._reformat_low(low)

        return mixins.AsyncClientMixin.cmd_async(self, reformatted_low)

    def cmd_sync(self, low, timeout=None, full_return=False):
        '''
        Execute a runner function synchronously; eauth is respected

        This function requires that :conf_master:`external_auth` is configured
        and the user is authorized to execute runner functions: (``@runner``).

        .. code-block:: python

            runner.eauth_sync({
                'fun': 'jobs.list_jobs',
                'username': 'saltdev',
                'password': 'saltdev',
                'eauth': 'pam',
            })
        '''
        reformatted_low = self._reformat_low(low)
        return mixins.SyncClientMixin.cmd_sync(self, reformatted_low, timeout, full_return)

    def cmd(self, fun, arg=None, pub_data=None, kwarg=None, print_event=True, full_return=False):
        '''
        Execute a function
        '''
        return super(RunnerClient, self).cmd(fun,
                                             arg,
                                             pub_data,
                                             kwarg,
                                             print_event,
                                             full_return)


class Runner(RunnerClient):
    '''
    Execute the salt runner interface
    '''
    def __init__(self, opts):
        super(Runner, self).__init__(opts)
        self.returners = salt.loader.returners(opts, self.functions)
        self.outputters = salt.loader.outputters(opts)

    def print_docs(self):
        '''
        Print out the documentation!
        '''
        arg = self.opts.get('fun', None)
        docs = super(Runner, self).get_docs(arg)
        for fun in sorted(docs):
            display_output('{0}:'.format(fun), 'text', self.opts)
            print(docs[fun])

    # TODO: move to mixin whenever we want a salt-wheel cli
    def run(self):
        '''
        Execute the runner sequence
        '''
        # Print documentation only
        if self.opts.get('doc', False):
            self.print_docs()
        else:
            return self._run_runner()

    def _run_runner(self):
        '''
        Actually execute specific runner
        :return:
        '''
        import salt.minion
        ret = {}
        low = {'fun': self.opts['fun']}
        try:
            # Allocate a jid
            async_pub = self._gen_async_pub()
            self.jid = async_pub['jid']

            fun_args = salt.utils.args.parse_input(
                    self.opts['arg'],
                    no_parse=self.opts.get('no_parse', []))

            verify_fun(self.functions, low['fun'])
            args, kwargs = salt.minion.load_args_and_kwargs(
                self.functions[low['fun']],
                fun_args)
            low['arg'] = args
            low['kwarg'] = kwargs

            if self.opts.get('eauth'):
                if 'token' in self.opts:
                    try:
                        with salt.utils.files.fopen(os.path.join(self.opts['cachedir'], '.root_key'), 'r') as fp_:
                            low['key'] = salt.utils.stringutils.to_unicode(fp_.readline())
                    except IOError:
                        low['token'] = self.opts['token']

                # If using eauth and a token hasn't already been loaded into
                # low, prompt the user to enter auth credentials
                if 'token' not in low and 'key' not in low and self.opts['eauth']:
                    # This is expensive. Don't do it unless we need to.
                    import salt.auth
                    resolver = salt.auth.Resolver(self.opts)
                    res = resolver.cli(self.opts['eauth'])
                    if self.opts['mktoken'] and res:
                        tok = resolver.token_cli(
                                self.opts['eauth'],
                                res
                                )
                        if tok:
                            low['token'] = tok.get('token', '')
                    if not res:
                        log.error('Authentication failed')
                        return ret
                    low.update(res)
                    low['eauth'] = self.opts['eauth']
            else:
                user = salt.utils.user.get_specific_user()

            if low['fun'] in ['state.orchestrate', 'state.orch', 'state.sls']:
                low['kwarg']['orchestration_jid'] = async_pub['jid']

            # Run the runner!
            if self.opts.get('async', False):
                if self.opts.get('eauth'):
                    async_pub = self.cmd_async(low)
                else:
                    async_pub = self.asynchronous(self.opts['fun'],
                                                  low,
                                                  user=user,
                                                  pub=async_pub)

                # by default: info will be not enough to be printed out !
                log.warning(
                    'Running in asynchronous mode. Results of this execution may '
                    'be collected by attaching to the master event bus or '
                    'by examing the master job cache, if configured. '
                    'This execution is running under tag %s', async_pub['tag']
                )
                return async_pub['jid']  # return the jid

            # otherwise run it in the main process
            if self.opts.get('eauth'):
                ret = self.cmd_sync(low)
                if isinstance(ret, dict) and set(ret) == {'data', 'outputter'}:
                    outputter = ret['outputter']
                    ret = ret['data']
                else:
                    outputter = None
                display_output(ret, outputter, self.opts)
            else:
                ret = self._proc_function(self.opts['fun'],
                                          low,
                                          user,
                                          async_pub['tag'],
                                          async_pub['jid'],
                                          daemonize=False)
        except salt.exceptions.SaltException as exc:
            with salt.utils.event.get_event('master', opts=self.opts) as evt:
                evt.fire_event({'success': False,
                                'return': '{0}'.format(exc),
                                'retcode': 254,
                                'fun': self.opts['fun'],
                                'fun_args': fun_args,
                                'jid': self.jid},
                               tag='salt/run/{0}/ret'.format(self.jid))
            # Attempt to grab documentation
            if 'fun' in low:
                ret = self.get_docs('{0}*'.format(low['fun']))
            else:
                ret = None

            # If we didn't get docs returned then
            # return the `not availble` message.
            if not ret:
                ret = '{0}'.format(exc)
            if not self.opts.get('quiet', False):
                display_output(ret, 'nested', self.opts)
        log.debug('Runner return: %s', ret)

        return ret