4 # Copyright (C) 2007 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.
32 from ganeti import utils
33 from ganeti import compat
34 from ganeti import constants
47 """Initializes the colour constants.
50 global _INFO_SEQ, _WARNING_SEQ, _ERROR_SEQ, _RESET_SEQ
52 # Don't use colours if stdout isn't a terminal
53 if not sys.stdout.isatty():
59 # Don't use colours if curses module can't be imported
64 _RESET_SEQ = curses.tigetstr("op")
66 setaf = curses.tigetstr("setaf")
67 _INFO_SEQ = curses.tparm(setaf, curses.COLOR_GREEN)
68 _WARNING_SEQ = curses.tparm(setaf, curses.COLOR_YELLOW)
69 _ERROR_SEQ = curses.tparm(setaf, curses.COLOR_RED)
75 def AssertIn(item, sequence):
76 """Raises an error when item is not in sequence.
79 if item not in sequence:
80 raise qa_error.Error('%r not in %r' % (item, sequence))
83 def AssertEqual(first, second):
84 """Raises an error when values aren't equal.
87 if not first == second:
88 raise qa_error.Error('%r == %r' % (first, second))
91 def AssertNotEqual(first, second):
92 """Raises an error when values are equal.
95 if not first != second:
96 raise qa_error.Error('%r != %r' % (first, second))
99 def AssertMatch(string, pattern):
100 """Raises an error when string doesn't match regexp pattern.
103 if not re.match(pattern, string):
104 raise qa_error.Error("%r doesn't match /%r/" % (string, pattern))
107 def AssertCommand(cmd, fail=False, node=None):
108 """Checks that a remote command succeeds.
110 @param cmd: either a string (the command to execute) or a list (to
111 be converted using L{utils.ShellQuoteArgs} into a string)
113 @param fail: if the command is expected to fail instead of succeeding
114 @param node: if passed, it should be the node on which the command
115 should be executed, instead of the master node (can be either a
120 node = qa_config.GetMasterNode()
122 if isinstance(node, basestring):
125 nodename = node["primary"]
127 if isinstance(cmd, basestring):
130 cmdstr = utils.ShellQuoteArgs(cmd)
132 rcode = StartSSH(nodename, cmdstr).wait()
136 raise qa_error.Error("Command '%s' on node %s was expected to fail but"
137 " didn't" % (cmdstr, nodename))
140 raise qa_error.Error("Command '%s' on node %s failed, exit code %s" %
141 (cmdstr, nodename, rcode))
146 def GetSSHCommand(node, cmd, strict=True):
147 """Builds SSH command to be executed.
150 @param node: node the command should run on
152 @param cmd: command to be executed in the node
153 @type strict: boolean
154 @param strict: whether to enable strict host key checking
157 args = [ 'ssh', '-oEscapeChar=none', '-oBatchMode=yes', '-l', 'root', '-t' ]
163 args.append('-oStrictHostKeyChecking=%s' % tmp)
164 args.append('-oClearAllForwardings=yes')
165 args.append('-oForwardAgent=yes')
172 def StartLocalCommand(cmd, **kwargs):
173 """Starts a local command.
176 print "Command: %s" % utils.ShellQuoteArgs(cmd)
177 return subprocess.Popen(cmd, shell=False, **kwargs)
180 def StartSSH(node, cmd, strict=True):
184 return StartLocalCommand(GetSSHCommand(node, cmd, strict=strict))
187 def GetCommandOutput(node, cmd):
188 """Returns the output of a command executed on the given node.
191 p = StartLocalCommand(GetSSHCommand(node, cmd), stdout=subprocess.PIPE)
192 AssertEqual(p.wait(), 0)
193 return p.stdout.read()
196 def UploadFile(node, src):
197 """Uploads a file to a node and returns the filename.
199 Caller needs to remove the returned file on the node when it's not needed
203 # Make sure nobody else has access to it while preserving local permissions
204 mode = os.stat(src).st_mode & 0700
206 cmd = ('tmp=$(tempfile --mode %o --prefix gnt) && '
207 '[[ -f "${tmp}" ]] && '
209 'echo "${tmp}"') % mode
213 p = subprocess.Popen(GetSSHCommand(node, cmd), shell=False, stdin=f,
214 stdout=subprocess.PIPE)
215 AssertEqual(p.wait(), 0)
217 # Return temporary filename
218 return p.stdout.read().strip()
223 def UploadData(node, data, mode=0600, filename=None):
224 """Uploads data to a node and returns the filename.
226 Caller needs to remove the returned file on the node when it's not needed
231 tmp = "tmp=%s" % utils.ShellQuote(filename)
233 tmp = "tmp=$(tempfile --mode %o --prefix gnt)" % mode
235 "[[ -f \"${tmp}\" ]] && "
236 "cat > \"${tmp}\" && "
237 "echo \"${tmp}\"") % tmp
239 p = subprocess.Popen(GetSSHCommand(node, cmd), shell=False,
240 stdin=subprocess.PIPE, stdout=subprocess.PIPE)
243 AssertEqual(p.wait(), 0)
245 # Return temporary filename
246 return p.stdout.read().strip()
249 def BackupFile(node, path):
250 """Creates a backup of a file on the node and returns the filename.
252 Caller needs to remove the returned file on the node when it's not needed
256 cmd = ("tmp=$(tempfile --prefix .gnt --directory=$(dirname %s)) && "
257 "[[ -f \"$tmp\" ]] && "
259 "echo $tmp") % (utils.ShellQuote(path), utils.ShellQuote(path))
261 # Return temporary filename
262 return GetCommandOutput(node, cmd).strip()
265 def _ResolveName(cmd, key):
269 master = qa_config.GetMasterNode()
271 output = GetCommandOutput(master['primary'], utils.ShellQuoteArgs(cmd))
272 for line in output.splitlines():
273 (lkey, lvalue) = line.split(':', 1)
275 return lvalue.lstrip()
276 raise KeyError("Key not found")
279 def ResolveInstanceName(instance):
280 """Gets the full name of an instance.
282 @type instance: string
283 @param instance: Instance name
286 return _ResolveName(['gnt-instance', 'info', instance],
290 def ResolveNodeName(node):
291 """Gets the full name of a node.
294 return _ResolveName(['gnt-node', 'info', node['primary']],
298 def GetNodeInstances(node, secondaries=False):
299 """Gets a list of instances on a node.
302 master = qa_config.GetMasterNode()
303 node_name = ResolveNodeName(node)
305 # Get list of all instances
306 cmd = ['gnt-instance', 'list', '--separator=:', '--no-headers',
307 '--output=name,pnode,snodes']
308 output = GetCommandOutput(master['primary'], utils.ShellQuoteArgs(cmd))
311 for line in output.splitlines():
312 (name, pnode, snodes) = line.split(':', 2)
313 if ((not secondaries and pnode == node_name) or
314 (secondaries and node_name in snodes.split(','))):
315 instances.append(name)
320 def _SelectQueryFields(rnd, fields):
321 """Generates a list of fields for query tests.
324 # Create copy for shuffling
325 fields = list(fields)
333 yield fields + fields
335 # Check small groups of fields
337 yield [fields.pop() for _ in range(rnd.randint(2, 10)) if fields]
340 def _List(listcmd, fields, names):
341 """Runs a list command.
344 master = qa_config.GetMasterNode()
346 cmd = [listcmd, "list", "--separator=|", "--no-header",
347 "--output", ",".join(fields)]
352 return GetCommandOutput(master["primary"],
353 utils.ShellQuoteArgs(cmd)).splitlines()
356 def GenericQueryTest(cmd, fields):
357 """Runs a number of tests on query commands.
359 @param cmd: Command name
360 @param fields: List of field names
363 rnd = random.Random(hash(cmd))
365 randfields = list(fields)
368 # Test a number of field combinations
369 for testfields in _SelectQueryFields(rnd, fields):
370 AssertCommand([cmd, "list", "--output", ",".join(testfields)])
372 namelist_fn = compat.partial(_List, cmd, ["name"])
374 # When no names were requested, the list must be sorted
375 names = namelist_fn(None)
376 AssertEqual(names, utils.NiceSort(names))
378 # When requesting specific names, the order must be kept
379 revnames = list(reversed(names))
380 AssertEqual(namelist_fn(revnames), revnames)
382 randnames = list(names)
383 rnd.shuffle(randnames)
384 AssertEqual(namelist_fn(randnames), randnames)
386 # Listing unknown items must fail
387 AssertCommand([cmd, "list", "this.name.certainly.does.not.exist"], fail=True)
389 # Check exit code for listing unknown field
390 AssertEqual(AssertCommand([cmd, "list", "--output=field/does/not/exist"],
392 constants.EXIT_UNKNOWN_FIELD)
395 def GenericQueryFieldsTest(cmd, fields):
396 master = qa_config.GetMasterNode()
399 AssertCommand([cmd, "list-fields"])
400 AssertCommand([cmd, "list-fields"] + fields)
402 # Check listed fields (all, must be sorted)
403 realcmd = [cmd, "list-fields", "--separator=|", "--no-headers"]
404 output = GetCommandOutput(master["primary"],
405 utils.ShellQuoteArgs(realcmd)).splitlines()
406 AssertEqual([line.split("|", 1)[0] for line in output],
407 utils.NiceSort(fields))
409 # Check exit code for listing unknown field
410 AssertEqual(AssertCommand([cmd, "list-fields", "field/does/not/exist"],
412 constants.EXIT_UNKNOWN_FIELD)
415 def _FormatWithColor(text, seq):
418 return "%s%s%s" % (seq, text, _RESET_SEQ)
421 FormatWarning = lambda text: _FormatWithColor(text, _WARNING_SEQ)
422 FormatError = lambda text: _FormatWithColor(text, _ERROR_SEQ)
423 FormatInfo = lambda text: _FormatWithColor(text, _INFO_SEQ)