From f7f02407a7221609cafff814860e6b5ea471bfb5 Mon Sep 17 00:00:00 2001 From: Thayne Harbaugh Date: Wed, 7 May 2014 16:19:32 -0600 Subject: [PATCH 1/2] IMPROVEMENT: Use Python for the thin shim instead of /bin/sh * /bin/sh is, unfortunately, not very portable * Additional commands (tar, md5sum, etc.) are not uniform * Python *already* must be there to run salt and is very predictable and portable * All of the tools necessary to deploy thin are in Python stdlibs --- salt/client/ssh/__init__.py | 430 ++++++++++++++++++--------------- salt/client/ssh/ssh_py_shim.py | 169 +++++++++++++ salt/exitcodes.py | 5 + 3 files changed, 408 insertions(+), 196 deletions(-) create mode 100644 salt/client/ssh/ssh_py_shim.py diff --git a/salt/client/ssh/__init__.py b/salt/client/ssh/__init__.py index 1c7dfafcf5..53a4b63943 100644 --- a/salt/client/ssh/__init__.py +++ b/salt/client/ssh/__init__.py @@ -22,6 +22,8 @@ import salt.client.ssh.shell import salt.client.ssh.wrapper import salt.config import salt.exceptions +import salt.exitcodes +import salt.log import salt.loader import salt.minion import salt.roster @@ -34,143 +36,115 @@ import salt.utils.thin import salt.utils.verify from salt._compat import string_types -# This is just a delimiter to distinguish the beginning of salt STDOUT. There -# is no special meaning +# The directory where salt thin is deployed +DEFAULT_THIN_DIR = '/tmp/.salt' + +# RSTR is just a delimiter to distinguish the beginning of salt STDOUT +# and STDERR. There is no special meaning. Messages prior to RSTR in +# stderr and stdout are either from SSH or from the shim. +# +# RSTR on both stdout and stderr: +# no errors in SHIM - output after RSTR is from salt +# No RSTR in stderr, RSTR in stdout: +# no errors in SSH_SH_SHIM, but SHIM commands for salt master are after +# RSTR in stdout +# No RSTR in stderr, no RSTR in stdout: +# Failure in SHIM +# RSTR in stderr, No RSTR in stdout: +# Undefined behavior RSTR = '_edbc7885e4f9aac9b83b35999b68d015148caf467b78fa39c05f669c0ff89878' +# The regex to find RSTR in output - Must be on an output line by itself +# NOTE - must use non-grouping match groups or output splitting will fail. +RSTR_RE = r'(?:^|\r?\n)' + RSTR + '(?:\r?\n|$)' -# This shim facilitates remote salt-call operations -# - Uses /bin/sh for maximum compatibility +# METHODOLOGY: +# +# 1) Make the _thinnest_ /bin/sh shim (SSH_SH_SHIM) to find the python +# interpreter and get it invoked +# 2) Once a qualified python is found start it with the SSH_PY_SHIM + +# NOTE: +# * SSH_SH_SHIM is generic and can be used to load+exec *any* python +# script on the target. +# * SSH_PY_SHIM is in a separate file rather than stuffed in a string +# in salt/client/ssh/__init__.py - this makes testing *easy* because +# it can be invoked directly. +# * SSH_PY_SHIM is base64 encoded and formatted into the SSH_SH_SHIM +# string. This makes the python script "armored" so that it can +# all be passed in the SSH command and will not need special quoting +# (which likely would be impossibe to do anyway) +# * The formatted SSH_SH_SHIM with the SSH_PY_SHIM payload is a bit +# big (~7.5k). If this proves problematic for an SSH command we +# might try simply invoking "/bin/sh -s" and passing the formatted +# SSH_SH_SHIM on SSH stdin. + +# NOTE: there are two passes of formatting: +# 1) Substitute in static values +# - EX_THIN_PYTHON_OLD - exit code if a suitable python is not found +# 2) Substitute in instance-specific commands +# - DEBUG - enable shim debugging (any non-zero string enables) +# - SUDO - load python and execute as root (any non-zero string enables) +# - SSH_PY_CODE - base64-encoded python code to execute +# - SSH_PY_ARGS - arguments to pass to python code +SSH_SH_SHIM = r'''/bin/sh << 'EOF' +# This shim generically loads python code . . . and *no* more. +# - Uses /bin/sh for maximum compatibility - then jumps to +# python for ultra-maximum compatibility. # # 1. Identify a suitable python -# 2. Test for remote salt-call and version if present -# 3. Signal to (re)deploy if missing or out of date -# - If this is a a first deploy, then test python version -# 4. Perform salt-call +# 2. Jump to python -# Note there are two levels of formatting. -# - First format pass inserts salt version and delimiter -# - Second pass at run-time and inserts optional "sudo" and command -SSH_SHIM = r'''/bin/sh << 'EOF' - #!/bin/sh +set -e +set -u - MISS_PKG="" +DEBUG="{{DEBUG}}" +if [ -n "$DEBUG" ]; then + set -x +fi - command -v tar >/dev/null - if [ $? -ne 0 ] - then - MISS_PKG="$MISS_PKG tar" - fi +SUDO="" +if [ -n "{{SUDO}}" ]; then + SUDO="sudo root -c" +fi - for py_candidate in \ - python27 \ - python2.7 \ - python26 \ - python2.6 \ - python2 \ - python ; - do - command -v $py_candidate >/dev/null - if [ $? -eq 0 ] - then - PYTHON="$py_candidate" - break - fi - done +EX_PYTHON_OLD={EX_THIN_PYTHON_OLD} # Python interpreter is too old and incompatible - if [ -z "$PYTHON" ] - then - MISS_PKG="$MISS_PKG python" - fi +PYTHON_CMDS=" + python27 + python2.7 + python26 + python2.6 + python2 + python +" - SALT="/tmp/.salt/salt-call" - if [ "{{2}}" = "md5" ] - then - for md5_candidate in \ - md5sum \ - md5 \ - csum \ - digest ; - do - command -v $md5_candidate >/dev/null - if [ $? -eq 0 ] - then - SUMCHECK="$md5_candidate" - break - fi - done - else - command -v "{{2}}" >/dev/null - if [ $? -eq 0 ] - then - SUMCHECK="{{2}}" - fi - fi +main() +{{{{ + local py_cmd + local py_cmd_path + for py_cmd in $PYTHON_CMDS; do + if "$py_cmd" -c 'import sys; sys.exit(not sys.hexversion >= 0x02060000);' >/dev/null 2>&1; then + local py_cmd_path + py_cmd_path=`"$py_cmd" -c 'import sys; print sys.executable;'` + echo "FOUND: $py_cmd_path" >&2 + exec $SUDO "$py_cmd_path" -c 'exec """{{SSH_PY_CODE}}""".decode("base64")' -- {{SSH_PY_ARGS}} + else + echo "WARNING: $py_cmd not found or too old" >&2 + fi + done - if [ -z "$SUMCHECK" ] - then - MISS_PKG="$MISS_PKG md5sum/md5/csum" - fi + echo "ERROR: Unable to locate appropriate python command" >&2 + exit $EX_PYTHON_OLD +}}}} - if [ -n "$MISS_PKG" ] - then - echo "The following required Packages are missing: $MISS_PKG" >&2 - exit 127 - fi +main +EOF'''.format( + EX_THIN_PYTHON_OLD=salt.exitcodes.EX_THIN_PYTHON_OLD, +) - # MD5 check for systems with a BSD userland (includes OSX). - if [ "$SUMCHECK" = "md5" ] - then - SUMCHECK="md5 -q" - # MD5 check for AIX systems. - elif [ "$SUMCHECK" = "csum" ] - then - SUMCHECK="csum -h MD5" - # MD5 check for Solaris systems. - elif [ "$SUMCHECK" = "digest" ] - then - SUMCHECK="digest -a md5" - fi - - if [ -f "$SALT" ] - then - if [ "`cat /tmp/.salt/version`" != "{0}" ] - then - {{0}} rm -rf /tmp/.salt && mkdir -m 0700 -p /tmp/.salt - if [ $? -ne 0 ]; then - exit 1 - fi - echo "{1}" - echo "deploy" - exit 1 - fi - else - PY_TOO_OLD=`$PYTHON -c "import sys; print sys.hexversion < 0x02060000"` - if [ "$PY_TOO_OLD" = "True" ]; - then - echo "Python too old" >&2 - exit 1 - fi - if [ -f /tmp/.salt/salt-thin.tgz ] - then - if [ "`$SUMCHECK /tmp/.salt/salt-thin.tgz | cut -f1 -d\ `" = "{{3}}" ] - then - cd /tmp/.salt/ && gunzip -c salt-thin.tgz | {{0}} tar opxvf - - else - echo "Mismatched checksum for /tmp/.salt/salt-thin.tgz" >&2 - exit 1 - fi - else - mkdir -m 0700 -p /tmp/.salt - echo "{1}" - echo "deploy" - exit 1 - fi - fi - echo "{{4}}" > /tmp/.salt/minion - echo "{1}" - eval {{0}} $PYTHON $SALT --local --out json -l quiet {{1}} -c /tmp/.salt -EOF'''.format(salt.__version__, RSTR) +with open(os.path.join(os.path.dirname(__file__), 'ssh_py_shim.py')) as ssh_py_shim: + SSH_PY_SHIM = ''.join(ssh_py_shim.readlines()).encode('base64') log = logging.getLogger(__name__) @@ -337,9 +311,6 @@ class SSH(object): **target) ret = {'id': single.id} stdout, stderr, retcode = single.run() - if stdout.startswith('deploy'): - single.deploy() - stdout, stderr, retcode = single.run() # This job is done, yield try: data = salt.utils.find_json(stdout) @@ -522,7 +493,7 @@ class Single(object): self.shell = salt.client.ssh.shell.Shell(opts, **args) self.minion_config = yaml.dump( { - 'root_dir': '/tmp/.salt/running_data', + 'root_dir': os.path.join(DEFAULT_THIN_DIR, 'running_data'), 'id': self.id, }).strip() self.target = kwargs @@ -570,8 +541,9 @@ class Single(object): ''' thin = salt.utils.thin.gen_thin(self.opts['cachedir']) self.shell.send( - thin, - '/tmp/.salt/salt-thin.tgz') + thin, + os.path.join(DEFAULT_THIN_DIR, 'salt-thin.tgz'), + ) return True def run(self, deploy_attempted=False): @@ -598,10 +570,6 @@ class Single(object): else: stdout, stderr, retcode = self.cmd_block() - if stdout.startswith('deploy') and not deploy_attempted: - self.deploy() - return self.run(deploy_attempted=True) - return stdout, stderr, retcode def run_wfunc(self): @@ -668,14 +636,39 @@ class Single(object): ret = json.dumps(self.wfuncs[self.fun](*self.args, **self.kwargs)) return ret, '', None + def _cmd_str(self): + ''' + Prepare the command string + ''' + sudo = 'sudo' if self.target['sudo'] else '' + thin_sum = salt.utils.thin.thin_sum(self.opts['cachedir'], 'sha1') + debug = '' + if salt.log.LOG_LEVELS['debug'] >= salt.log.LOG_LEVELS[self.opts['log_level']]: + debug = '1' + + ssh_py_shim_args = [ + '--config', self.minion_config, + '--delimeter', RSTR, + '--saltdir', DEFAULT_THIN_DIR, + '--checksum', thin_sum, + '--hashfunc', 'sha1', + '--version', salt.__version__, + '--', + ] + self.argv + + cmd = SSH_SH_SHIM.format( + DEBUG=debug, + SUDO=sudo, + SSH_PY_CODE=SSH_PY_SHIM, + SSH_PY_ARGS=' '.join([self._escape_arg(arg) for arg in ssh_py_shim_args]), + ) + + return cmd + def cmd(self): ''' Prepare the pre-check command to send to the subsystem ''' - # 1. check if python is on the target - # 2. check is salt-call is on the target - # 3. deploy salt-thin - # 4. execute command if self.fun.startswith('state.highstate'): self.highstate_seed() elif self.fun.startswith('state.sls'): @@ -684,17 +677,8 @@ class Single(object): salt.utils.args.parse_input(self.args) ) self.sls_seed(*args, **kwargs) - cmd_str = ' '.join([self._escape_arg(arg) for arg in self.argv]) - sudo = 'sudo' if self.target['sudo'] else '' - thin_sum = salt.utils.thin.thin_sum( - self.opts['cachedir'], - self.opts['hash_type']) - cmd = SSH_SHIM.format( - sudo, - cmd_str, - self.opts['hash_type'], - thin_sum, - self.minion_config) + cmd_str = self._cmd_str() + for stdout, stderr, retcode in self.shell.exec_nb_cmd(cmd): yield stdout, stderr, retcode @@ -702,70 +686,123 @@ class Single(object): ''' Prepare the pre-check command to send to the subsystem ''' - # 1. check if python is on the target - # 2. check is salt-call is on the target - # 3. deploy salt-thin - # 4. execute command - cmd_str = ' '.join([self._escape_arg(arg) for arg in self.argv]) - sudo = 'sudo' if self.target['sudo'] else '' - thin_sum = salt.utils.thin.thin_sum( - self.opts['cachedir'], - self.opts['hash_type']) - cmd = SSH_SHIM.format( - sudo, - cmd_str, - self.opts['hash_type'], - thin_sum, - self.minion_config) - log.debug('Performing shimmed command as follows:\n{0}'.format(cmd)) - stdout, stderr, retcode = self.shell.exec_cmd(cmd) + # 1. execute SHIM + command + # 2. check if SHIM returns a master request or if it completed + # 3. handle any master request + # 4. re-execute SHIM + command + # 5. split SHIM results from command results + # 6. return command results + + log.debug('Performing shimmed, blocking command as follows:\n{0}'.format(' '.join(self.argv))) + cmd_str = self._cmd_str() + stdout, stderr, retcode = self.shell.exec_cmd(cmd_str) log.debug('STDOUT {1}\n{0}'.format(stdout, self.target['host'])) log.debug('STDERR {1}\n{0}'.format(stderr, self.target['host'])) - log.debug('RETCODE {1}\n{0}'.format(retcode, self.target['host'])) + log.debug('RETCODE {1}: {0}'.format(retcode, self.target['host'])) error = self.categorize_shim_errors(stdout, stderr, retcode) if error: return 'ERROR: {0}'.format(error), stderr, retcode - if RSTR in stdout: - stdout = stdout.split(RSTR)[1].strip() - if stdout.startswith('deploy'): - self.deploy() - stdout, stderr, retcode = self.shell.exec_cmd(cmd) - if RSTR in stdout: - stdout = stdout.split(RSTR)[1].strip() + # FIXME: this discards output from ssh_shim if the shim succeeds. It should + # always save the shim output regardless of shim success or failure. + if re.search(RSTR_RE, stdout): + stdout = re.split(RSTR_RE, stdout, 1)[1].strip() + else: + # This is actually an error state prior to the shim but let it fall through + pass + + if re.search(RSTR_RE, stderr): + # Found RSTR in stderr which means SHIM completed and only + # and remaining output is only from salt. + stderr = re.split(RSTR_RE, stderr, 1)[1].strip() + + else: + # RSTR was found in stdout but not stderr - which means there + # is a SHIM command for the master. + shim_command = re.split(r'\r?\n', stdout, 1)[0].strip() + if 'deploy' == shim_command and retcode == salt.exitcodes.EX_THIN_DEPLOY: + self.deploy() + stdout, stderr, retcode = self.shell.exec_cmd(cmd_str) + if not re.search(RSTR_RE, stdout) or not re.search(RSTR_RE, stderr): + # If RSTR is not seen in both stdout and stderr then there + # was a thin deployment problem. + return 'ERROR: Failure deploying thin: {0}'.format(stdout), stderr, retcode + stdout = re.split(RSTR_RE, stdout, 1)[1].strip() + stderr = re.split(RSTR_RE, stderr, 1)[1].strip() return stdout, stderr, retcode def categorize_shim_errors(self, stdout, stderr, retcode): - # Unused stdout and retcode for now but these may be used to - # categorize errors - _ = stdout - _ = retcode + if re.search(RSTR_RE, stdout): + # RSTR was found in stdout which means that the shim + # functioned without *errors* . . . but there may be shim + # commands + return None + + if re.search(RSTR_RE, stderr): + # Undefined state + return 'Undefined SHIM state' + + if stderr.startswith('Permission denied'): + # SHIM was not even reached + return None perm_error_fmt = 'Permissions problem, target user may need '\ 'to be root or use sudo:\n {0}' - if stderr.startswith('Permission denied'): - return None + errors = [ - ('sudo: no tty present and no askpass program specified', - 'sudo expected a password, NOPASSWD required'), - ('Python too old', - 'salt requires python 2.6 or better on target hosts'), - ('sudo: sorry, you must have a tty to run sudo', - 'sudo is configured with requiretty'), - ('Failed to open log file', - perm_error_fmt.format(stderr)), - ('Permission denied:.*/salt', - perm_error_fmt.format(stderr)), - ('Failed to create directory path.*/salt', - perm_error_fmt.format(stderr)), - ] + ( + (), + 'sudo: no tty present and no askpass program specified', + 'sudo expected a password, NOPASSWD required' + ), + ( + (salt.exitcodes.EX_THIN_PYTHON_OLD,), + 'Python interpreter is too old', + 'salt requires python 2.6 or newer on target hosts' + ), + ( + (salt.exitcodes.EX_THIN_CHECKSUM,), + 'checksum mismatched', + 'The salt thin transfer was corrupted' + ), + ( + (os.EX_CANTCREAT,), + 'salt path .* exists but is not a directory', + 'A necessary path for salt thin unexpectedly exists:\n ' + stderr, + ), + ( + (), + 'sudo: sorry, you must have a tty to run sudo', + 'sudo is configured with requiretty' + ), + ( + (), + 'Failed to open log file', + perm_error_fmt.format(stderr) + ), + ( + (), + 'Permission denied:.*/salt', + perm_error_fmt.format(stderr) + ), + ( + (), + 'Failed to create directory path.*/salt', + perm_error_fmt.format(stderr) + ), + ( + (os.EX_SOFTWARE,), + 'exists but is not', + 'An internal error occurred with the shim, please investigate:\n ' + stderr, + ), + ] for error in errors: - if re.search(error[0], stderr): - return error[1] + if retcode in error[0] or re.search(error[1], stderr): + return error[2] return None def sls_seed(self, @@ -822,8 +859,9 @@ class Single(object): file_refs = lowstate_file_refs(chunks) trans_tar = prep_trans_tar(self.opts, chunks, file_refs) self.shell.send( - trans_tar, - '/tmp/salt_state.tgz') + trans_tar, + os.path.join(DEFAULT_THIN_DIR, 'salt_state.tgz'), + ) self.argv = ['state.pkg', '/tmp/salt_state.tgz', 'test={0}'.format(test)] diff --git a/salt/client/ssh/ssh_py_shim.py b/salt/client/ssh/ssh_py_shim.py new file mode 100644 index 0000000000..07b0599db2 --- /dev/null +++ b/salt/client/ssh/ssh_py_shim.py @@ -0,0 +1,169 @@ +# -*- coding: utf-8 -*- +''' +This is a shim that handles checking and updating salt thin and +then invoking thin. + +This is not intended to be instantiated as a module, rather it is a +helper script used by salt.client.ssh.Single. It is here, in a +seperate file, for convenience of development. +''' + +import optparse +import hashlib +import tarfile +import shutil +import sys +import os + +THIN_ARCHIVE = 'salt-thin.tgz' + +# FIXME - it would be ideal if these could be obtained directly from +# salt.exitcodes rather than duplicated. +EX_THIN_DEPLOY = 11 +EX_THIN_CHECKSUM = 12 + + +OPTIONS = None +ARGS = None + + +def parse_argv(argv): + global OPTIONS + global ARGS + + oparser = optparse.OptionParser(usage="%prog -- [SHIM_OPTIONS] -- [SALT_OPTIONS]") + oparser.add_option( + "-c", "--config", + default='', + help="YAML configuration for salt thin", + ) + oparser.add_option( + "-d", "--delimeter", + help="Delimeter string (viz. magic string) to indicate beginning of salt output", + ) + oparser.add_option( + "-s", "--saltdir", + help="Directory where salt thin is or will be installed.", + ) + oparser.add_option( + "--sum", "--checksum", + dest="checksum", + help="Salt thin checksum", + ) + oparser.add_option( + "--hashfunc", + default='sha1', + help="Hash function for computing checksum", + ) + oparser.add_option( + "-v", "--version", + help="Salt thin version to be deployed/verified", + ) + + if argv and '--' not in argv: + oparser.error('A "--" argument must be the initial argument indicating the start of options to this script') + + (OPTIONS, ARGS) = oparser.parse_args(argv[argv.index('--')+1:]) + + for option in ( + 'delimeter', + 'saltdir', + 'checksum', + 'version', + ): + if getattr(OPTIONS, option, None): + continue + oparser.error('Option "--{0}" is required.'.format(option)) + + +def need_deployment(): + if os.path.exists(OPTIONS.saltdir): + shutil.rmtree(OPTIONS.saltdir) + old_umask = os.umask(0077) + os.makedirs(OPTIONS.saltdir) + os.umask(old_umask) + # Delimeter emitted on stdout *only* to indicate shim message to master. + sys.stdout.write("%s\ndeploy\n" % OPTIONS.delimeter) + sys.exit(EX_THIN_DEPLOY) + + +# Adapted from salt.utils.get_hash() +def get_hash(path, form='md5', chunk_size=4096): + try: + hash_type = getattr(hashlib, form) + except AttributeError: + raise ValueError('Invalid hash type: {0}'.format(form)) + with open(path, 'rb') as ifile: + hash_obj = hash_type() + # read the file in in chunks, not the entire file + for chunk in iter(lambda: ifile.read(chunk_size), b''): + hash_obj.update(chunk) + return hash_obj.hexdigest() + + +def unpack_thin(thin_path): + tfile = tarfile.TarFile.gzopen(thin_path) + tfile.extractall(path=OPTIONS.saltdir) + tfile.close() + os.unlink(thin_path) + + +def main(argv): + parse_argv(argv) + + thin_path = os.path.join(OPTIONS.saltdir, THIN_ARCHIVE) + if os.path.exists(thin_path): + if OPTIONS.checksum != get_hash(thin_path, OPTIONS.hashfunc): + os.unlink(thin_path) + sys.stderr.write('WARNING: checksum mismatch for "{0}"\n'.format(thin_path)) + sys.exit(EX_THIN_CHECKSUM) + unpack_thin(thin_path) + # Salt thin now is available to use + else: + if not os.path.exists(OPTIONS.saltdir): + need_deployment() + + if not os.path.isdir(OPTIONS.saltdir): + sys.stderr.write('ERROR: salt path "{0}" exists but is not a directory\n'.format(OPTIONS.saltdir)) + sys.exit(os.EX_CANTCREAT) + + version_path = os.path.join(OPTIONS.saltdir, 'version') + if not os.path.exists(version_path) or not os.path.isfile(version_path): + sys.stderr.write('WARNING: Unable to locate current thin version.\n') + need_deployment() + with open(version_path, 'r') as vpo: + cur_version = vpo.readline().strip() + if cur_version != OPTIONS.version: + sys.stderr.write('WARNING: current thin version is not up-to-date.\n') + need_deployment() + # Salt thin exists and is up-to-date - fall through and use it + + salt_call_path = os.path.join(OPTIONS.saltdir, 'salt-call') + if not os.path.isfile(salt_call_path): + sys.stderr.write('ERROR: thin is missing "{0}"\n'.format(salt_call_path)) + sys.exit(os.EX_SOFTWARE) + + with open(os.path.join(OPTIONS.saltdir, 'minion'), 'w') as config: + config.write(OPTIONS.config + '\n') + + salt_argv = [ + sys.executable, + salt_call_path, + '--local', + '--out', 'json', + '-l', 'quiet', + '-c', OPTIONS.saltdir, + '--', + ] + ARGS + sys.stderr.write('SALT_ARGV: {0}\n'.format(salt_argv)) + + # Only emit the delimiter on *both* stdout and stderr when completely successful. + # Yes, the flush() is necessary. + sys.stdout.write(OPTIONS.delimeter + '\n') + sys.stdout.flush() + sys.stderr.write(OPTIONS.delimeter + '\n') + sys.stderr.flush() + os.execv(sys.executable, salt_argv) + +if __name__ == "__main__": + sys.exit(main(sys.argv)) diff --git a/salt/exitcodes.py b/salt/exitcodes.py index 8114e40c38..1535d1f570 100644 --- a/salt/exitcodes.py +++ b/salt/exitcodes.py @@ -8,3 +8,8 @@ prefix or in `sysexits.h`). # Too many situations use "exit 1" - try not to use it when something # else is more appropriate. EX_GENERIC = 1 + +# Salt SSH "Thin" deployment failures +EX_THIN_PYTHON_OLD = 10 +EX_THIN_DEPLOY = 11 +EX_THIN_CHECKSUM = 12 From cd27c6e9699408275e7a59510f2e5a9e22b2b498 Mon Sep 17 00:00:00 2001 From: Thayne Harbaugh Date: Wed, 7 May 2014 23:07:45 -0600 Subject: [PATCH 2/2] use str.format(); use correct variable --- salt/client/ssh/__init__.py | 2 +- salt/client/ssh/ssh_py_shim.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/salt/client/ssh/__init__.py b/salt/client/ssh/__init__.py index 53a4b63943..d3aef688e0 100644 --- a/salt/client/ssh/__init__.py +++ b/salt/client/ssh/__init__.py @@ -679,7 +679,7 @@ class Single(object): self.sls_seed(*args, **kwargs) cmd_str = self._cmd_str() - for stdout, stderr, retcode in self.shell.exec_nb_cmd(cmd): + for stdout, stderr, retcode in self.shell.exec_nb_cmd(cmd_str): yield stdout, stderr, retcode def cmd_block(self, is_retry=False): diff --git a/salt/client/ssh/ssh_py_shim.py b/salt/client/ssh/ssh_py_shim.py index 07b0599db2..dc50d9372e 100644 --- a/salt/client/ssh/ssh_py_shim.py +++ b/salt/client/ssh/ssh_py_shim.py @@ -83,7 +83,7 @@ def need_deployment(): os.makedirs(OPTIONS.saltdir) os.umask(old_umask) # Delimeter emitted on stdout *only* to indicate shim message to master. - sys.stdout.write("%s\ndeploy\n" % OPTIONS.delimeter) + sys.stdout.write("%s\ndeploy\n".format(OPTIONS.delimeter)) sys.exit(EX_THIN_DEPLOY)