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-msg=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 AssertEqual(first, second):
89 """Raises an error when values aren't equal.
92 if not first == second:
93 raise qa_error.Error('%r == %r' % (first, second))
96 def AssertNotEqual(first, second):
97 """Raises an error when values are equal.
100 if not first != second:
101 raise qa_error.Error('%r != %r' % (first, second))
104 def AssertMatch(string, pattern):
105 """Raises an error when string doesn't match regexp pattern.
108 if not re.match(pattern, string):
109 raise qa_error.Error("%r doesn't match /%r/" % (string, pattern))
112 def AssertCommand(cmd, fail=False, node=None):
113 """Checks that a remote command succeeds.
115 @param cmd: either a string (the command to execute) or a list (to
116 be converted using L{utils.ShellQuoteArgs} into a string)
118 @param fail: if the command is expected to fail instead of succeeding
119 @param node: if passed, it should be the node on which the command
120 should be executed, instead of the master node (can be either a
125 node = qa_config.GetMasterNode()
127 if isinstance(node, basestring):
130 nodename = node["primary"]
132 if isinstance(cmd, basestring):
135 cmdstr = utils.ShellQuoteArgs(cmd)
137 rcode = StartSSH(nodename, cmdstr).wait()
141 raise qa_error.Error("Command '%s' on node %s was expected to fail but"
142 " didn't" % (cmdstr, nodename))
145 raise qa_error.Error("Command '%s' on node %s failed, exit code %s" %
146 (cmdstr, nodename, rcode))
151 def GetSSHCommand(node, cmd, strict=True, opts=None, tty=True):
152 """Builds SSH command to be executed.
155 @param node: node the command should run on
157 @param cmd: command to be executed in the node; if None or empty
158 string, no command will be executed
159 @type strict: boolean
160 @param strict: whether to enable strict host key checking
162 @param opts: list of additional options
164 @param tty: If we should use tty
167 args = ["ssh", "-oEscapeChar=none", "-oBatchMode=yes", "-l", "root"]
176 args.append('-oStrictHostKeyChecking=%s' % tmp)
177 args.append('-oClearAllForwardings=yes')
178 args.append('-oForwardAgent=yes')
181 if node in _MULTIPLEXERS:
182 spath = _MULTIPLEXERS[node][0]
183 args.append('-oControlPath=%s' % spath)
184 args.append('-oControlMaster=no')
192 def StartLocalCommand(cmd, **kwargs):
193 """Starts a local command.
196 print "Command: %s" % utils.ShellQuoteArgs(cmd)
197 return subprocess.Popen(cmd, shell=False, **kwargs)
200 def StartSSH(node, cmd, strict=True):
204 return StartLocalCommand(GetSSHCommand(node, cmd, strict=strict))
207 def StartMultiplexer(node):
208 """Starts a multiplexer command.
210 @param node: the node for which to open the multiplexer
213 if node in _MULTIPLEXERS:
216 # Note: yes, we only need mktemp, since we'll remove the file anyway
217 sname = tempfile.mktemp(prefix="ganeti-qa-multiplexer.")
218 utils.RemoveFile(sname)
219 opts = ["-N", "-oControlPath=%s" % sname, "-oControlMaster=yes"]
220 print "Created socket at %s" % sname
221 child = StartLocalCommand(GetSSHCommand(node, None, opts=opts))
222 _MULTIPLEXERS[node] = (sname, child)
225 def CloseMultiplexers():
226 """Closes all current multiplexers and cleans up.
229 for node in _MULTIPLEXERS.keys():
230 (sname, child) = _MULTIPLEXERS.pop(node)
231 utils.KillProcess(child.pid, timeout=10, waitpid=True)
232 utils.RemoveFile(sname)
235 def GetCommandOutput(node, cmd, tty=True):
236 """Returns the output of a command executed on the given node.
239 p = StartLocalCommand(GetSSHCommand(node, cmd, tty=tty),
240 stdout=subprocess.PIPE)
241 AssertEqual(p.wait(), 0)
242 return p.stdout.read()
245 def UploadFile(node, src):
246 """Uploads a file to a node and returns the filename.
248 Caller needs to remove the returned file on the node when it's not needed
252 # Make sure nobody else has access to it while preserving local permissions
253 mode = os.stat(src).st_mode & 0700
255 cmd = ('tmp=$(tempfile --mode %o --prefix gnt) && '
256 '[[ -f "${tmp}" ]] && '
258 'echo "${tmp}"') % mode
262 p = subprocess.Popen(GetSSHCommand(node, cmd), shell=False, stdin=f,
263 stdout=subprocess.PIPE)
264 AssertEqual(p.wait(), 0)
266 # Return temporary filename
267 return p.stdout.read().strip()
272 def UploadData(node, data, mode=0600, filename=None):
273 """Uploads data to a node and returns the filename.
275 Caller needs to remove the returned file on the node when it's not needed
280 tmp = "tmp=%s" % utils.ShellQuote(filename)
282 tmp = "tmp=$(tempfile --mode %o --prefix gnt)" % mode
284 "[[ -f \"${tmp}\" ]] && "
285 "cat > \"${tmp}\" && "
286 "echo \"${tmp}\"") % tmp
288 p = subprocess.Popen(GetSSHCommand(node, cmd), shell=False,
289 stdin=subprocess.PIPE, stdout=subprocess.PIPE)
292 AssertEqual(p.wait(), 0)
294 # Return temporary filename
295 return p.stdout.read().strip()
298 def BackupFile(node, path):
299 """Creates a backup of a file on the node and returns the filename.
301 Caller needs to remove the returned file on the node when it's not needed
305 cmd = ("tmp=$(tempfile --prefix .gnt --directory=$(dirname %s)) && "
306 "[[ -f \"$tmp\" ]] && "
308 "echo $tmp") % (utils.ShellQuote(path), utils.ShellQuote(path))
310 # Return temporary filename
311 return GetCommandOutput(node, cmd).strip()
314 def _ResolveName(cmd, key):
318 master = qa_config.GetMasterNode()
320 output = GetCommandOutput(master['primary'], utils.ShellQuoteArgs(cmd))
321 for line in output.splitlines():
322 (lkey, lvalue) = line.split(':', 1)
324 return lvalue.lstrip()
325 raise KeyError("Key not found")
328 def ResolveInstanceName(instance):
329 """Gets the full name of an instance.
331 @type instance: string
332 @param instance: Instance name
335 return _ResolveName(['gnt-instance', 'info', instance],
339 def ResolveNodeName(node):
340 """Gets the full name of a node.
343 return _ResolveName(['gnt-node', 'info', node['primary']],
347 def GetNodeInstances(node, secondaries=False):
348 """Gets a list of instances on a node.
351 master = qa_config.GetMasterNode()
352 node_name = ResolveNodeName(node)
354 # Get list of all instances
355 cmd = ['gnt-instance', 'list', '--separator=:', '--no-headers',
356 '--output=name,pnode,snodes']
357 output = GetCommandOutput(master['primary'], utils.ShellQuoteArgs(cmd))
360 for line in output.splitlines():
361 (name, pnode, snodes) = line.split(':', 2)
362 if ((not secondaries and pnode == node_name) or
363 (secondaries and node_name in snodes.split(','))):
364 instances.append(name)
369 def _SelectQueryFields(rnd, fields):
370 """Generates a list of fields for query tests.
373 # Create copy for shuffling
374 fields = list(fields)
382 yield fields + fields
384 # Check small groups of fields
386 yield [fields.pop() for _ in range(rnd.randint(2, 10)) if fields]
389 def _List(listcmd, fields, names):
390 """Runs a list command.
393 master = qa_config.GetMasterNode()
395 cmd = [listcmd, "list", "--separator=|", "--no-header",
396 "--output", ",".join(fields)]
401 return GetCommandOutput(master["primary"],
402 utils.ShellQuoteArgs(cmd)).splitlines()
405 def GenericQueryTest(cmd, fields):
406 """Runs a number of tests on query commands.
408 @param cmd: Command name
409 @param fields: List of field names
412 rnd = random.Random(hash(cmd))
414 fields = list(fields)
417 # Test a number of field combinations
418 for testfields in _SelectQueryFields(rnd, fields):
419 AssertCommand([cmd, "list", "--output", ",".join(testfields)])
421 namelist_fn = compat.partial(_List, cmd, ["name"])
423 # When no names were requested, the list must be sorted
424 names = namelist_fn(None)
425 AssertEqual(names, utils.NiceSort(names))
427 # When requesting specific names, the order must be kept
428 revnames = list(reversed(names))
429 AssertEqual(namelist_fn(revnames), revnames)
431 randnames = list(names)
432 rnd.shuffle(randnames)
433 AssertEqual(namelist_fn(randnames), randnames)
435 # Listing unknown items must fail
436 AssertCommand([cmd, "list", "this.name.certainly.does.not.exist"], fail=True)
438 # Check exit code for listing unknown field
439 AssertEqual(AssertCommand([cmd, "list", "--output=field/does/not/exist"],
441 constants.EXIT_UNKNOWN_FIELD)
444 def GenericQueryFieldsTest(cmd, fields):
445 master = qa_config.GetMasterNode()
448 AssertCommand([cmd, "list-fields"])
449 AssertCommand([cmd, "list-fields"] + fields)
451 # Check listed fields (all, must be sorted)
452 realcmd = [cmd, "list-fields", "--separator=|", "--no-headers"]
453 output = GetCommandOutput(master["primary"],
454 utils.ShellQuoteArgs(realcmd)).splitlines()
455 AssertEqual([line.split("|", 1)[0] for line in output],
456 utils.NiceSort(fields))
458 # Check exit code for listing unknown field
459 AssertEqual(AssertCommand([cmd, "list-fields", "field/does/not/exist"],
461 constants.EXIT_UNKNOWN_FIELD)
464 def _FormatWithColor(text, seq):
467 return "%s%s%s" % (seq, text, _RESET_SEQ)
470 FormatWarning = lambda text: _FormatWithColor(text, _WARNING_SEQ)
471 FormatError = lambda text: _FormatWithColor(text, _ERROR_SEQ)
472 FormatInfo = lambda text: _FormatWithColor(text, _INFO_SEQ)
475 def AddToEtcHosts(hostnames):
476 """Adds hostnames to /etc/hosts.
478 @param hostnames: List of hostnames first used A records, all other CNAMEs
481 master = qa_config.GetMasterNode()
482 tmp_hosts = UploadData(master["primary"], "", mode=0644)
484 quoted_tmp_hosts = utils.ShellQuote(tmp_hosts)
486 for localhost in ("::1", "127.0.0.1"):
487 data.append("%s %s" % (localhost, " ".join(hostnames)))
490 AssertCommand(("cat /etc/hosts > %s && echo -e '%s' >> %s && mv %s"
491 " /etc/hosts") % (quoted_tmp_hosts, "\\n".join(data),
492 quoted_tmp_hosts, quoted_tmp_hosts))
493 except qa_error.Error:
494 AssertCommand(["rm", tmp_hosts])
497 def RemoveFromEtcHosts(hostnames):
498 """Remove hostnames from /etc/hosts.
500 @param hostnames: List of hostnames first used A records, all other CNAMEs
503 master = qa_config.GetMasterNode()
504 tmp_hosts = UploadData(master["primary"], "", mode=0644)
505 quoted_tmp_hosts = utils.ShellQuote(tmp_hosts)
507 sed_data = " ".join(hostnames)
509 AssertCommand(("sed -e '/^\(::1\|127\.0\.0\.1\)\s\+%s/d' /etc/hosts > %s"
510 " && mv %s /etc/hosts") % (sed_data, quoted_tmp_hosts,
512 except qa_error.Error:
513 AssertCommand(["rm", tmp_hosts])