Statistics
| Branch: | Tag: | Revision:

root / qa / qa_utils.py @ b9955569

History | View | Annotate | Download (10.6 kB)

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)