summaryrefslogtreecommitdiffstats
path: root/crmsh/ui_script.py
diff options
context:
space:
mode:
Diffstat (limited to 'crmsh/ui_script.py')
-rw-r--r--crmsh/ui_script.py523
1 files changed, 523 insertions, 0 deletions
diff --git a/crmsh/ui_script.py b/crmsh/ui_script.py
new file mode 100644
index 0000000..abf6f0b
--- /dev/null
+++ b/crmsh/ui_script.py
@@ -0,0 +1,523 @@
+# Copyright (C) 2013 Kristoffer Gronlund <kgronlund@suse.com>
+# See COPYING for license information.
+
+
+import sys
+
+try:
+ import json
+except ImportError:
+ import simplejson as json
+
+from . import config
+from . import command
+from . import scripts
+from . import utils
+from . import options
+from . import completers as compl
+from . import log
+
+
+logger = log.setup_logger(__name__)
+
+
+class ConsolePrinter(object):
+ def __init__(self):
+ self.in_progress = False
+
+ def print_header(self, script, params, hosts):
+ if script['shortdesc']:
+ logger.info(script['shortdesc'])
+ logger.info("Nodes: " + ', '.join([x[0] for x in hosts]))
+
+ def error(self, host, message):
+ logger.error("[%s]: %s", host, message)
+
+ def output(self, host, rc, out, err):
+ if out:
+ logger.info("[%s]: %s", host, out)
+ if err or rc != 0:
+ logger.error("[%s]: (rc=%d) %s", host, rc, err)
+
+ def start(self, action):
+ if not options.batch:
+ txt = '%s...' % (action['shortdesc'] or action['name'])
+ sys.stdout.write(txt)
+ sys.stdout.flush()
+ self.in_progress = True
+
+ def finish(self, action, rc, output):
+ self.flush()
+ if rc:
+ logger.info(action['shortdesc'] or action['name'])
+ else:
+ logger.error("%s (rc=%s)", action['shortdesc'] or action['name'], rc)
+ if output:
+ print(output)
+
+ def flush(self):
+ if self.in_progress:
+ self.in_progress = False
+ if not config.core.debug:
+ sys.stdout.write('\r')
+ else:
+ sys.stdout.write('\n')
+ sys.stdout.flush()
+
+ def debug(self, msg):
+ if config.core.debug or options.regression_tests:
+ self.flush()
+ logger.debug(msg)
+
+ def print_command(self, nodes, command):
+ self.flush()
+ sys.stdout.write("** %s - %s\n" % (nodes, command))
+
+
+class JsonPrinter(object):
+ def __init__(self):
+ self.results = []
+
+ def print_header(self, script, params, hosts):
+ pass
+
+ def error(self, host, message):
+ self.results.append({'host': str(host), 'error': str(message) if message else ''})
+
+ def output(self, host, rc, out, err):
+ ret = {'host': host, 'rc': rc, 'output': str(out)}
+ if err:
+ ret['error'] = str(err)
+ self.results.append(ret)
+
+ def start(self, action):
+ pass
+
+ def finish(self, action, rc, output):
+ ret = {'rc': rc, 'shortdesc': str(action['shortdesc'])}
+ if rc != 0 and not rc:
+ ret['error'] = str(output) if output else ''
+ else:
+ ret['output'] = str(output) if output else ''
+ print(json.dumps(ret, sort_keys=True))
+
+ def flush(self):
+ pass
+
+ def debug(self, msg):
+ if config.core.debug:
+ logger.debug(msg)
+
+ def print_command(self, nodes, command):
+ pass
+
+
+def describe_param(p, name, getall):
+ if not getall and p.get('advanced'):
+ return ""
+ opt = ' (required) ' if p['required'] else ''
+ opt += ' (unique) ' if p['unique'] else ''
+ if 'value' in p:
+ opt += (' (default: %s)' % (repr(p['value']))) if p['value'] else ''
+ s = " %s%s\n" % (name, opt)
+ s += " %s\n" % (p['shortdesc'])
+ return s
+
+
+def _scoped_name(context, name):
+ if context:
+ return ':'.join(context) + ':' + name
+ return name
+
+
+def describe_step(icontext, context, s, getall):
+ ret = "%s. %s" % ('.'.join([str(i + 1) for i in icontext]), scripts.format_desc(s['shortdesc']) or 'Parameters')
+ if not s['required']:
+ ret += ' (optional)'
+ ret += '\n\n'
+ if s.get('name'):
+ context = context + [s['name']]
+ for p in s.get('parameters', []):
+ ret += describe_param(p, _scoped_name(context, p['name']), getall)
+ for i, step in enumerate(s.get('steps', [])):
+ ret += describe_step(icontext + [i], context, step, getall)
+ return ret
+
+
+def _nvpairs2parameters(args):
+ """
+ input: list with name=value nvpairs, where each name is a :-path
+ output: dict tree of name:value, where value can be a nested dict tree
+ """
+ def _set(d, path, val):
+ if len(path) == 1:
+ d[path[0]] = val
+ else:
+ if path[0] not in d:
+ d[path[0]] = {}
+ _set(d[path[0]], path[1:], val)
+
+ ret = {}
+ for key, val in utils.nvpairs2dict(args).items():
+ _set(ret, key.split(':'), val)
+ return ret
+
+
+_fixups = {
+ 'wizard': 'Legacy Wizards',
+ 'sap': 'SAP',
+ 'nfs': 'NFS'
+}
+
+
+def _category_pretty(c):
+ v = _fixups.get(str(c).lower())
+ if v is not None:
+ return v
+ return str(c).capitalize()
+
+
+class Script(command.UI):
+ '''
+ Cluster scripts can perform cluster-wide configuration,
+ validation and management. See the `list` command for
+ an overview of available scripts.
+
+ The script UI is a thin veneer over the scripts
+ backend module.
+ '''
+ name = "script"
+
+ @command.completers_repeating(compl.choice(['all', 'names']))
+ def do_list(self, context, *args):
+ '''
+ List available scripts.
+ hides scripts with category Script or '' by default,
+ unless "all" is passed as argument
+ '''
+ for arg in args:
+ if arg.lower() not in ("all", "names"):
+ context.fatal_error("Unexpected argument '%s': expected [all|names]" % (arg))
+ show_all = any(x.lower() == 'all' for x in args)
+ names = any(x.lower() == 'names' for x in args)
+ if not names:
+ categories = {}
+ for name in scripts.list_scripts():
+ try:
+ script = scripts.load_script(name)
+ if script is None:
+ continue
+ cat = script['category'].lower()
+ if not show_all and cat == 'script':
+ continue
+ cat = _category_pretty(cat)
+ if cat not in categories:
+ categories[cat] = []
+ categories[cat].append("%-16s %s" % (script['name'], script['shortdesc']))
+ except ValueError as err:
+ logger.error(str(err))
+ continue
+ for c, lst in sorted(iter(categories.items()), key=lambda x: x[0]):
+ if c:
+ print("%s:\n" % (c))
+ for s in sorted(lst):
+ print(s)
+ print('')
+ elif show_all:
+ for name in scripts.list_scripts():
+ print(name)
+ else:
+ for name in scripts.list_scripts():
+ try:
+ script = scripts.load_script(name)
+ if script is None or script['category'].lower() == 'script':
+ continue
+ except ValueError as err:
+ logger.error(str(err))
+ continue
+ print(name)
+
+ @command.completers_repeating(compl.call(scripts.list_scripts))
+ @command.alias('info', 'describe')
+ def do_show(self, context, name, show_all=None):
+ '''
+ Describe the given script.
+ '''
+ script = scripts.load_script(name)
+ if script is None:
+ return False
+
+ show_all = show_all == 'all'
+
+ vals = {
+ 'name': script['name'],
+ 'category': _category_pretty(script['category']),
+ 'shortdesc': str(script['shortdesc']),
+ 'longdesc': scripts.format_desc(script['longdesc']),
+ 'steps': "\n".join((describe_step([i], [], s, show_all) for i, s in enumerate(script['steps'])))}
+ output = """%(name)s (%(category)s)
+%(shortdesc)s
+
+%(longdesc)s
+
+%(steps)s
+""" % vals
+ if show_all:
+ output += "Common Parameters\n\n"
+ for name, defval, desc in scripts.common_params():
+ output += " %s\n" % (name)
+ output += " %s\n" % (desc)
+ if defval is not None:
+ output += " (default: %s)\n" % (defval)
+ utils.page_string(output)
+
+ @command.completers(compl.call(scripts.list_scripts))
+ def do_verify(self, context, name, *args):
+ '''
+ Verify the script parameters
+ '''
+ script = scripts.load_script(name)
+ if script is None:
+ return False
+ ret = scripts.verify(script, _nvpairs2parameters(args))
+ if ret is None:
+ return False
+ if not ret:
+ print("OK (no actions)")
+ for i, action in enumerate(ret):
+ shortdesc = action.get('shortdesc', '')
+ text = str(action.get('text', ''))
+ longdesc = str(action.get('longdesc', ''))
+ print("%s. %s\n" % (i + 1, shortdesc))
+ if longdesc:
+ for line in str(longdesc).split('\n'):
+ print("\t%s" % (line))
+ print('')
+ if text:
+ for line in str(text).split('\n'):
+ print("\t%s" % (line))
+ print('')
+
+ @command.completers(compl.call(scripts.list_scripts))
+ def do_run(self, context, name, *args):
+ '''
+ Run the given script.
+ '''
+ script = scripts.load_script(name)
+ if script is not None:
+ return scripts.run(script, _nvpairs2parameters(args), ConsolePrinter())
+ return False
+
+ @command.name('_print')
+ @command.skill_level('administrator')
+ @command.completers(compl.call(scripts.list_scripts))
+ def do_print(self, context, name):
+ '''
+ Debug print the given script.
+ '''
+ script = scripts.load_script(name)
+ if script is None:
+ return False
+ import pprint
+ pprint.pprint(script)
+
+ @command.name('_actions')
+ @command.skill_level('administrator')
+ @command.completers(compl.call(scripts.list_scripts))
+ def do_actions(self, context, name, *args):
+ '''
+ Debug print the actions for the given script.
+ '''
+ script = scripts.load_script(name)
+ if script is None:
+ return False
+ ret = scripts.verify(script, _nvpairs2parameters(args))
+ if ret is None:
+ return False
+ import pprint
+ pprint.pprint(ret)
+
+ @command.name('_convert')
+ def do_convert(self, context, workflow, outdir=".", category="basic"):
+ """
+ Convert hawk wizards to cluster scripts
+ Needs more work to be really useful.
+ workflow: hawk workflow script
+ tgtdir: where the cluster script will be written
+ category: category set in new wizard
+ """
+ import yaml
+ import os
+ from .ordereddict import OrderedDict
+
+ def flatten(script):
+ if not isinstance(script, dict):
+ return script
+
+ for k, v in script.items():
+ if isinstance(v, scripts.Text):
+ script[k] = str(v)
+ elif isinstance(v, dict):
+ script[k] = flatten(v)
+ elif isinstance(v, tuple) or isinstance(v, list):
+ script[k] = [flatten(vv) for vv in v]
+ elif isinstance(v, str):
+ script[k] = v.strip()
+
+ return script
+
+ def order_rep(dumper, data):
+ return dumper.represent_mapping(u'tag:yaml.org,2002:map', list(data.items()), flow_style=False)
+
+ def scriptsorter(item):
+ order = ["version", "name", "category", "shortdesc", "longdesc", "include", "parameters", "steps", "actions"]
+ return order.index(item[0])
+
+ yaml.add_representer(OrderedDict, order_rep)
+ fromscript = os.path.abspath(workflow)
+ tgtdir = outdir
+
+ scripts.build_script_cache()
+ name = os.path.splitext(os.path.basename(fromscript))[0]
+ script = scripts.load_script_file(name, fromscript)
+ script = flatten(script)
+ script["category"] = category
+ del script["name"]
+ del script["dir"]
+ script["actions"] = [{"cib": "\n\n".join([action["cib"] for action in script["actions"]])}]
+
+ script = OrderedDict(sorted(list(script.items()), key=scriptsorter))
+ if script is not None:
+ try:
+ os.mkdir(os.path.join(tgtdir, name))
+ except:
+ pass
+ tgtfile = os.path.join(tgtdir, name, "main.yml")
+ with open(tgtfile, 'w') as tf:
+ try:
+ print("%s -> %s" % (fromscript, tgtfile))
+ yaml.dump([script], tf, explicit_start=True, default_flow_style=False)
+ except Exception as err:
+ print(err)
+
+ def _json_list(self, context, cmd):
+ """
+ ["list"]
+ """
+ for name in scripts.list_scripts():
+ try:
+ script = scripts.load_script(name)
+ if script is not None:
+ print(json.dumps({'name': name,
+ 'category': script['category'].lower(),
+ 'shortdesc': script['shortdesc'],
+ 'longdesc': scripts.format_desc(script['longdesc'])}, sort_keys=True))
+ except ValueError as err:
+ print(json.dumps({'name': name,
+ 'error': str(err)}, sort_keys=True))
+ return True
+
+ def _json_show(self, context, cmd):
+ """
+ ["show", <name>]
+ """
+ if len(cmd) < 2:
+ print(json.dumps({'error': 'Incorrect number of arguments: %s (expected %s)' % (len(cmd), 2)}))
+ return False
+ name = cmd[1]
+ script = scripts.load_script(name)
+ if script is None:
+ return False
+ print(json.dumps({'name': script['name'],
+ 'category': script['category'].lower(),
+ 'shortdesc': script['shortdesc'],
+ 'longdesc': scripts.format_desc(script['longdesc']),
+ 'steps': scripts.clean_steps(script['steps'])}, sort_keys=True))
+ return True
+
+ def _json_verify(self, context, cmd):
+ """
+ ["verify", <name>, <params>]
+ """
+ if len(cmd) < 3:
+ print(json.dumps({'error': 'Incorrect number of arguments: %s (expected %s)' % (len(cmd), 3)}))
+ return False
+ name = cmd[1]
+ params = cmd[2]
+ script = scripts.load_script(name)
+ if script is None:
+ return False
+ actions = scripts.verify(script, params)
+ if actions is None:
+ return False
+ else:
+ for action in actions:
+ obj = {'name': str(action.get('name', '')),
+ 'shortdesc': str(action.get('shortdesc', '')),
+ 'longdesc': str(action.get('longdesc', '')),
+ 'text': str(action.get('text', '')),
+ 'nodes': str(action.get('nodes', ''))}
+ if 'sudo' in action:
+ obj['sudo'] = action['sudo']
+ print(json.dumps(obj, sort_keys=True))
+ return True
+
+ def _json_run(self, context, cmd):
+ """
+ ["run", <name>, <params>]
+ """
+ if len(cmd) < 3:
+ print(json.dumps({'error': 'Incorrect number of arguments: %s (expected %s)' % (len(cmd), 3)}))
+ return False
+ name = cmd[1]
+ params = cmd[2]
+ script = scripts.load_script(name)
+ if script is None:
+ return False
+ printer = JsonPrinter()
+ ret = scripts.run(script, params, printer)
+ if not ret and printer.results:
+ for result in printer.results:
+ if 'error' in result:
+ print(json.dumps(result, sort_keys=True))
+ return ret
+
+ def do_json(self, context, command):
+ """
+ JSON API for the scripts, for use in web frontends.
+ Line-based output: enter a JSON command,
+ get lines of output back. In the description below, the output is
+ described as an array, but really it is returned line-by-line.
+
+ API:
+
+ ["list"]
+ => [{name, shortdesc, category}]
+ ["show", <name>]
+ => [{name, shortdesc, longdesc, category, <<steps>>}]
+ <<steps>> := [{name, shortdesc, longdesc, required, <<parameters>>, <<steps>>}]
+ <<params>> := [{name, shortdesc, longdesc, required, unique, type, advanced, value, example}]
+ ["verify", <name>, <values>]
+ => [{shortdesc, longdesc, nodes}]
+ ["run", <name>, <values>]
+ => [{shortdesc, rc, output|error}]
+ """
+ cmd = json.loads(command)
+ if len(cmd) < 1:
+ print(json.dumps({'error': 'Failed to decode valid JSON command'}))
+ return False
+ try:
+ if cmd[0] == "list":
+ return self._json_list(context, cmd)
+ elif cmd[0] == "show":
+ return self._json_show(context, cmd)
+ elif cmd[0] == "verify":
+ return self._json_verify(context, cmd)
+ elif cmd[0] == "run":
+ return self._json_run(context, cmd)
+ else:
+ print(json.dumps({'error': "Unknown command: %s" % (cmd[0])}))
+ return False
+ except ValueError as err:
+ print(json.dumps({'error': str(err)}))
+ return False