mirror of
https://github.com/valitydev/salt.git
synced 2024-11-08 09:23:56 +00:00
Merge pull request #12613 from plastikos/improvement-python_thin_shim
IMPROVEMENT: Use Python for the thin shim instead of /bin/sh @thatch45
This commit is contained in:
commit
be67d9d22d
@ -22,6 +22,8 @@ import salt.client.ssh.shell
|
|||||||
import salt.client.ssh.wrapper
|
import salt.client.ssh.wrapper
|
||||||
import salt.config
|
import salt.config
|
||||||
import salt.exceptions
|
import salt.exceptions
|
||||||
|
import salt.exitcodes
|
||||||
|
import salt.log
|
||||||
import salt.loader
|
import salt.loader
|
||||||
import salt.minion
|
import salt.minion
|
||||||
import salt.roster
|
import salt.roster
|
||||||
@ -34,143 +36,115 @@ import salt.utils.thin
|
|||||||
import salt.utils.verify
|
import salt.utils.verify
|
||||||
from salt._compat import string_types
|
from salt._compat import string_types
|
||||||
|
|
||||||
# This is just a delimiter to distinguish the beginning of salt STDOUT. There
|
# The directory where salt thin is deployed
|
||||||
# is no special meaning
|
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'
|
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
|
# METHODOLOGY:
|
||||||
# - Uses /bin/sh for maximum compatibility
|
#
|
||||||
|
# 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
|
# 1. Identify a suitable python
|
||||||
# 2. Test for remote salt-call and version if present
|
# 2. Jump to python
|
||||||
# 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
|
|
||||||
|
|
||||||
# Note there are two levels of formatting.
|
set -e
|
||||||
# - First format pass inserts salt version and delimiter
|
set -u
|
||||||
# - Second pass at run-time and inserts optional "sudo" and command
|
|
||||||
SSH_SHIM = r'''/bin/sh << 'EOF'
|
|
||||||
#!/bin/sh
|
|
||||||
|
|
||||||
MISS_PKG=""
|
DEBUG="{{DEBUG}}"
|
||||||
|
if [ -n "$DEBUG" ]; then
|
||||||
|
set -x
|
||||||
|
fi
|
||||||
|
|
||||||
command -v tar >/dev/null
|
SUDO=""
|
||||||
if [ $? -ne 0 ]
|
if [ -n "{{SUDO}}" ]; then
|
||||||
then
|
SUDO="sudo root -c"
|
||||||
MISS_PKG="$MISS_PKG tar"
|
fi
|
||||||
fi
|
|
||||||
|
|
||||||
for py_candidate in \
|
EX_PYTHON_OLD={EX_THIN_PYTHON_OLD} # Python interpreter is too old and incompatible
|
||||||
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
|
|
||||||
|
|
||||||
if [ -z "$PYTHON" ]
|
PYTHON_CMDS="
|
||||||
then
|
python27
|
||||||
MISS_PKG="$MISS_PKG python"
|
python2.7
|
||||||
fi
|
python26
|
||||||
|
python2.6
|
||||||
|
python2
|
||||||
|
python
|
||||||
|
"
|
||||||
|
|
||||||
SALT="/tmp/.salt/salt-call"
|
main()
|
||||||
if [ "{{2}}" = "md5" ]
|
{{{{
|
||||||
then
|
local py_cmd
|
||||||
for md5_candidate in \
|
local py_cmd_path
|
||||||
md5sum \
|
for py_cmd in $PYTHON_CMDS; do
|
||||||
md5 \
|
if "$py_cmd" -c 'import sys; sys.exit(not sys.hexversion >= 0x02060000);' >/dev/null 2>&1; then
|
||||||
csum \
|
local py_cmd_path
|
||||||
digest ;
|
py_cmd_path=`"$py_cmd" -c 'import sys; print sys.executable;'`
|
||||||
do
|
echo "FOUND: $py_cmd_path" >&2
|
||||||
command -v $md5_candidate >/dev/null
|
exec $SUDO "$py_cmd_path" -c 'exec """{{SSH_PY_CODE}}""".decode("base64")' -- {{SSH_PY_ARGS}}
|
||||||
if [ $? -eq 0 ]
|
else
|
||||||
then
|
echo "WARNING: $py_cmd not found or too old" >&2
|
||||||
SUMCHECK="$md5_candidate"
|
fi
|
||||||
break
|
done
|
||||||
fi
|
|
||||||
done
|
|
||||||
else
|
|
||||||
command -v "{{2}}" >/dev/null
|
|
||||||
if [ $? -eq 0 ]
|
|
||||||
then
|
|
||||||
SUMCHECK="{{2}}"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -z "$SUMCHECK" ]
|
echo "ERROR: Unable to locate appropriate python command" >&2
|
||||||
then
|
exit $EX_PYTHON_OLD
|
||||||
MISS_PKG="$MISS_PKG md5sum/md5/csum"
|
}}}}
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -n "$MISS_PKG" ]
|
main
|
||||||
then
|
EOF'''.format(
|
||||||
echo "The following required Packages are missing: $MISS_PKG" >&2
|
EX_THIN_PYTHON_OLD=salt.exitcodes.EX_THIN_PYTHON_OLD,
|
||||||
exit 127
|
)
|
||||||
fi
|
|
||||||
|
|
||||||
# MD5 check for systems with a BSD userland (includes OSX).
|
with open(os.path.join(os.path.dirname(__file__), 'ssh_py_shim.py')) as ssh_py_shim:
|
||||||
if [ "$SUMCHECK" = "md5" ]
|
SSH_PY_SHIM = ''.join(ssh_py_shim.readlines()).encode('base64')
|
||||||
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)
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -337,9 +311,6 @@ class SSH(object):
|
|||||||
**target)
|
**target)
|
||||||
ret = {'id': single.id}
|
ret = {'id': single.id}
|
||||||
stdout, stderr, retcode = single.run()
|
stdout, stderr, retcode = single.run()
|
||||||
if stdout.startswith('deploy'):
|
|
||||||
single.deploy()
|
|
||||||
stdout, stderr, retcode = single.run()
|
|
||||||
# This job is done, yield
|
# This job is done, yield
|
||||||
try:
|
try:
|
||||||
data = salt.utils.find_json(stdout)
|
data = salt.utils.find_json(stdout)
|
||||||
@ -522,7 +493,7 @@ class Single(object):
|
|||||||
self.shell = salt.client.ssh.shell.Shell(opts, **args)
|
self.shell = salt.client.ssh.shell.Shell(opts, **args)
|
||||||
self.minion_config = yaml.dump(
|
self.minion_config = yaml.dump(
|
||||||
{
|
{
|
||||||
'root_dir': '/tmp/.salt/running_data',
|
'root_dir': os.path.join(DEFAULT_THIN_DIR, 'running_data'),
|
||||||
'id': self.id,
|
'id': self.id,
|
||||||
}).strip()
|
}).strip()
|
||||||
self.target = kwargs
|
self.target = kwargs
|
||||||
@ -570,8 +541,9 @@ class Single(object):
|
|||||||
'''
|
'''
|
||||||
thin = salt.utils.thin.gen_thin(self.opts['cachedir'])
|
thin = salt.utils.thin.gen_thin(self.opts['cachedir'])
|
||||||
self.shell.send(
|
self.shell.send(
|
||||||
thin,
|
thin,
|
||||||
'/tmp/.salt/salt-thin.tgz')
|
os.path.join(DEFAULT_THIN_DIR, 'salt-thin.tgz'),
|
||||||
|
)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def run(self, deploy_attempted=False):
|
def run(self, deploy_attempted=False):
|
||||||
@ -598,10 +570,6 @@ class Single(object):
|
|||||||
else:
|
else:
|
||||||
stdout, stderr, retcode = self.cmd_block()
|
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
|
return stdout, stderr, retcode
|
||||||
|
|
||||||
def run_wfunc(self):
|
def run_wfunc(self):
|
||||||
@ -668,14 +636,39 @@ class Single(object):
|
|||||||
ret = json.dumps(self.wfuncs[self.fun](*self.args, **self.kwargs))
|
ret = json.dumps(self.wfuncs[self.fun](*self.args, **self.kwargs))
|
||||||
return ret, '', None
|
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):
|
def cmd(self):
|
||||||
'''
|
'''
|
||||||
Prepare the pre-check command to send to the subsystem
|
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'):
|
if self.fun.startswith('state.highstate'):
|
||||||
self.highstate_seed()
|
self.highstate_seed()
|
||||||
elif self.fun.startswith('state.sls'):
|
elif self.fun.startswith('state.sls'):
|
||||||
@ -684,88 +677,132 @@ class Single(object):
|
|||||||
salt.utils.args.parse_input(self.args)
|
salt.utils.args.parse_input(self.args)
|
||||||
)
|
)
|
||||||
self.sls_seed(*args, **kwargs)
|
self.sls_seed(*args, **kwargs)
|
||||||
cmd_str = ' '.join([self._escape_arg(arg) for arg in self.argv])
|
cmd_str = self._cmd_str()
|
||||||
sudo = 'sudo' if self.target['sudo'] else ''
|
|
||||||
thin_sum = salt.utils.thin.thin_sum(
|
for stdout, stderr, retcode in self.shell.exec_nb_cmd(cmd_str):
|
||||||
self.opts['cachedir'],
|
|
||||||
self.opts['hash_type'])
|
|
||||||
cmd = SSH_SHIM.format(
|
|
||||||
sudo,
|
|
||||||
cmd_str,
|
|
||||||
self.opts['hash_type'],
|
|
||||||
thin_sum,
|
|
||||||
self.minion_config)
|
|
||||||
for stdout, stderr, retcode in self.shell.exec_nb_cmd(cmd):
|
|
||||||
yield stdout, stderr, retcode
|
yield stdout, stderr, retcode
|
||||||
|
|
||||||
def cmd_block(self, is_retry=False):
|
def cmd_block(self, is_retry=False):
|
||||||
'''
|
'''
|
||||||
Prepare the pre-check command to send to the subsystem
|
Prepare the pre-check command to send to the subsystem
|
||||||
'''
|
'''
|
||||||
# 1. check if python is on the target
|
# 1. execute SHIM + command
|
||||||
# 2. check is salt-call is on the target
|
# 2. check if SHIM returns a master request or if it completed
|
||||||
# 3. deploy salt-thin
|
# 3. handle any master request
|
||||||
# 4. execute command
|
# 4. re-execute SHIM + command
|
||||||
cmd_str = ' '.join([self._escape_arg(arg) for arg in self.argv])
|
# 5. split SHIM results from command results
|
||||||
sudo = 'sudo' if self.target['sudo'] else ''
|
# 6. return command results
|
||||||
thin_sum = salt.utils.thin.thin_sum(
|
|
||||||
self.opts['cachedir'],
|
log.debug('Performing shimmed, blocking command as follows:\n{0}'.format(' '.join(self.argv)))
|
||||||
self.opts['hash_type'])
|
cmd_str = self._cmd_str()
|
||||||
cmd = SSH_SHIM.format(
|
stdout, stderr, retcode = self.shell.exec_cmd(cmd_str)
|
||||||
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)
|
|
||||||
|
|
||||||
log.debug('STDOUT {1}\n{0}'.format(stdout, self.target['host']))
|
log.debug('STDOUT {1}\n{0}'.format(stdout, self.target['host']))
|
||||||
log.debug('STDERR {1}\n{0}'.format(stderr, 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)
|
error = self.categorize_shim_errors(stdout, stderr, retcode)
|
||||||
if error:
|
if error:
|
||||||
return 'ERROR: {0}'.format(error), stderr, retcode
|
return 'ERROR: {0}'.format(error), stderr, retcode
|
||||||
|
|
||||||
if RSTR in stdout:
|
# FIXME: this discards output from ssh_shim if the shim succeeds. It should
|
||||||
stdout = stdout.split(RSTR)[1].strip()
|
# always save the shim output regardless of shim success or failure.
|
||||||
if stdout.startswith('deploy'):
|
if re.search(RSTR_RE, stdout):
|
||||||
self.deploy()
|
stdout = re.split(RSTR_RE, stdout, 1)[1].strip()
|
||||||
stdout, stderr, retcode = self.shell.exec_cmd(cmd)
|
else:
|
||||||
if RSTR in stdout:
|
# This is actually an error state prior to the shim but let it fall through
|
||||||
stdout = stdout.split(RSTR)[1].strip()
|
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
|
return stdout, stderr, retcode
|
||||||
|
|
||||||
def categorize_shim_errors(self, stdout, stderr, retcode):
|
def categorize_shim_errors(self, stdout, stderr, retcode):
|
||||||
# Unused stdout and retcode for now but these may be used to
|
if re.search(RSTR_RE, stdout):
|
||||||
# categorize errors
|
# RSTR was found in stdout which means that the shim
|
||||||
_ = stdout
|
# functioned without *errors* . . . but there may be shim
|
||||||
_ = retcode
|
# 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 '\
|
perm_error_fmt = 'Permissions problem, target user may need '\
|
||||||
'to be root or use sudo:\n {0}'
|
'to be root or use sudo:\n {0}'
|
||||||
if stderr.startswith('Permission denied'):
|
|
||||||
return None
|
|
||||||
errors = [
|
errors = [
|
||||||
('sudo: no tty present and no askpass program specified',
|
(
|
||||||
'sudo expected a password, NOPASSWD required'),
|
(),
|
||||||
('Python too old',
|
'sudo: no tty present and no askpass program specified',
|
||||||
'salt requires python 2.6 or better on target hosts'),
|
'sudo expected a password, NOPASSWD required'
|
||||||
('sudo: sorry, you must have a tty to run sudo',
|
),
|
||||||
'sudo is configured with requiretty'),
|
(
|
||||||
('Failed to open log file',
|
(salt.exitcodes.EX_THIN_PYTHON_OLD,),
|
||||||
perm_error_fmt.format(stderr)),
|
'Python interpreter is too old',
|
||||||
('Permission denied:.*/salt',
|
'salt requires python 2.6 or newer on target hosts'
|
||||||
perm_error_fmt.format(stderr)),
|
),
|
||||||
('Failed to create directory path.*/salt',
|
(
|
||||||
perm_error_fmt.format(stderr)),
|
(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:
|
for error in errors:
|
||||||
if re.search(error[0], stderr):
|
if retcode in error[0] or re.search(error[1], stderr):
|
||||||
return error[1]
|
return error[2]
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def sls_seed(self,
|
def sls_seed(self,
|
||||||
@ -822,8 +859,9 @@ class Single(object):
|
|||||||
file_refs = lowstate_file_refs(chunks)
|
file_refs = lowstate_file_refs(chunks)
|
||||||
trans_tar = prep_trans_tar(self.opts, chunks, file_refs)
|
trans_tar = prep_trans_tar(self.opts, chunks, file_refs)
|
||||||
self.shell.send(
|
self.shell.send(
|
||||||
trans_tar,
|
trans_tar,
|
||||||
'/tmp/salt_state.tgz')
|
os.path.join(DEFAULT_THIN_DIR, 'salt_state.tgz'),
|
||||||
|
)
|
||||||
self.argv = ['state.pkg', '/tmp/salt_state.tgz', 'test={0}'.format(test)]
|
self.argv = ['state.pkg', '/tmp/salt_state.tgz', 'test={0}'.format(test)]
|
||||||
|
|
||||||
|
|
||||||
|
169
salt/client/ssh/ssh_py_shim.py
Normal file
169
salt/client/ssh/ssh_py_shim.py
Normal file
@ -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".format(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))
|
@ -8,3 +8,8 @@ prefix or in `sysexits.h`).
|
|||||||
# Too many situations use "exit 1" - try not to use it when something
|
# Too many situations use "exit 1" - try not to use it when something
|
||||||
# else is more appropriate.
|
# else is more appropriate.
|
||||||
EX_GENERIC = 1
|
EX_GENERIC = 1
|
||||||
|
|
||||||
|
# Salt SSH "Thin" deployment failures
|
||||||
|
EX_THIN_PYTHON_OLD = 10
|
||||||
|
EX_THIN_DEPLOY = 11
|
||||||
|
EX_THIN_CHECKSUM = 12
|
||||||
|
Loading…
Reference in New Issue
Block a user