Many minor changes

+ add quiet mode
+ add warning loglevel argument
+ minor arg changes (-F -> -G)
+ fix put_files bugs
+ add automatic shell command escaping and fix awk commands after this change
+ small log message changes
+ remove use of temporary files for get_logs
+ implement reliable stdin in ssh_node
+ add exit PIPESTATUS into get_logs
+ add function to check if given node attrs are non-empty, used for cli prints
+ make args optional in RunIttem and SemaphoreProcess
+ fix stderr utf-8 decode issue (not a fix, more of a workaround, need better fix)
+ 'inputfile' now unnecessary in ssh_node (not used) but left in there for now
+ used 'xxd' to encode stdin to keep \0 bytes. Can use base64 instead, no real difference.
This commit is contained in:
f3flight 2016-05-17 04:27:41 +00:00
parent 0d1e00d785
commit 44ac4b893f
5 changed files with 127 additions and 82 deletions

View File

@ -1 +1,2 @@
source "$OPENRC"
nova help | grep 'service-list' &> /dev/null && nova service-list || nova-manage service list

View File

@ -24,11 +24,13 @@ from timmy.conf import load_conf
from timmy.tools import interrupt_wrapper
def pretty_run(msg, f, args=[], kwargs={}):
sys.stdout.write('%s...\r' % msg)
sys.stdout.flush()
def pretty_run(quiet, msg, f, args=[], kwargs={}):
if not quiet:
sys.stdout.write('%s...\r' % msg)
sys.stdout.flush()
result = f(*args, **kwargs)
print('%s: done' % msg)
if not quiet:
print('%s: done' % msg)
return result
@ -65,17 +67,20 @@ def main(argv=None):
parser.add_argument('--fake-logs',
help='Do not collect logs, only calculate size.',
action='store_true')
parser.add_argument('-d', '--debug',
help='Be extremely verbose.',
parser.add_argument('-w', '--warning',
help='Sets log level to warning (default).',
action='store_true')
parser.add_argument('-v', '--verbose',
help='Be verbose.',
action='store_true')
parser.add_argument('-d', '--debug',
help='Be extremely verbose.',
action='store_true')
parser.add_argument('-C', '--command',
help=('Enables shell mode. Shell command to'
' execute. For help on shell mode, read'
' timmy/conf.py'))
parser.add_argument('-F', '--file', action='append',
parser.add_argument('-G', '--get', action='append',
help=('Enables shell mode. Can be specified multiple'
' times. Filemask to collect via "scp -r".'
' Result is placed into a folder specified'
@ -93,11 +98,20 @@ def main(argv=None):
' accumulating results across runs.'),
action='store_true')
parser.add_argument('-P', '--put', nargs=2, action='append',
help=('Upload filemask via "scp -r" to node(s).'
' Each argument must contain two strings -'
' source file/path/mask and destination.'))
help=('Enables shell mode. Upload filemask via'
' "scp -r" to node(s). Each argument must'
'contain two strings - source file/path/mask'
' and destination.'))
parser.add_argument('-q', '--quiet',
help=('Print only command execution results and log'
' messages. Good for quick runs / "watch" wrap.'
' Also sets default loglevel to ERROR.'),
action='store_true')
args = parser.parse_args(argv[1:])
loglevel = logging.WARNING
if args.quiet and not args.warning:
loglevel = logging.ERROR
else:
loglevel = logging.WARNING
if args.verbose:
loglevel = logging.INFO
if args.debug:
@ -106,7 +120,7 @@ def main(argv=None):
level=loglevel,
format='%(asctime)s %(levelname)s %(message)s')
conf = load_conf(args.conf)
if args.command or args.file:
if args.command or args.get or args.put:
conf['shell_mode'] = True
if args.no_clean:
conf['clean'] = False
@ -118,12 +132,12 @@ def main(argv=None):
for k in conf:
if k.startswith(Node.conf_match_prefix):
conf.pop(k)
for src_dst in args.put:
conf[Node.pkey].append(src_dst)
if args.put:
conf[Node.pkey] = args.put
if args.command:
conf[Node.ckey] = [{'stdout': args.command}]
if args.file:
conf[Node.fkey] = args.file
if args.get:
conf[Node.fkey] = args.get
else:
filter = conf['soft_filter']
if args.role:
@ -133,50 +147,59 @@ def main(argv=None):
main_arc = os.path.join(conf['archives'], 'general.tar.gz')
if args.dest_file:
main_arc = args.dest_file
nm = pretty_run('Initializing node data',
nm = pretty_run(args.quiet, 'Initializing node data',
NodeManager,
kwargs={'conf': conf, 'extended': args.extended})
if not args.only_logs:
if conf[Node.pkey]:
pretty_run('Uploading files', nm.put_files)
if not (conf['shell_mode'] and not args.command):
pretty_run('Executing commands and scripts', nm.run_commands,
args=(conf['outdir'], args.maxthreads))
if not (conf['shell_mode'] and not args.file):
pretty_run('Collecting files and filelists', nm.get_files,
args=(conf['outdir'], args.maxthreads))
if not args.no_archive:
pretty_run('Creating outputs and files archive',
nm.create_archive_general,
args=(conf['outdir'], main_arc, 60))
if nm.has(Node.pkey):
pretty_run(args.quiet, 'Uploading files', nm.put_files)
if nm.has(Node.ckey, Node.skey):
pretty_run(args.quiet, 'Executing commands and scripts',
nm.run_commands, args=(conf['outdir'],
args.maxthreads))
if nm.has(Node.fkey, Node.flkey):
pretty_run(args.quiet, 'Collecting files and filelists',
nm.get_files, args=(conf['outdir'], args.maxthreads))
if not args.no_archive and nm.has(*Node.conf_archive_general):
pretty_run(args.quiet, 'Creating outputs and files archive',
nm.create_archive_general, args=(conf['outdir'],
main_arc, 60))
if args.only_logs or args.getlogs:
size = pretty_run('Calculating logs size', nm.calculate_log_size,
args=(args.maxthreads,))
size = pretty_run(args.quiet, 'Calculating logs size',
nm.calculate_log_size, args=(args.maxthreads,))
if size == 0:
logging.warning('Size zero - no logs to collect.')
return
enough = pretty_run('Checking free space', nm.is_enough_space,
args=(conf['archives'],))
enough = pretty_run(args.quiet, 'Checking free space',
nm.is_enough_space, args=(conf['archives'],))
if enough:
pretty_run('Collecting and packing logs', nm.get_logs,
pretty_run(args.quiet, 'Collecting and packing logs', nm.get_logs,
args=(conf['archives'], conf['compress_timeout']),
kwargs={'maxthreads': args.logs_maxthreads,
'fake': args.fake_logs})
else:
logging.warning(('Not enough space for logs in "%s", skipping'
'log collection.') %
conf['archives'])
logging.info("Nodes:\n%s" % nm)
print('Run complete. Node information:')
print(nm)
if not args.quiet:
print('Run complete. Node information:')
print(nm)
if conf['shell_mode']:
if args.command:
print('Results:')
if not args.quiet:
print('Results:')
for node in nm.nodes.values():
for cmd, path in node.mapcmds.items():
with open(path, 'r') as f:
for line in f.readlines():
print('node-%s: %s' % (node.id, line.rstrip('\n')))
if args.file:
print('Files collected into "%s".' % conf['outdir'])
if not args.no_archive:
print('Results packed and available in "%s".' % conf['archives'])
print('node-%s:\t%s' %
(node.id, line.rstrip('\n')))
if nm.has(Node.fkey, Node.flkey) and not args.quiet:
print('Outputs and files available in "%s".' % conf['outdir'])
if all([not args.no_archive, nm.has(*Node.conf_archive_general),
not args.quiet]):
print('Archives available in "%s".' % conf['archives'])
return 0
if __name__ == '__main__':

View File

@ -30,6 +30,10 @@ def load_conf(filename):
conf['archives'] = os.path.join(gettempdir(), 'timmy', 'archives')
conf['cmds_archive'] = ''
conf['put'] = []
conf['cmds'] = []
conf['scripts'] = []
conf['files'] = []
conf['filelists'] = []
conf['logs'] = {'path': '/var/log',
'exclude': '[-_]\d{8}$|atop[-_]|\.gz$'}
'''Shell mode - only run what was specified via command line.

View File

@ -39,6 +39,7 @@ class Node(object):
pkey = 'put'
conf_actionable = [lkey, ckey, skey, fkey, flkey, pkey]
conf_appendable = [lkey, ckey, skey, fkey, flkey, pkey]
conf_archive_general = [ckey, skey, fkey, flkey]
conf_keep_default = [skey, ckey, fkey, flkey]
conf_once_prefix = 'once_'
conf_match_prefix = 'by_'
@ -189,8 +190,8 @@ class Node(object):
logging.error("exec_cmd: can't write to file %s" % dfile)
return self
def exec_simple_cmd(self, cmd, infile, outfile, timeout=15,
fake=False, ok_codes=None):
def exec_simple_cmd(self, cmd, timeout=15, infile=None, outfile=None,
fake=False, ok_codes=None, input=None):
logging.info('node:%s(%s), exec: %s' % (self.id, self.ip, cmd))
if not fake:
outs, errs, code = tools.ssh_node(ip=self.ip,
@ -199,8 +200,8 @@ class Node(object):
env_vars=self.env_vars,
timeout=timeout,
outputfile=outfile,
inputfile=infile,
ok_codes=ok_codes)
ok_codes=ok_codes,
input=input)
self.check_code(code, 'exec_simple_cmd', cmd, ok_codes)
def get_files(self, odir='info', timeout=15):
@ -241,7 +242,7 @@ class Node(object):
def put_files(self):
logging.info('put_files: node: %s, IP: %s' % (self.id, self.ip))
for f in self.put:
outs, errs, code = tools.get_file_scp(ip=self.ip,
outs, errs, code = tools.put_file_scp(ip=self.ip,
file=f[0],
dest=f[1],
recursive=True)
@ -453,7 +454,7 @@ class NodeManager(object):
self.nodes[node.ip] = node
def get_version(self):
cmd = "awk -F ':' '/release/ {print \$2}' /etc/nailgun/version.yaml"
cmd = "awk -F ':' '/release/ {print $2}' /etc/nailgun/version.yaml"
fuelnode = self.nodes[self.fuelip]
release, err, code = tools.ssh_node(ip=fuelnode.ip,
command=cmd,
@ -468,7 +469,7 @@ class NodeManager(object):
logging.info('release:%s' % (self.version))
def nodes_get_release(self):
cmd = "awk -F ':' '/fuel_version/ {print \$2}' /etc/astute.yaml"
cmd = "awk -F ':' '/fuel_version/ {print $2}' /etc/astute.yaml"
for node in self.nodes.values():
if node.id == 0:
# skip master
@ -605,7 +606,7 @@ class NodeManager(object):
@run_with_lock
def get_logs(self, outdir, timeout, fake=False, maxthreads=10, speed=100):
if fake:
logging.info('archive_logs:skip creating archives(fake:%s)' % fake)
logging.info('get_logs: fake = True, skipping' % fake)
return
txtfl = []
speed = self.find_adm_interface_speed(speed)
@ -614,31 +615,27 @@ class NodeManager(object):
run_items = []
for node in [n for n in self.nodes.values() if not n.filtered_out]:
if not node.logs_dict():
logging.info(("create_archive_logs: node %s - no logs "
logging.info(("get_logs: node %s - no logs "
"to collect") % node.id)
continue
node.archivelogsfile = os.path.join(outdir,
'logs-node-%s.tar.gz' %
str(node.id))
tools.mdir(outdir)
logslistfile = node.archivelogsfile + '.txt'
txtfl.append(logslistfile)
try:
with open(logslistfile, 'w') as llf:
for fn in node.logs_dict():
llf.write(fn.lstrip(os.path.abspath(os.sep))+"\0")
except:
logging.error("create_archive_logs: Can't write to file %s" %
logslistfile)
continue
input = ''
for fn in node.logs_dict():
input += '%s\0' % fn.lstrip(os.path.abspath(os.sep))
with open('test-%s' % node.id, 'w') as fi:
fi.write(input)
cmd = ("tar --gzip -C %s --create --warning=no-file-changed "
" --file - --null --files-from -" % os.path.abspath(os.sep))
if not (node.ip == 'localhost' or node.ip.startswith('127.')):
cmd = ' '.join([cmd, "| python -c '%s'" % pythonslowpipe])
cmd = ' '.join([cmd, "| python -c '%s'; exit ${PIPESTATUS}" %
pythonslowpipe])
args = {'cmd': cmd,
'infile': logslistfile,
'outfile': node.archivelogsfile,
'timeout': timeout,
'outfile': node.archivelogsfile,
'input': input,
'ok_codes': [0, 1]}
run_items.append(tools.RunItem(target=node.exec_simple_cmd,
args=args))
@ -647,7 +644,7 @@ class NodeManager(object):
try:
os.remove(tfile)
except:
logging.error("archive_logs: can't delete file %s" % tfile)
logging.error("get_logs: can't delete file %s" % tfile)
@run_with_lock
def get_files(self, odir=Node.fkey, timeout=15):
@ -664,6 +661,18 @@ class NodeManager(object):
run_items.append(tools.RunItem(target=n.put_files))
tools.run_batch(run_items, 10)
def has(self, *keys):
nodes = {}
for k in keys:
for n in self.nodes.values():
if hasattr(n, k):
attr = getattr(n, k)
if attr:
if k not in nodes:
nodes[k] = []
nodes[k].append(n)
return nodes
def main(argv=None):
return 0

View File

@ -28,6 +28,7 @@ import subprocess
import yaml
from flock import FLock
from tempfile import gettempdir
from pipes import quote
slowpipe = '''
@ -73,19 +74,21 @@ def run_with_lock(f):
class RunItem():
def __init__(self, target, args, key=None):
def __init__(self, target, args=None, key=None):
self.target = target
self.args = args
self.key = key
self.process = None
self.queue = None
self.key = key
class SemaphoreProcess(Process):
def __init__(self, semaphore, target, args, queue=None):
def __init__(self, semaphore, target, args=None, queue=None):
Process.__init__(self)
self.semaphore = semaphore
self.target = target
if not args:
args = {}
self.args = args
self.queue = queue
@ -188,12 +191,13 @@ def mdir(directory):
def launch_cmd(cmd, timeout, input=None, ok_codes=None):
def _log_msg(cmd, stderr, code, debug=False, stdout=None):
message = ('launch_cmd:\n'
def _log_msg(cmd, stderr, code, debug=False, stdin=None, stdout=None):
message = (u'launch_cmd:\n'
'___command: %s\n'
'______code: %s\n'
'____stderr: %s\n' % (cmd, code, stderr))
'____stderr: %s' % (cmd, code, stderr.decode('utf-8')))
if debug:
message += '\n_____stdin: %s\n' % stdin
message += '____stdout: %s' % stdout
return message
@ -228,7 +232,8 @@ def launch_cmd(cmd, timeout, input=None, ok_codes=None):
if timeout_killer:
timeout_killer.cancel()
logging.info(_log_msg(cmd, errs, p.returncode))
logging.debug(_log_msg(cmd, errs, p.returncode, debug=True, stdout=outs))
logging.debug(_log_msg(cmd, errs, p.returncode, debug=True,
stdin=input, stdout=outs))
if p.returncode:
if not ok_codes or p.returncode not in ok_codes:
logging.warning(_log_msg(cmd, errs, p.returncode))
@ -237,7 +242,7 @@ def launch_cmd(cmd, timeout, input=None, ok_codes=None):
def ssh_node(ip, command='', ssh_opts=None, env_vars=None, timeout=15,
filename=None, inputfile=None, outputfile=None,
ok_codes=None, prefix='nice -n 19 ionice -c 3'):
ok_codes=None, input=None, prefix='nice -n 19 ionice -c 3'):
if not ssh_opts:
ssh_opts = ''
if not env_vars:
@ -252,21 +257,24 @@ def ssh_node(ip, command='', ssh_opts=None, env_vars=None, timeout=15,
env_vars, timeout)
else:
logging.info("exec ssh")
# base cmd str
bstr = "timeout '%s' ssh -t -T %s '%s' '%s' " % (
timeout, ssh_opts, ip, env_vars)
if filename is None:
cmd = bstr + '"' + prefix + ' ' + command + '"'
cmd = '%s %s' % (bstr, quote(prefix + ' ' + command))
if inputfile is not None:
'''inputfile and stdin will not work together,
give priority to inputfile'''
input = None
cmd = "%s < '%s'" % (cmd, inputfile)
else:
cmd = bstr + " '%s bash -s' < '%s'" % (prefix, filename)
if inputfile is not None:
cmd = bstr + '"' + prefix + " " + command + '" < ' + inputfile
cmd = "%s'%s bash -s' < '%s'" % (bstr, prefix, filename)
logging.info("ssh_node: inputfile selected, cmd: %s" % cmd)
if outputfile is not None:
cmd += ' > "' + outputfile + '"'
cmd = ("trap 'kill $pid' 15; " +
"trap 'kill $pid' 2; " + cmd + '&:; pid=$!; wait $!')
return launch_cmd(cmd, timeout, ok_codes=ok_codes)
cmd = "%s > '%s'" % (cmd, outputfile)
cmd = ("input=\"$(cat | xxd -p)\"; trap 'kill $pid' 15; " +
"trap 'kill $pid' 2; echo -n \"$input\" | xxd -r -p | " + cmd +
' &:; pid=$!; wait $!')
return launch_cmd(cmd, timeout, input=input, ok_codes=ok_codes)
def get_files_rsync(ip, data, ssh_opts, dpath, timeout=15):