Statistics
| Branch: | Tag: | Revision:

root / qa / qa_utils.py @ ff699aa9

History | View | Annotate | Download (11.9 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, tty=True):
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
  @type tty: Bool
164
  @param tty: If we should use tty
165

166
  """
167
  args = ["ssh", "-oEscapeChar=none", "-oBatchMode=yes", "-l", "root"]
168

    
169
  if tty:
170
    args.append("-t")
171

    
172
  if strict:
173
    tmp = 'yes'
174
  else:
175
    tmp = 'no'
176
  args.append('-oStrictHostKeyChecking=%s' % tmp)
177
  args.append('-oClearAllForwardings=yes')
178
  args.append('-oForwardAgent=yes')
179
  if opts:
180
    args.extend(opts)
181
  if node in _MULTIPLEXERS:
182
    spath = _MULTIPLEXERS[node][0]
183
    args.append('-oControlPath=%s' % spath)
184
    args.append('-oControlMaster=no')
185
  args.append(node)
186
  if cmd:
187
    args.append(cmd)
188

    
189
  return args
190

    
191

    
192
def StartLocalCommand(cmd, **kwargs):
193
  """Starts a local command.
194

195
  """
196
  print "Command: %s" % utils.ShellQuoteArgs(cmd)
197
  return subprocess.Popen(cmd, shell=False, **kwargs)
198

    
199

    
200
def StartSSH(node, cmd, strict=True):
201
  """Starts SSH.
202

203
  """
204
  return StartLocalCommand(GetSSHCommand(node, cmd, strict=strict))
205

    
206

    
207
def StartMultiplexer(node):
208
  """Starts a multiplexer command.
209

210
  @param node: the node for which to open the multiplexer
211

212
  """
213
  if node in _MULTIPLEXERS:
214
    return
215

    
216
  # Note: yes, we only need mktemp, since we'll remove the file anyway
217
  sname = tempfile.mktemp(prefix="ganeti-qa-multiplexer.")
218
  utils.RemoveFile(sname)
219
  opts = ["-N", "-oControlPath=%s" % sname, "-oControlMaster=yes"]
220
  print "Created socket at %s" % sname
221
  child = StartLocalCommand(GetSSHCommand(node, None, opts=opts))
222
  _MULTIPLEXERS[node] = (sname, child)
223

    
224

    
225
def CloseMultiplexers():
226
  """Closes all current multiplexers and cleans up.
227

228
  """
229
  for node in _MULTIPLEXERS.keys():
230
    (sname, child) = _MULTIPLEXERS.pop(node)
231
    utils.KillProcess(child.pid, timeout=10, waitpid=True)
232
    utils.RemoveFile(sname)
233

    
234

    
235
def GetCommandOutput(node, cmd, tty=True):
236
  """Returns the output of a command executed on the given node.
237

238
  """
239
  p = StartLocalCommand(GetSSHCommand(node, cmd, tty=tty),
240
                        stdout=subprocess.PIPE)
241
  AssertEqual(p.wait(), 0)
242
  return p.stdout.read()
243

    
244

    
245
def UploadFile(node, src):
246
  """Uploads a file to a node and returns the filename.
247

248
  Caller needs to remove the returned file on the node when it's not needed
249
  anymore.
250

251
  """
252
  # Make sure nobody else has access to it while preserving local permissions
253
  mode = os.stat(src).st_mode & 0700
254

    
255
  cmd = ('tmp=$(tempfile --mode %o --prefix gnt) && '
256
         '[[ -f "${tmp}" ]] && '
257
         'cat > "${tmp}" && '
258
         'echo "${tmp}"') % mode
259

    
260
  f = open(src, 'r')
261
  try:
262
    p = subprocess.Popen(GetSSHCommand(node, cmd), shell=False, stdin=f,
263
                         stdout=subprocess.PIPE)
264
    AssertEqual(p.wait(), 0)
265

    
266
    # Return temporary filename
267
    return p.stdout.read().strip()
268
  finally:
269
    f.close()
270

    
271

    
272
def UploadData(node, data, mode=0600, filename=None):
273
  """Uploads data to a node and returns the filename.
274

275
  Caller needs to remove the returned file on the node when it's not needed
276
  anymore.
277

278
  """
279
  if filename:
280
    tmp = "tmp=%s" % utils.ShellQuote(filename)
281
  else:
282
    tmp = "tmp=$(tempfile --mode %o --prefix gnt)" % mode
283
  cmd = ("%s && "
284
         "[[ -f \"${tmp}\" ]] && "
285
         "cat > \"${tmp}\" && "
286
         "echo \"${tmp}\"") % tmp
287

    
288
  p = subprocess.Popen(GetSSHCommand(node, cmd), shell=False,
289
                       stdin=subprocess.PIPE, stdout=subprocess.PIPE)
290
  p.stdin.write(data)
291
  p.stdin.close()
292
  AssertEqual(p.wait(), 0)
293

    
294
  # Return temporary filename
295
  return p.stdout.read().strip()
296

    
297

    
298
def BackupFile(node, path):
299
  """Creates a backup of a file on the node and returns the filename.
300

301
  Caller needs to remove the returned file on the node when it's not needed
302
  anymore.
303

304
  """
305
  cmd = ("tmp=$(tempfile --prefix .gnt --directory=$(dirname %s)) && "
306
         "[[ -f \"$tmp\" ]] && "
307
         "cp %s $tmp && "
308
         "echo $tmp") % (utils.ShellQuote(path), utils.ShellQuote(path))
309

    
310
  # Return temporary filename
311
  return GetCommandOutput(node, cmd).strip()
312

    
313

    
314
def _ResolveName(cmd, key):
315
  """Helper function.
316

317
  """
318
  master = qa_config.GetMasterNode()
319

    
320
  output = GetCommandOutput(master['primary'], utils.ShellQuoteArgs(cmd))
321
  for line in output.splitlines():
322
    (lkey, lvalue) = line.split(':', 1)
323
    if lkey == key:
324
      return lvalue.lstrip()
325
  raise KeyError("Key not found")
326

    
327

    
328
def ResolveInstanceName(instance):
329
  """Gets the full name of an instance.
330

331
  @type instance: string
332
  @param instance: Instance name
333

334
  """
335
  return _ResolveName(['gnt-instance', 'info', instance],
336
                      'Instance name')
337

    
338

    
339
def ResolveNodeName(node):
340
  """Gets the full name of a node.
341

342
  """
343
  return _ResolveName(['gnt-node', 'info', node['primary']],
344
                      'Node name')
345

    
346

    
347
def GetNodeInstances(node, secondaries=False):
348
  """Gets a list of instances on a node.
349

350
  """
351
  master = qa_config.GetMasterNode()
352
  node_name = ResolveNodeName(node)
353

    
354
  # Get list of all instances
355
  cmd = ['gnt-instance', 'list', '--separator=:', '--no-headers',
356
         '--output=name,pnode,snodes']
357
  output = GetCommandOutput(master['primary'], utils.ShellQuoteArgs(cmd))
358

    
359
  instances = []
360
  for line in output.splitlines():
361
    (name, pnode, snodes) = line.split(':', 2)
362
    if ((not secondaries and pnode == node_name) or
363
        (secondaries and node_name in snodes.split(','))):
364
      instances.append(name)
365

    
366
  return instances
367

    
368

    
369
def _SelectQueryFields(rnd, fields):
370
  """Generates a list of fields for query tests.
371

372
  """
373
  # Create copy for shuffling
374
  fields = list(fields)
375
  rnd.shuffle(fields)
376

    
377
  # Check all fields
378
  yield fields
379
  yield sorted(fields)
380

    
381
  # Duplicate fields
382
  yield fields + fields
383

    
384
  # Check small groups of fields
385
  while fields:
386
    yield [fields.pop() for _ in range(rnd.randint(2, 10)) if fields]
387

    
388

    
389
def _List(listcmd, fields, names):
390
  """Runs a list command.
391

392
  """
393
  master = qa_config.GetMasterNode()
394

    
395
  cmd = [listcmd, "list", "--separator=|", "--no-header",
396
         "--output", ",".join(fields)]
397

    
398
  if names:
399
    cmd.extend(names)
400

    
401
  return GetCommandOutput(master["primary"],
402
                          utils.ShellQuoteArgs(cmd)).splitlines()
403

    
404

    
405
def GenericQueryTest(cmd, fields):
406
  """Runs a number of tests on query commands.
407

408
  @param cmd: Command name
409
  @param fields: List of field names
410

411
  """
412
  rnd = random.Random(hash(cmd))
413

    
414
  fields = list(fields)
415
  rnd.shuffle(fields)
416

    
417
  # Test a number of field combinations
418
  for testfields in _SelectQueryFields(rnd, fields):
419
    AssertCommand([cmd, "list", "--output", ",".join(testfields)])
420

    
421
  namelist_fn = compat.partial(_List, cmd, ["name"])
422

    
423
  # When no names were requested, the list must be sorted
424
  names = namelist_fn(None)
425
  AssertEqual(names, utils.NiceSort(names))
426

    
427
  # When requesting specific names, the order must be kept
428
  revnames = list(reversed(names))
429
  AssertEqual(namelist_fn(revnames), revnames)
430

    
431
  randnames = list(names)
432
  rnd.shuffle(randnames)
433
  AssertEqual(namelist_fn(randnames), randnames)
434

    
435
  # Listing unknown items must fail
436
  AssertCommand([cmd, "list", "this.name.certainly.does.not.exist"], fail=True)
437

    
438
  # Check exit code for listing unknown field
439
  AssertEqual(AssertCommand([cmd, "list", "--output=field/does/not/exist"],
440
                            fail=True),
441
              constants.EXIT_UNKNOWN_FIELD)
442

    
443

    
444
def GenericQueryFieldsTest(cmd, fields):
445
  master = qa_config.GetMasterNode()
446

    
447
  # Listing fields
448
  AssertCommand([cmd, "list-fields"])
449
  AssertCommand([cmd, "list-fields"] + fields)
450

    
451
  # Check listed fields (all, must be sorted)
452
  realcmd = [cmd, "list-fields", "--separator=|", "--no-headers"]
453
  output = GetCommandOutput(master["primary"],
454
                            utils.ShellQuoteArgs(realcmd)).splitlines()
455
  AssertEqual([line.split("|", 1)[0] for line in output],
456
              utils.NiceSort(fields))
457

    
458
  # Check exit code for listing unknown field
459
  AssertEqual(AssertCommand([cmd, "list-fields", "field/does/not/exist"],
460
                            fail=True),
461
              constants.EXIT_UNKNOWN_FIELD)
462

    
463

    
464
def _FormatWithColor(text, seq):
465
  if not seq:
466
    return text
467
  return "%s%s%s" % (seq, text, _RESET_SEQ)
468

    
469

    
470
FormatWarning = lambda text: _FormatWithColor(text, _WARNING_SEQ)
471
FormatError = lambda text: _FormatWithColor(text, _ERROR_SEQ)
472
FormatInfo = lambda text: _FormatWithColor(text, _INFO_SEQ)