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):
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
165 args = [ 'ssh', '-oEscapeChar=none', '-oBatchMode=yes', '-l', 'root', '-t' ]
171 args.append('-oStrictHostKeyChecking=%s' % tmp)
172 args.append('-oClearAllForwardings=yes')
173 args.append('-oForwardAgent=yes')
176 if node in _MULTIPLEXERS:
177 spath = _MULTIPLEXERS[node][0]
178 args.append('-oControlPath=%s' % spath)
179 args.append('-oControlMaster=no')
187 def StartLocalCommand(cmd, **kwargs):
188 """Starts a local command.
191 print "Command: %s" % utils.ShellQuoteArgs(cmd)
192 return subprocess.Popen(cmd, shell=False, **kwargs)
195 def StartSSH(node, cmd, strict=True):
199 return StartLocalCommand(GetSSHCommand(node, cmd, strict=strict))
202 def StartMultiplexer(node):
203 """Starts a multiplexer command.
205 @param node: the node for which to open the multiplexer
208 if node in _MULTIPLEXERS:
211 # Note: yes, we only need mktemp, since we'll remove the file anyway
212 sname = tempfile.mktemp(prefix="ganeti-qa-multiplexer.")
213 utils.RemoveFile(sname)
214 opts = ["-N", "-oControlPath=%s" % sname, "-oControlMaster=yes"]
215 print "Created socket at %s" % sname
216 child = StartLocalCommand(GetSSHCommand(node, None, opts=opts))
217 _MULTIPLEXERS[node] = (sname, child)
220 def CloseMultiplexers():
221 """Closes all current multiplexers and cleans up.
224 for node in _MULTIPLEXERS.keys():
225 (sname, child) = _MULTIPLEXERS.pop(node)
226 utils.KillProcess(child.pid, timeout=10, waitpid=True)
227 utils.RemoveFile(sname)
230 def GetCommandOutput(node, cmd):
231 """Returns the output of a command executed on the given node.
234 p = StartLocalCommand(GetSSHCommand(node, cmd), stdout=subprocess.PIPE)
235 AssertEqual(p.wait(), 0)
236 return p.stdout.read()
239 def UploadFile(node, src):
240 """Uploads a file to a node and returns the filename.
242 Caller needs to remove the returned file on the node when it's not needed
246 # Make sure nobody else has access to it while preserving local permissions
247 mode = os.stat(src).st_mode & 0700
249 cmd = ('tmp=$(tempfile --mode %o --prefix gnt) && '
250 '[[ -f "${tmp}" ]] && '
252 'echo "${tmp}"') % mode
256 p = subprocess.Popen(GetSSHCommand(node, cmd), shell=False, stdin=f,
257 stdout=subprocess.PIPE)
258 AssertEqual(p.wait(), 0)
260 # Return temporary filename
261 return p.stdout.read().strip()
266 def UploadData(node, data, mode=0600, filename=None):
267 """Uploads data to a node and returns the filename.
269 Caller needs to remove the returned file on the node when it's not needed
274 tmp = "tmp=%s" % utils.ShellQuote(filename)
276 tmp = "tmp=$(tempfile --mode %o --prefix gnt)" % mode
278 "[[ -f \"${tmp}\" ]] && "
279 "cat > \"${tmp}\" && "
280 "echo \"${tmp}\"") % tmp
282 p = subprocess.Popen(GetSSHCommand(node, cmd), shell=False,
283 stdin=subprocess.PIPE, stdout=subprocess.PIPE)
286 AssertEqual(p.wait(), 0)
288 # Return temporary filename
289 return p.stdout.read().strip()
292 def BackupFile(node, path):
293 """Creates a backup of a file on the node and returns the filename.
295 Caller needs to remove the returned file on the node when it's not needed
299 cmd = ("tmp=$(tempfile --prefix .gnt --directory=$(dirname %s)) && "
300 "[[ -f \"$tmp\" ]] && "
302 "echo $tmp") % (utils.ShellQuote(path), utils.ShellQuote(path))
304 # Return temporary filename
305 return GetCommandOutput(node, cmd).strip()
308 def _ResolveName(cmd, key):
312 master = qa_config.GetMasterNode()
314 output = GetCommandOutput(master['primary'], utils.ShellQuoteArgs(cmd))
315 for line in output.splitlines():
316 (lkey, lvalue) = line.split(':', 1)
318 return lvalue.lstrip()
319 raise KeyError("Key not found")
322 def ResolveInstanceName(instance):
323 """Gets the full name of an instance.
325 @type instance: string
326 @param instance: Instance name
329 return _ResolveName(['gnt-instance', 'info', instance],
333 def ResolveNodeName(node):
334 """Gets the full name of a node.
337 return _ResolveName(['gnt-node', 'info', node['primary']],
341 def GetNodeInstances(node, secondaries=False):
342 """Gets a list of instances on a node.
345 master = qa_config.GetMasterNode()
346 node_name = ResolveNodeName(node)
348 # Get list of all instances
349 cmd = ['gnt-instance', 'list', '--separator=:', '--no-headers',
350 '--output=name,pnode,snodes']
351 output = GetCommandOutput(master['primary'], utils.ShellQuoteArgs(cmd))
354 for line in output.splitlines():
355 (name, pnode, snodes) = line.split(':', 2)
356 if ((not secondaries and pnode == node_name) or
357 (secondaries and node_name in snodes.split(','))):
358 instances.append(name)
363 def _SelectQueryFields(rnd, fields):
364 """Generates a list of fields for query tests.
367 # Create copy for shuffling
368 fields = list(fields)
376 yield fields + fields
378 # Check small groups of fields
380 yield [fields.pop() for _ in range(rnd.randint(2, 10)) if fields]
383 def _List(listcmd, fields, names):
384 """Runs a list command.
387 master = qa_config.GetMasterNode()
389 cmd = [listcmd, "list", "--separator=|", "--no-header",
390 "--output", ",".join(fields)]
395 return GetCommandOutput(master["primary"],
396 utils.ShellQuoteArgs(cmd)).splitlines()
399 def GenericQueryTest(cmd, fields):
400 """Runs a number of tests on query commands.
402 @param cmd: Command name
403 @param fields: List of field names
406 rnd = random.Random(hash(cmd))
408 fields = list(fields)
411 # Test a number of field combinations
412 for testfields in _SelectQueryFields(rnd, fields):
413 AssertCommand([cmd, "list", "--output", ",".join(testfields)])
415 namelist_fn = compat.partial(_List, cmd, ["name"])
417 # When no names were requested, the list must be sorted
418 names = namelist_fn(None)
419 AssertEqual(names, utils.NiceSort(names))
421 # When requesting specific names, the order must be kept
422 revnames = list(reversed(names))
423 AssertEqual(namelist_fn(revnames), revnames)
425 randnames = list(names)
426 rnd.shuffle(randnames)
427 AssertEqual(namelist_fn(randnames), randnames)
429 # Listing unknown items must fail
430 AssertCommand([cmd, "list", "this.name.certainly.does.not.exist"], fail=True)
432 # Check exit code for listing unknown field
433 AssertEqual(AssertCommand([cmd, "list", "--output=field/does/not/exist"],
435 constants.EXIT_UNKNOWN_FIELD)
438 def GenericQueryFieldsTest(cmd, fields):
439 master = qa_config.GetMasterNode()
442 AssertCommand([cmd, "list-fields"])
443 AssertCommand([cmd, "list-fields"] + fields)
445 # Check listed fields (all, must be sorted)
446 realcmd = [cmd, "list-fields", "--separator=|", "--no-headers"]
447 output = GetCommandOutput(master["primary"],
448 utils.ShellQuoteArgs(realcmd)).splitlines()
449 AssertEqual([line.split("|", 1)[0] for line in output],
450 utils.NiceSort(fields))
452 # Check exit code for listing unknown field
453 AssertEqual(AssertCommand([cmd, "list-fields", "field/does/not/exist"],
455 constants.EXIT_UNKNOWN_FIELD)
458 def _FormatWithColor(text, seq):
461 return "%s%s%s" % (seq, text, _RESET_SEQ)
464 FormatWarning = lambda text: _FormatWithColor(text, _WARNING_SEQ)
465 FormatError = lambda text: _FormatWithColor(text, _ERROR_SEQ)
466 FormatInfo = lambda text: _FormatWithColor(text, _INFO_SEQ)