QA: Allow upload of string data
[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   Args:
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
153
154   """
155   args = [ 'ssh', '-oEscapeChar=none', '-oBatchMode=yes', '-l', 'root', '-t' ]
156
157   if strict:
158     tmp = 'yes'
159   else:
160     tmp = 'no'
161   args.append('-oStrictHostKeyChecking=%s' % tmp)
162   args.append('-oClearAllForwardings=yes')
163   args.append('-oForwardAgent=yes')
164   args.append(node)
165   args.append(cmd)
166
167   return args
168
169
170 def StartLocalCommand(cmd, **kwargs):
171   """Starts a local command.
172
173   """
174   print "Command: %s" % utils.ShellQuoteArgs(cmd)
175   return subprocess.Popen(cmd, shell=False, **kwargs)
176
177
178 def StartSSH(node, cmd, strict=True):
179   """Starts SSH.
180
181   """
182   return StartLocalCommand(GetSSHCommand(node, cmd, strict=strict))
183
184
185 def GetCommandOutput(node, cmd):
186   """Returns the output of a command executed on the given node.
187
188   """
189   p = StartLocalCommand(GetSSHCommand(node, cmd), stdout=subprocess.PIPE)
190   AssertEqual(p.wait(), 0)
191   return p.stdout.read()
192
193
194 def UploadFile(node, src):
195   """Uploads a file to a node and returns the filename.
196
197   Caller needs to remove the returned file on the node when it's not needed
198   anymore.
199
200   """
201   # Make sure nobody else has access to it while preserving local permissions
202   mode = os.stat(src).st_mode & 0700
203
204   cmd = ('tmp=$(tempfile --mode %o --prefix gnt) && '
205          '[[ -f "${tmp}" ]] && '
206          'cat > "${tmp}" && '
207          'echo "${tmp}"') % mode
208
209   f = open(src, 'r')
210   try:
211     p = subprocess.Popen(GetSSHCommand(node, cmd), shell=False, stdin=f,
212                          stdout=subprocess.PIPE)
213     AssertEqual(p.wait(), 0)
214
215     # Return temporary filename
216     return p.stdout.read().strip()
217   finally:
218     f.close()
219
220
221 def UploadData(node, data, mode=0600, filename=None):
222   """Uploads data to a node and returns the filename.
223
224   Caller needs to remove the returned file on the node when it's not needed
225   anymore.
226
227   """
228   if filename:
229     tmp = "tmp=%s" % utils.ShellQuote(filename)
230   else:
231     tmp = "tmp=$(tempfile --mode %o --prefix gnt)" % mode
232   cmd = ("%s && "
233          "[[ -f \"${tmp}\" ]] && "
234          "cat > \"${tmp}\" && "
235          "echo \"${tmp}\"") % tmp
236
237   p = subprocess.Popen(GetSSHCommand(node, cmd), shell=False,
238                        stdin=subprocess.PIPE, stdout=subprocess.PIPE)
239   p.stdin.write(data)
240   p.stdin.close()
241   AssertEqual(p.wait(), 0)
242
243   # Return temporary filename
244   return p.stdout.read().strip()
245
246
247 def BackupFile(node, path):
248   """Creates a backup of a file on the node and returns the filename.
249
250   Caller needs to remove the returned file on the node when it's not needed
251   anymore.
252
253   """
254   cmd = ("tmp=$(tempfile --prefix .gnt --directory=$(dirname %s)) && "
255          "[[ -f \"$tmp\" ]] && "
256          "cp %s $tmp && "
257          "echo $tmp") % (utils.ShellQuote(path), utils.ShellQuote(path))
258
259   # Return temporary filename
260   return GetCommandOutput(node, cmd).strip()
261
262
263 def _ResolveName(cmd, key):
264   """Helper function.
265
266   """
267   master = qa_config.GetMasterNode()
268
269   output = GetCommandOutput(master['primary'], utils.ShellQuoteArgs(cmd))
270   for line in output.splitlines():
271     (lkey, lvalue) = line.split(':', 1)
272     if lkey == key:
273       return lvalue.lstrip()
274   raise KeyError("Key not found")
275
276
277 def ResolveInstanceName(instance):
278   """Gets the full name of an instance.
279
280   @type instance: string
281   @param instance: Instance name
282
283   """
284   return _ResolveName(['gnt-instance', 'info', instance],
285                       'Instance name')
286
287
288 def ResolveNodeName(node):
289   """Gets the full name of a node.
290
291   """
292   return _ResolveName(['gnt-node', 'info', node['primary']],
293                       'Node name')
294
295
296 def GetNodeInstances(node, secondaries=False):
297   """Gets a list of instances on a node.
298
299   """
300   master = qa_config.GetMasterNode()
301   node_name = ResolveNodeName(node)
302
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))
307
308   instances = []
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)
314
315   return instances
316
317
318 def _SelectQueryFields(rnd, fields):
319   """Generates a list of fields for query tests.
320
321   """
322   # Create copy for shuffling
323   fields = list(fields)
324   rnd.shuffle(fields)
325
326   # Check all fields
327   yield fields
328   yield sorted(fields)
329
330   # Duplicate fields
331   yield fields + fields
332
333   # Check small groups of fields
334   while fields:
335     yield [fields.pop() for _ in range(rnd.randint(2, 10)) if fields]
336
337
338 def _List(listcmd, fields, names):
339   """Runs a list command.
340
341   """
342   master = qa_config.GetMasterNode()
343
344   cmd = [listcmd, "list", "--separator=|", "--no-header",
345          "--output", ",".join(fields)]
346
347   if names:
348     cmd.extend(names)
349
350   return GetCommandOutput(master["primary"],
351                           utils.ShellQuoteArgs(cmd)).splitlines()
352
353
354 def GenericQueryTest(cmd, fields):
355   """Runs a number of tests on query commands.
356
357   @param cmd: Command name
358   @param fields: List of field names
359
360   """
361   rnd = random.Random(hash(cmd))
362
363   randfields = list(fields)
364   rnd.shuffle(fields)
365
366   # Test a number of field combinations
367   for testfields in _SelectQueryFields(rnd, fields):
368     AssertCommand([cmd, "list", "--output", ",".join(testfields)])
369
370   namelist_fn = compat.partial(_List, cmd, ["name"])
371
372   # When no names were requested, the list must be sorted
373   names = namelist_fn(None)
374   AssertEqual(names, utils.NiceSort(names))
375
376   # When requesting specific names, the order must be kept
377   revnames = list(reversed(names))
378   AssertEqual(namelist_fn(revnames), revnames)
379
380   randnames = list(names)
381   rnd.shuffle(randnames)
382   AssertEqual(namelist_fn(randnames), randnames)
383
384   # Listing unknown items must fail
385   AssertCommand([cmd, "list", "this.name.certainly.does.not.exist"], fail=True)
386
387   # Check exit code for listing unknown field
388   AssertEqual(AssertCommand([cmd, "list", "--output=field/does/not/exist"],
389                             fail=True),
390               constants.EXIT_UNKNOWN_FIELD)
391
392
393 def GenericQueryFieldsTest(cmd, fields):
394   master = qa_config.GetMasterNode()
395
396   # Listing fields
397   AssertCommand([cmd, "list-fields"])
398   AssertCommand([cmd, "list-fields"] + fields)
399
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],
405               sorted(fields))
406
407   # Check exit code for listing unknown field
408   AssertEqual(AssertCommand([cmd, "list-fields", "field/does/not/exist"],
409                             fail=True),
410               constants.EXIT_UNKNOWN_FIELD)
411
412
413 def _FormatWithColor(text, seq):
414   if not seq:
415     return text
416   return "%s%s%s" % (seq, text, _RESET_SEQ)
417
418
419 FormatWarning = lambda text: _FormatWithColor(text, _WARNING_SEQ)
420 FormatError = lambda text: _FormatWithColor(text, _ERROR_SEQ)
421 FormatInfo = lambda text: _FormatWithColor(text, _INFO_SEQ)