Statistics
| Branch: | Tag: | Revision:

root / qa / qa_utils.py @ 288d6440

History | View | Annotate | Download (8.8 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

    
35
import qa_config
36
import qa_error
37

    
38

    
39
_INFO_SEQ = None
40
_WARNING_SEQ = None
41
_ERROR_SEQ = None
42
_RESET_SEQ = None
43

    
44

    
45
def _SetupColours():
46
  """Initializes the colour constants.
47

48
  """
49
  global _INFO_SEQ, _WARNING_SEQ, _ERROR_SEQ, _RESET_SEQ
50

    
51
  # Don't use colours if stdout isn't a terminal
52
  if not sys.stdout.isatty():
53
    return
54

    
55
  try:
56
    import curses
57
  except ImportError:
58
    # Don't use colours if curses module can't be imported
59
    return
60

    
61
  curses.setupterm()
62

    
63
  _RESET_SEQ = curses.tigetstr("op")
64

    
65
  setaf = curses.tigetstr("setaf")
66
  _INFO_SEQ = curses.tparm(setaf, curses.COLOR_GREEN)
67
  _WARNING_SEQ = curses.tparm(setaf, curses.COLOR_YELLOW)
68
  _ERROR_SEQ = curses.tparm(setaf, curses.COLOR_RED)
69

    
70

    
71
_SetupColours()
72

    
73

    
74
def AssertIn(item, sequence):
75
  """Raises an error when item is not in sequence.
76

77
  """
78
  if item not in sequence:
79
    raise qa_error.Error('%r not in %r' % (item, sequence))
80

    
81

    
82
def AssertEqual(first, second):
83
  """Raises an error when values aren't equal.
84

85
  """
86
  if not first == second:
87
    raise qa_error.Error('%r == %r' % (first, second))
88

    
89

    
90
def AssertNotEqual(first, second):
91
  """Raises an error when values are equal.
92

93
  """
94
  if not first != second:
95
    raise qa_error.Error('%r != %r' % (first, second))
96

    
97

    
98
def AssertMatch(string, pattern):
99
  """Raises an error when string doesn't match regexp pattern.
100

101
  """
102
  if not re.match(pattern, string):
103
    raise qa_error.Error("%r doesn't match /%r/" % (string, pattern))
104

    
105

    
106
def AssertCommand(cmd, fail=False, node=None):
107
  """Checks that a remote command succeeds.
108

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

117
  """
118
  if node is None:
119
    node = qa_config.GetMasterNode()
120

    
121
  if isinstance(node, basestring):
122
    nodename = node
123
  else:
124
    nodename = node["primary"]
125

    
126
  if isinstance(cmd, basestring):
127
    cmdstr = cmd
128
  else:
129
    cmdstr = utils.ShellQuoteArgs(cmd)
130

    
131
  rcode = StartSSH(nodename, cmdstr).wait()
132

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

    
142

    
143
def GetSSHCommand(node, cmd, strict=True):
144
  """Builds SSH command to be executed.
145

146
  Args:
147
  - node: Node the command should run on
148
  - cmd: Command to be executed as a list with all parameters
149
  - strict: Whether to enable strict host key checking
150

151
  """
152
  args = [ 'ssh', '-oEscapeChar=none', '-oBatchMode=yes', '-l', 'root', '-t' ]
153

    
154
  if strict:
155
    tmp = 'yes'
156
  else:
157
    tmp = 'no'
158
  args.append('-oStrictHostKeyChecking=%s' % tmp)
159
  args.append('-oClearAllForwardings=yes')
160
  args.append('-oForwardAgent=yes')
161
  args.append(node)
162
  args.append(cmd)
163

    
164
  return args
165

    
166

    
167
def StartLocalCommand(cmd, **kwargs):
168
  """Starts a local command.
169

170
  """
171
  print "Command: %s" % utils.ShellQuoteArgs(cmd)
172
  return subprocess.Popen(cmd, shell=False, **kwargs)
173

    
174

    
175
def StartSSH(node, cmd, strict=True):
176
  """Starts SSH.
177

178
  """
179
  return StartLocalCommand(GetSSHCommand(node, cmd, strict=strict))
180

    
181

    
182
def GetCommandOutput(node, cmd):
183
  """Returns the output of a command executed on the given node.
184

185
  """
186
  p = StartLocalCommand(GetSSHCommand(node, cmd), stdout=subprocess.PIPE)
187
  AssertEqual(p.wait(), 0)
188
  return p.stdout.read()
189

    
190

    
191
def UploadFile(node, src):
192
  """Uploads a file to a node and returns the filename.
193

194
  Caller needs to remove the returned file on the node when it's not needed
195
  anymore.
196

197
  """
198
  # Make sure nobody else has access to it while preserving local permissions
199
  mode = os.stat(src).st_mode & 0700
200

    
201
  cmd = ('tmp=$(tempfile --mode %o --prefix gnt) && '
202
         '[[ -f "${tmp}" ]] && '
203
         'cat > "${tmp}" && '
204
         'echo "${tmp}"') % mode
205

    
206
  f = open(src, 'r')
207
  try:
208
    p = subprocess.Popen(GetSSHCommand(node, cmd), shell=False, stdin=f,
209
                         stdout=subprocess.PIPE)
210
    AssertEqual(p.wait(), 0)
211

    
212
    # Return temporary filename
213
    return p.stdout.read().strip()
214
  finally:
215
    f.close()
216

    
217

    
218
def BackupFile(node, path):
219
  """Creates a backup of a file on the node and returns the filename.
220

221
  Caller needs to remove the returned file on the node when it's not needed
222
  anymore.
223

224
  """
225
  cmd = ("tmp=$(tempfile --prefix .gnt --directory=$(dirname %s)) && "
226
         "[[ -f \"$tmp\" ]] && "
227
         "cp %s $tmp && "
228
         "echo $tmp") % (utils.ShellQuote(path), utils.ShellQuote(path))
229

    
230
  # Return temporary filename
231
  return GetCommandOutput(node, cmd).strip()
232

    
233

    
234
def _ResolveName(cmd, key):
235
  """Helper function.
236

237
  """
238
  master = qa_config.GetMasterNode()
239

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

    
247

    
248
def ResolveInstanceName(instance):
249
  """Gets the full name of an instance.
250

251
  @type instance: string
252
  @param instance: Instance name
253

254
  """
255
  return _ResolveName(['gnt-instance', 'info', instance],
256
                      'Instance name')
257

    
258

    
259
def ResolveNodeName(node):
260
  """Gets the full name of a node.
261

262
  """
263
  return _ResolveName(['gnt-node', 'info', node['primary']],
264
                      'Node name')
265

    
266

    
267
def GetNodeInstances(node, secondaries=False):
268
  """Gets a list of instances on a node.
269

270
  """
271
  master = qa_config.GetMasterNode()
272
  node_name = ResolveNodeName(node)
273

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

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

    
286
  return instances
287

    
288

    
289
def _SelectQueryFields(rnd, fields):
290
  """Generates a list of fields for query tests.
291

292
  """
293
  # Create copy for shuffling
294
  fields = list(fields)
295
  rnd.shuffle(fields)
296

    
297
  # Check all fields
298
  yield fields
299
  yield sorted(fields)
300

    
301
  # Duplicate fields
302
  yield fields + fields
303

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

    
308

    
309
def _List(listcmd, fields, names):
310
  """Runs a list command.
311

312
  """
313
  master = qa_config.GetMasterNode()
314

    
315
  cmd = [listcmd, "list", "--separator=|", "--no-header",
316
         "--output", ",".join(fields)]
317

    
318
  if names:
319
    cmd.extend(names)
320

    
321
  return GetCommandOutput(master["primary"],
322
                          utils.ShellQuoteArgs(cmd)).splitlines()
323

    
324

    
325
def GenericQueryTest(cmd, fields):
326
  """Runs a number of tests on query commands.
327

328
  @param cmd: Command name
329
  @param fields: List of field names
330

331
  """
332
  rnd = random.Random(hash(cmd))
333

    
334
  randfields = list(fields)
335
  rnd.shuffle(fields)
336

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

    
341
  namelist_fn = compat.partial(_List, cmd, ["name"])
342

    
343
  # When no names were requested, the list must be sorted
344
  names = namelist_fn(None)
345
  AssertEqual(names, utils.NiceSort(names))
346

    
347
  # When requesting specific names, the order must be kept
348
  revnames = list(reversed(names))
349
  AssertEqual(namelist_fn(revnames), revnames)
350

    
351
  randnames = list(names)
352
  rnd.shuffle(randnames)
353
  AssertEqual(namelist_fn(randnames), randnames)
354

    
355

    
356
def _FormatWithColor(text, seq):
357
  if not seq:
358
    return text
359
  return "%s%s%s" % (seq, text, _RESET_SEQ)
360

    
361

    
362
FormatWarning = lambda text: _FormatWithColor(text, _WARNING_SEQ)
363
FormatError = lambda text: _FormatWithColor(text, _ERROR_SEQ)
364
FormatInfo = lambda text: _FormatWithColor(text, _INFO_SEQ)