Statistics
| Branch: | Tag: | Revision:

root / qa / qa_utils.py @ 2214cf14

History | View | Annotate | Download (9.9 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 BackupFile(node, path):
222
  """Creates a backup of a file on the 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
  cmd = ("tmp=$(tempfile --prefix .gnt --directory=$(dirname %s)) && "
229
         "[[ -f \"$tmp\" ]] && "
230
         "cp %s $tmp && "
231
         "echo $tmp") % (utils.ShellQuote(path), utils.ShellQuote(path))
232

    
233
  # Return temporary filename
234
  return GetCommandOutput(node, cmd).strip()
235

    
236

    
237
def _ResolveName(cmd, key):
238
  """Helper function.
239

240
  """
241
  master = qa_config.GetMasterNode()
242

    
243
  output = GetCommandOutput(master['primary'], utils.ShellQuoteArgs(cmd))
244
  for line in output.splitlines():
245
    (lkey, lvalue) = line.split(':', 1)
246
    if lkey == key:
247
      return lvalue.lstrip()
248
  raise KeyError("Key not found")
249

    
250

    
251
def ResolveInstanceName(instance):
252
  """Gets the full name of an instance.
253

254
  @type instance: string
255
  @param instance: Instance name
256

257
  """
258
  return _ResolveName(['gnt-instance', 'info', instance],
259
                      'Instance name')
260

    
261

    
262
def ResolveNodeName(node):
263
  """Gets the full name of a node.
264

265
  """
266
  return _ResolveName(['gnt-node', 'info', node['primary']],
267
                      'Node name')
268

    
269

    
270
def GetNodeInstances(node, secondaries=False):
271
  """Gets a list of instances on a node.
272

273
  """
274
  master = qa_config.GetMasterNode()
275
  node_name = ResolveNodeName(node)
276

    
277
  # Get list of all instances
278
  cmd = ['gnt-instance', 'list', '--separator=:', '--no-headers',
279
         '--output=name,pnode,snodes']
280
  output = GetCommandOutput(master['primary'], utils.ShellQuoteArgs(cmd))
281

    
282
  instances = []
283
  for line in output.splitlines():
284
    (name, pnode, snodes) = line.split(':', 2)
285
    if ((not secondaries and pnode == node_name) or
286
        (secondaries and node_name in snodes.split(','))):
287
      instances.append(name)
288

    
289
  return instances
290

    
291

    
292
def _SelectQueryFields(rnd, fields):
293
  """Generates a list of fields for query tests.
294

295
  """
296
  # Create copy for shuffling
297
  fields = list(fields)
298
  rnd.shuffle(fields)
299

    
300
  # Check all fields
301
  yield fields
302
  yield sorted(fields)
303

    
304
  # Duplicate fields
305
  yield fields + fields
306

    
307
  # Check small groups of fields
308
  while fields:
309
    yield [fields.pop() for _ in range(rnd.randint(2, 10)) if fields]
310

    
311

    
312
def _List(listcmd, fields, names):
313
  """Runs a list command.
314

315
  """
316
  master = qa_config.GetMasterNode()
317

    
318
  cmd = [listcmd, "list", "--separator=|", "--no-header",
319
         "--output", ",".join(fields)]
320

    
321
  if names:
322
    cmd.extend(names)
323

    
324
  return GetCommandOutput(master["primary"],
325
                          utils.ShellQuoteArgs(cmd)).splitlines()
326

    
327

    
328
def GenericQueryTest(cmd, fields):
329
  """Runs a number of tests on query commands.
330

331
  @param cmd: Command name
332
  @param fields: List of field names
333

334
  """
335
  rnd = random.Random(hash(cmd))
336

    
337
  randfields = list(fields)
338
  rnd.shuffle(fields)
339

    
340
  # Test a number of field combinations
341
  for testfields in _SelectQueryFields(rnd, fields):
342
    AssertCommand([cmd, "list", "--output", ",".join(testfields)])
343

    
344
  namelist_fn = compat.partial(_List, cmd, ["name"])
345

    
346
  # When no names were requested, the list must be sorted
347
  names = namelist_fn(None)
348
  AssertEqual(names, utils.NiceSort(names))
349

    
350
  # When requesting specific names, the order must be kept
351
  revnames = list(reversed(names))
352
  AssertEqual(namelist_fn(revnames), revnames)
353

    
354
  randnames = list(names)
355
  rnd.shuffle(randnames)
356
  AssertEqual(namelist_fn(randnames), randnames)
357

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

    
361
  # Check exit code for listing unknown field
362
  AssertEqual(AssertCommand([cmd, "list", "--output=field/does/not/exist"],
363
                            fail=True),
364
              constants.EXIT_UNKNOWN_FIELD)
365

    
366

    
367
def GenericQueryFieldsTest(cmd, fields):
368
  master = qa_config.GetMasterNode()
369

    
370
  # Listing fields
371
  AssertCommand([cmd, "list-fields"])
372
  AssertCommand([cmd, "list-fields"] + fields)
373

    
374
  # Check listed fields (all, must be sorted)
375
  realcmd = [cmd, "list-fields", "--separator=|", "--no-headers"]
376
  output = GetCommandOutput(master["primary"],
377
                            utils.ShellQuoteArgs(realcmd)).splitlines()
378
  AssertEqual([line.split("|", 1)[0] for line in output],
379
              sorted(fields))
380

    
381
  # Check exit code for listing unknown field
382
  AssertEqual(AssertCommand([cmd, "list-fields", "field/does/not/exist"],
383
                            fail=True),
384
              constants.EXIT_UNKNOWN_FIELD)
385

    
386

    
387
def _FormatWithColor(text, seq):
388
  if not seq:
389
    return text
390
  return "%s%s%s" % (seq, text, _RESET_SEQ)
391

    
392

    
393
FormatWarning = lambda text: _FormatWithColor(text, _WARNING_SEQ)
394
FormatError = lambda text: _FormatWithColor(text, _ERROR_SEQ)
395
FormatInfo = lambda text: _FormatWithColor(text, _INFO_SEQ)