4 # Copyright (C) 2007, 2011 Google Inc.
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
11 # This program is distributed in the hope that it will be useful, but
12 # WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 # General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
22 """Utilities for QA tests.
33 from ganeti import utils
34 from ganeti import compat
35 from ganeti import constants
50 """Initializes the colour constants.
53 # pylint: disable=W0603
55 global _INFO_SEQ, _WARNING_SEQ, _ERROR_SEQ, _RESET_SEQ
57 # Don't use colours if stdout isn't a terminal
58 if not sys.stdout.isatty():
64 # Don't use colours if curses module can't be imported
69 _RESET_SEQ = curses.tigetstr("op")
71 setaf = curses.tigetstr("setaf")
72 _INFO_SEQ = curses.tparm(setaf, curses.COLOR_GREEN)
73 _WARNING_SEQ = curses.tparm(setaf, curses.COLOR_YELLOW)
74 _ERROR_SEQ = curses.tparm(setaf, curses.COLOR_RED)
80 def AssertIn(item, sequence):
81 """Raises an error when item is not in sequence.
84 if item not in sequence:
85 raise qa_error.Error("%r not in %r" % (item, sequence))
88 def AssertNotIn(item, sequence):
89 """Raises an error when item is in sequence.
93 raise qa_error.Error("%r in %r" % (item, sequence))
96 def AssertEqual(first, second):
97 """Raises an error when values aren't equal.
100 if not first == second:
101 raise qa_error.Error("%r == %r" % (first, second))
104 def AssertNotEqual(first, second):
105 """Raises an error when values are equal.
108 if not first != second:
109 raise qa_error.Error("%r != %r" % (first, second))
112 def AssertMatch(string, pattern):
113 """Raises an error when string doesn't match regexp pattern.
116 if not re.match(pattern, string):
117 raise qa_error.Error("%r doesn't match /%r/" % (string, pattern))
120 def AssertCommand(cmd, fail=False, node=None):
121 """Checks that a remote command succeeds.
123 @param cmd: either a string (the command to execute) or a list (to
124 be converted using L{utils.ShellQuoteArgs} into a string)
126 @param fail: if the command is expected to fail instead of succeeding
127 @param node: if passed, it should be the node on which the command
128 should be executed, instead of the master node (can be either a
133 node = qa_config.GetMasterNode()
135 if isinstance(node, basestring):
138 nodename = node["primary"]
140 if isinstance(cmd, basestring):
143 cmdstr = utils.ShellQuoteArgs(cmd)
145 rcode = StartSSH(nodename, cmdstr).wait()
149 raise qa_error.Error("Command '%s' on node %s was expected to fail but"
150 " didn't" % (cmdstr, nodename))
153 raise qa_error.Error("Command '%s' on node %s failed, exit code %s" %
154 (cmdstr, nodename, rcode))
159 def GetSSHCommand(node, cmd, strict=True, opts=None, tty=True):
160 """Builds SSH command to be executed.
163 @param node: node the command should run on
165 @param cmd: command to be executed in the node; if None or empty
166 string, no command will be executed
167 @type strict: boolean
168 @param strict: whether to enable strict host key checking
170 @param opts: list of additional options
172 @param tty: If we should use tty
175 args = ["ssh", "-oEscapeChar=none", "-oBatchMode=yes", "-l", "root"]
184 args.append("-oStrictHostKeyChecking=%s" % tmp)
185 args.append("-oClearAllForwardings=yes")
186 args.append("-oForwardAgent=yes")
189 if node in _MULTIPLEXERS:
190 spath = _MULTIPLEXERS[node][0]
191 args.append("-oControlPath=%s" % spath)
192 args.append("-oControlMaster=no")
200 def StartLocalCommand(cmd, **kwargs):
201 """Starts a local command.
204 print "Command: %s" % utils.ShellQuoteArgs(cmd)
205 return subprocess.Popen(cmd, shell=False, **kwargs)
208 def StartSSH(node, cmd, strict=True):
212 return StartLocalCommand(GetSSHCommand(node, cmd, strict=strict))
215 def StartMultiplexer(node):
216 """Starts a multiplexer command.
218 @param node: the node for which to open the multiplexer
221 if node in _MULTIPLEXERS:
224 # Note: yes, we only need mktemp, since we'll remove the file anyway
225 sname = tempfile.mktemp(prefix="ganeti-qa-multiplexer.")
226 utils.RemoveFile(sname)
227 opts = ["-N", "-oControlPath=%s" % sname, "-oControlMaster=yes"]
228 print "Created socket at %s" % sname
229 child = StartLocalCommand(GetSSHCommand(node, None, opts=opts))
230 _MULTIPLEXERS[node] = (sname, child)
233 def CloseMultiplexers():
234 """Closes all current multiplexers and cleans up.
237 for node in _MULTIPLEXERS.keys():
238 (sname, child) = _MULTIPLEXERS.pop(node)
239 utils.KillProcess(child.pid, timeout=10, waitpid=True)
240 utils.RemoveFile(sname)
243 def GetCommandOutput(node, cmd, tty=True):
244 """Returns the output of a command executed on the given node.
247 p = StartLocalCommand(GetSSHCommand(node, cmd, tty=tty),
248 stdout=subprocess.PIPE)
249 AssertEqual(p.wait(), 0)
250 return p.stdout.read()
253 def UploadFile(node, src):
254 """Uploads a file to a node and returns the filename.
256 Caller needs to remove the returned file on the node when it's not needed
260 # Make sure nobody else has access to it while preserving local permissions
261 mode = os.stat(src).st_mode & 0700
263 cmd = ('tmp=$(tempfile --mode %o --prefix gnt) && '
264 '[[ -f "${tmp}" ]] && '
266 'echo "${tmp}"') % mode
270 p = subprocess.Popen(GetSSHCommand(node, cmd), shell=False, stdin=f,
271 stdout=subprocess.PIPE)
272 AssertEqual(p.wait(), 0)
274 # Return temporary filename
275 return p.stdout.read().strip()
280 def UploadData(node, data, mode=0600, filename=None):
281 """Uploads data to a node and returns the filename.
283 Caller needs to remove the returned file on the node when it's not needed
288 tmp = "tmp=%s" % utils.ShellQuote(filename)
290 tmp = "tmp=$(tempfile --mode %o --prefix gnt)" % mode
292 "[[ -f \"${tmp}\" ]] && "
293 "cat > \"${tmp}\" && "
294 "echo \"${tmp}\"") % tmp
296 p = subprocess.Popen(GetSSHCommand(node, cmd), shell=False,
297 stdin=subprocess.PIPE, stdout=subprocess.PIPE)
300 AssertEqual(p.wait(), 0)
302 # Return temporary filename
303 return p.stdout.read().strip()
306 def BackupFile(node, path):
307 """Creates a backup of a file on the node and returns the filename.
309 Caller needs to remove the returned file on the node when it's not needed
313 cmd = ("tmp=$(tempfile --prefix .gnt --directory=$(dirname %s)) && "
314 "[[ -f \"$tmp\" ]] && "
316 "echo $tmp") % (utils.ShellQuote(path), utils.ShellQuote(path))
318 # Return temporary filename
319 return GetCommandOutput(node, cmd).strip()
322 def _ResolveName(cmd, key):
326 master = qa_config.GetMasterNode()
328 output = GetCommandOutput(master["primary"], utils.ShellQuoteArgs(cmd))
329 for line in output.splitlines():
330 (lkey, lvalue) = line.split(":", 1)
332 return lvalue.lstrip()
333 raise KeyError("Key not found")
336 def ResolveInstanceName(instance):
337 """Gets the full name of an instance.
339 @type instance: string
340 @param instance: Instance name
343 return _ResolveName(["gnt-instance", "info", instance],
347 def ResolveNodeName(node):
348 """Gets the full name of a node.
351 return _ResolveName(["gnt-node", "info", node["primary"]],
355 def GetNodeInstances(node, secondaries=False):
356 """Gets a list of instances on a node.
359 master = qa_config.GetMasterNode()
360 node_name = ResolveNodeName(node)
362 # Get list of all instances
363 cmd = ["gnt-instance", "list", "--separator=:", "--no-headers",
364 "--output=name,pnode,snodes"]
365 output = GetCommandOutput(master["primary"], utils.ShellQuoteArgs(cmd))
368 for line in output.splitlines():
369 (name, pnode, snodes) = line.split(":", 2)
370 if ((not secondaries and pnode == node_name) or
371 (secondaries and node_name in snodes.split(","))):
372 instances.append(name)
377 def _SelectQueryFields(rnd, fields):
378 """Generates a list of fields for query tests.
381 # Create copy for shuffling
382 fields = list(fields)
390 yield fields + fields
392 # Check small groups of fields
394 yield [fields.pop() for _ in range(rnd.randint(2, 10)) if fields]
397 def _List(listcmd, fields, names):
398 """Runs a list command.
401 master = qa_config.GetMasterNode()
403 cmd = [listcmd, "list", "--separator=|", "--no-headers",
404 "--output", ",".join(fields)]
409 return GetCommandOutput(master["primary"],
410 utils.ShellQuoteArgs(cmd)).splitlines()
413 def GenericQueryTest(cmd, fields, namefield="name", test_unknown=True):
414 """Runs a number of tests on query commands.
416 @param cmd: Command name
417 @param fields: List of field names
420 rnd = random.Random(hash(cmd))
422 fields = list(fields)
425 # Test a number of field combinations
426 for testfields in _SelectQueryFields(rnd, fields):
427 AssertCommand([cmd, "list", "--output", ",".join(testfields)])
429 if namefield is not None:
430 namelist_fn = compat.partial(_List, cmd, [namefield])
432 # When no names were requested, the list must be sorted
433 names = namelist_fn(None)
434 AssertEqual(names, utils.NiceSort(names))
436 # When requesting specific names, the order must be kept
437 revnames = list(reversed(names))
438 AssertEqual(namelist_fn(revnames), revnames)
440 randnames = list(names)
441 rnd.shuffle(randnames)
442 AssertEqual(namelist_fn(randnames), randnames)
445 # Listing unknown items must fail
446 AssertCommand([cmd, "list", "this.name.certainly.does.not.exist"],
449 # Check exit code for listing unknown field
450 AssertEqual(AssertCommand([cmd, "list", "--output=field/does/not/exist"],
452 constants.EXIT_UNKNOWN_FIELD)
455 def GenericQueryFieldsTest(cmd, fields):
456 master = qa_config.GetMasterNode()
459 AssertCommand([cmd, "list-fields"])
460 AssertCommand([cmd, "list-fields"] + fields)
462 # Check listed fields (all, must be sorted)
463 realcmd = [cmd, "list-fields", "--separator=|", "--no-headers"]
464 output = GetCommandOutput(master["primary"],
465 utils.ShellQuoteArgs(realcmd)).splitlines()
466 AssertEqual([line.split("|", 1)[0] for line in output],
467 utils.NiceSort(fields))
469 # Check exit code for listing unknown field
470 AssertEqual(AssertCommand([cmd, "list-fields", "field/does/not/exist"],
472 constants.EXIT_UNKNOWN_FIELD)
475 def _FormatWithColor(text, seq):
478 return "%s%s%s" % (seq, text, _RESET_SEQ)
481 FormatWarning = lambda text: _FormatWithColor(text, _WARNING_SEQ)
482 FormatError = lambda text: _FormatWithColor(text, _ERROR_SEQ)
483 FormatInfo = lambda text: _FormatWithColor(text, _INFO_SEQ)
486 def AddToEtcHosts(hostnames):
487 """Adds hostnames to /etc/hosts.
489 @param hostnames: List of hostnames first used A records, all other CNAMEs
492 master = qa_config.GetMasterNode()
493 tmp_hosts = UploadData(master["primary"], "", mode=0644)
495 quoted_tmp_hosts = utils.ShellQuote(tmp_hosts)
497 for localhost in ("::1", "127.0.0.1"):
498 data.append("%s %s" % (localhost, " ".join(hostnames)))
501 AssertCommand(("cat /etc/hosts > %s && echo -e '%s' >> %s && mv %s"
502 " /etc/hosts") % (quoted_tmp_hosts, "\\n".join(data),
503 quoted_tmp_hosts, quoted_tmp_hosts))
504 except qa_error.Error:
505 AssertCommand(["rm", tmp_hosts])
508 def RemoveFromEtcHosts(hostnames):
509 """Remove hostnames from /etc/hosts.
511 @param hostnames: List of hostnames first used A records, all other CNAMEs
514 master = qa_config.GetMasterNode()
515 tmp_hosts = UploadData(master["primary"], "", mode=0644)
516 quoted_tmp_hosts = utils.ShellQuote(tmp_hosts)
518 sed_data = " ".join(hostnames)
520 AssertCommand(("sed -e '/^\(::1\|127\.0\.0\.1\)\s\+%s/d' /etc/hosts > %s"
521 " && mv %s /etc/hosts") % (sed_data, quoted_tmp_hosts,
523 except qa_error.Error:
524 AssertCommand(["rm", tmp_hosts])