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 - node: Node the command should run on
151 - cmd: Command to be executed as a list with all parameters
152 - strict: Whether to enable strict host key checking
155 args = [ 'ssh', '-oEscapeChar=none', '-oBatchMode=yes', '-l', 'root', '-t' ]
161 args.append('-oStrictHostKeyChecking=%s' % tmp)
162 args.append('-oClearAllForwardings=yes')
163 args.append('-oForwardAgent=yes')
170 def StartLocalCommand(cmd, **kwargs):
171 """Starts a local command.
174 print "Command: %s" % utils.ShellQuoteArgs(cmd)
175 return subprocess.Popen(cmd, shell=False, **kwargs)
178 def StartSSH(node, cmd, strict=True):
182 return StartLocalCommand(GetSSHCommand(node, cmd, strict=strict))
185 def GetCommandOutput(node, cmd):
186 """Returns the output of a command executed on the given node.
189 p = StartLocalCommand(GetSSHCommand(node, cmd), stdout=subprocess.PIPE)
190 AssertEqual(p.wait(), 0)
191 return p.stdout.read()
194 def UploadFile(node, src):
195 """Uploads a file to a node and returns the filename.
197 Caller needs to remove the returned file on the node when it's not needed
201 # Make sure nobody else has access to it while preserving local permissions
202 mode = os.stat(src).st_mode & 0700
204 cmd = ('tmp=$(tempfile --mode %o --prefix gnt) && '
205 '[[ -f "${tmp}" ]] && '
207 'echo "${tmp}"') % mode
211 p = subprocess.Popen(GetSSHCommand(node, cmd), shell=False, stdin=f,
212 stdout=subprocess.PIPE)
213 AssertEqual(p.wait(), 0)
215 # Return temporary filename
216 return p.stdout.read().strip()
221 def UploadData(node, data, mode=0600, filename=None):
222 """Uploads data to a node and returns the filename.
224 Caller needs to remove the returned file on the node when it's not needed
229 tmp = "tmp=%s" % utils.ShellQuote(filename)
231 tmp = "tmp=$(tempfile --mode %o --prefix gnt)" % mode
233 "[[ -f \"${tmp}\" ]] && "
234 "cat > \"${tmp}\" && "
235 "echo \"${tmp}\"") % tmp
237 p = subprocess.Popen(GetSSHCommand(node, cmd), shell=False,
238 stdin=subprocess.PIPE, stdout=subprocess.PIPE)
241 AssertEqual(p.wait(), 0)
243 # Return temporary filename
244 return p.stdout.read().strip()
247 def BackupFile(node, path):
248 """Creates a backup of a file on the node and returns the filename.
250 Caller needs to remove the returned file on the node when it's not needed
254 cmd = ("tmp=$(tempfile --prefix .gnt --directory=$(dirname %s)) && "
255 "[[ -f \"$tmp\" ]] && "
257 "echo $tmp") % (utils.ShellQuote(path), utils.ShellQuote(path))
259 # Return temporary filename
260 return GetCommandOutput(node, cmd).strip()
263 def _ResolveName(cmd, key):
267 master = qa_config.GetMasterNode()
269 output = GetCommandOutput(master['primary'], utils.ShellQuoteArgs(cmd))
270 for line in output.splitlines():
271 (lkey, lvalue) = line.split(':', 1)
273 return lvalue.lstrip()
274 raise KeyError("Key not found")
277 def ResolveInstanceName(instance):
278 """Gets the full name of an instance.
280 @type instance: string
281 @param instance: Instance name
284 return _ResolveName(['gnt-instance', 'info', instance],
288 def ResolveNodeName(node):
289 """Gets the full name of a node.
292 return _ResolveName(['gnt-node', 'info', node['primary']],
296 def GetNodeInstances(node, secondaries=False):
297 """Gets a list of instances on a node.
300 master = qa_config.GetMasterNode()
301 node_name = ResolveNodeName(node)
303 # Get list of all instances
304 cmd = ['gnt-instance', 'list', '--separator=:', '--no-headers',
305 '--output=name,pnode,snodes']
306 output = GetCommandOutput(master['primary'], utils.ShellQuoteArgs(cmd))
309 for line in output.splitlines():
310 (name, pnode, snodes) = line.split(':', 2)
311 if ((not secondaries and pnode == node_name) or
312 (secondaries and node_name in snodes.split(','))):
313 instances.append(name)
318 def _SelectQueryFields(rnd, fields):
319 """Generates a list of fields for query tests.
322 # Create copy for shuffling
323 fields = list(fields)
331 yield fields + fields
333 # Check small groups of fields
335 yield [fields.pop() for _ in range(rnd.randint(2, 10)) if fields]
338 def _List(listcmd, fields, names):
339 """Runs a list command.
342 master = qa_config.GetMasterNode()
344 cmd = [listcmd, "list", "--separator=|", "--no-header",
345 "--output", ",".join(fields)]
350 return GetCommandOutput(master["primary"],
351 utils.ShellQuoteArgs(cmd)).splitlines()
354 def GenericQueryTest(cmd, fields):
355 """Runs a number of tests on query commands.
357 @param cmd: Command name
358 @param fields: List of field names
361 rnd = random.Random(hash(cmd))
363 randfields = list(fields)
366 # Test a number of field combinations
367 for testfields in _SelectQueryFields(rnd, fields):
368 AssertCommand([cmd, "list", "--output", ",".join(testfields)])
370 namelist_fn = compat.partial(_List, cmd, ["name"])
372 # When no names were requested, the list must be sorted
373 names = namelist_fn(None)
374 AssertEqual(names, utils.NiceSort(names))
376 # When requesting specific names, the order must be kept
377 revnames = list(reversed(names))
378 AssertEqual(namelist_fn(revnames), revnames)
380 randnames = list(names)
381 rnd.shuffle(randnames)
382 AssertEqual(namelist_fn(randnames), randnames)
384 # Listing unknown items must fail
385 AssertCommand([cmd, "list", "this.name.certainly.does.not.exist"], fail=True)
387 # Check exit code for listing unknown field
388 AssertEqual(AssertCommand([cmd, "list", "--output=field/does/not/exist"],
390 constants.EXIT_UNKNOWN_FIELD)
393 def GenericQueryFieldsTest(cmd, fields):
394 master = qa_config.GetMasterNode()
397 AssertCommand([cmd, "list-fields"])
398 AssertCommand([cmd, "list-fields"] + fields)
400 # Check listed fields (all, must be sorted)
401 realcmd = [cmd, "list-fields", "--separator=|", "--no-headers"]
402 output = GetCommandOutput(master["primary"],
403 utils.ShellQuoteArgs(realcmd)).splitlines()
404 AssertEqual([line.split("|", 1)[0] for line in output],
407 # Check exit code for listing unknown field
408 AssertEqual(AssertCommand([cmd, "list-fields", "field/does/not/exist"],
410 constants.EXIT_UNKNOWN_FIELD)
413 def _FormatWithColor(text, seq):
416 return "%s%s%s" % (seq, text, _RESET_SEQ)
419 FormatWarning = lambda text: _FormatWithColor(text, _WARNING_SEQ)
420 FormatError = lambda text: _FormatWithColor(text, _ERROR_SEQ)
421 FormatInfo = lambda text: _FormatWithColor(text, _INFO_SEQ)