X-Git-Url: https://code.grnet.gr/git/ganeti-local/blobdiff_plain/b9955569acd44e0f1938745cf252544d03935f03..8e66b9bfff134a6190aebdbe786a102778b3ecec:/qa/qa_utils.py diff --git a/qa/qa_utils.py b/qa/qa_utils.py index 6fa949a..a91cc94 100644 --- a/qa/qa_utils.py +++ b/qa/qa_utils.py @@ -1,7 +1,7 @@ # # -# Copyright (C) 2007 Google Inc. +# Copyright (C) 2007, 2011, 2012 Google Inc. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -28,10 +28,17 @@ import re import sys import subprocess import random +import tempfile + +try: + import functools +except ImportError, err: + raise ImportError("Python 2.5 or higher is required: %s" % err) from ganeti import utils from ganeti import compat from ganeti import constants +from ganeti import ht import qa_config import qa_error @@ -42,11 +49,25 @@ _WARNING_SEQ = None _ERROR_SEQ = None _RESET_SEQ = None +_MULTIPLEXERS = {} + +#: Unique ID per QA run +_RUN_UUID = utils.NewUUID() + + +(INST_DOWN, + INST_UP) = range(500, 502) + +(FIRST_ARG, + RETURN_VALUE) = range(1000, 1002) + def _SetupColours(): """Initializes the colour constants. """ + # pylint: disable=W0603 + # due to global usage global _INFO_SEQ, _WARNING_SEQ, _ERROR_SEQ, _RESET_SEQ # Don't use colours if stdout isn't a terminal @@ -77,7 +98,15 @@ def AssertIn(item, sequence): """ if item not in sequence: - raise qa_error.Error('%r not in %r' % (item, sequence)) + raise qa_error.Error("%r not in %r" % (item, sequence)) + + +def AssertNotIn(item, sequence): + """Raises an error when item is in sequence. + + """ + if item in sequence: + raise qa_error.Error("%r in %r" % (item, sequence)) def AssertEqual(first, second): @@ -85,7 +114,7 @@ def AssertEqual(first, second): """ if not first == second: - raise qa_error.Error('%r == %r' % (first, second)) + raise qa_error.Error("%r == %r" % (first, second)) def AssertNotEqual(first, second): @@ -93,7 +122,7 @@ def AssertNotEqual(first, second): """ if not first != second: - raise qa_error.Error('%r != %r' % (first, second)) + raise qa_error.Error("%r != %r" % (first, second)) def AssertMatch(string, pattern): @@ -104,6 +133,28 @@ def AssertMatch(string, pattern): raise qa_error.Error("%r doesn't match /%r/" % (string, pattern)) +def _GetName(entity, key): + """Tries to get name of an entity. + + @type entity: string or dict + @type key: string + @param key: Dictionary key containing name + + """ + if isinstance(entity, basestring): + result = entity + elif isinstance(entity, dict): + result = entity[key] + else: + raise qa_error.Error("Expected string or dictionary, got %s: %s" % + (type(entity), entity)) + + if not ht.TNonEmptyString(result): + raise Exception("Invalid name '%s'" % result) + + return result + + def AssertCommand(cmd, fail=False, node=None): """Checks that a remote command succeeds. @@ -119,10 +170,7 @@ def AssertCommand(cmd, fail=False, node=None): if node is None: node = qa_config.GetMasterNode() - if isinstance(node, basestring): - nodename = node - else: - nodename = node["primary"] + nodename = _GetName(node, "primary") if isinstance(cmd, basestring): cmdstr = cmd @@ -143,35 +191,59 @@ def AssertCommand(cmd, fail=False, node=None): return rcode -def GetSSHCommand(node, cmd, strict=True): +def GetSSHCommand(node, cmd, strict=True, opts=None, tty=None): """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 + @type node: string + @param node: node the command should run on + @type cmd: string + @param cmd: command to be executed in the node; if None or empty + string, no command will be executed + @type strict: boolean + @param strict: whether to enable strict host key checking + @type opts: list + @param opts: list of additional options + @type tty: boolean or None + @param tty: if we should use tty; if None, will be auto-detected """ - args = [ 'ssh', '-oEscapeChar=none', '-oBatchMode=yes', '-l', 'root', '-t' ] + args = ["ssh", "-oEscapeChar=none", "-oBatchMode=yes", "-lroot"] + + if tty is None: + tty = sys.stdout.isatty() + + if tty: + args.append("-t") if strict: - tmp = 'yes' + tmp = "yes" else: - tmp = 'no' - args.append('-oStrictHostKeyChecking=%s' % tmp) - args.append('-oClearAllForwardings=yes') - args.append('-oForwardAgent=yes') + tmp = "no" + args.append("-oStrictHostKeyChecking=%s" % tmp) + args.append("-oClearAllForwardings=yes") + args.append("-oForwardAgent=yes") + if opts: + args.extend(opts) + if node in _MULTIPLEXERS: + spath = _MULTIPLEXERS[node][0] + args.append("-oControlPath=%s" % spath) + args.append("-oControlMaster=no") args.append(node) - args.append(cmd) + if cmd: + args.append(cmd) return args -def StartLocalCommand(cmd, **kwargs): +def StartLocalCommand(cmd, _nolog_opts=False, **kwargs): """Starts a local command. """ - print "Command: %s" % utils.ShellQuoteArgs(cmd) + if _nolog_opts: + pcmd = [i for i in cmd if not i.startswith("-")] + else: + pcmd = cmd + print "Command: %s" % utils.ShellQuoteArgs(pcmd) return subprocess.Popen(cmd, shell=False, **kwargs) @@ -179,14 +251,44 @@ def StartSSH(node, cmd, strict=True): """Starts SSH. """ - return StartLocalCommand(GetSSHCommand(node, cmd, strict=strict)) + return StartLocalCommand(GetSSHCommand(node, cmd, strict=strict), + _nolog_opts=True) + + +def StartMultiplexer(node): + """Starts a multiplexer command. + + @param node: the node for which to open the multiplexer + + """ + if node in _MULTIPLEXERS: + return + + # Note: yes, we only need mktemp, since we'll remove the file anyway + sname = tempfile.mktemp(prefix="ganeti-qa-multiplexer.") + utils.RemoveFile(sname) + opts = ["-N", "-oControlPath=%s" % sname, "-oControlMaster=yes"] + print "Created socket at %s" % sname + child = StartLocalCommand(GetSSHCommand(node, None, opts=opts)) + _MULTIPLEXERS[node] = (sname, child) + +def CloseMultiplexers(): + """Closes all current multiplexers and cleans up. -def GetCommandOutput(node, cmd): + """ + for node in _MULTIPLEXERS.keys(): + (sname, child) = _MULTIPLEXERS.pop(node) + utils.KillProcess(child.pid, timeout=10, waitpid=True) + utils.RemoveFile(sname) + + +def GetCommandOutput(node, cmd, tty=None): """Returns the output of a command executed on the given node. """ - p = StartLocalCommand(GetSSHCommand(node, cmd), stdout=subprocess.PIPE) + p = StartLocalCommand(GetSSHCommand(node, cmd, tty=tty), + stdout=subprocess.PIPE) AssertEqual(p.wait(), 0) return p.stdout.read() @@ -206,7 +308,7 @@ def UploadFile(node, src): 'cat > "${tmp}" && ' 'echo "${tmp}"') % mode - f = open(src, 'r') + f = open(src, "r") try: p = subprocess.Popen(GetSSHCommand(node, cmd), shell=False, stdin=f, stdout=subprocess.PIPE) @@ -266,9 +368,9 @@ def _ResolveName(cmd, key): """ master = qa_config.GetMasterNode() - output = GetCommandOutput(master['primary'], utils.ShellQuoteArgs(cmd)) + output = GetCommandOutput(master["primary"], utils.ShellQuoteArgs(cmd)) for line in output.splitlines(): - (lkey, lvalue) = line.split(':', 1) + (lkey, lvalue) = line.split(":", 1) if lkey == key: return lvalue.lstrip() raise KeyError("Key not found") @@ -281,16 +383,16 @@ def ResolveInstanceName(instance): @param instance: Instance name """ - return _ResolveName(['gnt-instance', 'info', instance], - 'Instance name') + return _ResolveName(["gnt-instance", "info", instance], + "Instance name") def ResolveNodeName(node): """Gets the full name of a node. """ - return _ResolveName(['gnt-node', 'info', node['primary']], - 'Node name') + return _ResolveName(["gnt-node", "info", node["primary"]], + "Node name") def GetNodeInstances(node, secondaries=False): @@ -301,15 +403,15 @@ def GetNodeInstances(node, secondaries=False): 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)) + 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) + (name, pnode, snodes) = line.split(":", 2) if ((not secondaries and pnode == node_name) or - (secondaries and node_name in snodes.split(','))): + (secondaries and node_name in snodes.split(","))): instances.append(name) return instances @@ -341,7 +443,7 @@ def _List(listcmd, fields, names): """ master = qa_config.GetMasterNode() - cmd = [listcmd, "list", "--separator=|", "--no-header", + cmd = [listcmd, "list", "--separator=|", "--no-headers", "--output", ",".join(fields)] if names: @@ -351,7 +453,7 @@ def _List(listcmd, fields, names): utils.ShellQuoteArgs(cmd)).splitlines() -def GenericQueryTest(cmd, fields): +def GenericQueryTest(cmd, fields, namefield="name", test_unknown=True): """Runs a number of tests on query commands. @param cmd: Command name @@ -360,29 +462,32 @@ def GenericQueryTest(cmd, fields): """ rnd = random.Random(hash(cmd)) - randfields = list(fields) + fields = list(fields) rnd.shuffle(fields) # Test a number of field combinations for testfields in _SelectQueryFields(rnd, fields): AssertCommand([cmd, "list", "--output", ",".join(testfields)]) - namelist_fn = compat.partial(_List, cmd, ["name"]) + if namefield is not None: + namelist_fn = compat.partial(_List, cmd, [namefield]) - # When no names were requested, the list must be sorted - names = namelist_fn(None) - AssertEqual(names, utils.NiceSort(names)) + # When no names were requested, the list must be sorted + names = namelist_fn(None) + AssertEqual(names, utils.NiceSort(names)) - # When requesting specific names, the order must be kept - revnames = list(reversed(names)) - AssertEqual(namelist_fn(revnames), revnames) + # When requesting specific names, the order must be kept + revnames = list(reversed(names)) + AssertEqual(namelist_fn(revnames), revnames) - randnames = list(names) - rnd.shuffle(randnames) - AssertEqual(namelist_fn(randnames), randnames) + randnames = list(names) + rnd.shuffle(randnames) + AssertEqual(namelist_fn(randnames), randnames) - # Listing unknown items must fail - AssertCommand([cmd, "list", "this.name.certainly.does.not.exist"], fail=True) + if test_unknown: + # Listing unknown items must fail + AssertCommand([cmd, "list", "this.name.certainly.does.not.exist"], + fail=True) # Check exit code for listing unknown field AssertEqual(AssertCommand([cmd, "list", "--output=field/does/not/exist"], @@ -402,7 +507,7 @@ def GenericQueryFieldsTest(cmd, fields): output = GetCommandOutput(master["primary"], utils.ShellQuoteArgs(realcmd)).splitlines() AssertEqual([line.split("|", 1)[0] for line in output], - sorted(fields)) + utils.NiceSort(fields)) # Check exit code for listing unknown field AssertEqual(AssertCommand([cmd, "list-fields", "field/does/not/exist"], @@ -419,3 +524,125 @@ def _FormatWithColor(text, seq): FormatWarning = lambda text: _FormatWithColor(text, _WARNING_SEQ) FormatError = lambda text: _FormatWithColor(text, _ERROR_SEQ) FormatInfo = lambda text: _FormatWithColor(text, _INFO_SEQ) + + +def AddToEtcHosts(hostnames): + """Adds hostnames to /etc/hosts. + + @param hostnames: List of hostnames first used A records, all other CNAMEs + + """ + master = qa_config.GetMasterNode() + tmp_hosts = UploadData(master["primary"], "", mode=0644) + + quoted_tmp_hosts = utils.ShellQuote(tmp_hosts) + data = [] + for localhost in ("::1", "127.0.0.1"): + data.append("%s %s" % (localhost, " ".join(hostnames))) + + try: + AssertCommand(("cat /etc/hosts > %s && echo -e '%s' >> %s && mv %s" + " /etc/hosts") % (quoted_tmp_hosts, "\\n".join(data), + quoted_tmp_hosts, quoted_tmp_hosts)) + except qa_error.Error: + AssertCommand(["rm", tmp_hosts]) + + +def RemoveFromEtcHosts(hostnames): + """Remove hostnames from /etc/hosts. + + @param hostnames: List of hostnames first used A records, all other CNAMEs + + """ + master = qa_config.GetMasterNode() + tmp_hosts = UploadData(master["primary"], "", mode=0644) + quoted_tmp_hosts = utils.ShellQuote(tmp_hosts) + + sed_data = " ".join(hostnames) + try: + AssertCommand(("sed -e '/^\(::1\|127\.0\.0\.1\)\s\+%s/d' /etc/hosts > %s" + " && mv %s /etc/hosts") % (sed_data, quoted_tmp_hosts, + quoted_tmp_hosts)) + except qa_error.Error: + AssertCommand(["rm", tmp_hosts]) + + +def RunInstanceCheck(instance, running): + """Check if instance is running or not. + + """ + instance_name = _GetName(instance, "name") + + script = qa_config.GetInstanceCheckScript() + if not script: + return + + master_node = qa_config.GetMasterNode() + + # Build command to connect to master node + master_ssh = GetSSHCommand(master_node["primary"], "--") + + if running: + running_shellval = "1" + running_text = "" + else: + running_shellval = "" + running_text = "not " + + print FormatInfo("Checking if instance '%s' is %srunning" % + (instance_name, running_text)) + + args = [script, instance_name] + env = { + "PATH": constants.HOOKS_PATH, + "RUN_UUID": _RUN_UUID, + "MASTER_SSH": utils.ShellQuoteArgs(master_ssh), + "INSTANCE_NAME": instance_name, + "INSTANCE_RUNNING": running_shellval, + } + + result = os.spawnve(os.P_WAIT, script, args, env) + if result != 0: + raise qa_error.Error("Instance check failed with result %s" % result) + + +def _InstanceCheckInner(expected, instarg, args, result): + """Helper function used by L{InstanceCheck}. + + """ + if instarg == FIRST_ARG: + instance = args[0] + elif instarg == RETURN_VALUE: + instance = result + else: + raise Exception("Invalid value '%s' for instance argument" % instarg) + + if expected in (INST_DOWN, INST_UP): + RunInstanceCheck(instance, (expected == INST_UP)) + elif expected is not None: + raise Exception("Invalid value '%s'" % expected) + + +def InstanceCheck(before, after, instarg): + """Decorator to check instance status before and after test. + + @param before: L{INST_DOWN} if instance must be stopped before test, + L{INST_UP} if instance must be running before test, L{None} to not check. + @param after: L{INST_DOWN} if instance must be stopped after test, + L{INST_UP} if instance must be running after test, L{None} to not check. + @param instarg: L{FIRST_ARG} to use first argument to test as instance (a + dictionary), L{RETURN_VALUE} to use return value (disallows pre-checks) + + """ + def decorator(fn): + @functools.wraps(fn) + def wrapper(*args, **kwargs): + _InstanceCheckInner(before, instarg, args, NotImplemented) + + result = fn(*args, **kwargs) + + _InstanceCheckInner(after, instarg, args, result) + + return result + return wrapper + return decorator