Execute a single module function on a remote minion via salt or salt-ssh

    name
        The name of the function to run, aka cmd.run or pkg.install

    tgt
        The target specification, aka '*' for all minions

    tgt_type
        The target type, defaults to ``glob``

    arg
        The list of arguments to pass into the function

    kwarg
        The dict (not a list) of keyword arguments to pass into the function

    ret
        Optionally set a single or a list of returners to use

    ret_config
        Use an alternative returner configuration

    ret_kwargs
        Override individual returner configuration items

    expect_minions
        An optional boolean for failing if some minions do not respond

    fail_minions
        An optional list of targeted minions where failure is an option

    fail_function
        An optional string that points to a salt module that returns True or False
        based on the returned data dict for individual minions

    ssh
        Set to `True` to use the ssh client instead of the standard salt client

    batch
        Execute the command :ref:`in batches <targeting-batch>`. E.g.: ``10%``.

    subset
        Number of minions from the targeted set to randomly use

        .. versionadded:: 2017.7.0

    asynchronous
        Run the salt command but don't wait for a reply.

        .. versionadded:: neon

def function(
        name,
        tgt,
        ssh=False,
        tgt_type='glob',
        ret='',
        ret_config=None,
        ret_kwargs=None,
        expect_minions=False,
        fail_minions=None,
        fail_function=None,
        arg=None,
        kwarg=None,
        timeout=None,
        batch=None,
        subset=None,
        **kwargs):  # pylint: disable=unused-argument
    '''
    Execute a single module function on a remote minion via salt or salt-ssh

    name
        The name of the function to run, aka cmd.run or pkg.install

    tgt
        The target specification, aka '*' for all minions

    tgt_type
        The target type, defaults to ``glob``

    arg
        The list of arguments to pass into the function

    kwarg
        The dict (not a list) of keyword arguments to pass into the function

    ret
        Optionally set a single or a list of returners to use

    ret_config
        Use an alternative returner configuration

    ret_kwargs
        Override individual returner configuration items

    expect_minions
        An optional boolean for failing if some minions do not respond

    fail_minions
        An optional list of targeted minions where failure is an option

    fail_function
        An optional string that points to a salt module that returns True or False
        based on the returned data dict for individual minions

    ssh
        Set to `True` to use the ssh client instead of the standard salt client

    batch
        Execute the command :ref:`in batches <targeting-batch>`. E.g.: ``10%``.

    subset
        Number of minions from the targeted set to randomly use

        .. versionadded:: 2017.7.0

    asynchronous
        Run the salt command but don't wait for a reply.

        .. versionadded:: neon

    '''
    func_ret = {'name': name,
                'changes': {},
                'comment': '',
                'result': True}
    if kwarg is None:
        kwarg = {}
    if isinstance(arg, six.string_types):
        func_ret['warnings'] = [
            'Please specify \'arg\' as a list of arguments.'
        ]
        arg = arg.split()

    cmd_kw = {'arg': arg or [], 'kwarg': kwarg, 'ret': ret, 'timeout': timeout}

    if batch is not None:
        cmd_kw['batch'] = six.text_type(batch)
    if subset is not None:
        cmd_kw['subset'] = subset

    cmd_kw['tgt_type'] = tgt_type
    cmd_kw['ssh'] = ssh
    cmd_kw['expect_minions'] = expect_minions
    cmd_kw['_cmd_meta'] = True
    cmd_kw['asynchronous'] = kwargs.pop('asynchronous', False)

    if ret_config:
        cmd_kw['ret_config'] = ret_config

    if ret_kwargs:
        cmd_kw['ret_kwargs'] = ret_kwargs

    fun = name
    if __opts__['test'] is True:
        func_ret['comment'] = \
            'Function {0} would be executed on target {1}'.format(fun, tgt)
        func_ret['result'] = None
        return func_ret
    try:
        _fire_args({'type': 'function', 'tgt': tgt, 'name': name, 'args': cmd_kw})
        cmd_ret = __salt__['saltutil.cmd'](tgt, fun, **cmd_kw)
    except Exception as exc:
        func_ret['result'] = False
        func_ret['comment'] = six.text_type(exc)
        return func_ret

    if cmd_kw['asynchronous']:
        func_ret['__jid__'] = cmd_ret.get('jid')
        func_ret['changes'] = cmd_ret
        if int(cmd_ret.get('jid', 0)) > 0:
            func_ret['result'] = True
            func_ret['comment'] = 'Function submitted successfully.'
        else:
            func_ret['result'] = False
            func_ret['comment'] = 'Function failed to run.'
        return func_ret

    try:
        func_ret['__jid__'] = cmd_ret[next(iter(cmd_ret))]['jid']
    except (StopIteration, KeyError):
        pass

    changes = {}
    fail = set()

    if fail_minions is None:
        fail_minions = ()
    elif isinstance(fail_minions, six.string_types):
        fail_minions = [minion.strip() for minion in fail_minions.split(',')]
    elif not isinstance(fail_minions, list):
        func_ret.setdefault('warnings', []).append(
            '\'fail_minions\' needs to be a list or a comma separated '
            'string. Ignored.'
        )
        fail_minions = ()
    for minion, mdata in six.iteritems(cmd_ret):
        m_ret = False
        if mdata.get('retcode'):
            func_ret['result'] = False
            fail.add(minion)
        if mdata.get('failed', False):
            m_func = False
        else:
            if 'return' in mdata and 'ret' not in mdata:
                mdata['ret'] = mdata.pop('return')
            m_ret = mdata['ret']
            m_func = (not fail_function and True) or __salt__[fail_function](m_ret)

            if m_ret is False:
                m_func = False

        if not m_func:
            if minion not in fail_minions:
                fail.add(minion)
        changes[minion] = m_ret
    if not cmd_ret:
        func_ret['result'] = False
        func_ret['command'] = 'No minions responded'
    else:
        if changes:
            func_ret['changes'] = {'out': 'highstate', 'ret': changes}
        if fail:
            func_ret['result'] = False
            func_ret['comment'] = 'Running function {0} failed on minions: {1}'.format(name, ', '.join(fail))
        else:
            func_ret['comment'] = 'Function ran successfully.'
        if changes:
            func_ret['comment'] += ' Function {0} ran on {1}.'.format(name, ', '.join(changes))
    return func_ret