Statistics
| Branch: | Tag: | Revision:

root / qa / qa_utils.py @ 0e79564a

History | View | Annotate | Download (21.3 kB)

1
#
2
#
3

    
4
# Copyright (C) 2007, 2011, 2012, 2013 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 operator
27
import os
28
import random
29
import re
30
import subprocess
31
import sys
32
import tempfile
33
import yaml
34

    
35
try:
36
  import functools
37
except ImportError, err:
38
  raise ImportError("Python 2.5 or higher is required: %s" % err)
39

    
40
from ganeti import utils
41
from ganeti import compat
42
from ganeti import constants
43
from ganeti import ht
44
from ganeti import pathutils
45
from ganeti import vcluster
46

    
47
import qa_config
48
import qa_error
49

    
50

    
51
_INFO_SEQ = None
52
_WARNING_SEQ = None
53
_ERROR_SEQ = None
54
_RESET_SEQ = None
55

    
56
_MULTIPLEXERS = {}
57

    
58
#: Unique ID per QA run
59
_RUN_UUID = utils.NewUUID()
60

    
61
#: Path to the QA query output log file
62
_QA_OUTPUT = pathutils.GetLogFilename("qa-output")
63

    
64

    
65
(INST_DOWN,
66
 INST_UP) = range(500, 502)
67

    
68
(FIRST_ARG,
69
 RETURN_VALUE) = range(1000, 1002)
70

    
71

    
72
def _SetupColours():
73
  """Initializes the colour constants.
74

75
  """
76
  # pylint: disable=W0603
77
  # due to global usage
78
  global _INFO_SEQ, _WARNING_SEQ, _ERROR_SEQ, _RESET_SEQ
79

    
80
  # Don't use colours if stdout isn't a terminal
81
  if not sys.stdout.isatty():
82
    return
83

    
84
  try:
85
    import curses
86
  except ImportError:
87
    # Don't use colours if curses module can't be imported
88
    return
89

    
90
  curses.setupterm()
91

    
92
  _RESET_SEQ = curses.tigetstr("op")
93

    
94
  setaf = curses.tigetstr("setaf")
95
  _INFO_SEQ = curses.tparm(setaf, curses.COLOR_GREEN)
96
  _WARNING_SEQ = curses.tparm(setaf, curses.COLOR_YELLOW)
97
  _ERROR_SEQ = curses.tparm(setaf, curses.COLOR_RED)
98

    
99

    
100
_SetupColours()
101

    
102

    
103
def AssertIn(item, sequence):
104
  """Raises an error when item is not in sequence.
105

106
  """
107
  if item not in sequence:
108
    raise qa_error.Error("%r not in %r" % (item, sequence))
109

    
110

    
111
def AssertNotIn(item, sequence):
112
  """Raises an error when item is in sequence.
113

114
  """
115
  if item in sequence:
116
    raise qa_error.Error("%r in %r" % (item, sequence))
117

    
118

    
119
def AssertEqual(first, second):
120
  """Raises an error when values aren't equal.
121

122
  """
123
  if not first == second:
124
    raise qa_error.Error("%r == %r" % (first, second))
125

    
126

    
127
def AssertMatch(string, pattern):
128
  """Raises an error when string doesn't match regexp pattern.
129

130
  """
131
  if not re.match(pattern, string):
132
    raise qa_error.Error("%r doesn't match /%r/" % (string, pattern))
133

    
134

    
135
def _GetName(entity, fn):
136
  """Tries to get name of an entity.
137

138
  @type entity: string or dict
139
  @param fn: Function retrieving name from entity
140

141
  """
142
  if isinstance(entity, basestring):
143
    result = entity
144
  else:
145
    result = fn(entity)
146

    
147
  if not ht.TNonEmptyString(result):
148
    raise Exception("Invalid name '%s'" % result)
149

    
150
  return result
151

    
152

    
153
def _AssertRetCode(rcode, fail, cmdstr, nodename):
154
  """Check the return value from a command and possibly raise an exception.
155

156
  """
157
  if fail and rcode == 0:
158
    raise qa_error.Error("Command '%s' on node %s was expected to fail but"
159
                         " didn't" % (cmdstr, nodename))
160
  elif not fail and rcode != 0:
161
    raise qa_error.Error("Command '%s' on node %s failed, exit code %s" %
162
                         (cmdstr, nodename, rcode))
163

    
164

    
165
def AssertCommand(cmd, fail=False, node=None, log_cmd=True):
166
  """Checks that a remote command succeeds.
167

168
  @param cmd: either a string (the command to execute) or a list (to
169
      be converted using L{utils.ShellQuoteArgs} into a string)
170
  @type fail: boolean
171
  @param fail: if the command is expected to fail instead of succeeding
172
  @param node: if passed, it should be the node on which the command
173
      should be executed, instead of the master node (can be either a
174
      dict or a string)
175
  @param log_cmd: if False, the command won't be logged (simply passed to
176
      StartSSH)
177
  @return: the return code of the command
178
  @raise qa_error.Error: if the command fails when it shouldn't or vice versa
179

180
  """
181
  if node is None:
182
    node = qa_config.GetMasterNode()
183

    
184
  nodename = _GetName(node, operator.attrgetter("primary"))
185

    
186
  if isinstance(cmd, basestring):
187
    cmdstr = cmd
188
  else:
189
    cmdstr = utils.ShellQuoteArgs(cmd)
190

    
191
  rcode = StartSSH(nodename, cmdstr, log_cmd=log_cmd).wait()
192
  _AssertRetCode(rcode, fail, cmdstr, nodename)
193

    
194
  return rcode
195

    
196

    
197
def AssertRedirectedCommand(cmd, fail=False, node=None, log_cmd=True):
198
  """Executes a command with redirected output.
199

200
  The log will go to the qa-output log file in the ganeti log
201
  directory on the node where the command is executed. The fail and
202
  node parameters are passed unchanged to AssertCommand.
203

204
  @param cmd: the command to be executed, as a list; a string is not
205
      supported
206

207
  """
208
  if not isinstance(cmd, list):
209
    raise qa_error.Error("Non-list passed to AssertRedirectedCommand")
210
  ofile = utils.ShellQuote(_QA_OUTPUT)
211
  cmdstr = utils.ShellQuoteArgs(cmd)
212
  AssertCommand("echo ---- $(date) %s ---- >> %s" % (cmdstr, ofile),
213
                fail=False, node=node, log_cmd=False)
214
  return AssertCommand(cmdstr + " >> %s" % ofile,
215
                       fail=fail, node=node, log_cmd=log_cmd)
216

    
217

    
218
def GetSSHCommand(node, cmd, strict=True, opts=None, tty=None):
219
  """Builds SSH command to be executed.
220

221
  @type node: string
222
  @param node: node the command should run on
223
  @type cmd: string
224
  @param cmd: command to be executed in the node; if None or empty
225
      string, no command will be executed
226
  @type strict: boolean
227
  @param strict: whether to enable strict host key checking
228
  @type opts: list
229
  @param opts: list of additional options
230
  @type tty: boolean or None
231
  @param tty: if we should use tty; if None, will be auto-detected
232

233
  """
234
  args = ["ssh", "-oEscapeChar=none", "-oBatchMode=yes", "-lroot"]
235

    
236
  if tty is None:
237
    tty = sys.stdout.isatty()
238

    
239
  if tty:
240
    args.append("-t")
241

    
242
  if strict:
243
    tmp = "yes"
244
  else:
245
    tmp = "no"
246
  args.append("-oStrictHostKeyChecking=%s" % tmp)
247
  args.append("-oClearAllForwardings=yes")
248
  args.append("-oForwardAgent=yes")
249
  if opts:
250
    args.extend(opts)
251
  if node in _MULTIPLEXERS:
252
    spath = _MULTIPLEXERS[node][0]
253
    args.append("-oControlPath=%s" % spath)
254
    args.append("-oControlMaster=no")
255

    
256
  (vcluster_master, vcluster_basedir) = \
257
    qa_config.GetVclusterSettings()
258

    
259
  if vcluster_master:
260
    args.append(vcluster_master)
261
    args.append("%s/%s/cmd" % (vcluster_basedir, node))
262

    
263
    if cmd:
264
      # For virtual clusters the whole command must be wrapped using the "cmd"
265
      # script, as that script sets a number of environment variables. If the
266
      # command contains shell meta characters the whole command needs to be
267
      # quoted.
268
      args.append(utils.ShellQuote(cmd))
269
  else:
270
    args.append(node)
271

    
272
    if cmd:
273
      args.append(cmd)
274

    
275
  return args
276

    
277

    
278
def StartLocalCommand(cmd, _nolog_opts=False, log_cmd=True, **kwargs):
279
  """Starts a local command.
280

281
  """
282
  if log_cmd:
283
    if _nolog_opts:
284
      pcmd = [i for i in cmd if not i.startswith("-")]
285
    else:
286
      pcmd = cmd
287
    print "Command: %s" % utils.ShellQuoteArgs(pcmd)
288
  return subprocess.Popen(cmd, shell=False, **kwargs)
289

    
290

    
291
def StartSSH(node, cmd, strict=True, log_cmd=True):
292
  """Starts SSH.
293

294
  """
295
  return StartLocalCommand(GetSSHCommand(node, cmd, strict=strict),
296
                           _nolog_opts=True, log_cmd=log_cmd)
297

    
298

    
299
def StartMultiplexer(node):
300
  """Starts a multiplexer command.
301

302
  @param node: the node for which to open the multiplexer
303

304
  """
305
  if node in _MULTIPLEXERS:
306
    return
307

    
308
  # Note: yes, we only need mktemp, since we'll remove the file anyway
309
  sname = tempfile.mktemp(prefix="ganeti-qa-multiplexer.")
310
  utils.RemoveFile(sname)
311
  opts = ["-N", "-oControlPath=%s" % sname, "-oControlMaster=yes"]
312
  print "Created socket at %s" % sname
313
  child = StartLocalCommand(GetSSHCommand(node, None, opts=opts))
314
  _MULTIPLEXERS[node] = (sname, child)
315

    
316

    
317
def CloseMultiplexers():
318
  """Closes all current multiplexers and cleans up.
319

320
  """
321
  for node in _MULTIPLEXERS.keys():
322
    (sname, child) = _MULTIPLEXERS.pop(node)
323
    utils.KillProcess(child.pid, timeout=10, waitpid=True)
324
    utils.RemoveFile(sname)
325

    
326

    
327
def GetCommandOutput(node, cmd, tty=None, fail=False):
328
  """Returns the output of a command executed on the given node.
329

330
  @type node: string
331
  @param node: node the command should run on
332
  @type cmd: string
333
  @param cmd: command to be executed in the node (cannot be empty or None)
334
  @type tty: bool or None
335
  @param tty: if we should use tty; if None, it will be auto-detected
336
  @type fail: bool
337
  @param fail: whether the command is expected to fail
338
  """
339
  assert cmd
340
  p = StartLocalCommand(GetSSHCommand(node, cmd, tty=tty),
341
                        stdout=subprocess.PIPE)
342
  rcode = p.wait()
343
  _AssertRetCode(rcode, fail, cmd, node)
344
  return p.stdout.read()
345

    
346

    
347
def GetObjectInfo(infocmd):
348
  """Get and parse information about a Ganeti object.
349

350
  @type infocmd: list of strings
351
  @param infocmd: command to be executed, e.g. ["gnt-cluster", "info"]
352
  @return: the information parsed, appropriately stored in dictionaries,
353
      lists...
354

355
  """
356
  master = qa_config.GetMasterNode()
357
  cmdline = utils.ShellQuoteArgs(infocmd)
358
  info_out = GetCommandOutput(master.primary, cmdline)
359
  return yaml.load(info_out)
360

    
361

    
362
def UploadFile(node, src):
363
  """Uploads a file to a node and returns the filename.
364

365
  Caller needs to remove the returned file on the node when it's not needed
366
  anymore.
367

368
  """
369
  # Make sure nobody else has access to it while preserving local permissions
370
  mode = os.stat(src).st_mode & 0700
371

    
372
  cmd = ('tmp=$(tempfile --mode %o --prefix gnt) && '
373
         '[[ -f "${tmp}" ]] && '
374
         'cat > "${tmp}" && '
375
         'echo "${tmp}"') % mode
376

    
377
  f = open(src, "r")
378
  try:
379
    p = subprocess.Popen(GetSSHCommand(node, cmd), shell=False, stdin=f,
380
                         stdout=subprocess.PIPE)
381
    AssertEqual(p.wait(), 0)
382

    
383
    # Return temporary filename
384
    return p.stdout.read().strip()
385
  finally:
386
    f.close()
387

    
388

    
389
def UploadData(node, data, mode=0600, filename=None):
390
  """Uploads data to a node and returns the filename.
391

392
  Caller needs to remove the returned file on the node when it's not needed
393
  anymore.
394

395
  """
396
  if filename:
397
    tmp = "tmp=%s" % utils.ShellQuote(filename)
398
  else:
399
    tmp = "tmp=$(tempfile --mode %o --prefix gnt)" % mode
400
  cmd = ("%s && "
401
         "[[ -f \"${tmp}\" ]] && "
402
         "cat > \"${tmp}\" && "
403
         "echo \"${tmp}\"") % tmp
404

    
405
  p = subprocess.Popen(GetSSHCommand(node, cmd), shell=False,
406
                       stdin=subprocess.PIPE, stdout=subprocess.PIPE)
407
  p.stdin.write(data)
408
  p.stdin.close()
409
  AssertEqual(p.wait(), 0)
410

    
411
  # Return temporary filename
412
  return p.stdout.read().strip()
413

    
414

    
415
def BackupFile(node, path):
416
  """Creates a backup of a file on the node and returns the filename.
417

418
  Caller needs to remove the returned file on the node when it's not needed
419
  anymore.
420

421
  """
422
  vpath = MakeNodePath(node, path)
423

    
424
  cmd = ("tmp=$(tempfile --prefix .gnt --directory=$(dirname %s)) && "
425
         "[[ -f \"$tmp\" ]] && "
426
         "cp %s $tmp && "
427
         "echo $tmp") % (utils.ShellQuote(vpath), utils.ShellQuote(vpath))
428

    
429
  # Return temporary filename
430
  result = GetCommandOutput(node, cmd).strip()
431

    
432
  print "Backup filename: %s" % result
433

    
434
  return result
435

    
436

    
437
def _ResolveName(cmd, key):
438
  """Helper function.
439

440
  """
441
  master = qa_config.GetMasterNode()
442

    
443
  output = GetCommandOutput(master.primary, utils.ShellQuoteArgs(cmd))
444
  for line in output.splitlines():
445
    (lkey, lvalue) = line.split(":", 1)
446
    if lkey == key:
447
      return lvalue.lstrip()
448
  raise KeyError("Key not found")
449

    
450

    
451
def ResolveInstanceName(instance):
452
  """Gets the full name of an instance.
453

454
  @type instance: string
455
  @param instance: Instance name
456

457
  """
458
  return _ResolveName(["gnt-instance", "info", instance],
459
                      "Instance name")
460

    
461

    
462
def ResolveNodeName(node):
463
  """Gets the full name of a node.
464

465
  """
466
  return _ResolveName(["gnt-node", "info", node.primary],
467
                      "Node name")
468

    
469

    
470
def GetNodeInstances(node, secondaries=False):
471
  """Gets a list of instances on a node.
472

473
  """
474
  master = qa_config.GetMasterNode()
475
  node_name = ResolveNodeName(node)
476

    
477
  # Get list of all instances
478
  cmd = ["gnt-instance", "list", "--separator=:", "--no-headers",
479
         "--output=name,pnode,snodes"]
480
  output = GetCommandOutput(master.primary, utils.ShellQuoteArgs(cmd))
481

    
482
  instances = []
483
  for line in output.splitlines():
484
    (name, pnode, snodes) = line.split(":", 2)
485
    if ((not secondaries and pnode == node_name) or
486
        (secondaries and node_name in snodes.split(","))):
487
      instances.append(name)
488

    
489
  return instances
490

    
491

    
492
def _SelectQueryFields(rnd, fields):
493
  """Generates a list of fields for query tests.
494

495
  """
496
  # Create copy for shuffling
497
  fields = list(fields)
498
  rnd.shuffle(fields)
499

    
500
  # Check all fields
501
  yield fields
502
  yield sorted(fields)
503

    
504
  # Duplicate fields
505
  yield fields + fields
506

    
507
  # Check small groups of fields
508
  while fields:
509
    yield [fields.pop() for _ in range(rnd.randint(2, 10)) if fields]
510

    
511

    
512
def _List(listcmd, fields, names):
513
  """Runs a list command.
514

515
  """
516
  master = qa_config.GetMasterNode()
517

    
518
  cmd = [listcmd, "list", "--separator=|", "--no-headers",
519
         "--output", ",".join(fields)]
520

    
521
  if names:
522
    cmd.extend(names)
523

    
524
  return GetCommandOutput(master.primary,
525
                          utils.ShellQuoteArgs(cmd)).splitlines()
526

    
527

    
528
def GenericQueryTest(cmd, fields, namefield="name", test_unknown=True):
529
  """Runs a number of tests on query commands.
530

531
  @param cmd: Command name
532
  @param fields: List of field names
533

534
  """
535
  rnd = random.Random(hash(cmd))
536

    
537
  fields = list(fields)
538
  rnd.shuffle(fields)
539

    
540
  # Test a number of field combinations
541
  for testfields in _SelectQueryFields(rnd, fields):
542
    AssertRedirectedCommand([cmd, "list", "--output", ",".join(testfields)])
543

    
544
  if namefield is not None:
545
    namelist_fn = compat.partial(_List, cmd, [namefield])
546

    
547
    # When no names were requested, the list must be sorted
548
    names = namelist_fn(None)
549
    AssertEqual(names, utils.NiceSort(names))
550

    
551
    # When requesting specific names, the order must be kept
552
    revnames = list(reversed(names))
553
    AssertEqual(namelist_fn(revnames), revnames)
554

    
555
    randnames = list(names)
556
    rnd.shuffle(randnames)
557
    AssertEqual(namelist_fn(randnames), randnames)
558

    
559
  if test_unknown:
560
    # Listing unknown items must fail
561
    AssertCommand([cmd, "list", "this.name.certainly.does.not.exist"],
562
                  fail=True)
563

    
564
  # Check exit code for listing unknown field
565
  AssertEqual(AssertRedirectedCommand([cmd, "list",
566
                                       "--output=field/does/not/exist"],
567
                                      fail=True),
568
              constants.EXIT_UNKNOWN_FIELD)
569

    
570

    
571
def GenericQueryFieldsTest(cmd, fields):
572
  master = qa_config.GetMasterNode()
573

    
574
  # Listing fields
575
  AssertRedirectedCommand([cmd, "list-fields"])
576
  AssertRedirectedCommand([cmd, "list-fields"] + fields)
577

    
578
  # Check listed fields (all, must be sorted)
579
  realcmd = [cmd, "list-fields", "--separator=|", "--no-headers"]
580
  output = GetCommandOutput(master.primary,
581
                            utils.ShellQuoteArgs(realcmd)).splitlines()
582
  AssertEqual([line.split("|", 1)[0] for line in output],
583
              utils.NiceSort(fields))
584

    
585
  # Check exit code for listing unknown field
586
  AssertEqual(AssertCommand([cmd, "list-fields", "field/does/not/exist"],
587
                            fail=True),
588
              constants.EXIT_UNKNOWN_FIELD)
589

    
590

    
591
def _FormatWithColor(text, seq):
592
  if not seq:
593
    return text
594
  return "%s%s%s" % (seq, text, _RESET_SEQ)
595

    
596

    
597
FormatWarning = lambda text: _FormatWithColor(text, _WARNING_SEQ)
598
FormatError = lambda text: _FormatWithColor(text, _ERROR_SEQ)
599
FormatInfo = lambda text: _FormatWithColor(text, _INFO_SEQ)
600

    
601

    
602
def AddToEtcHosts(hostnames):
603
  """Adds hostnames to /etc/hosts.
604

605
  @param hostnames: List of hostnames first used A records, all other CNAMEs
606

607
  """
608
  master = qa_config.GetMasterNode()
609
  tmp_hosts = UploadData(master.primary, "", mode=0644)
610

    
611
  data = []
612
  for localhost in ("::1", "127.0.0.1"):
613
    data.append("%s %s" % (localhost, " ".join(hostnames)))
614

    
615
  try:
616
    AssertCommand("{ cat %s && echo -e '%s'; } > %s && mv %s %s" %
617
                  (utils.ShellQuote(pathutils.ETC_HOSTS),
618
                   "\\n".join(data),
619
                   utils.ShellQuote(tmp_hosts),
620
                   utils.ShellQuote(tmp_hosts),
621
                   utils.ShellQuote(pathutils.ETC_HOSTS)))
622
  except Exception:
623
    AssertCommand(["rm", "-f", tmp_hosts])
624
    raise
625

    
626

    
627
def RemoveFromEtcHosts(hostnames):
628
  """Remove hostnames from /etc/hosts.
629

630
  @param hostnames: List of hostnames first used A records, all other CNAMEs
631

632
  """
633
  master = qa_config.GetMasterNode()
634
  tmp_hosts = UploadData(master.primary, "", mode=0644)
635
  quoted_tmp_hosts = utils.ShellQuote(tmp_hosts)
636

    
637
  sed_data = " ".join(hostnames)
638
  try:
639
    AssertCommand(("sed -e '/^\(::1\|127\.0\.0\.1\)\s\+%s/d' %s > %s"
640
                   " && mv %s %s") %
641
                   (sed_data, utils.ShellQuote(pathutils.ETC_HOSTS),
642
                    quoted_tmp_hosts, quoted_tmp_hosts,
643
                    utils.ShellQuote(pathutils.ETC_HOSTS)))
644
  except Exception:
645
    AssertCommand(["rm", "-f", tmp_hosts])
646
    raise
647

    
648

    
649
def RunInstanceCheck(instance, running):
650
  """Check if instance is running or not.
651

652
  """
653
  instance_name = _GetName(instance, operator.attrgetter("name"))
654

    
655
  script = qa_config.GetInstanceCheckScript()
656
  if not script:
657
    return
658

    
659
  master_node = qa_config.GetMasterNode()
660

    
661
  # Build command to connect to master node
662
  master_ssh = GetSSHCommand(master_node.primary, "--")
663

    
664
  if running:
665
    running_shellval = "1"
666
    running_text = ""
667
  else:
668
    running_shellval = ""
669
    running_text = "not "
670

    
671
  print FormatInfo("Checking if instance '%s' is %srunning" %
672
                   (instance_name, running_text))
673

    
674
  args = [script, instance_name]
675
  env = {
676
    "PATH": constants.HOOKS_PATH,
677
    "RUN_UUID": _RUN_UUID,
678
    "MASTER_SSH": utils.ShellQuoteArgs(master_ssh),
679
    "INSTANCE_NAME": instance_name,
680
    "INSTANCE_RUNNING": running_shellval,
681
    }
682

    
683
  result = os.spawnve(os.P_WAIT, script, args, env)
684
  if result != 0:
685
    raise qa_error.Error("Instance check failed with result %s" % result)
686

    
687

    
688
def _InstanceCheckInner(expected, instarg, args, result):
689
  """Helper function used by L{InstanceCheck}.
690

691
  """
692
  if instarg == FIRST_ARG:
693
    instance = args[0]
694
  elif instarg == RETURN_VALUE:
695
    instance = result
696
  else:
697
    raise Exception("Invalid value '%s' for instance argument" % instarg)
698

    
699
  if expected in (INST_DOWN, INST_UP):
700
    RunInstanceCheck(instance, (expected == INST_UP))
701
  elif expected is not None:
702
    raise Exception("Invalid value '%s'" % expected)
703

    
704

    
705
def InstanceCheck(before, after, instarg):
706
  """Decorator to check instance status before and after test.
707

708
  @param before: L{INST_DOWN} if instance must be stopped before test,
709
    L{INST_UP} if instance must be running before test, L{None} to not check.
710
  @param after: L{INST_DOWN} if instance must be stopped after test,
711
    L{INST_UP} if instance must be running after test, L{None} to not check.
712
  @param instarg: L{FIRST_ARG} to use first argument to test as instance (a
713
    dictionary), L{RETURN_VALUE} to use return value (disallows pre-checks)
714

715
  """
716
  def decorator(fn):
717
    @functools.wraps(fn)
718
    def wrapper(*args, **kwargs):
719
      _InstanceCheckInner(before, instarg, args, NotImplemented)
720

    
721
      result = fn(*args, **kwargs)
722

    
723
      _InstanceCheckInner(after, instarg, args, result)
724

    
725
      return result
726
    return wrapper
727
  return decorator
728

    
729

    
730
def GetNonexistentGroups(count):
731
  """Gets group names which shouldn't exist on the cluster.
732

733
  @param count: Number of groups to get
734
  @rtype: integer
735

736
  """
737
  return GetNonexistentEntityNames(count, "groups", "group")
738

    
739

    
740
def GetNonexistentEntityNames(count, name_config, name_prefix):
741
  """Gets entity names which shouldn't exist on the cluster.
742

743
  The actualy names can refer to arbitrary entities (for example
744
  groups, networks).
745

746
  @param count: Number of names to get
747
  @rtype: integer
748
  @param name_config: name of the leaf in the config containing
749
    this entity's configuration, including a 'inexistent-'
750
    element
751
  @rtype: string
752
  @param name_prefix: prefix of the entity's names, used to compose
753
    the default values; for example for groups, the prefix is
754
    'group' and the generated names are then group1, group2, ...
755
  @rtype: string
756

757
  """
758
  entities = qa_config.get(name_config, {})
759

    
760
  default = [name_prefix + str(i) for i in range(count)]
761
  assert count <= len(default)
762

    
763
  name_config_inexistent = "inexistent-" + name_config
764
  candidates = entities.get(name_config_inexistent, default)[:count]
765

    
766
  if len(candidates) < count:
767
    raise Exception("At least %s non-existent %s are needed" %
768
                    (count, name_config))
769

    
770
  return candidates
771

    
772

    
773
def MakeNodePath(node, path):
774
  """Builds an absolute path for a virtual node.
775

776
  @type node: string or L{qa_config._QaNode}
777
  @param node: Node
778
  @type path: string
779
  @param path: Path without node-specific prefix
780

781
  """
782
  (_, basedir) = qa_config.GetVclusterSettings()
783

    
784
  if isinstance(node, basestring):
785
    name = node
786
  else:
787
    name = node.primary
788

    
789
  if basedir:
790
    assert path.startswith("/")
791
    return "%s%s" % (vcluster.MakeNodeRoot(basedir, name), path)
792
  else:
793
    return path