Fix disk adoption breakage
[ganeti-local] / qa / qa_utils.py
1 #
2 #
3
4 # Copyright (C) 2007, 2011 Google Inc.
5 #
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.
10 #
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.
15 #
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
19 # 02110-1301, USA.
20
21
22 """Utilities for QA tests.
23
24 """
25
26 import os
27 import re
28 import sys
29 import subprocess
30 import random
31 import tempfile
32
33 from ganeti import utils
34 from ganeti import compat
35 from ganeti import constants
36
37 import qa_config
38 import qa_error
39
40
41 _INFO_SEQ = None
42 _WARNING_SEQ = None
43 _ERROR_SEQ = None
44 _RESET_SEQ = None
45
46 _MULTIPLEXERS = {}
47
48
49 def _SetupColours():
50   """Initializes the colour constants.
51
52   """
53   # pylint: disable-msg=W0603
54   # due to global usage
55   global _INFO_SEQ, _WARNING_SEQ, _ERROR_SEQ, _RESET_SEQ
56
57   # Don't use colours if stdout isn't a terminal
58   if not sys.stdout.isatty():
59     return
60
61   try:
62     import curses
63   except ImportError:
64     # Don't use colours if curses module can't be imported
65     return
66
67   curses.setupterm()
68
69   _RESET_SEQ = curses.tigetstr("op")
70
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)
75
76
77 _SetupColours()
78
79
80 def AssertIn(item, sequence):
81   """Raises an error when item is not in sequence.
82
83   """
84   if item not in sequence:
85     raise qa_error.Error('%r not in %r' % (item, sequence))
86
87
88 def AssertEqual(first, second):
89   """Raises an error when values aren't equal.
90
91   """
92   if not first == second:
93     raise qa_error.Error('%r == %r' % (first, second))
94
95
96 def AssertNotEqual(first, second):
97   """Raises an error when values are equal.
98
99   """
100   if not first != second:
101     raise qa_error.Error('%r != %r' % (first, second))
102
103
104 def AssertMatch(string, pattern):
105   """Raises an error when string doesn't match regexp pattern.
106
107   """
108   if not re.match(pattern, string):
109     raise qa_error.Error("%r doesn't match /%r/" % (string, pattern))
110
111
112 def AssertCommand(cmd, fail=False, node=None):
113   """Checks that a remote command succeeds.
114
115   @param cmd: either a string (the command to execute) or a list (to
116       be converted using L{utils.ShellQuoteArgs} into a string)
117   @type fail: boolean
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
121       dict or a string)
122
123   """
124   if node is None:
125     node = qa_config.GetMasterNode()
126
127   if isinstance(node, basestring):
128     nodename = node
129   else:
130     nodename = node["primary"]
131
132   if isinstance(cmd, basestring):
133     cmdstr = cmd
134   else:
135     cmdstr = utils.ShellQuoteArgs(cmd)
136
137   rcode = StartSSH(nodename, cmdstr).wait()
138
139   if fail:
140     if rcode == 0:
141       raise qa_error.Error("Command '%s' on node %s was expected to fail but"
142                            " didn't" % (cmdstr, nodename))
143   else:
144     if rcode != 0:
145       raise qa_error.Error("Command '%s' on node %s failed, exit code %s" %
146                            (cmdstr, nodename, rcode))
147
148   return rcode
149
150
151 def GetSSHCommand(node, cmd, strict=True, opts=None):
152   """Builds SSH command to be executed.
153
154   @type node: string
155   @param node: node the command should run on
156   @type cmd: string
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
161   @type opts: list
162   @param opts: list of additional options
163
164   """
165   args = [ 'ssh', '-oEscapeChar=none', '-oBatchMode=yes', '-l', 'root', '-t' ]
166
167   if strict:
168     tmp = 'yes'
169   else:
170     tmp = 'no'
171   args.append('-oStrictHostKeyChecking=%s' % tmp)
172   args.append('-oClearAllForwardings=yes')
173   args.append('-oForwardAgent=yes')
174   if opts:
175     args.extend(opts)
176   if node in _MULTIPLEXERS:
177     spath = _MULTIPLEXERS[node][0]
178     args.append('-oControlPath=%s' % spath)
179     args.append('-oControlMaster=no')
180   args.append(node)
181   if cmd:
182     args.append(cmd)
183
184   return args
185
186
187 def StartLocalCommand(cmd, **kwargs):
188   """Starts a local command.
189
190   """
191   print "Command: %s" % utils.ShellQuoteArgs(cmd)
192   return subprocess.Popen(cmd, shell=False, **kwargs)
193
194
195 def StartSSH(node, cmd, strict=True):
196   """Starts SSH.
197
198   """
199   return StartLocalCommand(GetSSHCommand(node, cmd, strict=strict))
200
201
202 def StartMultiplexer(node):
203   """Starts a multiplexer command.
204
205   @param node: the node for which to open the multiplexer
206
207   """
208   if node in _MULTIPLEXERS:
209     return
210
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)
218
219
220 def CloseMultiplexers():
221   """Closes all current multiplexers and cleans up.
222
223   """
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)
228
229
230 def GetCommandOutput(node, cmd):
231   """Returns the output of a command executed on the given node.
232
233   """
234   p = StartLocalCommand(GetSSHCommand(node, cmd), stdout=subprocess.PIPE)
235   AssertEqual(p.wait(), 0)
236   return p.stdout.read()
237
238
239 def UploadFile(node, src):
240   """Uploads a file to a node and returns the filename.
241
242   Caller needs to remove the returned file on the node when it's not needed
243   anymore.
244
245   """
246   # Make sure nobody else has access to it while preserving local permissions
247   mode = os.stat(src).st_mode & 0700
248
249   cmd = ('tmp=$(tempfile --mode %o --prefix gnt) && '
250          '[[ -f "${tmp}" ]] && '
251          'cat > "${tmp}" && '
252          'echo "${tmp}"') % mode
253
254   f = open(src, 'r')
255   try:
256     p = subprocess.Popen(GetSSHCommand(node, cmd), shell=False, stdin=f,
257                          stdout=subprocess.PIPE)
258     AssertEqual(p.wait(), 0)
259
260     # Return temporary filename
261     return p.stdout.read().strip()
262   finally:
263     f.close()
264
265
266 def UploadData(node, data, mode=0600, filename=None):
267   """Uploads data to a node and returns the filename.
268
269   Caller needs to remove the returned file on the node when it's not needed
270   anymore.
271
272   """
273   if filename:
274     tmp = "tmp=%s" % utils.ShellQuote(filename)
275   else:
276     tmp = "tmp=$(tempfile --mode %o --prefix gnt)" % mode
277   cmd = ("%s && "
278          "[[ -f \"${tmp}\" ]] && "
279          "cat > \"${tmp}\" && "
280          "echo \"${tmp}\"") % tmp
281
282   p = subprocess.Popen(GetSSHCommand(node, cmd), shell=False,
283                        stdin=subprocess.PIPE, stdout=subprocess.PIPE)
284   p.stdin.write(data)
285   p.stdin.close()
286   AssertEqual(p.wait(), 0)
287
288   # Return temporary filename
289   return p.stdout.read().strip()
290
291
292 def BackupFile(node, path):
293   """Creates a backup of a file on the node and returns the filename.
294
295   Caller needs to remove the returned file on the node when it's not needed
296   anymore.
297
298   """
299   cmd = ("tmp=$(tempfile --prefix .gnt --directory=$(dirname %s)) && "
300          "[[ -f \"$tmp\" ]] && "
301          "cp %s $tmp && "
302          "echo $tmp") % (utils.ShellQuote(path), utils.ShellQuote(path))
303
304   # Return temporary filename
305   return GetCommandOutput(node, cmd).strip()
306
307
308 def _ResolveName(cmd, key):
309   """Helper function.
310
311   """
312   master = qa_config.GetMasterNode()
313
314   output = GetCommandOutput(master['primary'], utils.ShellQuoteArgs(cmd))
315   for line in output.splitlines():
316     (lkey, lvalue) = line.split(':', 1)
317     if lkey == key:
318       return lvalue.lstrip()
319   raise KeyError("Key not found")
320
321
322 def ResolveInstanceName(instance):
323   """Gets the full name of an instance.
324
325   @type instance: string
326   @param instance: Instance name
327
328   """
329   return _ResolveName(['gnt-instance', 'info', instance],
330                       'Instance name')
331
332
333 def ResolveNodeName(node):
334   """Gets the full name of a node.
335
336   """
337   return _ResolveName(['gnt-node', 'info', node['primary']],
338                       'Node name')
339
340
341 def GetNodeInstances(node, secondaries=False):
342   """Gets a list of instances on a node.
343
344   """
345   master = qa_config.GetMasterNode()
346   node_name = ResolveNodeName(node)
347
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))
352
353   instances = []
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)
359
360   return instances
361
362
363 def _SelectQueryFields(rnd, fields):
364   """Generates a list of fields for query tests.
365
366   """
367   # Create copy for shuffling
368   fields = list(fields)
369   rnd.shuffle(fields)
370
371   # Check all fields
372   yield fields
373   yield sorted(fields)
374
375   # Duplicate fields
376   yield fields + fields
377
378   # Check small groups of fields
379   while fields:
380     yield [fields.pop() for _ in range(rnd.randint(2, 10)) if fields]
381
382
383 def _List(listcmd, fields, names):
384   """Runs a list command.
385
386   """
387   master = qa_config.GetMasterNode()
388
389   cmd = [listcmd, "list", "--separator=|", "--no-header",
390          "--output", ",".join(fields)]
391
392   if names:
393     cmd.extend(names)
394
395   return GetCommandOutput(master["primary"],
396                           utils.ShellQuoteArgs(cmd)).splitlines()
397
398
399 def GenericQueryTest(cmd, fields):
400   """Runs a number of tests on query commands.
401
402   @param cmd: Command name
403   @param fields: List of field names
404
405   """
406   rnd = random.Random(hash(cmd))
407
408   fields = list(fields)
409   rnd.shuffle(fields)
410
411   # Test a number of field combinations
412   for testfields in _SelectQueryFields(rnd, fields):
413     AssertCommand([cmd, "list", "--output", ",".join(testfields)])
414
415   namelist_fn = compat.partial(_List, cmd, ["name"])
416
417   # When no names were requested, the list must be sorted
418   names = namelist_fn(None)
419   AssertEqual(names, utils.NiceSort(names))
420
421   # When requesting specific names, the order must be kept
422   revnames = list(reversed(names))
423   AssertEqual(namelist_fn(revnames), revnames)
424
425   randnames = list(names)
426   rnd.shuffle(randnames)
427   AssertEqual(namelist_fn(randnames), randnames)
428
429   # Listing unknown items must fail
430   AssertCommand([cmd, "list", "this.name.certainly.does.not.exist"], fail=True)
431
432   # Check exit code for listing unknown field
433   AssertEqual(AssertCommand([cmd, "list", "--output=field/does/not/exist"],
434                             fail=True),
435               constants.EXIT_UNKNOWN_FIELD)
436
437
438 def GenericQueryFieldsTest(cmd, fields):
439   master = qa_config.GetMasterNode()
440
441   # Listing fields
442   AssertCommand([cmd, "list-fields"])
443   AssertCommand([cmd, "list-fields"] + fields)
444
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))
451
452   # Check exit code for listing unknown field
453   AssertEqual(AssertCommand([cmd, "list-fields", "field/does/not/exist"],
454                             fail=True),
455               constants.EXIT_UNKNOWN_FIELD)
456
457
458 def _FormatWithColor(text, seq):
459   if not seq:
460     return text
461   return "%s%s%s" % (seq, text, _RESET_SEQ)
462
463
464 FormatWarning = lambda text: _FormatWithColor(text, _WARNING_SEQ)
465 FormatError = lambda text: _FormatWithColor(text, _ERROR_SEQ)
466 FormatInfo = lambda text: _FormatWithColor(text, _INFO_SEQ)