diff --git a/salt/client/ssh/__init__.py b/salt/client/ssh/__init__.py index 92ea0565b4..62fa629d0f 100644 --- a/salt/client/ssh/__init__.py +++ b/salt/client/ssh/__init__.py @@ -169,14 +169,26 @@ class SSH(object): self.targets = self.roster.targets( self.opts['tgt'], self.tgt_type) - priv = self.opts.get( - 'ssh_priv', - os.path.join( - self.opts['pki_dir'], - 'ssh', - 'salt-ssh.rsa' + # If we're in a wfunc, we need to get the ssh key location from the + # top level opts, stored in __master_opts__ + if '__master_opts__' in self.opts: + priv = self.opts['__master_opts__'].get( + 'ssh_priv', + os.path.join( + self.opts['__master_opts__']['pki_dir'], + 'ssh', + 'salt-ssh.rsa' + ) + ) + else: + priv = self.opts.get( + 'ssh_priv', + os.path.join( + self.opts['pki_dir'], + 'ssh', + 'salt-ssh.rsa' + ) ) - ) if not os.path.isfile(priv): try: salt.client.ssh.shell.gen_key(priv) @@ -399,7 +411,40 @@ class SSH(object): ''' Execute and yield returns as they come in, do not print to the display ''' + fstr = '{0}.prep_jid'.format(self.opts['master_job_cache']) + jid = self.returners[fstr]() + + # Save the invocation information + argv = self.opts['argv'] + + if self.opts['raw_shell']: + fun = 'ssh._raw' + args = argv + else: + fun = argv[0] if argv else '' + args = argv[1:] + + job_load = { + 'jid': jid, + 'tgt_type': self.tgt_type, + 'tgt': self.opts['tgt'], + 'user': self.opts['user'], + 'fun': fun, + 'arg': args, + } + + # save load to the master job cache + self.returners['{0}.save_load'.format(self.opts['master_job_cache'])](jid, job_load) + for ret in self.handle_ssh(): + host = next(ret.iterkeys()) + self.cache_job(jid, host, ret[host]) + if self.event: + self.event.fire_event( + ret, + salt.utils.event.tagify( + [jid, 'ret', host], + 'job')) yield ret def cache_job(self, jid, id_, ret): @@ -412,7 +457,7 @@ class SSH(object): def run(self): ''' - Execute the overall routine + Execute the overall routine, print results via outputters ''' fstr = '{0}.prep_jid'.format(self.opts['master_job_cache']) jid = self.returners[fstr]() @@ -681,6 +726,7 @@ class Single(object): opts_pkg['ext_pillar'] = self.opts['ext_pillar'] opts_pkg['extension_modules'] = self.opts['extension_modules'] opts_pkg['_ssh_version'] = self.opts['_ssh_version'] + opts_pkg['__master_opts__'] = self.context['master_opts'] if '_caller_cachedir' in self.opts: opts_pkg['_caller_cachedir'] = self.opts['_caller_cachedir'] else: diff --git a/salt/client/ssh/wrapper/publish.py b/salt/client/ssh/wrapper/publish.py new file mode 100644 index 0000000000..b88ec2c49f --- /dev/null +++ b/salt/client/ssh/wrapper/publish.py @@ -0,0 +1,208 @@ +# -*- coding: utf-8 -*- +''' +.. versionadded:: Lithium + +Salt-ssh wrapper functions for the publish module. + +Publish will never actually execute on the minions, so we just create new +salt-ssh calls and return the data from them. + +No access control is needed because calls cannot originate from the minions. +''' +# Import python libs +import copy +import logging +# Import salt libs +import salt.client.ssh + +log = logging.getLogger(__name__) + + +def _publish(tgt, + fun, + arg=None, + expr_form='glob', + returner='', + timeout=None, + form='clean', + roster=None): + ''' + Publish a command "from the minion out to other minions". In reality, the + minion does not execute this function, it is executed by the master. Thus, + no access control is enabled, as minions cannot initiate publishes + themselves. + + Salt-ssh publishes will default to whichever roster was used for the + initiating salt-ssh call, and can be overridden using the ``roster`` + argument + + Returners are not currently supported + + The arguments sent to the minion publish function are separated with + commas. This means that for a minion executing a command with multiple + args it will look like this:: + + salt-ssh system.example.com publish.publish '*' user.add 'foo,1020,1020' + + CLI Example: + + .. code-block:: bash + + salt-ssh system.example.com publish.publish '*' cmd.run 'ls -la /tmp' + ''' + if fun.startswith('publish.'): + log.info('Cannot publish publish calls. Returning {}') + return {} + + # TODO: implement returners? Do they make sense for salt-ssh calls? + if returner: + log.warning('Returners currently not supported in salt-ssh publish') + + # Make sure args have been processed + if arg is None: + arg = [] + elif not isinstance(arg, list): + arg = [salt.utils.args.yamlify_arg(arg)] + else: + arg = [salt.utils.args.yamlify_arg(x) for x in arg] + if len(arg) == 1 and arg[0] is None: + arg = [] + + # Set up opts for the SSH object + opts = copy.deepcopy(__opts__) + if roster: + opts['roster'] = roster + if timeout: + opts['timeout'] = timeout + opts['argv'] = [fun] + arg + opts['selected_target_option'] = expr_form + opts['tgt'] = tgt + opts['arg'] = arg + + # Create the SSH object to handle the actual call + ssh = salt.client.ssh.SSH(opts) + + # Run salt-ssh to get the minion returns + rets = {} + for ret in ssh.run_iter(): + rets.update(ret) + + if form == 'clean': + cret = {} + for host in rets: + if 'return' in rets[host]: + cret[host] = rets[host]['return'] + else: + cret[host] = rets[host] + return cret + else: + return rets + + +def publish(tgt, + fun, + arg=None, + expr_form='glob', + returner='', + timeout=5, + roster=None): + ''' + Publish a command "from the minion out to other minions". In reality, the + minion does not execute this function, it is executed by the master. Thus, + no access control is enabled, as minions cannot initiate publishes + themselves. + + + Salt-ssh publishes will default to whichever roster was used for the + initiating salt-ssh call, and can be overridden using the ``roster`` + argument + + Returners are not currently supported + + The expr_form argument is used to pass a target other than a glob into + the execution, the available options are: + + - glob + - pcre + + The arguments sent to the minion publish function are separated with + commas. This means that for a minion executing a command with multiple + args it will look like this: + + .. code-block:: bash + + salt-ssh system.example.com publish.publish '*' user.add 'foo,1020,1020' + salt-ssh system.example.com publish.publish '127.0.0.1' network.interfaces '' roster=scan + + CLI Example: + + .. code-block:: bash + + salt-ssh system.example.com publish.publish '*' cmd.run 'ls -la /tmp' + + + .. admonition:: Attention + + If you need to pass a value to a function argument and that value + contains an equal sign, you **must** include the argument name. + For example: + + .. code-block:: bash + + salt-ssh '*' publish.publish test.kwarg arg='cheese=spam' + + Multiple keyword arguments should be passed as a list. + + .. code-block:: bash + + salt-ssh '*' publish.publish test.kwarg arg="['cheese=spam','spam=cheese']" + + + + ''' + return _publish(tgt, + fun, + arg=arg, + expr_form=expr_form, + returner=returner, + timeout=timeout, + form='clean', + roster=roster) + + +def full_data(tgt, + fun, + arg=None, + expr_form='glob', + returner='', + timeout=5, + roster=None): + ''' + Return the full data about the publication, this is invoked in the same + way as the publish function + + CLI Example: + + .. code-block:: bash + + salt-ssh system.example.com publish.full_data '*' cmd.run 'ls -la /tmp' + + .. admonition:: Attention + + If you need to pass a value to a function argument and that value + contains an equal sign, you **must** include the argument name. + For example: + + .. code-block:: bash + + salt-ssh '*' publish.full_data test.kwarg arg='cheese=spam' + + ''' + return _publish(tgt, + fun, + arg=arg, + expr_form=expr_form, + returner=returner, + timeout=timeout, + form='full', + roster=roster) diff --git a/salt/roster/__init__.py b/salt/roster/__init__.py index a4c55ee08a..efd985d6fb 100644 --- a/salt/roster/__init__.py +++ b/salt/roster/__init__.py @@ -21,6 +21,9 @@ log = logging.getLogger(__name__) def get_roster_file(options): if options.get('roster_file'): template = options.get('roster_file') + elif 'config_dir' in options.get('__master_opts__', {}): + template = os.path.join(options['__master_opts__']['config_dir'], + 'roster') elif 'config_dir' in options: template = os.path.join(options['config_dir'], 'roster') else: