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 global _INFO_SEQ, _WARNING_SEQ, _ERROR_SEQ, _RESET_SEQ
55 # Don't use colours if stdout isn't a terminal
56 if not sys.stdout.isatty():
62 # Don't use colours if curses module can't be imported
67 _RESET_SEQ = curses.tigetstr("op")
69 setaf = curses.tigetstr("setaf")
70 _INFO_SEQ = curses.tparm(setaf, curses.COLOR_GREEN)
71 _WARNING_SEQ = curses.tparm(setaf, curses.COLOR_YELLOW)
72 _ERROR_SEQ = curses.tparm(setaf, curses.COLOR_RED)
78 def AssertIn(item, sequence):
79 """Raises an error when item is not in sequence.
82 if item not in sequence:
83 raise qa_error.Error('%r not in %r' % (item, sequence))
86 def AssertEqual(first, second):
87 """Raises an error when values aren't equal.
90 if not first == second:
91 raise qa_error.Error('%r == %r' % (first, second))
94 def AssertNotEqual(first, second):
95 """Raises an error when values are equal.
98 if not first != second:
99 raise qa_error.Error('%r != %r' % (first, second))
102 def AssertMatch(string, pattern):
103 """Raises an error when string doesn't match regexp pattern.
106 if not re.match(pattern, string):
107 raise qa_error.Error("%r doesn't match /%r/" % (string, pattern))
110 def AssertCommand(cmd, fail=False, node=None):
111 """Checks that a remote command succeeds.
113 @param cmd: either a string (the command to execute) or a list (to
114 be converted using L{utils.ShellQuoteArgs} into a string)
116 @param fail: if the command is expected to fail instead of succeeding
117 @param node: if passed, it should be the node on which the command
118 should be executed, instead of the master node (can be either a
123 node = qa_config.GetMasterNode()
125 if isinstance(node, basestring):
128 nodename = node["primary"]
130 if isinstance(cmd, basestring):
133 cmdstr = utils.ShellQuoteArgs(cmd)
135 rcode = StartSSH(nodename, cmdstr).wait()
139 raise qa_error.Error("Command '%s' on node %s was expected to fail but"
140 " didn't" % (cmdstr, nodename))
143 raise qa_error.Error("Command '%s' on node %s failed, exit code %s" %
144 (cmdstr, nodename, rcode))
149 def GetSSHCommand(node, cmd, strict=True, opts=None):
150 """Builds SSH command to be executed.
153 @param node: node the command should run on
155 @param cmd: command to be executed in the node; if None or empty
156 string, no command will be executed
157 @type strict: boolean
158 @param strict: whether to enable strict host key checking
160 @param opts: list of additional options
163 args = [ 'ssh', '-oEscapeChar=none', '-oBatchMode=yes', '-l', 'root', '-t' ]
169 args.append('-oStrictHostKeyChecking=%s' % tmp)
170 args.append('-oClearAllForwardings=yes')
171 args.append('-oForwardAgent=yes')
174 if node in _MULTIPLEXERS:
175 spath = _MULTIPLEXERS[node][0]
176 args.append('-oControlPath=%s' % spath)
177 args.append('-oControlMaster=no')
185 def StartLocalCommand(cmd, **kwargs):
186 """Starts a local command.
189 print "Command: %s" % utils.ShellQuoteArgs(cmd)
190 return subprocess.Popen(cmd, shell=False, **kwargs)
193 def StartSSH(node, cmd, strict=True):
197 return StartLocalCommand(GetSSHCommand(node, cmd, strict=strict))
200 def StartMultiplexer(node):
201 """Starts a multiplexer command.
203 @param node: the node for which to open the multiplexer
206 if node in _MULTIPLEXERS:
209 # Note: yes, we only need mktemp, since we'll remove the file anyway
210 sname = tempfile.mktemp(prefix="ganeti-qa-multiplexer.")
211 utils.RemoveFile(sname)
212 opts = ["-N", "-oControlPath=%s" % sname, "-oControlMaster=yes"]
213 print "Created socket at %s" % sname
214 child = StartLocalCommand(GetSSHCommand(node, None, opts=opts))
215 _MULTIPLEXERS[node] = (sname, child)
218 def CloseMultiplexers():
219 """Closes all current multiplexers and cleans up.
222 for node in _MULTIPLEXERS.keys():
223 (sname, child) = _MULTIPLEXERS.pop(node)
224 utils.KillProcess(child.pid, timeout=10, waitpid=True)
225 utils.RemoveFile(sname)
228 def GetCommandOutput(node, cmd):
229 """Returns the output of a command executed on the given node.
232 p = StartLocalCommand(GetSSHCommand(node, cmd), stdout=subprocess.PIPE)
233 AssertEqual(p.wait(), 0)
234 return p.stdout.read()
237 def UploadFile(node, src):
238 """Uploads a file to a node and returns the filename.
240 Caller needs to remove the returned file on the node when it's not needed
244 # Make sure nobody else has access to it while preserving local permissions
245 mode = os.stat(src).st_mode & 0700
247 cmd = ('tmp=$(tempfile --mode %o --prefix gnt) && '
248 '[[ -f "${tmp}" ]] && '
250 'echo "${tmp}"') % mode
254 p = subprocess.Popen(GetSSHCommand(node, cmd), shell=False, stdin=f,
255 stdout=subprocess.PIPE)
256 AssertEqual(p.wait(), 0)
258 # Return temporary filename
259 return p.stdout.read().strip()
264 def UploadData(node, data, mode=0600, filename=None):
265 """Uploads data to a node and returns the filename.
267 Caller needs to remove the returned file on the node when it's not needed
272 tmp = "tmp=%s" % utils.ShellQuote(filename)
274 tmp = "tmp=$(tempfile --mode %o --prefix gnt)" % mode
276 "[[ -f \"${tmp}\" ]] && "
277 "cat > \"${tmp}\" && "
278 "echo \"${tmp}\"") % tmp
280 p = subprocess.Popen(GetSSHCommand(node, cmd), shell=False,
281 stdin=subprocess.PIPE, stdout=subprocess.PIPE)
284 AssertEqual(p.wait(), 0)
286 # Return temporary filename
287 return p.stdout.read().strip()
290 def BackupFile(node, path):
291 """Creates a backup of a file on the node and returns the filename.
293 Caller needs to remove the returned file on the node when it's not needed
297 cmd = ("tmp=$(tempfile --prefix .gnt --directory=$(dirname %s)) && "
298 "[[ -f \"$tmp\" ]] && "
300 "echo $tmp") % (utils.ShellQuote(path), utils.ShellQuote(path))
302 # Return temporary filename
303 return GetCommandOutput(node, cmd).strip()
306 def _ResolveName(cmd, key):
310 master = qa_config.GetMasterNode()
312 output = GetCommandOutput(master['primary'], utils.ShellQuoteArgs(cmd))
313 for line in output.splitlines():
314 (lkey, lvalue) = line.split(':', 1)
316 return lvalue.lstrip()
317 raise KeyError("Key not found")
320 def ResolveInstanceName(instance):
321 """Gets the full name of an instance.
323 @type instance: string
324 @param instance: Instance name
327 return _ResolveName(['gnt-instance', 'info', instance],
331 def ResolveNodeName(node):
332 """Gets the full name of a node.
335 return _ResolveName(['gnt-node', 'info', node['primary']],
339 def GetNodeInstances(node, secondaries=False):
340 """Gets a list of instances on a node.
343 master = qa_config.GetMasterNode()
344 node_name = ResolveNodeName(node)
346 # Get list of all instances
347 cmd = ['gnt-instance', 'list', '--separator=:', '--no-headers',
348 '--output=name,pnode,snodes']
349 output = GetCommandOutput(master['primary'], utils.ShellQuoteArgs(cmd))
352 for line in output.splitlines():
353 (name, pnode, snodes) = line.split(':', 2)
354 if ((not secondaries and pnode == node_name) or
355 (secondaries and node_name in snodes.split(','))):
356 instances.append(name)
361 def _SelectQueryFields(rnd, fields):
362 """Generates a list of fields for query tests.
365 # Create copy for shuffling
366 fields = list(fields)
374 yield fields + fields
376 # Check small groups of fields
378 yield [fields.pop() for _ in range(rnd.randint(2, 10)) if fields]
381 def _List(listcmd, fields, names):
382 """Runs a list command.
385 master = qa_config.GetMasterNode()
387 cmd = [listcmd, "list", "--separator=|", "--no-header",
388 "--output", ",".join(fields)]
393 return GetCommandOutput(master["primary"],
394 utils.ShellQuoteArgs(cmd)).splitlines()
397 def GenericQueryTest(cmd, fields):
398 """Runs a number of tests on query commands.
400 @param cmd: Command name
401 @param fields: List of field names
404 rnd = random.Random(hash(cmd))
406 randfields = list(fields)
409 # Test a number of field combinations
410 for testfields in _SelectQueryFields(rnd, fields):
411 AssertCommand([cmd, "list", "--output", ",".join(testfields)])
413 namelist_fn = compat.partial(_List, cmd, ["name"])
415 # When no names were requested, the list must be sorted
416 names = namelist_fn(None)
417 AssertEqual(names, utils.NiceSort(names))
419 # When requesting specific names, the order must be kept
420 revnames = list(reversed(names))
421 AssertEqual(namelist_fn(revnames), revnames)
423 randnames = list(names)
424 rnd.shuffle(randnames)
425 AssertEqual(namelist_fn(randnames), randnames)
427 # Listing unknown items must fail
428 AssertCommand([cmd, "list", "this.name.certainly.does.not.exist"], fail=True)
430 # Check exit code for listing unknown field
431 AssertEqual(AssertCommand([cmd, "list", "--output=field/does/not/exist"],
433 constants.EXIT_UNKNOWN_FIELD)
436 def GenericQueryFieldsTest(cmd, fields):
437 master = qa_config.GetMasterNode()
440 AssertCommand([cmd, "list-fields"])
441 AssertCommand([cmd, "list-fields"] + fields)
443 # Check listed fields (all, must be sorted)
444 realcmd = [cmd, "list-fields", "--separator=|", "--no-headers"]
445 output = GetCommandOutput(master["primary"],
446 utils.ShellQuoteArgs(realcmd)).splitlines()
447 AssertEqual([line.split("|", 1)[0] for line in output],
448 utils.NiceSort(fields))
450 # Check exit code for listing unknown field
451 AssertEqual(AssertCommand([cmd, "list-fields", "field/does/not/exist"],
453 constants.EXIT_UNKNOWN_FIELD)
456 def _FormatWithColor(text, seq):
459 return "%s%s%s" % (seq, text, _RESET_SEQ)
462 FormatWarning = lambda text: _FormatWithColor(text, _WARNING_SEQ)
463 FormatError = lambda text: _FormatWithColor(text, _ERROR_SEQ)
464 FormatInfo = lambda text: _FormatWithColor(text, _INFO_SEQ)