Statistics
| Branch: | Tag: | Revision:

root / qa / qa_utils.py @ f7e6f3c8

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
  global _INFO_SEQ, _WARNING_SEQ, _ERROR_SEQ, _RESET_SEQ
54

    
55
  # Don't use colours if stdout isn't a terminal
56
  if not sys.stdout.isatty():
57
    return
58

    
59
  try:
60
    import curses
61
  except ImportError:
62
    # Don't use colours if curses module can't be imported
63
    return
64

    
65
  curses.setupterm()
66

    
67
  _RESET_SEQ = curses.tigetstr("op")
68

    
69
  setaf = curses.tigetstr("setaf")
70
  _INFO_SEQ = curses.tparm(setaf, curses.COLOR_GREEN)
71
  _WARNING_SEQ = curses.tparm(setaf, curses.COLOR_YELLOW)
72
  _ERROR_SEQ = curses.tparm(setaf, curses.COLOR_RED)
73

    
74

    
75
_SetupColours()
76

    
77

    
78
def AssertIn(item, sequence):
79
  """Raises an error when item is not in sequence.
80

81
  """
82
  if item not in sequence:
83
    raise qa_error.Error('%r not in %r' % (item, sequence))
84

    
85

    
86
def AssertEqual(first, second):
87
  """Raises an error when values aren't equal.
88

89
  """
90
  if not first == second:
91
    raise qa_error.Error('%r == %r' % (first, second))
92

    
93

    
94
def AssertNotEqual(first, second):
95
  """Raises an error when values are equal.
96

97
  """
98
  if not first != second:
99
    raise qa_error.Error('%r != %r' % (first, second))
100

    
101

    
102
def AssertMatch(string, pattern):
103
  """Raises an error when string doesn't match regexp pattern.
104

105
  """
106
  if not re.match(pattern, string):
107
    raise qa_error.Error("%r doesn't match /%r/" % (string, pattern))
108

    
109

    
110
def AssertCommand(cmd, fail=False, node=None):
111
  """Checks that a remote command succeeds.
112

113
  @param cmd: either a string (the command to execute) or a list (to
114
      be converted using L{utils.ShellQuoteArgs} into a string)
115
  @type fail: boolean
116
  @param fail: if the command is expected to fail instead of succeeding
117
  @param node: if passed, it should be the node on which the command
118
      should be executed, instead of the master node (can be either a
119
      dict or a string)
120

121
  """
122
  if node is None:
123
    node = qa_config.GetMasterNode()
124

    
125
  if isinstance(node, basestring):
126
    nodename = node
127
  else:
128
    nodename = node["primary"]
129

    
130
  if isinstance(cmd, basestring):
131
    cmdstr = cmd
132
  else:
133
    cmdstr = utils.ShellQuoteArgs(cmd)
134

    
135
  rcode = StartSSH(nodename, cmdstr).wait()
136

    
137
  if fail:
138
    if rcode == 0:
139
      raise qa_error.Error("Command '%s' on node %s was expected to fail but"
140
                           " didn't" % (cmdstr, nodename))
141
  else:
142
    if rcode != 0:
143
      raise qa_error.Error("Command '%s' on node %s failed, exit code %s" %
144
                           (cmdstr, nodename, rcode))
145

    
146
  return rcode
147

    
148

    
149
def GetSSHCommand(node, cmd, strict=True, opts=None):
150
  """Builds SSH command to be executed.
151

152
  @type node: string
153
  @param node: node the command should run on
154
  @type cmd: string
155
  @param cmd: command to be executed in the node; if None or empty
156
      string, no command will be executed
157
  @type strict: boolean
158
  @param strict: whether to enable strict host key checking
159
  @type opts: list
160
  @param opts: list of additional options
161

162
  """
163
  args = [ 'ssh', '-oEscapeChar=none', '-oBatchMode=yes', '-l', 'root', '-t' ]
164

    
165
  if strict:
166
    tmp = 'yes'
167
  else:
168
    tmp = 'no'
169
  args.append('-oStrictHostKeyChecking=%s' % tmp)
170
  args.append('-oClearAllForwardings=yes')
171
  args.append('-oForwardAgent=yes')
172
  if opts:
173
    args.extend(opts)
174
  if node in _MULTIPLEXERS:
175
    spath = _MULTIPLEXERS[node][0]
176
    args.append('-oControlPath=%s' % spath)
177
    args.append('-oControlMaster=no')
178
  args.append(node)
179
  if cmd:
180
    args.append(cmd)
181

    
182
  return args
183

    
184

    
185
def StartLocalCommand(cmd, **kwargs):
186
  """Starts a local command.
187

188
  """
189
  print "Command: %s" % utils.ShellQuoteArgs(cmd)
190
  return subprocess.Popen(cmd, shell=False, **kwargs)
191

    
192

    
193
def StartSSH(node, cmd, strict=True):
194
  """Starts SSH.
195

196
  """
197
  return StartLocalCommand(GetSSHCommand(node, cmd, strict=strict))
198

    
199

    
200
def StartMultiplexer(node):
201
  """Starts a multiplexer command.
202

203
  @param node: the node for which to open the multiplexer
204

205
  """
206
  if node in _MULTIPLEXERS:
207
    return
208

    
209
  # Note: yes, we only need mktemp, since we'll remove the file anyway
210
  sname = tempfile.mktemp(prefix="ganeti-qa-multiplexer.")
211
  utils.RemoveFile(sname)
212
  opts = ["-N", "-oControlPath=%s" % sname, "-oControlMaster=yes"]
213
  print "Created socket at %s" % sname
214
  child = StartLocalCommand(GetSSHCommand(node, None, opts=opts))
215
  _MULTIPLEXERS[node] = (sname, child)
216

    
217

    
218
def CloseMultiplexers():
219
  """Closes all current multiplexers and cleans up.
220

221
  """
222
  for node in _MULTIPLEXERS.keys():
223
    (sname, child) = _MULTIPLEXERS.pop(node)
224
    utils.KillProcess(child.pid, timeout=10, waitpid=True)
225
    utils.RemoveFile(sname)
226

    
227

    
228
def GetCommandOutput(node, cmd):
229
  """Returns the output of a command executed on the given node.
230

231
  """
232
  p = StartLocalCommand(GetSSHCommand(node, cmd), stdout=subprocess.PIPE)
233
  AssertEqual(p.wait(), 0)
234
  return p.stdout.read()
235

    
236

    
237
def UploadFile(node, src):
238
  """Uploads a file to a node and returns the filename.
239

240
  Caller needs to remove the returned file on the node when it's not needed
241
  anymore.
242

243
  """
244
  # Make sure nobody else has access to it while preserving local permissions
245
  mode = os.stat(src).st_mode & 0700
246

    
247
  cmd = ('tmp=$(tempfile --mode %o --prefix gnt) && '
248
         '[[ -f "${tmp}" ]] && '
249
         'cat > "${tmp}" && '
250
         'echo "${tmp}"') % mode
251

    
252
  f = open(src, 'r')
253
  try:
254
    p = subprocess.Popen(GetSSHCommand(node, cmd), shell=False, stdin=f,
255
                         stdout=subprocess.PIPE)
256
    AssertEqual(p.wait(), 0)
257

    
258
    # Return temporary filename
259
    return p.stdout.read().strip()
260
  finally:
261
    f.close()
262

    
263

    
264
def UploadData(node, data, mode=0600, filename=None):
265
  """Uploads data to a node and returns the filename.
266

267
  Caller needs to remove the returned file on the node when it's not needed
268
  anymore.
269

270
  """
271
  if filename:
272
    tmp = "tmp=%s" % utils.ShellQuote(filename)
273
  else:
274
    tmp = "tmp=$(tempfile --mode %o --prefix gnt)" % mode
275
  cmd = ("%s && "
276
         "[[ -f \"${tmp}\" ]] && "
277
         "cat > \"${tmp}\" && "
278
         "echo \"${tmp}\"") % tmp
279

    
280
  p = subprocess.Popen(GetSSHCommand(node, cmd), shell=False,
281
                       stdin=subprocess.PIPE, stdout=subprocess.PIPE)
282
  p.stdin.write(data)
283
  p.stdin.close()
284
  AssertEqual(p.wait(), 0)
285

    
286
  # Return temporary filename
287
  return p.stdout.read().strip()
288

    
289

    
290
def BackupFile(node, path):
291
  """Creates a backup of a file on the node and returns the filename.
292

293
  Caller needs to remove the returned file on the node when it's not needed
294
  anymore.
295

296
  """
297
  cmd = ("tmp=$(tempfile --prefix .gnt --directory=$(dirname %s)) && "
298
         "[[ -f \"$tmp\" ]] && "
299
         "cp %s $tmp && "
300
         "echo $tmp") % (utils.ShellQuote(path), utils.ShellQuote(path))
301

    
302
  # Return temporary filename
303
  return GetCommandOutput(node, cmd).strip()
304

    
305

    
306
def _ResolveName(cmd, key):
307
  """Helper function.
308

309
  """
310
  master = qa_config.GetMasterNode()
311

    
312
  output = GetCommandOutput(master['primary'], utils.ShellQuoteArgs(cmd))
313
  for line in output.splitlines():
314
    (lkey, lvalue) = line.split(':', 1)
315
    if lkey == key:
316
      return lvalue.lstrip()
317
  raise KeyError("Key not found")
318

    
319

    
320
def ResolveInstanceName(instance):
321
  """Gets the full name of an instance.
322

323
  @type instance: string
324
  @param instance: Instance name
325

326
  """
327
  return _ResolveName(['gnt-instance', 'info', instance],
328
                      'Instance name')
329

    
330

    
331
def ResolveNodeName(node):
332
  """Gets the full name of a node.
333

334
  """
335
  return _ResolveName(['gnt-node', 'info', node['primary']],
336
                      'Node name')
337

    
338

    
339
def GetNodeInstances(node, secondaries=False):
340
  """Gets a list of instances on a node.
341

342
  """
343
  master = qa_config.GetMasterNode()
344
  node_name = ResolveNodeName(node)
345

    
346
  # Get list of all instances
347
  cmd = ['gnt-instance', 'list', '--separator=:', '--no-headers',
348
         '--output=name,pnode,snodes']
349
  output = GetCommandOutput(master['primary'], utils.ShellQuoteArgs(cmd))
350

    
351
  instances = []
352
  for line in output.splitlines():
353
    (name, pnode, snodes) = line.split(':', 2)
354
    if ((not secondaries and pnode == node_name) or
355
        (secondaries and node_name in snodes.split(','))):
356
      instances.append(name)
357

    
358
  return instances
359

    
360

    
361
def _SelectQueryFields(rnd, fields):
362
  """Generates a list of fields for query tests.
363

364
  """
365
  # Create copy for shuffling
366
  fields = list(fields)
367
  rnd.shuffle(fields)
368

    
369
  # Check all fields
370
  yield fields
371
  yield sorted(fields)
372

    
373
  # Duplicate fields
374
  yield fields + fields
375

    
376
  # Check small groups of fields
377
  while fields:
378
    yield [fields.pop() for _ in range(rnd.randint(2, 10)) if fields]
379

    
380

    
381
def _List(listcmd, fields, names):
382
  """Runs a list command.
383

384
  """
385
  master = qa_config.GetMasterNode()
386

    
387
  cmd = [listcmd, "list", "--separator=|", "--no-header",
388
         "--output", ",".join(fields)]
389

    
390
  if names:
391
    cmd.extend(names)
392

    
393
  return GetCommandOutput(master["primary"],
394
                          utils.ShellQuoteArgs(cmd)).splitlines()
395

    
396

    
397
def GenericQueryTest(cmd, fields):
398
  """Runs a number of tests on query commands.
399

400
  @param cmd: Command name
401
  @param fields: List of field names
402

403
  """
404
  rnd = random.Random(hash(cmd))
405

    
406
  randfields = list(fields)
407
  rnd.shuffle(fields)
408

    
409
  # Test a number of field combinations
410
  for testfields in _SelectQueryFields(rnd, fields):
411
    AssertCommand([cmd, "list", "--output", ",".join(testfields)])
412

    
413
  namelist_fn = compat.partial(_List, cmd, ["name"])
414

    
415
  # When no names were requested, the list must be sorted
416
  names = namelist_fn(None)
417
  AssertEqual(names, utils.NiceSort(names))
418

    
419
  # When requesting specific names, the order must be kept
420
  revnames = list(reversed(names))
421
  AssertEqual(namelist_fn(revnames), revnames)
422

    
423
  randnames = list(names)
424
  rnd.shuffle(randnames)
425
  AssertEqual(namelist_fn(randnames), randnames)
426

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

    
430
  # Check exit code for listing unknown field
431
  AssertEqual(AssertCommand([cmd, "list", "--output=field/does/not/exist"],
432
                            fail=True),
433
              constants.EXIT_UNKNOWN_FIELD)
434

    
435

    
436
def GenericQueryFieldsTest(cmd, fields):
437
  master = qa_config.GetMasterNode()
438

    
439
  # Listing fields
440
  AssertCommand([cmd, "list-fields"])
441
  AssertCommand([cmd, "list-fields"] + fields)
442

    
443
  # Check listed fields (all, must be sorted)
444
  realcmd = [cmd, "list-fields", "--separator=|", "--no-headers"]
445
  output = GetCommandOutput(master["primary"],
446
                            utils.ShellQuoteArgs(realcmd)).splitlines()
447
  AssertEqual([line.split("|", 1)[0] for line in output],
448
              utils.NiceSort(fields))
449

    
450
  # Check exit code for listing unknown field
451
  AssertEqual(AssertCommand([cmd, "list-fields", "field/does/not/exist"],
452
                            fail=True),
453
              constants.EXIT_UNKNOWN_FIELD)
454

    
455

    
456
def _FormatWithColor(text, seq):
457
  if not seq:
458
    return text
459
  return "%s%s%s" % (seq, text, _RESET_SEQ)
460

    
461

    
462
FormatWarning = lambda text: _FormatWithColor(text, _WARNING_SEQ)
463
FormatError = lambda text: _FormatWithColor(text, _ERROR_SEQ)
464
FormatInfo = lambda text: _FormatWithColor(text, _INFO_SEQ)