Statistics
| Branch: | Tag: | Revision:

root / qa / qa_utils.py @ 931413f6

History | View | Annotate | Download (11.8 kB)

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)