X-Git-Url: https://code.grnet.gr/git/ganeti-local/blobdiff_plain/cec9845cb50cd5a76b3f8421895ee52684ae4d5c..3f752d976d077d1eed3e8aa1a4bf39dd72296b0d:/qa/qa_utils.py diff --git a/qa/qa_utils.py b/qa/qa_utils.py index 3889352..2005634 100644 --- a/qa/qa_utils.py +++ b/qa/qa_utils.py @@ -1,3 +1,6 @@ +# +# + # Copyright (C) 2007 Google Inc. # # This program is free software; you can redistribute it and/or modify @@ -21,6 +24,7 @@ """ import os +import sys import subprocess from ganeti import utils @@ -29,17 +33,69 @@ import qa_config import qa_error -def AssertEqual(first, second, msg=None): +_INFO_SEQ = None +_WARNING_SEQ = None +_ERROR_SEQ = None +_RESET_SEQ = None + + +# List of all hooks +_hooks = [] + + +def _SetupColours(): + """Initializes the colour constants. + + """ + global _INFO_SEQ, _WARNING_SEQ, _ERROR_SEQ, _RESET_SEQ + + # Don't use colours if stdout isn't a terminal + if not sys.stdout.isatty(): + return + + try: + import curses + except ImportError: + # Don't use colours if curses module can't be imported + return + + curses.setupterm() + + _RESET_SEQ = curses.tigetstr("op") + + setaf = curses.tigetstr("setaf") + _INFO_SEQ = curses.tparm(setaf, curses.COLOR_GREEN) + _WARNING_SEQ = curses.tparm(setaf, curses.COLOR_YELLOW) + _ERROR_SEQ = curses.tparm(setaf, curses.COLOR_RED) + + +_SetupColours() + + +def AssertEqual(first, second): """Raises an error when values aren't equal. """ if not first == second: - raise qa_error.Error(msg or '%r == %r' % (first, second)) + raise qa_error.Error('%r == %r' % (first, second)) + + +def AssertNotEqual(first, second): + """Raises an error when values are equal. + + """ + if not first != second: + raise qa_error.Error('%r != %r' % (first, second)) def GetSSHCommand(node, cmd, strict=True): """Builds SSH command to be executed. + Args: + - node: Node the command should run on + - cmd: Command to be executed as a list with all parameters + - strict: Whether to enable strict host key checking + """ args = [ 'ssh', '-oEscapeChar=none', '-oBatchMode=yes', '-l', 'root' ] @@ -68,8 +124,18 @@ def StartSSH(node, cmd, strict=True): """Starts SSH. """ - args = GetSSHCommand(node, cmd, strict=strict) - return subprocess.Popen(args, shell=False) + return subprocess.Popen(GetSSHCommand(node, cmd, strict=strict), + shell=False) + + +def GetCommandOutput(node, cmd): + """Returns the output of a command executed on the given node. + + """ + p = subprocess.Popen(GetSSHCommand(node, cmd), + shell=False, stdout=subprocess.PIPE) + AssertEqual(p.wait(), 0) + return p.stdout.read() def UploadFile(node, src): @@ -96,4 +162,150 @@ def UploadFile(node, src): return p.stdout.read().strip() finally: f.close() -# }}} + + +def _ResolveName(cmd, key): + """Helper function. + + """ + master = qa_config.GetMasterNode() + + output = GetCommandOutput(master['primary'], utils.ShellQuoteArgs(cmd)) + for line in output.splitlines(): + (lkey, lvalue) = line.split(':', 1) + if lkey == key: + return lvalue.lstrip() + raise KeyError("Key not found") + + +def ResolveInstanceName(instance): + """Gets the full name of an instance. + + """ + return _ResolveName(['gnt-instance', 'info', instance['name']], + 'Instance name') + + +def ResolveNodeName(node): + """Gets the full name of a node. + + """ + return _ResolveName(['gnt-node', 'info', node['primary']], + 'Node name') + + +def GetNodeInstances(node, secondaries=False): + """Gets a list of instances on a node. + + """ + master = qa_config.GetMasterNode() + node_name = ResolveNodeName(node) + + # Get list of all instances + cmd = ['gnt-instance', 'list', '--separator=:', '--no-headers', + '--output=name,pnode,snodes'] + output = GetCommandOutput(master['primary'], utils.ShellQuoteArgs(cmd)) + + instances = [] + for line in output.splitlines(): + (name, pnode, snodes) = line.split(':', 2) + if ((not secondaries and pnode == node_name) or + (secondaries and node_name in snodes.split(','))): + instances.append(name) + + return instances + + +def _FormatWithColor(text, seq): + if not seq: + return text + return "%s%s%s" % (seq, text, _RESET_SEQ) + + +FormatWarning = lambda text: _FormatWithColor(text, _WARNING_SEQ) +FormatError = lambda text: _FormatWithColor(text, _ERROR_SEQ) +FormatInfo = lambda text: _FormatWithColor(text, _INFO_SEQ) + + +def LoadHooks(): + """Load all QA hooks. + + """ + hooks_dir = qa_config.get('options', {}).get('hooks-dir', None) + if not hooks_dir: + return + if hooks_dir not in sys.path: + sys.path.insert(0, hooks_dir) + for name in utils.ListVisibleFiles(hooks_dir): + if name.endswith('.py'): + # Load and instanciate hook + print "Loading hook %s" % name + _hooks.append(__import__(name[:-3], None, None, ['']).hook()) + + +class QaHookContext: + """Definition of context passed to hooks. + + """ + name = None + phase = None + success = None + args = None + kwargs = None + + +def _CallHooks(ctx): + """Calls all hooks with the given context. + + """ + if not _hooks: + return + + name = "%s-%s" % (ctx.phase, ctx.name) + if ctx.success is not None: + msg = "%s (success=%s)" % (name, ctx.success) + else: + msg = name + print FormatInfo("Begin %s" % msg) + for hook in _hooks: + hook.run(ctx) + print FormatInfo("End %s" % name) + + +def DefineHook(name): + """Wraps a function with calls to hooks. + + Usage: prefix function with @qa_utils.DefineHook(...) + + This is based on PEP 318, "Decorators for Functions and Methods". + + """ + def wrapper(fn): + def new_f(*args, **kwargs): + # Create context + ctx = QaHookContext() + ctx.name = name + ctx.phase = 'pre' + ctx.args = args + ctx.kwargs = kwargs + + _CallHooks(ctx) + try: + ctx.phase = 'post' + ctx.success = True + try: + # Call real function + return fn(*args, **kwargs) + except: + ctx.success = False + raise + finally: + _CallHooks(ctx) + + # Override function metadata + new_f.func_name = fn.func_name + new_f.func_doc = fn.func_doc + + return new_f + + return wrapper