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