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.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,88 +677,132 @@ 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)
|
||||
for stdout, stderr, retcode in self.shell.exec_nb_cmd(cmd):
|
||||
cmd_str = self._cmd_str()
|
||||
|
||||
for stdout, stderr, retcode in self.shell.exec_nb_cmd(cmd_str):
|
||||
yield stdout, stderr, retcode
|
||||
|
||||
def cmd_block(self, is_retry=False):
|
||||
'''
|
||||
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)]
|
||||
|
||||
|
||||
|
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
|
||||
# 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
|
||||
|
Loading…
Reference in New Issue
Block a user